LDA简介

LDA(Latent Dirichlet Allocation)是一种文档主题模型,包含词、主题和文档三层结构。

使用LDA进行文档主题建模-图1

LDA认为一篇文档由一些主题按照一定概率组成,一个主题又由一些词语按照一定概率组成。早期人们用词袋模型对一篇文章进行建模,把一篇文档表示为若干单词的计数。无论是中文还是英文,都由大量单词组成,这就造成词袋向量的维数巨大,少则几千多则上万,在使用分类模型进行训练时,非常容易造成训练缓慢以及过拟合。LDA本质上把词袋模型进行了降维,把一篇文档以主题的形式进行了表示。主题的个数通常为几百,这就把文档使用了维数为几百的向量进行了表示,大大加快了训练速度,并且相对不容易造成过拟合。从某种程度上来说,主题是对若干词语的抽象表示。

使用LDA进行文档主题建模-图3

以最近一部电视剧《南方有乔木》为例。假设一篇文章介绍了这部电视剧的主要内容。我们可以把这篇文章表示为:

  1. 0.30*"创业"+0.3*"三角恋"+0.2*"无人机"

然后我们可以把三角恋表示为:

  1. 0.4*"南乔"+0.3*"时樾"+0.3*"安宁"

需要指出的是,计算出文档、主题以及词语之间的表示关系,需要基于大量的文档,这样才具有普遍的意义。LDA正是提供了这种算法,自动从训练文档中计算出这种对应关系。

数据集

本文演示用的数据集,依然使用搜狗新闻数据集SogouCS。我们从SogouCS中提取正文内容,每个URL对应的正文当做一篇文档,并且使用jieba进行了分词。演示期间我们提取SogouCS的前10000条数据用于计算LDA。

  1. def load_sougou_content():
  2. with open("../data/news_sohusite_content_10000.txt") as F:
  3. content=F.readlines()
  4. F.close()
  5. return content

计算LDA时,需要删除停用词,加载我们之前保存的停用词。

  1. def load_stopwords():
  2. with open("stopwords.txt") as F:
  3. stopwords=F.readlines()
  4. F.close()
  5. return [word.strip() for word in stopwords]

计算主题

我们使用gensim提供的API进行LDA计算。首先从语料中提取字典,并用该字典把预料转换成词袋。

  1. # 得到文档-单词矩阵 (直接利用统计词频得到特征)
  2. dictionary = corpora.Dictionary(content)
  3. # 将dictionary转化为一个词袋,得到文档-单词矩阵
  4. texts = [dictionary.doc2bow(text) for text in content]

然后进行LDA计算,演示期间设置只计算5个主题,通常生产环境经验值为200。

  1. num_topics=5
  2. lda = models.ldamodel.LdaModel(corpus=texts, id2word=dictionary, num_topics=num_topics)

其中比较重要的几个参数含义如下:

  • corpus,计算LDA的语料
  • id2word,语料对应的字典
  • num_topics,计算的主题的数量

我们打印前5个主题。

  1. for index,topic in lda.print_topics(5):
  2. print topic

主题内容如下所示。

  1. 0.007*"月" + 0.006*"中" + 0.005*"年" + 0.005*"日" + 0.004*"公司" + 0.004*"时间" + 0.003*"北京" + 0.003*"比赛" + 0.002*"中国" + 0.002*"记者"
  2. 0.006*"月" + 0.006*"发展" + 0.005*"年" + 0.005*"日" + 0.005*"中" + 0.005*"中国" + 0.004*"工作" + 0.004*"说" + 0.003*"记者" + 0.003*"建设"
  3. 0.008*"月" + 0.007*"市场" + 0.006*"经济" + 0.005*"增长" + 0.004*"元" + 0.004*"中国" + 0.004*"企业" + 0.004*"产品" + 0.004*"年" + 0.004*"记者"
  4. 0.011*"日" + 0.011*"月" + 0.007*"记者" + 0.005*"时" + 0.005*"年" + 0.004*"公司" + 0.004*"说" + 0.004*"中" + 0.003*"发现" + 0.002*"亿元"
  5. 0.006*"o" + 0.006*"i" + 0.006*"月" + 0.005*"日" + 0.005*"e" + 0.005*"说" + 0.005*"n" + 0.005*"中" + 0.004*"中国" + 0.004*"a"

如果需要设置每个话题对应的关键字的个数,可以通过参数num_words设置,默认为10,这里的num_topics参数容易导致误解,它的含义是显示排名前几个话题,类似topN参数。

  1. print_topics(num_topics=20, num_words=10)

词袋处理后的结果,使用TFIDF算法处理后,可以进一步提升LDA的效果。

  1. # 利用tf-idf来做为特征进行处理
  2. texts_tf_idf = models.TfidfModel(texts)[texts]
  3. lda = models.ldamodel.LdaModel(corpus=texts_tf_idf, id2word=dictionary, num_topics=num_topics)

运行的效果如下所示。

  1. 0.001*"比赛" + 0.001*"联赛" + 0.001*"意甲" + 0.001*"主场" + 0.001*"轮" + 0.001*"赛季" + 0.001*"时间" + 0.001*"孩子" + 0.001*"北京" + 0.000*"航天员"
  2. 0.001*"叙利亚" + 0.001*"奥运会" + 0.001*"伦敦" + 0.000*"选手" + 0.000*"中国" + 0.000*"说" + 0.000*"男子" + 0.000*"米" + 0.000*"北京" + 0.000*"日"
  3. 0.000*"梅西" + 0.000*"林书豪" + 0.000*"鲁尼" + 0.000*"稀土" + 0.000*"钓鱼岛" + 0.000*"巴萨" + 0.000*"试用" + 0.000*"常规赛" + 0.000*"蛋黄派" + 0.000*"尼克斯"
  4. 0.002*"体育" + 0.001*"搜狐" + 0.001*"北京" + 0.001*"o" + 0.001*"时间" + 0.001*"n" + 0.001*"i" + 0.001*"日" + 0.001*"C" + 0.001*"e"
  5. 0.001*"市场" + 0.001*"经济" + 0.001*"增长" + 0.001*"投资" + 0.001*"亿元" + 0.001*"基金" + 0.001*"公司" + 0.001*"银行" + 0.001*"企业" + 0.001*"同比"

使用LDA提取文档特征

通常LDA的结果可以作为进一步文档分类、文档相似度计算以及文档聚类的依据,可以把LDA当做一种特征提取方法。

  1. #获取语料对应的LDA特征
  2. corpus_lda = lda[texts_tf_idf]
  3. #打印0号文档对应的LDA值
  4. print corpus_lda[0]

输出0号文档对应的LDA值如下,即把0号文档以5个话题形式表示。

  1. [(0, 0.019423252), (1, 0.019521076), (2, 0.92217809), (3, 0.01954053), (4, 0.019337002)]

这里需要解释的是,无论是词袋模型还是LDA生成的结果,都可能存在大量的0,这会占用大量的内存空间。因此默认情况下,词袋以及LDA计算的结果都以稀疏矩阵的形式保存。稀疏矩阵的最小单元定义为:

  1. (元素所在的位置,元素的值)

比如一个稀疏矩阵只有0号和2号元素不为0,分别为1和5,那么它的表示方法如下:

  1. [(0,1),(2,5)]

使用多核计算

LDA在生产环境中运行遇到的最大问题就是默认只能使用单核资源,运行速度过慢。gensim针对这一情况也提供了多核版本。

  1. lda = models.ldamulticore.LdaMulticore(corpus=texts_tf_idf, id2word=dictionary, num_topics=num_topics)

该版本默认情况默认使用cpu_count()-1 即使用几乎全部CPU,仅保留一个CPU不参与LDA计算,也可以通过参数workers指定使用的CPU个数,这里的CPU指的物理CPU,不是超线程的CPU数。一般认为:

  1. CPU总核数 = 物理CPU个数 * 每颗物理CPU的核数
  2. 总逻辑CPU = 物理CPU个数 * 每颗物理CPU的核数 * 超线程数

可以尝试使用如下命令查看服务器的CPU信息。

  1. # 查看CPU信息(型号)
  2. cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c
  3. 24 Intel(R) Xeon(R) CPU E5-2630 0 @ 2.30GHz
  4. # 查看物理CPU个数
  5. cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
  6. 2
  7. # 查看每个物理CPU中core的个数(即核数)
  8. cat /proc/cpuinfo| grep "cpu cores"| uniq
  9. cpu cores : 6
  10. # 查看逻辑CPU的个数
  11. cat /proc/cpuinfo| grep "processor"| wc -l
  12. 24

gensim官网上公布了一组测试数据,数据集为Wikipedia英文版数据集,该数据集有350万个文档,10万个特征,话题数设置为100的情况下运行LDA算法。硬件环境为一台拥有4个物理i7 CPU的服务器。使用单核接口需要使用3小时44分,当使用多核接口且使用3个物理CPU仅需要1小时6分钟。我们延续上面的例子,继续使用搜狗的数据集,为了要效果更加明显,我们使用SogouCS的前50000的数据进行LDA运算,话题数设置为100,并使用time.clock()获取当前时间,便于计算各个环节消耗的时间,比如计算LDA消耗的时间的方法如下所示。

  1. #获取当前时间
  2. start = time.clock()
  3. #workers指定使用的CPU个数 默认使用cpu_count()-1 即使用几乎全部CPU 仅保留一个CPU不参与LDA计算
  4. lda = models.ldamulticore.LdaMulticore(corpus=texts_tf_idf, id2word=dictionary, num_topics=num_topics)
  5. #计算耗时
  6. end = time.clock()
  7. print('[lda]Running time: %s Seconds' % (end - start))

分别计算使用1,2,4等不同CPU的情况,统计了预处理环节、LDA环节的耗时,其中预处理环节主要进行了词袋处理和TFIDF处理,测试用的服务器一共有12个物理CPU。

CPU数 预处理环节耗时(单位:秒) LDA环节耗时(单位:秒)
默认 24 169
1 25 437
2 24 296
4 25 215
6 23 189
8 25 186
10 23 171
12 23 160

测试结果表明,使用多CPU资源可以提升LDA的计算效率,尤其对于CPU使用个数较少时,几乎成线性下降,当使用CPU个数较多时,性能改善不明显。在本例中默认参数将使用11个CPU,因此默认参数下计算速率略低于使用12个CPU。

使用LDA进行文档主题建模-图4.png

在线学习

LDA可以进行在线学习,动态更新模型。

  1. lda = LdaMulticore(corpus, num_topics=10)
  2. lda.update(other_corpus)

参考文档