用 Python 实现 LDA
原文出处:Jordan Barber
- LDA 是什么
- LDA 演练
- 需要用到的包
- 导入文档
- 清洗文档
- 分词
- 移除停用词
- 词干提取
- 创建 document-term matrix
- 应用 LDA 模型
- 检查结果
- LDA 原理
- 完整代码
隐含狄利克雷分布(以下简写为 LDA)是一种主题模型,它基于一组文档中的词频生成主题。对于在给定的文档集中准确合理地找到主题的混合,LDA 是一种非常有效的方法。
这一部分,我会用一个高度简化过的文档集来演练生成一个 LDA 模型的过程。这并不是对 LDA 的全面讲解。这个演练的目的是为大家准备数据,以及用 LDA 模型得到相应输出的核心步骤提供指导。
需要用到的包
该演练当中使用的 Python 包有:
- NLTK, Python 的一个自然语言处理工具包。对于任何一种自然语言的处理都非常有用。
- 在 Mac/Unix 下使用 pip 安装:$ sudo pip install -U nltk.
- stop_words,一个包含停用词的 Python 包。
- 在 Mac/Unix 下使用 pip 安装:$ sudo pip install stop-words.
- gensim,包含我们要用到的 LDA 模型的一个主题模型包。
- 在 Mac/Unix 下使用 pip 安装:$ sudo pip install gensim.
导入文档
这是我们的文档用例:
doc_a = "Brocolli is good to eat. My brother likes to eat good brocolli, but not my mother."
doc_b = "My mother spends a lot of time driving my brother around to baseball practice."
doc_c = "Some health experts suggest that driving may cause increased tension and blood pressure."
doc_d = "I often feel pressure to perform well at school, but my mother never seems to drive my brother to do better."
doc_e = "Health professionals say that brocolli is good for your health."
# compile sample documents into a list
doc_set = [doc_a, doc_b, doc_c, doc_d, doc_e]
清洗文档
数据清洗对于生成一个有效的主题模型是极其极其重要的:俗话说,“输入的是垃圾,得到的一定也是垃圾”(Garbage in, garbarge out.)。下面的步骤就是自然语言处理的常见方法:
- 分词:将文档转化为其原子元素。
- 停用词处理:移除无意义的词。
- 词干提取:将同义词合并。
分词
分词即将一个文档分成其原子元素。在这个例子中,我们将其分为单词。分词有很多种方法,我们用的是 NLTK 的 tokenize.regexp 模块:
from nltk.tokenize import RegexpTokenizer
tokenizer = RegexpTokenizer(r'\w+')
上面的代码会匹配所有单字字符,直到其遇到像空格这样的非单字的字符。这是个很简单的方法,但是会出现一些问题,比如像“don't”这样的单词就会被分成两个词“don”和“t“。NLTK 提供了很多像 nltk.tokenize.simple 这样的预留的特定结构的词。对于特殊用例,最好还是用 regex 和不断的迭代来使你的文档精确地分词。
注意:这个例子是对单个文档调用 tokenize()。你需要创建一个 for 循环来遍历所有文档。用下面这个脚本做个示例。
raw = doc_a.lower()
tokens = tokenizer.tokenize(raw)
>>> print(tokens)
['brocolli', 'is', 'good', 'to', 'eat', 'my', 'brother', 'likes', 'to', 'eat', 'good', 'brocolli', 'but', 'not', 'my', 'mother']
文档 doc_a 现在就是一个词的列表了。
停用词
英语中的一些特定组成部分,比如“for”,“or”这样的连词,或是“the”这种词对主题模型毫无意义。这些词叫做停用词,需要从我们的单词列表中移除。
停用词的定义是非常灵活的,在不同种类的文档中对应的停用词的含义是不同的。比如,如果我们要为一系列音乐评论做主题模型,那像“The Who”(英国的一支摇滚乐队)这种词就会有点麻烦,因为“the”通常都会作为一个常见的停用词而被移除。你可以根据实际情况来创建自己的停用词列表,或者用其他的包。
在我们的例子中,我们使用 Pypi 的 stop_words 包,这是一个相对比较保守的列表。我们可以调用 get_stop_words() 来创建一个停用词列表:
from stop_words import get_stop_words
# create English stop words list
en_stop = get_stop_words('en')
现在,移除停用词只是一个循环遍历我们的单词的工作了,将每一个词都和 en_list 列表作比较。
# remove stop words from tokens
stopped_tokens = [i for i in tokens if not i in en_stop]
>>> print(stopped_tokens)
['brocolli', 'good', 'eat', 'brother', 'likes', 'eat', 'good', 'brocolli', 'mother']
词干提取
词干提取是 NLP 的另一个常见技术,它用于将相似的单词去除词缀得到词根。例如:“stemming”,“stemmer”,“stemmed”都有相似的意思;词干提取就是去除这些词的词缀而得到词根“stem”。这对主题模型来说很重要,否则如果将这些单词看做不同的实体,会降低他们在模型中的重要程度。
和停用词一样,词干提取也是非常灵活的,有些方法在特定的情形下可能会出问题。Porter stemming algorithm 是使用最广泛的方法。我们从 NLTK 中引入 Porter Stemmer 模块来实现这个算法:
from nltk.stem.porter import PorterStemmer
# Create p_stemmer of class PorterStemmer
p_stemmer = PorterStemmer()
注意,p_stemmer
要求所有单词的类型都是 str。p_stemmer 以词干的形式返回字符串参数。
# stem token
texts = [p_stemmer.stem(i) for i in stopped_tokens]
>>> print(stemmed_tokens)
['brocolli', 'good', 'eat', 'brother', 'like', 'eat', 'good', 'brocolli', 'mother']
构建 document-term matrix
(译注:document-term matrix 是一个描述文档词频的矩阵,每一行对应文档集中的一篇文档,每一列对应一个单词,这个矩阵可以根据实际情况,采用不同的统计方法来构建。)
清洗阶段的结果就是文本(texts),从单个的文档中整理出来的分好词,去除了停用词而且提取了词干的单词列表。假设我们已经循环遍历了所有文档,将每份文档都整理成为了文本。现在,文本就是一个(单词)列表的列表了,每个单词列表就代表一份原文档。
要生成一个 LDA model,我们需要知道每个词在文档中出现的频繁程度。为此我们需要用到一个叫 gensim 的包来构建 document-term matrix:
from gensim import corpora, models
dictionary = corpora.Dictionary(texts)
Dictionary() 方法遍历所有的文本,为每个不重复的单词分配一个单独的整数 ID,同时收集该单词出现次数以及相关的统计信息。试试用 print(dictionary.token2id) 来查看每个单词的id。
接下来,我们要将 dictionary 转化为一个词袋:
doc2bow() 方法将 dictionary 转化为一个词袋。得到的结果 corpus 是一个向量的列表,向量的个数就是文档数。在每个文档向量中都包含一系列元组。举个例子,print(corpus[0]) 结果如下:
>>> print(corpus[0])
[(0, 2), (1, 1), (2, 2), (3, 2), (4, 1), (5, 1)]
这个元组列表代表我们的第一个文档 doc_a。元组的形式是(单词 ID,词频)。所以如果 print(dictionary.roken2id) 显示 brocolli 的 id 是 0,那么第一个元组就代表 brocolli 这个词在 doc_a 里出现了两次。只有在文档中出现过的词才会包含在 doc2bow() 中,否则它将不会出现在文档向量之中。
应用 LDA 模型
corpus 是一个 document-term matrix,现在,我们已经为生成一个 LDA 模型做好准备了:
ldamodel = gensim.models.ldamodel.LdaModel(corpus, num_topics=3, id2word = dictionary, passes=20)
LdaModel 类的详细描述可以在 gensim 文档中查看。我们的实例中用到的参数:
参数:
- num_topics: 必须。LDA 模型要求用户决定应该生成多少个主题。由于我们的文档集很小,所以我们只生成三个主题。
- id2word:必须。LdaModel 类要求我们之前的 dictionary 把 id 都映射成为字符串。
- passes:可选。模型遍历语料库的次数。遍历的次数越多,模型越精确。但是对于非常大的语料库,遍历太多次会花费很长的时间。
检查结果
我们的 LDA 模型已经用 ldamodel 储存好了。我们可以用 print_topic 和 print_topics 方法来查看主题:
>>> print(ldamodel.print_topics(num_topics=3, num_words=3))
['0.141*health + 0.080*brocolli + 0.080*good', '0.060*eat + 0.060*drive + 0.060*brother', '0.059*pressur + 0.059*mother + 0.059*brother']
这是什么意思呢?每一个生成的主题都用逗号分隔开。每个主题当中有三个该主题当中最可能出现的单词。即使我们的文档集很小,这个模型依旧是很可靠的。还有一些需要我们考虑的问题:
- health, brocolli 和 good 在一起时有很好的含义。
- 第二个主题有点让人疑惑,如果我们重新查看源文档,可以看到 drive 有很多种含义:driving a car 意思是开车,driving oneself to improve 是激励自己进步。这是我们在结果中需要注意的地方。
- 第三个主题包含 mother 和 brother,这很合理。
调整模型的主题数和遍历次数对于得到一个好的结果是很重要的。两个主题看起来更适合我们的文档。
ldamodel = gensim.models.ldamodel.LdaModel(corpus, num_topics=2, id2word = dictionary, passes=20)
>>> print(ldamodel.print_topics(num_topics=2, num_words=4))
['0.054*pressur + 0.054*drive + 0.054*brother + 0.054*mother', '0.070*brocolli + 0.070*good + 0.070*health + 0.050*eat']
LDA 到底做了什么?
这个解释有点长,但是对于理解我们千辛万苦生成的模型非常有帮助。
LDA 假定文档是从主题的混合生成的。这些主题又是由一些单词的特定概率分布而生成的。就像我们演练的模型一样。换句话说,LDA 假定文档以以下步骤生成:
- 确定一个文档中的单词数。假设我们的文档有六个单词。
- 确定该文档由哪些主题混合而来,例如,这个文档包含 1/2 的“健康”(health)主题和 1/2 的“蔬菜”(vegetables)主题。
- 用每个主题的多项分布生成的单词来填充文档中的单词槽。在我们的例子中,“健康”主题占文档的 1/2,或者说占三个词。“健康”主题有“diet”这个词的可能性是 20%,或者有“execise" 这个词的概率是 15%,单词槽就是基于这些概率来填充的。
基于文档如何生成的假定,LDA 反其道而行之,并尝试找出最初哪些主题会创建这些文档。
完整代码
from nltk.tokenize import RegexpTokenizer
from stop_words import get_stop_words
from nltk.stem.porter import PorterStemmer
from gensim import corpora, models
import gensim
tokenizer = RegexpTokenizer(r'\w+')
# create English stop words list
en_stop = get_stop_words('en')
# Create p_stemmer of class PorterStemmer
p_stemmer = PorterStemmer()
# create sample documents
doc_a = "Brocolli is good to eat. My brother likes to eat good brocolli, but not my mother."
doc_b = "My mother spends a lot of time driving my brother around to baseball practice."
doc_c = "Some health experts suggest that driving may cause increased tension and blood pressure."
doc_d = "I often feel pressure to perform well at school, but my mother never seems to drive my brother to do better."
doc_e = "Health professionals say that brocolli is good for your health."
# compile sample documents into a list
doc_set = [doc_a, doc_b, doc_c, doc_d, doc_e]
# list for tokenized documents in loop
texts = []
# loop through document list
for i in doc_set:
# clean and tokenize document string
raw = i.lower()
tokens = tokenizer.tokenize(raw)
# remove stop words from tokens
stopped_tokens = [i for i in tokens if not i in en_stop]
# stem tokens
stemmed_tokens = [p_stemmer.stem(i) for i in stopped_tokens]
# add tokens to list
texts.append(stemmed_tokens)
# turn our tokenized documents into a id <-> term dictionary
dictionary = corpora.Dictionary(texts)
# convert tokenized documents into a document-term matrix
corpus = [dictionary.doc2bow(text) for text in texts]
# generate LDA model
ldamodel = gensim.models.ldamodel.LdaModel(corpus, num_topics=2, id2word = dictionary, passes=20)
关于 LDA 原理的更多分析,推荐两份资料《LDA 数学八卦》和《LDA 漫游指南》。
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341