python自然语言处理之lda

时间:2022-01-19 06:39:07

自然语言处理之LDA

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

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

符号

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

公式中的符号 描述 代码中的符号 描述
N N 文档总数 N  
Ni Ni 第i篇文档单词数    
K K topic总数 K  
M M 词表长度 M  
d d 文档编号,是一个整数 d  
z z topic编号,是一个整数 z  
w w 单词编号,是一个整数 w  
α⃗  α→ document-topic分布的超参数,是一个K维向量 alpha 是一个标量, α⃗  α→的每一维都是alpha
β⃗  β→ topic-word分布的超参数,是一个M维向量 beta 是一个标量, β⃗  β→的每一维都是beta
θi θi→ p(z|d=i) p(z|d=i)组成的K维向量,其中 z[1,K] z∈[1,K]    
Θ Θ Θ={θi}Ni=1 Θ={θi→}i=1N    
φk φk→ p(w|z=k) p(w|z=k)组成的M维向量,其中 w[1,M] w∈[1,M]    
Φ Φ Φ={φk}Kk=1 Φ={φk→}k=1K    
zi,j zi,j 第i篇文档的第j个单词的topic编号 Z[i,j] Z是一个元素为list的list
wi,j wi,j 第i篇文档的第j个单词在词表中的编号 docs[i,j] docs是一个元素为list的list
l l 二维下标(i,j),对应第i篇文档的第j个单词    
l ⌝l 下标包含这一项表示将第i篇文档的第j个单词排除在外    
niz niz 第i篇文档中由z这个topic产生的单词计数 ndz[i,z] ndz是一个二维矩阵,ndz[i,z]等于 niz niz加上伪计数alpha
ni ni 第i篇文档由每个topic产生的单词计数组成的向量    
nzw nzw 第z个topic产生单词w的计数 nzw[z,w] nzw是一个二维矩阵,nzw[z,w]等于 nzw nzw加上伪计数beta
nz nz 第z个topic产生全部单词的计数 nz[z] nz是一个一维矩阵,nz[z]等于 nz nz加上M倍的beta

概率图模型

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

生成过程

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

p(z⃗ |α⃗ )=i=1NΔ(ni+α⃗ )Δ(α⃗ ) p(z→|α→)=∏i=1NΔ(ni→+α→)Δ(α→)

p(w⃗ |z⃗ ,β⃗ )=z=1KΔ(nz+β⃗ )Δ(β⃗ ) p(w→|z→,β→)=∏z=1KΔ(nz→+β→)Δ(β→)

p(w⃗ ,z⃗ |α⃗ ,β⃗ )=p(w⃗ |z⃗ ,β⃗ )p(z⃗ |α⃗ )=z=1KΔ(nz+β⃗ )Δ(β⃗ )i=1NΔ(ni+α⃗ )Δ(α⃗ ) p(w→,z→|α→,β→)=p(w→|z→,β→)p(z→|α→)=∏z=1KΔ(nz→+β→)Δ(β→)∏i=1NΔ(ni→+α→)Δ(α→)

从而得到了联合概率分布  p(w⃗ ,z⃗ ) p(w→,z→)

似然性

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

p(wi,zi,θi,Φ|α⃗ ,β⃗ )=j=1Nip(wi,j|φzi,j)p(zi,j|θi)p(θi|α⃗ )p(Φ|β⃗ ) p(wi→,zi→,θi→,Φ|α→,β→)=∏j=1Nip(wi,j|φzi,j→)p(zi,j|θi→)⋅p(θi→|α→)⋅p(Φ|β→)

第i篇文档第j个单词在词表中编号为w的概率为:
p(wi,j=w|θi,Φ)=k=1Kp(wi,j=w|φk)p(zi,j=k|θi) p(wi,j=w|θi→,Φ)=∑k=1Kp(wi,j=w|φk→)p(zi,j=k|θi→)

整个数据集的似然性为:
p(|Θ,Φ)=i=1Np(wi|θi,Φ)=i=1Nj=1Nip(wi,j|θi,Φ) p(D|Θ,Φ)=∏i=1Np(wi→|θi→,Φ)=∏i=1N∏j=1Nip(wi,j|θi→,Φ)

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

gibbs sampling

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

p(zl=z|zl,w⃗ )=p(w⃗ ,z⃗ )p(w⃗ ,zl)=p(w⃗ |z⃗ )p(z⃗ )p(wl|zl)p(wl)p(zl)p(w⃗ |z⃗ )p(z⃗ )p(wl|zl)p(zl)=Δ(ni+α⃗ )Δ(ni,l+α⃗ )Δ(nz+β⃗ )Δ(nz,l+β⃗ )=Γ(niz+αz)Γ(Kz=1(niz,l+αz))Γ(niz,l+αz)Γ(Kz=1(niz+αz))Γ(nzw+βw)Γ(Mw=1(nzw,l+βw))Γ(nzw,l+βw)Γ(Mw=1(nzw+βw))=niz,l+αzKz=1(niz,l+αz)nzw,l+βwMw=1(nzw,l+βw)nzw,l+βwMw=1(nzw,l+βw)(niz,l+αz)=nzw[z,w]nz[z](ndz[i,z]) p(zl=z|z⌝l→,w→)=p(w→,z→)p(w→,z⌝l→)=p(w→|z→)p(z→)p(w⌝l→|z⌝l→)p(wl)p(z⌝l→)∝p(w→|z→)p(z→)p(w⌝l→|z⌝l→)p(z⌝l→)=Δ(ni→+α→)Δ(ni,⌝l→+α→)⋅Δ(nz→+β→)Δ(nz,⌝l→+β→)=Γ(niz+αz)Γ(∑z=1K(niz,⌝l+αz))Γ(niz,⌝l+αz)Γ(∑z=1K(niz+αz))⋅Γ(nzw+βw)Γ(∑w=1M(nzw,⌝l+βw))Γ(nzw,⌝l+βw)Γ(∑w=1M(nzw+βw))=niz,⌝l+αz∑z=1K(niz,⌝l+αz)⋅nzw,⌝l+βw∑w=1M(nzw,⌝l+βw)∝nzw,⌝l+βw∑w=1M(nzw,⌝l+βw)⋅(niz,⌝l+αz)=nzw[z,w]nz[z]⋅(ndz[i,z])

下面解释一下这个推导过程,前两个等号是利用了贝叶斯公式,第三步的正比于符号这一行是去掉了一项常数  p(wl) p(wl),第四步利用了前面计算好的  p(w⃗ ,z⃗ ) p(w→,z→) 联合概率公式,第五、六步是将函数展开并化简,第七步的正比于符号是去掉了一项不依赖于z的项(即z取何值该项都相同),最后一行给出了对应代码中的表达。
这样我们就得到了吉布斯采样公式,每一轮gibbs sampling的迭代中依次遍历每个二维下标  l l, 即遍历每篇文档的每一个单词,重新采样这个下标对应的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数设置大一些,得到的主题将变得更为具体一些。由于停止词表的选用问题,有些停止词没有被过滤掉,比如一种,一只等数量词,完善停止词表可以让结果更好。