隐狄利克雷分布(Latent Dirichlet Allocation 或 LDA)的 Python 实现
作者:Enes Gokce
原文:Topic Modeling with NLP on Amazon Reviews Application of Latent Dirichlet Allocation (LDA) with Python
译者:Ivy Lee
译文于 2020-07-06 始发自【biendata】公众号:《以隐含狄利克雷分布,实现对亚马逊评论的主题建模》
主题建模(Topic modeling)是另一种流行的文本分析技术,最终目标是在评论中找到中心思想,并发现隐藏的主题。语料库中的文档都包含一个或多个主题。
主题建模有很多技术,在这里,我将介绍 隐狄利克雷分布(Latent Dirichlet Allocation 或 LDA) 的结果,这是一种无监督分类方法。当文本量很大而又不知道从哪里开始时,可以应用主题建模,然后查看出现的组别。LDA 是专门为文本数据设计的。
要使用主题建模技术,你需要提供:
- 文档-术语矩阵(document-term matrix)
- 你想要算法提取的主题数
在主题建模的过程中,我会创建不同的模型进行比较,最后选择最有意义的主题模型。
如何使用隐狄利克雷分布(LDA)
本文不会介绍 LDA 的数学基础知识,主要是讨论的是如何解释 LDA 主题模型的结果。
LDA 主题建模过程会创建很多不同的主题组,作为研究人员,我们需要决定输出中的组数,但是我们并不知道最好的组数是多少。因此,我们需要尝试不同的组数,检验并比较主题模型,确定哪个主题模型更有意义、最有意义,在模型中具有最明显的区别,然后在所有主题组中选择最有意义的组(模型)。
必须指出的是,LDA 的性质是主观的。在选择最有意义的主题组时,不同的人可能会得出不同的结论。我们寻找的是最合理的主题组,不同背景、不同领域专业知识的人可能会做出不同的选择。
LDA 是一种无监督聚类方法。提到无监督聚类方法,就不得不提一下 K-Means 聚类——最著名的无监督聚类方法之一。K-Means 聚类在很多情况下都非常实用且有用,已应用于文本挖掘多年。与每个单词只能属于一个集群(硬聚类)的 K-Means 聚类相反,LDA 允许“模糊”成员资格(软聚类)。软聚类允许集群重叠,而在硬聚类中,集群是互斥的。也就是说,在 LDA 中,一个单词可以属于多个组,而在 K-Means 聚类中则不可能。在 LDA 中,这种折衷使查找单词之间的相似性更加容易。然而,这种软聚类会导致很难划分组别,因为同一个单词可以出现在不同的组中。后续分析中我们会体会到这种影响。
应用了主题建模技术之后,研究人员的工作就是解释结果,查看每个主题中单词的混合是否有意义。如果没有意义,则可以尝试修改主题的数量、文档-术语矩阵中的术语、模型参数,甚至尝试使用其他模型。
数据准备
本文使用的数据简介: 数据下载自 Kaggle,由 Stanford Network Analysis Project 上传。原始数据来自于 J. McAuley 和 J. Leskovec 所做的研究“从业余爱好者到鉴赏家:通过网络评论对用户专业知识的发展进行建模”(2013)。该数据集由亚马逊网站的美食评论构成,包括 1999 年至 2012 年的全部 568,454 条评论。每条评论包括产品和用户信息,评分以及纯文本评价。
在本次研究中,我将重点关注对亚马逊的“好评”。我对“好评”的定义是:具有 4 星或 5 星(最高 5 星)的评论。换句话说,如果某条评论为 4 星或 5 星,就属于本次研究中的“好评”;1 2 3 星则被标记为“差评”。
数据准备至关重要,如果准备数据出现失误,就无法实施主题建模。不过在本次研究中,我们不会深入探讨如何准备数据,因为这不是本次研究的重点。但是,你需要做好心理准备,这一步如果出现问题,会花费一些时间。如果你在处理自己的数据集时,需要对应调整本文提供的代码,希望一切顺利。
先检查数据的列行数:
df.shape
(392384, 19) —— 数据集有 392,384 条评论。
对于很多家用计算机(以及 Google Colab)来说,这样的数据量都是难以处理的。因此,我会只使用其中 10,000 条评论。非常遗憾,如果我们无法使用超级计算机,就无法使用所有评论数据。
# 从数据集中获取前 10000 条好评
df_good_reviews= df.loc[df.Good_reviews ==1][0:10000]
下一步是计数向量化器(Count Vectorizer):
# 创建文档-术语矩阵
from sklearn.feature_extraction import text
from sklearn.feature_extraction.text import CountVectorizer
cv = CountVectorizer()
data_cv = cv.fit_transform(df_good_reviews.Text)
data_cv.shape
Pickle 存储数据,为后续创建文档-术语矩阵做准备。
# Pickle it
import pickle
pickle.dump(cv, open("cv_stop.pkl", "wb"))
data_stop.to_pickle("dtm_stop.pkl") #saving the pickled data
data = pd.read_pickle('dtm_stop.pkl')
data
Pickle 数据后,我们得到一个宽格式的词典:
现在到了创建术语-文档矩阵的时候了,该术语-文档矩阵数据将用于生成主题模型,所以对此次研究至关重要。
# 所需的输入之一就是术语-文档矩阵
tdm = data.transpose()
tdm.head()
# 通过从 df -> 稀疏矩阵 -> Gensim 语料库,将术语-文档矩阵转换为新的 Gensim 格式
sparse_counts = scipy.sparse.csr_matrix(tdm)
corpus = matutils.Sparse2Corpus(sparse_counts)
# Gensim 还需要一个字典,表示所有术语及其在术语-文档矩阵中的对应位置
cv = pickle.load(open("cv_stop.pkl", "rb"))
id2word = dict((v, k) for k, v in cv.vocabulary_.items())
现在开始创建主题模型!
使用 LDA 构建主题模型
构建主题模型的方法有很多。根据特定条件筛选文本,可以获得不同的主题组。在本次研究中,我们会使用以下内容创建主题模型:
- 所有文本
- 仅包含文本中的所有名词
- 文本中的所有名词和形容词
主题建模 - 尝试 1(所有文本)
首先,先尝试使用所有评论数据,在此过程中不进行文本筛选。
lda = models.LdaModel(corpus=corpus, id2word=id2word, num_topics=2, passes=10)
lda.print_topics()
发现了两组主题:
[(0, ‘0.020”coffee” + 0.011”like” + 0.010”tea” + 0.010”flavor” + 0.009”good” + 0.009”taste” + 0.007”one” + 0.007”great” + 0.006”use” + 0.006”cup”’),
(1, ‘0.010”great” + 0.009”like” + 0.009”good” + 0.007”love” + 0.007”food” + 0.007”one” + 0.007”product” + 0.005”taste” + 0.005”get” + 0.005”amazon”’)]
如何处理这一结果? 此时,我们可以检查这两个主题组,确定它们是否是具有差异性的组。我们不是预期一个组中的所有单词都必须相关,而是查看主题组的整体趋势。具体到以上两组,很难看到明显的差异。这是我的个人看法,如果你认为它们确实是两个不同的组,并且能够证明其合理性,也可以放心使用这一结果。
注意,之前提到过 LDA 是一种软聚类模型。在这里,我们看到“like”一词包含在两组中,这是正常现象,因为在软聚类模型中,一个单词可以同时出现在不同的组。
不需要强迫自己努力使上述模型有意义,我们可以继续,创建更多的主题模型。
与创建 2 组主题模型的操作类似,创建 3 组和 4 组。
# LDA for num_topics = 3
lda = models.LdaModel(corpus=corpus, id2word=id2word, num_topics=3, passes=10)
lda.print_topics()
# LDA for num_topics = 4
lda = models.LdaModel(corpus=corpus, id2word=id2word, num_topics=4, passes=10)
lda.print_topics()
所有结果可在表 1(下表)中看到。此时,我们应该检查对比,尝试找到最有意义的组。通过查看表 1,可以说“两组主题的模型”是最有意义的,第一组与饮料有关,第二组与反应有关。当然,你完全可以得出与我不同的结论!现在,这些结果先保留在这里,不会再进一步调整,因为我们会再创建 6 个主题模型。得到所有结果后,我可以更仔细地检查输出。在生成进一步的主题模型之后,我们会重新考虑表 1。
主题建模 - 尝试 2(仅名词): 在此步骤中,通过 LDA 方法仅使用名词创建主题。同样,我们的目的是在评论中找到隐藏的模式。现在,只需要使用不同的条件进行筛选。
与上一步类似,我们将运行带有 2、3、4 个主题组的 LDA。
import nltk
from nltk.corpus import stopwords
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')
import string
from nltk import word_tokenize, pos_tag
创建一个从文本字符串中提取名词的函数:
from nltk import word_tokenize, pos_tag
def nouns(text):
'''给定一个文本字符串,对其进行分词,只提取出其中的名词'''
is_noun = lambda pos: pos[:2] == 'NN'
tokenized = word_tokenize(text)
all_nouns = [word for (word, pos) in pos_tag(tokenized) if is_noun(pos)]
return ' '.join(all_nouns)
# 如果此代码不能运行,可能是由于页面格式导致的缩进错误。
将定义好的 nouns 函数应用于评论文本数据:
data_nouns = pd.DataFrame(df_good_reviews.Text.apply(nouns))
data_nouns
仅使用名词创建一个新的文档-术语矩阵:
from sklearn.feature_extraction import text
from sklearn.feature_extraction.text import CountVectorizer
# 重新添加其他停用词,因为我们正在重新创建文档-术语矩阵
add_stop_words = ['like', 'im', 'know', 'just', 'dont', 'thats', 'right', 'people','youre', 'got', 'gonna', 'time', 'think', 'yeah', 'said']
stop_words = text.ENGLISH_STOP_WORDS.union(add_stop_words)
# 重新创建仅包含名词的文档-术语矩阵
cvn = CountVectorizer(stop_words=stop_words)
data_cvn = cvn.fit_transform(data_nouns.Text)
data_dtmn = pd.DataFrame(data_cvn.toarray(), columns=cvn.get_feature_names())
data_dtmn.index = data_nouns.index
data_dtmn
创建 Gensim 语料库:
corpusn=matutils.Sparse2Corpus(scipy.sparse.csr_matrix(data_dtmn.transpose()))
# 创建词汇字典
id2wordn = dict((v, k) for k, v in cvn.vocabulary_.items())
⚫ 从 2 组主题开始
ldan = models.LdaModel(corpus=corpusn, num_topics=2, id2word=id2wordn, passes=10)
ldan.print_topics()
⚫ 3 组主题的 LDA
ldan = models.LdaModel(corpus=corpusn, num_topics=3, id2word=id2wordn, passes=10)
ldan.print_topics()
⚫ 4 组主题的 LDA
ldan = models.LdaModel(corpus=corpusn, num_topics=4, id2word=id2wordn, passes=10)
ldan.print_topics()
表 2 是仅使用名词尝试的 LDA 主题模型输出。现在,我们需要再次检查主题,尝试找到具有不同主题组的模型。同样,仍然不需要花费很多时间,因为我们还会生成更多的主题模型。
对我来说,其中具有三个小组的主题模型很有意义。它包含以下组:
- 宠物食品
- 饼干和零食
- 饮品
当然,得出不同的结论也是完全正常的。
主题建模-尝试 3(名词和形容词): 在此步骤中,仅使用名词和形容词通过 LDA 方法创建主题模型。
准备数据:
from nltk.corpus import stopwords
import nltk
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')
import string
from nltk import word_tokenize, pos_tag
自定义获取名词或形容词的函数:
def nouns_adj(text):
'''给定一个文本字符串,对其进行分词,获取其中的名词和形容词'''
is_noun_adj = lambda pos: pos[:2] == 'NN' or pos[:2] == 'JJ'
tokenized = word_tokenize(text)
nouns_adj = [word for (word, pos) in pos_tag(tokenized) if is_noun_adj(pos)]
return ' '.join(nouns_adj)
# 如果此代码不能运行,可能是由于页面格式导致的缩进错误。
对评论数据使用 nouns_adj 进行筛选:
data_nouns_adj = pd.DataFrame(df_good_reviews.Text.apply(nouns_adj))
data_nouns_adj
如你所见,这里仅包含名词和形容词。现在,我们要求 LDA 使用筛选后的这一个数据集版本创建主题模型。
为 LDA 准备数据:
# 仅使用名词和形容词创建一个新的文档-术语矩阵,同时使用 max_df 删除常见单词
cvna = CountVectorizer(max_df=.8) # max_df 用于删除出现频率过高的数据值,也称为“特定语料库的停用词(corpus-specific stop words)”
# 例如,max_df=.8 表示会忽略文档中出现频率大于 80% 的词。
data_cvna = cvna.fit_transform(data_nouns_adj.Text)
data_dtmna = pd.DataFrame(data_cvna.toarray(), columns=cvna.get_feature_names())
data_dtmna.index = data_nouns_adj.index
data_dtmna
创建 Gensim 语料库:
corpusna=matutils.Sparse2Corpus(scipy.sparse.csr_matrix(data_dtmna.transpose()))
# 创建词汇字典
id2wordna = dict((v, k) for k, v in cvna.vocabulary_.items())
现在可以运行 LDA 了。
⚫ 从 2 组主题开始
ldana = models.LdaModel(corpus=corpusna, num_topics=2, id2word=id2wordna, passes=10)
ldana.print_topics()
⚫ 尝试 3 组主题
ldana = models.LdaModel(corpus=corpusna, num_topics=3, id2word=id2wordna, passes=10)
ldana.print_topics()
⚫ 尝试 4 组主题
ldana = models.LdaModel(corpus=corpusna, num_topics=4, id2word=id2wordna, passes=10)
ldana.print_topics()
表 3 是仅使用名词和形容词的 LDA 主题模型输出。我们再次检查主题,确定是否具有有意义的主题组。
评估结果
现在到了最后阶段,表 1、表 2 和表 3 的结果必须一起评估。我们一共创建了 9 个主题模型,问一下自己:“哪一组更有意义?”现在,需要集中精力仔细检查各个主题。哪一组有意义?如果都没有意义,我们需要返回到数据清理步骤,更改参数或使用其他筛选条件和模型,这是一个递归过程。
在 9 个主题模型中,对我来说最有意义的是:仅名词具有 3 个主题组的模型(表 4)。我在这里看到三个不同的类别:(1)宠物食品、(2)饼干和零食、(3)饮品。同样,找到其他更有意义的主题模型是完全可以的。
记住一点,该数据集仅包含食品评论。因此,看到这些组都与食品有关是很正常的。
找出最有意义的主题模型后,通过更多迭代,获得微调模型。3 组主题(仅名词)对我来说最有意义,所以我将微调这一主题模型,把迭代数从 10 调整到 80。
# Fine-tuned LDA with topics = 3
ldan = models.LdaModel(corpus=corpusn, num_topics=3, id2word=id2wordn, passes=80)
ldan.print_topics()
在上表中,我们看到与表 4 类似的组,只是顺序不同。通过以上使用 LDA 方法进行主题分析的所有步骤,可以得出结论,亚马逊评论的好评可分为三个主要主题:(1)饮品、(2)宠物食品、(3)饼干和零食。
最后需要注意的是,要了解哪个主题组更有意义,我们可能需要对应领域的专业知识。作为一名研究人员,有责任证明我们对主题组的选择是正确的。只要理由充足,我们可以将任何主题组确定为最终主题模型。
感谢阅读!分析愉快!
特别感谢我的朋友 Bibor Szabo 在撰写本文时提供的宝贵建议。
提示: 此次研究所使用的 Python 代码,可以在我的 GitHub 文件夹 中查找。另外,这次的主题模型研究是另一个更大项目的一部分。如果你对前面的步骤感兴趣,可以查看我之前的文章:数据清洗 和 情绪分析。