自然语言处理从小白到大白系列(1)Word Embedding之主题模型

一直想开启一个专题来整理一下NLP的相关内容,总算克服懒癌着手开始干了。如果同学有缘看到这篇,恭喜你,这是本系列(自然语言处理从小白到大白系列)的第一篇,后续会不断更新,欢迎关注!

有些同学一提到Word Embedding,马上就想到的是word2vec,glove,fasttext,cBOW,skip-gram,然而却常常忽略最基本的文本嵌入方法,如本文提到的主题模型,包括pLSA,LDA, 还有后面会提到的 MF(matrix factorization), NMF以及sparse coding等传统的方法。

讲到主题模型,大家普遍觉得是LDA,但是其实前面还有一个模型pLSA,即概率隐语义分析,然而再往前面追溯,就有一个叫潜在语义分析的模型(LSA)。很多同学试图搞懂主题模型,然而经常都是懵逼地来,懵逼地去。因此,我们先给一个比较性感,错了,感性的概念,什么是潜在语义分析?为什么要潜在语义分析?如果我们有一堆文本,每个文本有对应的单词,那么组成一个单词-文本向量矩阵,单词张成的空间表示能够衡量两个文本之间的相似度。然后你会问,这不就行了吗?但是我们无法知道这些文章到底在写什么内容,因此想要知道文本的潜在话题,怎么办?提供两个思路,一:是对单词-文本矩阵进行奇异值分解,以分解后的第一个矩阵作为话题向量空间,第二三个矩阵的乘积,作为文本在话题中的表示;二:通过非负矩阵分解,也可以得到话题向量空间和文本在话题中的表示。总而言之一句话,矩阵分解可以挖掘潜在的话题,由于本文的重点不在这里,不过多讨论这个,只要知道,LDA的前世今生大致来源就可以了。不想看原理的同学也可以直接移步到代码区实践一波https://github.com/wujie0001/NLP-learning

下面开始讲讲硬的东西,如果没看懂没关系,可能我讲得不清晰,有必要的话后面可以再更新。

我们知道,当我们要写作的时候,是怎么样写成一篇文章的?我们肯定要先确定我们要写作的主题,然后我们写的词,则都是和这个主题相关的(可以看成是从主题中以一定的概率挑选词出来)。

1.pLSA

【这里规定一下符号:w是词,K是主题数量,k是某个主题,d是某篇文档,M是文档总数,对某篇文档,N是这个文档的词数。以上这些符号一定要牢记,不然后面公式看不懂】

有个看起来不像贝叶斯网络的贝叶斯网络图应运而生:(也就是我们刚才说的一个词是如何产生的)

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第1张图片

公式来了:

这是某篇文档的某个词的概率,我们得把所有词都考虑到,因此整个语料库的生成概率可以用以下似然函数表示:

其中,p(dm, wn)是在第dm篇文档中,第wn个单词出现的概率,和上文的p(w|d)一致,c(dm,wn)是在第dm篇文档中,wn出现的次数。然后呢,我们看着连乘的形式就很不爽,要加以对数,于是得到了:

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第2张图片

现在这个式子,我们来看看,c(dm,wn)是可以数出来的,我们要知道的是dm这篇文章,各个主题的分布是怎么样的即:p(zk|dm),以及第k个主题上,词的分布是怎样的:p(wn|zk)。这样一个似然函数,很容易想到极大似然法,但是遗憾的是,zk是隐变量,那么这里可以用EM算法迭代得到结果。

好了,pLSA就说到这里,重点讲LDA。

2.LDA

下面开始讲LDA,LDA不一样在哪里呢,就是假设了主题分布和某主题的词分布服从了先验狄利克雷分布,这是什么意思呢:在pLSA中,我们认为主题分布p(zk|dm)和词分布 p(wn|zk)就是一个确定的值,我们的目的就是估计出来这个确定的值;而在LDA中,我们假设这两个参数,是不确定的,是服从一定分布的一个变化的东西,这个先验的分布,就是狄利克雷分布,【这里为什么是狄利克雷分布,是由于狄利克雷分布是多项分布的共轭先验分布,后验分布依然是服从狄利克雷分布,简单理解就是在后续要对这个分布更新的时候,计算起来很方便】

LDA也有一个不像贝叶斯网络的贝叶斯网络:

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第3张图片

【注意,这里的方框的意思是一对多,上文同】

【符号说明:theta是主题分布,z是某主题,alpha是主题分布的先验参数,ϕ是词分布,beta是词分布的先验参数,M是文档数,N是每个文档的词数,K是主题数】

这里,解释一下这个贝叶斯网络:

根据先验参数alpha,确定一个主题分布theta,再从这个多项式分布中采样一个主题Zij;

根据参数beta的狄利克雷分布中采样一个词分布【这个词分布是主题Zij对应的词分布Phi_ij】

然后再由词分布phi_ij抽样生成词wij.

(这里普遍的疑惑可能是:为什么主题分布Z没有指向词分布phi,不是说词分布是主题分布Zij得来的吗?我的理解是:因为对于主题Zij来说,Zij里面的词汇就那么多,phi和Zij有关系,仅仅是因为从超参数beta采样出来的某个多项分布,把他用在主题Zij上,这个意思,才是“”这句话要表达的,而不是说,主题Zij和词分布phi_ij有依赖关系,这个是没有关系的,词分布仅仅受超参数beta的影响!)

下面开始变态的公式推导:

然后,我们在LDA中,最最最重要的就是求到词和主题的联合分布:

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第4张图片

第一项因子:

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第5张图片

同样的道理第二项因子:

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第6张图片

注意,这里推出来这两项是很有用的。

别忘了我们的最终的目的:给定当前词,计算它属于某主题的概率是多少:p(zi=k|w)

这里有个重要的思想,就是当我要看当前词的主题时,我看看除当前词外周围所有词的主题。

的意思是,给定其他所有词的主题,当前词主题为k的概率是多少,这是很有意义的事情,因为如果给所有词初始化了一个主题,这个概率是可以算出来的!当然怎么算,这就很考手艺了。

大招来了。

干蒙了吧,脑袋嗡嗡的吧?我甚至已经不知道自己在说什么

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第7张图片

其中:

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第8张图片

上面这几个都是可以统计出来的,把所有的主题都可以计算一遍!归一化否已经不重要了。

现在推出了结论,而这个结论,只是某个词的主题!我们要求的是,这篇文档的主题分布,以及主题对应的词分布。

这里由于我们的是可以通过迭代计算出来的了,那么我们设给定第m个文档的主题,计算如下式子:

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第9张图片

解释一下,这个式子成立是由于在这个贝叶斯网络中的马尔科夫毯:

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第10张图片

给定了alpha和z,第m篇文档的主题theta_m怎么求?可以通过采样的方法,给定alpha对第m篇文档主题分布采样得到theta,再从theta对某个词采样一个主题z,对所有的词连乘。

然后式中是一个多点分布和狄利克雷分布相乘,依然是狄利克雷分布。

狄利克雷分布的期望很好求,直接用期望估计参数本身:

自然语言处理从小白到大白系列(1)Word Embedding之主题模型_第11张图片

这个式子:对词分布而言,第k个主题第t的词的概率如何计算呢?分子:超参数beta+第t个词在主题k中出现的次数,分母:beta+ 所有的词表中的词在主题k中出现的次数之和。

类似的,对主题分布而言,第m个文档的主题分布,(是文档m中出现主题k的次数+alpha)/(文档m中出现所有主题的次数之和+alpha)。

好了,结论怎么样?结论是不是很简单?结论是不是难以置信。

可能本文比较简洁,因为这里并没有补充关于多项分布,beta分布,狄利克雷分布,共轭先验,贝叶斯网络,马尔科夫毯等等等等的知识。如果要补充的话,文章显得繁琐且冗长,让人没有了阅读的欲望。这里假设各位看官已经懂了那些知识,然后本文的梳理,应该比较清楚了。

时间仓促,排版比较乱,这个富文本编辑器真是不好用,下次用Markdown来编辑,可能会好很多。

下面来一个实际的例子吧:

更详细的代码和数据放在了GitHub上面:https://github.com/wujie0001/NLP-learning

import numpy as np
import pandas as pd
import re
df = pd.read_csv("../input/HillaryEmails.csv")
# 原邮件数据中有很多Nan的值,直接扔了。
df = df[['Id','ExtractedBodyText']].dropna()

def clean_email_text(text):
    text = text.replace('\n'," ") #新行,我们是不需要的
    text = re.sub(r"-", " ", text) #把 "-" 的两个单词,分开。(比如:july-edu ==> july edu)
    text = re.sub(r"\d+/\d+/\d+", "", text) #日期,对主体模型没什么意义
    text = re.sub(r"[0-2]?[0-9]:[0-6][0-9]", "", text) #时间,没意义
    text = re.sub(r"[\w]+@[\.\w]+", "", text) #邮件地址,没意义
    text = re.sub(r"/[a-zA-Z]*[:\//\]*[A-Za-z0-9\-_]+\.+[A-Za-z0-9\.\/%&=\?\-_]+/i", "", text) #网址,没意义
    pure_text = ''
    # 以防还有其他特殊字符(数字)等等,我们直接把他们loop一遍,过滤掉
    for letter in text:
        # 只留下字母和空格
        if letter.isalpha() or letter==' ':
            pure_text += letter
    # 再把那些去除特殊字符后落单的单词,直接排除。
    # 我们就只剩下有意义的单词了。
    text = ' '.join(word for word in pure_text.split() if len(word)>1)
    return text

docs = df['ExtractedBodyText']
docs = docs.apply(lambda s: clean_email_text(s))  
doclist = docs.values

然后开始模型的构建:

from gensim import corpora, models, similarities
import gensim
stoplist = ['very', 'ourselves', 'am', 'doesn', 'through', 'me', 'against', 'up', 'just', 'her', 'ours', 
            'couldn', 'because', 'is', 'isn', 'it', 'only', 'in', 'such', 'too', 'mustn', 'under', 'their', 
            'if', 'to', 'my', 'himself', 'after', 'why', 'while', 'can', 'each', 'itself', 'his', 'all', 'once', 
            'herself', 'more', 'our', 'they', 'hasn', 'on', 'ma', 'them', 'its', 'where', 'did', 'll', 'you', 
            'didn', 'nor', 'as', 'now', 'before', 'those', 'yours', 'from', 'who', 'was', 'm', 'been', 'will', 
            'into', 'same', 'how', 'some', 'of', 'out', 'with', 's', 'being', 't', 'mightn', 'she', 'again', 'be', 
            'by', 'shan', 'have', 'yourselves', 'needn', 'and', 'are', 'o', 'these', 'further', 'most', 'yourself', 
            'having', 'aren', 'here', 'he', 'were', 'but', 'this', 'myself', 'own', 'we', 'so', 'i', 'does', 'both', 
            'when', 'between', 'd', 'had', 'the', 'y', 'has', 'down', 'off', 'than', 'haven', 'whom', 'wouldn', 
            'should', 've', 'over', 'themselves', 'few', 'then', 'hadn', 'what', 'until', 'won', 'no', 'about', 
            'any', 'that', 'for', 'shouldn', 'don', 'do', 'there', 'doing', 'an', 'or', 'ain', 'hers', 'wasn', 
            'weren', 'above', 'a', 'at', 'your', 'theirs', 'below', 'other', 'not', 're', 'him', 'during', 'which']

texts = [[word for word in doc.lower().split() if word not in stoplist] for doc in doclist]

dictionary = corpora.Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]

lda = gensim.models.ldamodel.LdaModel(corpus=corpus, id2word=dictionary, num_topics=20)

# 我们可以看到,第10号分类,其中最常出现的单词是
lda.print_topic(10, topn=5)
# 我们把所有的主题打印出来看看
lda.print_topics(num_topics=20, num_words=5)

接下来通过lda.get_document_topics(bow)或者lda.get_term_topics(word_id)两个方法,我们可以把新鲜的文本/单词,分类成20个主题中的一个。

*但是注意,我们这里的文本和单词,都必须得经过同样步骤的文本预处理+词袋化,也就是说,变成数字表示每个单词的形式。*

可以使用训练好的模型,判断每句话分别来自哪个topic。

以上就是关于LDA的全部内容,LDA的模型确实比较复杂,数学的公式就直接搞懵一大部分同学,本文也比较浅显,如果哪里不清楚的同学,欢迎留言,我会继续完善。

参考文献:

[1] 百面机器学习第五章149-153.

[2] 统计学习方法第二版,17,18章

[3] 统计自然语言处理第14章

[4] 邹博的七月在线视频及课件

你可能感兴趣的:(自然语言处理)