LDA主题模型

本文主要是介绍如何利用sklearn框架中LatentDirichletAllocation类完成LDA模型的训练。

理论基础

  LDA是一个无监督的学习模型,它假设每个文档包含多个主题,文档中的每个主题都是基于词的概率分布。作为一个基于贝叶斯网络的文档生成模型,LDA刻画的是文档生成的一个概率化过程。
  LDA的输入由一组文档D组成的词料库。LDA的输出包括文档主题分布$\theta$和主题中的词语的分布$\phi$。这里的$\theta$和$\phi$都假设服从多项式分布。为让分布更平滑,再假设这两个参数的先验分布服从狄利克雷分布,参数分别为$\alpha$和$\beta$。因为狄利克雷分布是多项式分布的共轭先验分布,所以假设该多项式分布的先验服从狄利克雷分布可以极大简化统计计算的过程。狄利克雷分布中有多个参数,再LDA中利用狄利克雷分布时,大多将参数设为同一个数值,这种设为同一个数值的狄利克雷分布称为对称的狄利克雷分布。以下时LDA模型生成一篇文档的方式。

  1. 按照先验概率$p(d_i)$的方式选择一篇文档$d_i$。

  2. 从超参数$\alpha$的狄利克雷分布中取样生成文档$d_i$的多项式主题分布$\theta_i$,即主题分布$\theta_i$由超参数$\alpha$的狄利克雷分布生成。

  3. 用$z_{i,j}$表示从主题的多项式分布$\theta_i$中采样生成文档$d_i$第j个词的主题。

  4. 从超参数为$\beta$的狄利克雷分布中采样生成主题$z_{i,j}$对应的词语分布$\phi_z$,即词语分布时由超参数$\beta$的狄利克雷分布生成的。

  5. $W_{i,j}$表示从词语多项式分布$\phi_z$中采样生成最终的词语。

刘兵情感分析:挖掘观点、情感和情绪

sklearn常用类

CountVectorizer

  sklearn常用的文本特征提取类有CountVectorizer和TfidfVectorizer,以下依据sklearn的官方文档对CountVectorizer类中的部分参数做出解释。

  • input:参数类型为string,当参数为filename 预计时需要读取文件原始内容来进行分析;当参数为 file,官方文档给出的解释必须要有一个read方法来读取内存中的字节内容。具体用法不知;如果不是上述两项参数,则将视为直接进行分析的字符串序列或字节流内容。

  • encoding:参数类型为string,表示对输入字符串的解码方式,默认值为utf-8

  • decoder_error: 参数可选为{‘strict’, ‘ignore’, ‘replace’},表示对输入内容呢,按照encoding参数设置的内容进行解码时,若出现不符合编码方式的错误时的解决办法。默认参数为strict,表示将会抛出UnicodeDecodeError错误;ignore表示忽略当前解码错误;replace参数作用尚不明确。

  • analyzer:参数可选为{‘word’, ‘char’, ‘char_wb’},该参数决定特征是否应该由单词或字符的n-gram组成,char_b表示创建的n-gram特征的字符范围为文本字符,n-gram不足部分用空格填充。

  • preprocessor: 重写预处理阶段,但标记化和n-gram的步骤会保留下来,默认参数为None。

  • tokenizer:类似于preprocessor参数,重写字符串标记化过程,但会保留预处理和n-gram步骤,该参数当analyer为word时才生效。

  • ngram_range: n-gram特征提取范围,参数类型为元组(tuple): (min_n, max_n),在$min_n \leq n \leq max_n$的n-gram都将被提取。

  • stop_word: 停用词去除参数。参数为’english’时,去除英语中的停用词;当参数为list类型的数据时,会假定该list中包含所有需要去除的停用词,将去除原始文本中该list指向的所有词,该参数当analyzer参数为word时才生效。

  • lowercase:参数类型为布尔值,默认参数为True,表示是否对文本进行小写化。

  • max_df: 该参数表示一个最大阈值,当参数类型为float时,参数范围为$[0.0, 1.0]$;当参数类型为int时,默认值为1。在建立词汇表时,若词汇中某个单词出现的频率(float)或次数(int)大于当前阈值时,该单词将不会加入到词汇统计中。

  • min_df: 该参数类似于max_df,表示最小阈值。

  • max_features: 参数类型为int,默认值为None。当参数为int时,表示构建的词汇表的词汇仅是语料中词频在排在max_features之前的词。

LatentDirichletAllocation

  sklearn中训练LDA主题模型的类是LatentDirichletAllocation

参数

  • n_components: 模型训练的预设主题数,参数类型为int,默认值为10。

  • doc_topic_prior: 即文档主题分布$\theta$的参数$\alpha$,参数类型为float,若参数为None,则$\alpha$参数默认为$1 / n_components$

  • topic_word_prior: 即主题词语分布$\phi$的参数$\beta$,参数类型为float,若参数为None,则$\beta$参数默认为$1 / n_components$

  • learning_method: LDA的求解算法,有’batch’和’online’两种,默认为’batch’。当数据规模较大时,’online’将比’batch’更快。

  • max_iter: 最大迭代次数,参数类型为int。

方法

  • fit(X[, y]): 利用训练数据训练LDA模型,输入参数为CountVectorizer类提取的文本词频矩阵。

  • transform(X[, y]): 利用训练好的模型推断语料X中文档的主题分布。

  • fit_transform(X[, y]): 对输入语料(训练数据)训练LDA模型,并推断输入语料的主题分布。

  • perplexity(X[, doc_topic_distr, sub_sampling]):计算语料X的的困惑度。

模型训练与调参

以困惑度(Perplexity)为基础调参

  LDA在进行训练之前,需要由算法设计人员指定主题数目参数n,主题数目的选择会在一定程度上影响主题检测的效果。 因此可以考虑计算Perplexity的值来帮助选主题数目参数。具体调参方式为:

  1. 指定主题的数目范围为:n_topics = range(min, max)。

  2. 对$min \leq n \leq max$进行LDA主题模型训练的迭代。计算每一次迭代的Perplexity。

  3. 绘制Perplexity与n_topics的曲线图,从图像最低点附近寻找最合适的参数。

类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
from sklearn.externals import joblib

class LDATrain:
def __init__(self, DocLst, NumTopics, MaxIter, MaxFeatures):
"""初始化,DocLst为输入的训练或测试文档"""
self.DocLst = DocLst #输入参数,List类型数据结构,List中每一个元素为一个文档
self.NumTopics = NumTopics #话题数目
self.MaxIter = MaxIter #最大迭代数据
self.MaxFeatures = MaxFeatures #进行词频统计的最大数目

#计算词频时需要用到的变量
self.TFVectorizer = None
self.TF = None #词频统计结果
#进行模型迭代时需要的变量
self.LDAModelLst = [] #迭代产生的LDA模型列表
self.PerplexityLst = [] #困惑度列表
self.BestIndex = None
self.BestLDAModel = None #最佳LDA 模型
self.BestLDAModelPerplexity = None #最佳LDA模型的困惑度
self.BestTopicNum = None #最合适的主题数

def LDACountVectorizer(self, MaxDf = 0.95, MinDf = 2):#这个值还有待确认
"""统计词频函数,调用CountVectorizer完成"""
self.TFVectorizer = CountVectorizer(max_df = MaxDf,\
min_df = MinDf,\
max_features = self.MaxFeatures)
self.TF = self.TFVectorizer.fit_transform(self.DocLst)

def LDASaveTF(self, TFModelPath):
"""保存词频"""
joblib.dump(self.TF, TFModelPath)

def LDALoadVectorizer(self, TFModelPath):
"""导入之前计算得到的词频统计结果"""
self.TF = joblib.load(TFModelPath)

def LDATrain(self, NumTopic, MaxIter):
"""一次LDA训练,NumTopic为当前训练的主题数,MaxIter为最大迭代数"""
LDAResult = LatentDirichletAllocation(n_components = NumTopic, \
max_iter = MaxIter,\
learning_method = 'batch',\
# evaluate_every = 200, \
# perp_tol = 0.01
)
# LDAResult.fit(self.TF)
# TrainGamma = LDAResult.transform(self.TF)
# TranPerplexity = LDAResult.perplexity(self.TF, TrainGamma)
# return TrainGamma, TranPerplexity
return LDAResult.fit(self.TF), LDAResult.perplexity(self.TF)

def IterationLDATrain(self):
"""迭代训练最佳的LDA模型,
NumTopics为包括所有可能的主题数一个list,
MaxIter为一次LDA训练的最大迭代数"""
#开始进行迭代训练
index = 0
for NumTopic in self.NumTopics:
lda, perplexity = self.__LDATrain(NumTopic, self.MaxIter)
self.LDAModelLst.append(lda)
self.PerplexityLst.append(perplexity)
print(index)
index += 1
#保存最佳模型到
BestIndex = self.PerplexityLst.index(min(self.PerplexityLst))#获取最佳模型的索引
self.BestLDAModelPerplexity = min(self.PerplexityLst)
self.BestTopicNum = self.NumTopics[BestIndex]
self.BestLDAModel = self.LDAModelLst[BestIndex]
self.BestIndex = BestIndex

def TransformBestModel(self):
for doc in self.TF:
print(self.BestLDAModel.transform(doc))

def __print_top_Words(self, model, FeatureNames, NumTopWords):
for topic_idx, topic in enumerate(model.components_):
print("Topic #%d:" % topic_idx)
print(" ".join([FeatureNames[i]
for i in topic.argsort()[:-NumTopWords - 1:-1]]))

def SaveTopicWords(self, NumTopWords, FilePath):
"""保存主题关键词,NumTopWord表示前多少个词"""
TopicWords = ""
FeatureNames = self.TFVectorizer.get_feature_names()
for topic_idx, topic in enumerate(self.BestLDAModel.components_):
TopicWords += "Topic #%d:" % topic_idx
TopicWords += " ".join([FeatureNames[i]
for i in topic.argsort()[:-NumTopWords - 1:-1]]) +'\n'
f = open(FilePath + 'TopicWords.txt', 'w')
f.write(TopicWords)
f.close()

def PrintBestModelAndPerplexity(self, NumTopWords):
"""打印出最佳模型"""
print("Best Number of Topic in LDA Model is ", self.BestTopicNum)
print("the min Perplexity is", self.BestLDAModelPerplexity)
print("Best Model is \n")
self.__print_top_Words(self.BestLDAModel, self.TFVectorizer.get_feature_names(), NumTopWords)

def SaveAllLDAMode(self, FilePath):
"""保存所有LDAModel"""
#检查该目录是否存在,若不存在则创建
CreateDir(FilePath)
index = 0
for m in self.LDAModelLst:
joblib.dump(m, FilePath + 'LDA-model-' + str(index) + '.model')
index += 1

def SaveBestModel(self, FilePath):
"""保存最好的LDAmodel"""
joblib.dump(self.BestLDAModel, FilePath + 'BestModel.model')

def SavePerplexityCurveAndText(self, FilePath):
"""保存所有的困惑度(Perplexity),对应的曲线图像"""
#检查该目录是否存在,若不存在则创建
CreateDir(FilePath)
# 保存perplexity结果
with open(FilePath + 'Perplexity.txt', 'w') as f:
PerplexityLstStr = ""
index = 0
for x in self.PerplexityLst:
PerplexityLstStr += str(index) + '|' + str(self.NumTopics[index]) + '|' + str(x) + '\n'
index += 1
f.write(PerplexityLstStr)
#绘制曲线并保存
plt.close('all')
Figure = plt.figure()
ax = Figure.add_subplot(1, 1, 1)
ax.plot(self.NumTopics, self.PerplexityLst)
ax.set_xlabel("# of topics")
ax.set_ylabel("Approximate Perplexity")
plt.grid(True)
plt.savefig(FilePath + 'PerplexityTrend.png')
#plt.show()

def PrintDocTopicDist(self):
"""打印出文档关于主题的矩阵,每一行表示文档,列表示是当前主题概率"""
doc_topic_dist = self.BestLDAModel.transform(self.TF)
for idx, dist in enumerate(doc_topic_dist):
# 注意:由于sklearn LDA函数限制,此函数中输出的topic_word矩阵未normalize
dist = [str(x) for x in dist]
print(str(idx + 1) + ',')
print(','.join(dist) + '\n')

def SaveDocTopicDist(self, FilePath):
"""保存出文档关于主题的矩阵,每一行表示文档,列表示是当前主题概率"""
doc_topic_dist = self.BestLDAModel.transform(self.TF)
DocTopic = ''
for idx, dist in enumerate(doc_topic_dist):
# 注意:由于sklearn LDA函数限制,此函数中输出的topic_word矩阵未normalize
dist = [str(x) for x in dist]
DocTopic += 'Document ' + str(idx + 1) + ':' +','.join(dist) + '\n'
# print str(idx + 1) + ','
# print ','.join(dist) + '\n'
f = open(FilePath + 'DocTopicDist.txt', 'w')
f.write(DocTopic)
f.close()

def SaveConfigFile(self, FilePath):
"""保存文件配置"""
f = open(FilePath + 'Config.txt', 'a')
Config = \
'k param = ' + str(max(self.NumTopics) + 1) + '\n' + \
'MaxFeatures = ' + str(self.MaxFeatures) + '\n' + \
'BestTopicNum = ' + str(self.BestTopicNum) + '\n' + \
'BestIndex = ' + str(self.BestIndex)
f.write(Config)
f.close()

实验

主题-Perplexity曲线图

从图中可以看出,困惑度的最低点是主题数为4时。

主题-关键词

以下时主题数为4的主题-关键词分布。

Topic #0:couple counseling marriage therapy tip gloria relationship save saving expert help dr back get cambridge
Topic #1:today via thought bomber day go say im year love prayer http dont around affected
Topic #2:marathon explosion people new looking line injured finish runner victim friend today area via dead
Topic #3:suspect bombing police marathon amp photo news say breaking local official officer fbi three area

从关键词的性质可以看出,主题的分类效果较好。

参考

[1] 靳志辉,LDA数学八卦[M],2013.
[2] https://blog.csdn.net/TiffanyRabbit/article/details/76445909

文中代码链接:https://github.com/zhaohuang123/sentiment-analysis/tree/master/blog/LDA