【译】对亚马逊评论使用 NLP 进行主题建模

隐狄利克雷分布(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 是专门为文本数据设计的。

要使用主题建模技术,你需要提供:

  1. 文档-术语矩阵(document-term matrix)
  2. 你想要算法提取的主题数

在主题建模的过程中,我会创建不同的模型进行比较,最后选择最有意义的主题模型。

如何使用隐狄利克雷分布(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
Count Vectorizer 之后的数据形状

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. 所有文本
  2. 仅包含文本中的所有名词
  3. 文本中的所有名词和形容词

主题建模 - 尝试 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。

表 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 主题模型输出。现在,我们需要再次检查主题,尝试找到具有不同主题组的模型。同样,仍然不需要花费很多时间,因为我们还会生成更多的主题模型。

表 2:仅包含名词的主题模型

对我来说,其中具有三个小组的主题模型很有意义。它包含以下组:

  1. 宠物食品
  2. 饼干和零食
  3. 饮品

当然,得出不同的结论也是完全正常的。

主题建模-尝试 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 主题模型输出。我们再次检查主题,确定是否具有有意义的主题组。

表 3:仅包含名词和形容词的主题模型

评估结果

现在到了最后阶段,表 1、表 2 和表 3 的结果必须一起评估。我们一共创建了 9 个主题模型,问一下自己:“哪一组更有意义?”现在,需要集中精力仔细检查各个主题。哪一组有意义?如果都没有意义,我们需要返回到数据清理步骤,更改参数或使用其他筛选条件和模型,这是一个递归过程。

在 9 个主题模型中,对我来说最有意义的是:仅名词具有 3 个主题组的模型(表 4)。我在这里看到三个不同的类别:(1)宠物食品、(2)饼干和零食、(3)饮品。同样,找到其他更有意义的主题模型是完全可以的。

表 4:最有意义的主题 — 仅名词的三组主题

记住一点,该数据集仅包含食品评论。因此,看到这些组都与食品有关是很正常的。

找出最有意义的主题模型后,通过更多迭代,获得微调模型。3 组主题(仅名词)对我来说最有意义,所以我将微调这一主题模型,把迭代数从 10 调整到 80。

# Fine-tuned LDA with topics = 3
ldan = models.LdaModel(corpus=corpusn, num_topics=3, id2word=id2wordn, passes=80)

ldan.print_topics()
表 5:微调参数后的最终主题模型

在上表中,我们看到与表 4 类似的组,只是顺序不同。通过以上使用 LDA 方法进行主题分析的所有步骤,可以得出结论,亚马逊评论的好评可分为三个主要主题:(1)饮品、(2)宠物食品、(3)饼干和零食。

最后需要注意的是,要了解哪个主题组更有意义,我们可能需要对应领域的专业知识。作为一名研究人员,有责任证明我们对主题组的选择是正确的。只要理由充足,我们可以将任何主题组确定为最终主题模型。

感谢阅读!分析愉快!

特别感谢我的朋友 Bibor Szabo 在撰写本文时提供的宝贵建议。


提示: 此次研究所使用的 Python 代码,可以在我的 GitHub 文件夹 中查找。另外,这次的主题模型研究是另一个更大项目的一部分。如果你对前面的步骤感兴趣,可以查看我之前的文章:数据清洗情绪分析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,561评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,218评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,162评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,470评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,550评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,806评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,951评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,712评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,166评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,510评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,643评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,306评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,930评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,745评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,983评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,351评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,509评论 2 348

推荐阅读更多精彩内容