自然语言处理之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 | 是一个标量,
|
|
topic-word分布的超参数,是一个M维向量 | beta | 是一个标量,
|
|
|
||
|
|
||
|
|
||
|
|
||
|
第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]等于
|
|
第i篇文档由每个topic产生的单词计数组成的向量 | ||
|
第z个topic产生单词w的计数 | nzw[z,w] | nzw是一个二维矩阵,nzw[z,w]等于
|
|
第z个topic产生全部单词的计数 | nz[z] | nz是一个一维矩阵,nz[z]等于
|
概率图模型
LDA的概率图模型见下图。箭头定义了随机变量间的关系。每个方框定义了每个过程,方框右下角的变量是该过程的重复次数。
生成过程
LDA认为数据的生成过程如下图。
由该生成过程以及dirichlet-multinomial共轭可得:
从而得到了联合概率分布
似然性
根据上面的概率图模型,可以得出第i篇文档的complete-data似然性为:
第i篇文档第j个单词在词表中编号为w的概率为:
整个数据集的似然性为:
那么接下来怎么办呢?我们回忆一下在PLSA中是怎么做的。
PLSA中的概率图模型由于没有先验,模型比LDA简单一些,认为文档决定topic,topic决定单词,写出了整个数据集的对数似然性,然后由于要求解的参数以求和的形式出现在了对数函数中,无法通过直接求导使用梯度下降或牛顿法来使得这个对数似然最大,因此使用了EM算法。
LDA同样可以使用EM算法求解参数,但需要在E步计算隐变量的后验概率时使用变分推断进行近似,一种更简单的方法是使用gibbs sampling。
gibbs sampling
根据生成过程已经得到了联合概率分布
下面推导gibbs sampling采样
下面解释一下这个推导过程,前两个等号是利用了贝叶斯公式,第三步的正比于符号这一行是去掉了一项常数
这样我们就得到了吉布斯采样公式,每一轮gibbs sampling的迭代中依次遍历每个二维下标
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篇新浪社会新闻,是中文文档。
第一个英文数据集的部分截图如下。
设置的参数为K=10,iterationNum=50。
实验结果如下图。给出了每个主题top10的词语。
第二个中文数据集的部分截图如下。
参数设置为K=10,iterationNum=50:
主题0关于犯罪,主题8关于法院,主题2关于医疗,主题3关于交通事故,主题4关于学校,主题7是一些称谓,主题6关于社区。
参数设置为K=30,iterationNum=50:
可以看到,当topic数设置大一些,得到的主题将变得更为具体一些。由于停止词表的选用问题,有些停止词没有被过滤掉,比如一种,一只等数量词,完善停止词表可以让结果更好。