python自然语言处理之lda

自然语言处理之LDA

LDA由PLSA发展而来,PLSA由LSA发展而来,同样用于隐含语义分析,这里先给出两篇实现LSA和PLSA的文章链接。
自然语言处理之LSA
自然语言处理之PLSA

我们知道,PLSA也定义了一个概率图模型,假设了数据的生成过程,但是不是一个完全的生成过程:没有给出先验。因此PLSA给出的是一个最大似然估计(ML)或者最大后验估计(MAP)。
LDA拓展了PLSA,定义了先验,因此LDA给出的是一个完整的贝叶斯估计。

符号

这里先给出本文公式中以及python代码中使用到的符号及其含义。关于编号,公式中从1开始编号,代码中从0开始编号,只是为了方便。

公式中的符号 描述 代码中的符号 描述
文档总数 N  
第i篇文档单词数    
topic总数 K  
词表长度 M  
文档编号,是一个整数 d  
topic编号,是一个整数 z  
单词编号,是一个整数 w  
document-topic分布的超参数,是一个K维向量 alpha 是一个标量,的每一维都是alpha
topic-word分布的超参数,是一个M维向量 beta 是一个标量,的每一维都是beta
组成的K维向量,其中    
   
组成的M维向量,其中    
   
第i篇文档的第j个单词的topic编号 Z[i,j] Z是一个元素为list的list
第i篇文档的第j个单词在词表中的编号 docs[i,j] docs是一个元素为list的list
二维下标(i,j),对应第i篇文档的第j个单词    
下标包含这一项表示将第i篇文档的第j个单词排除在外    
第i篇文档中由z这个topic产生的单词计数 ndz[i,z] ndz是一个二维矩阵,ndz[i,z]等于加上伪计数alpha
第i篇文档由每个topic产生的单词计数组成的向量    
第z个topic产生单词w的计数 nzw[z,w] nzw是一个二维矩阵,nzw[z,w]等于加上伪计数beta
第z个topic产生全部单词的计数 nz[z] nz是一个一维矩阵,nz[z]等于加上M倍的beta

概率图模型

LDA的概率图模型见下图。箭头定义了随机变量间的关系。每个方框定义了每个过程,方框右下角的变量是该过程的重复次数。
python自然语言处理之lda

生成过程

LDA认为数据的生成过程如下图。
python自然语言处理之lda
由该生成过程以及dirichlet-multinomial共轭可得:




从而得到了联合概率分布 。

似然性

根据上面的概率图模型,可以得出第i篇文档的complete-data似然性为:


第i篇文档第j个单词在词表中编号为w的概率为:

整个数据集的似然性为:

那么接下来怎么办呢?我们回忆一下在PLSA中是怎么做的。
PLSA中的概率图模型由于没有先验,模型比LDA简单一些,认为文档决定topic,topic决定单词,写出了整个数据集的对数似然性,然后由于要求解的参数以求和的形式出现在了对数函数中,无法通过直接求导使用梯度下降或牛顿法来使得这个对数似然最大,因此使用了EM算法。
LDA同样可以使用EM算法求解参数,但需要在E步计算隐变量的后验概率时使用变分推断进行近似,一种更简单的方法是使用gibbs sampling。

gibbs sampling

根据生成过程已经得到了联合概率分布 。由于每个文档中的单词  是可以观测到的已知数据,我们需要采样的分布是 。
下面推导gibbs sampling采样  分布的公式。

下面解释一下这个推导过程,前两个等号是利用了贝叶斯公式,第三步的正比于符号这一行是去掉了一项常数 ,第四步利用了前面计算好的  联合概率公式,第五、六步是将函数展开并化简,第七步的正比于符号是去掉了一项不依赖于z的项(即z取何值该项都相同),最后一行给出了对应代码中的表达。
这样我们就得到了吉布斯采样公式,每一轮gibbs sampling的迭代中依次遍历每个二维下标 , 即遍历每篇文档的每一个单词,重新采样这个下标对应的topic编号。

LDA的实现

用python编写了一个使用gibbs sampling实现的LDA。数据集放在文件dataset.txt中,一行表示一篇文档,只需使用原始数据即可,可处理中文和英文。
采用了jieba分词工具,停止词使用英文停止词、中文停止词、标点符号以及所有日文字符。
下面给出代码。

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
# 预处理(分词,去停用词,为每个word赋予一个编号,文档使用word编号的列表表示)
def preprocessing(filename):
    # 读取停止词文件
    file = codecs.open('stopwords.dic','r','utf-8')
    stopwords = [line.strip() for line in file] 
    file.close()
    
    # 读数据集
    file = codecs.open('dataset.txt','r','utf-8')
    documents = [document.strip() for document in file] 
    file.close()
    
    word2id = {}
    id2word = {}
    docs = []
    currentDocument = []
    currentWordId = 0
    
    for document in documents:
        # 分词
        segList = jieba.cut(document)
        for word in segList: 
            word = word.lower().strip()
            # 单词长度大于1并且不包含数字并且不是停止词
            if len(word) > 1 and not re.search('[0-9]', word) and word not in stopwords:
                if word in word2id:
                    currentDocument.append(word2id[word])
                else:
                    currentDocument.append(currentWordId)
                    word2id[word] = currentWordId
                    id2word[currentWordId] = word
                    currentWordId += 1
        docs.append(currentDocument);
        currentDocument = []
    return docs, word2id, id2word
    
# 初始化,按照每个topic概率都相等的multinomial分布采样,等价于取随机数,并更新采样出的topic的相关计数
def randomInitialize():
	for d, doc in enumerate(docs):
		zCurrentDoc = []
		for w in doc:
			pz = np.divide(np.multiply(ndz[d, :], nzw[:, w]), nz)
			z = np.random.multinomial(1, pz / pz.sum()).argmax()
			zCurrentDoc.append(z)
			ndz[d, z] += 1
			nzw[z, w] += 1
			nz[z] += 1
		Z.append(zCurrentDoc)

# gibbs采样
def gibbsSampling():
	# 为每个文档中的每个单词重新采样topic
	for d, doc in enumerate(docs):
		for index, w in enumerate(doc):
			z = Z[d][index]
			# 将当前文档当前单词原topic相关计数减去1
			ndz[d, z] -= 1
			nzw[z, w] -= 1
			nz[z] -= 1
			# 重新计算当前文档当前单词属于每个topic的概率
			pz = np.divide(np.multiply(ndz[d, :], nzw[:, w]), nz)
			# 按照计算出的分布进行采样
			z = np.random.multinomial(1, pz / pz.sum()).argmax()
			Z[d][index] = z 
			# 将当前文档当前单词新采样的topic相关计数加上1
			ndz[d, z] += 1
			nzw[z, w] += 1
			nz[z] += 1

alpha = 5
beta = 0.1	
iterationNum = 50
K = 10
# 预处理
docs, word2id, id2word = preprocessing("data.txt")
Z = []
N = len(docs)
M = len(word2id)
# ndz[d,z]表示文档d中由topic z产生的单词计数加伪计数alpha
ndz = np.zeros([N, K]) + alpha
# nzw[z,w]表示topic z产生的单词w的计数加伪计数beta
nzw = np.zeros([K, M]) + beta
# nz[z]表示topic z产生的所有单词的总计数加伪计数
nz = np.zeros([K]) + M * beta
# 初始化
randomInitialize()
# gibbs sampling
for i in range(0, iterationNum):
	gibbsSampling()
# 产生每个topic的top10的词
topicwords = []
maxTopicWordsNum = 10
for z in range(0, K):
	ids = nzw[z, :].argsort()
	topicword = []
	for j in ids:
		topicword.insert(0, id2word[j])
	topicwords.append(topicword[0 : min(10, len(topicword))])

实验结果

实验部分,使用两个数据集进行测试,第一个数据集是PLSA实验中使用的第二个关于one piece的16个英文文档,来源于*,其中包含日文字符,因此停止词中加入了日语字符,第二个数据集是5000篇新浪社会新闻,是中文文档。
第一个英文数据集的部分截图如下。
python自然语言处理之lda
设置的参数为K=10,iterationNum=50。
实验结果如下图。给出了每个主题top10的词语。
python自然语言处理之lda
第二个中文数据集的部分截图如下。
python自然语言处理之lda
参数设置为K=10,iterationNum=50:
python自然语言处理之lda
主题0关于犯罪,主题8关于法院,主题2关于医疗,主题3关于交通事故,主题4关于学校,主题7是一些称谓,主题6关于社区。
参数设置为K=30,iterationNum=50:
python自然语言处理之lda
可以看到,当topic数设置大一些,得到的主题将变得更为具体一些。由于停止词表的选用问题,有些停止词没有被过滤掉,比如一种,一只等数量词,完善停止词表可以让结果更好。