文本主题抽取:用gensim训练LDA模型

释放双眼,带上耳机,听听看~!

得知李航老师的《统计学习方法》出了第二版,我第一时间就买了。看了这本书的目录,非常高兴,好家伙,居然把主题模型都写了,还有pagerank。一路看到了马尔科夫蒙特卡罗方法和LDA主题模型这里,被打击到了,满满都是数学公式。LDA是目前为止我见过最复杂的模型了。

找了培训班的视频看,对LDA模型有了大致的认识。下面总结一点东西。

1、LDA与PLSA的联系

LDA模型和PLSA的联系非常紧密,都是概率模型(LSA是非概率模型),是利用概率生成模型对文本集合进行主题分析的无监督学习方法。

不同在于,PLSA是用了频率学派的方法,用极大似然估计进行学习,而LDA是用了贝叶斯学派的方法,进行贝叶斯推断,所以LDA就是在pLSA的基础上加了⻉叶斯框架,即LDA就是pLSA的⻉叶斯版本 。

LDA和PLSA都假设存在两个多项分布:话题是单词的多项分布,文本是话题的多项分布。不同在于,LDA认为多项分布的参数也服从一个分布,而不是固定不变的,使用狄利克雷分布作为多项分布的先验分布,也就是多项分布的参数服从狄利克雷分布。

为啥引入先验分布呢?因为这样能防止过拟合。为啥选择狄利克雷分布呢作为先验分布呢?因为狄利克雷分布是多项分布的共轭先验分布,那么先验分布和后验分布的形式相同,便于由先验分布得到后验分布。

2、LDA的文本集合生成过程

首先由狄立克雷分布得到话题分布的参数的分布,然后随机生成一个文本的话题分布,之后在该文本的每个位置,依据该文本的话题分布随机生成一个话题;

然后由狄利克雷分布得到单词分布的参数的分布,再得到话题的单词分布,在该位置依据该话题的单词分布随机生成一个单词,直到文本的最后一个位置,生成整个文本;

最后重复以上过程,生成所有的文本。

下面是两个小案例,用gensim训练LDA模型,进行新闻文本主题抽取,还有一个是希拉里邮件的主题抽取。

github:https://github.com/DengYangyong/LDA_gensim

一、LDA新闻文本主题抽取

第一步:对新闻进行分词

这次使用的新闻文档中有5000条新闻,有10类新闻,[\'体育\', \'财经\', \'房产\', \'家居\', \'教育\', \'科技\', \'时尚\', \'时政\', \'游戏\', \'娱乐\'],每类有500条新闻。首先对文本进行清洗,去掉停用词、非汉字的特殊字符等。然后用jieba进行分词,将分词结果保存好。

#!/usr/bin/python
# -*- coding:utf-8 -*-

import jieba,os,re
from gensim import corpora, models, similarities

\"\"\"创建停用词列表\"\"\"
def stopwordslist():
    stopwords = [line.strip() for line in open(\'./stopwords.txt\',encoding=\'UTF-8\').readlines()]
    return stopwords

\"\"\"对句子进行中文分词\"\"\"
def seg_depart(sentence):
    sentence_depart = jieba.cut(sentence.strip())
    stopwords = stopwordslist()
    outstr = \'\'
    for word in sentence_depart:
        if word not in stopwords:
            outstr += word
            outstr += \" \"
    # outstr:\'黄蜂 湖人 首发 科比 带伤 战 保罗 加索尔 ...\'       
    return outstr

\"\"\"如果文档还没分词,就进行分词\"\"\"
if not os.path.exists(\'./cnews.train_jieba.txt\'):
    # 给出文档路径
    filename = \"./cnews.train.txt\"
    outfilename = \"./cnews.train_jieba.txt\"
    inputs = open(filename, \'r\', encoding=\'UTF-8\')
    outputs = open(outfilename, \'w\', encoding=\'UTF-8\')

    # 把非汉字的字符全部去掉
    for line in inputs:
        line = line.split(\'\\t\')[1]
        line = re.sub(r\'[^\\u4e00-\\u9fa5]+\',\'\',line)
        line_seg = seg_depart(line.strip())
        outputs.write(line_seg.strip() + \'\\n\')
    
    outputs.close()
    inputs.close()
    print(\"删除停用词和分词成功!!!\")

第二步:构建词频矩阵,训练LDA模型

gensim所需要的输入格式为:[\'黄蜂\', \'湖人\', \'首发\', \'科比\', \'带伤\', \'战\',...],也就是每篇文档是一个列表,元素为词语。

然后构建语料库,再利用语料库把每篇新闻进行数字化,corpus就是数字化后的结果。

第一条新闻ID化后的结果为corpus[0]:[(0, 1), (1, 1), (2, 1), (3, 1), (4, 1),...],每个元素是新闻中的每个词语的ID和频率。

最后训练LDA模型。LDA是一种无监督学习方法,我们可以自由选择主题的个数。这里我们做了弊,事先知道了新闻有10类,就选择10个主题吧。

LDA模型训练好之后,我们可以查看10个主题的单词分布。

第6个主题(从0开始计数)的单词分布如下。还行,从“拍摄、电影、柯达”这些词,可以大致看出是娱乐主题。

(5, \'0.007*\"中\" + 0.004*\"拍摄\" + 0.004*\"说\" + 0.003*\"英语\" + 0.002*\"时间\" + 0.002*\"柯达\" + 0.002*\"中国\" + 0.002*\"国泰\" + 0.002*\"市场\" + 0.002*\"电影\"\')

从第10个主题的单词分布也大致可以看出是财经主题。

(9, \'0.085*\"基金\" + 0.016*\"市场\" + 0.014*\"公司\" + 0.013*\"投资\" + 0.012*\"股票\" + 0.011*\"分红\" + 0.008*\"中\" + 0.007*\"一季度\" + 0.006*\"经理\" + 0.006*\"收益\"\')

但效果还是不太令人满意,因为其他的主题不太看得出来是什么。

\"\"\"准备好训练语料,整理成gensim需要的输入格式\"\"\"
fr = open(\'./cnews.train_jieba.txt\', \'r\',encoding=\'utf-8\')
train = []
for line in fr.readlines():
    line = [word.strip() for word in line.split(\' \')]
    train.append(line)
    # train: [[\'黄蜂\', \'湖人\', \'首发\', \'科比\', \'带伤\', \'战\',...],[...],...]
    
\"\"\"构建词频矩阵,训练LDA模型\"\"\"
dictionary = corpora.Dictionary(train)
# corpus[0]: [(0, 1), (1, 1), (2, 1), (3, 1), (4, 1),...]
# corpus是把每条新闻ID化后的结果,每个元素是新闻中的每个词语,在字典中的ID和频率
corpus = [dictionary.doc2bow(text) for text in train]

lda = models.LdaModel(corpus=corpus, id2word=dictionary, num_topics=10)
topic_list = lda.print_topics(10)
print(\"10个主题的单词分布为:\\n\")
for topic in topic_list:
    print(topic)
10个主题的单词分布为:

(0, \'0.008*\"中\" + 0.005*\"市场\" + 0.004*\"中国\" + 0.004*\"货币\" + 0.004*\"托管\" + 0.003*\"新\" + 0.003*\"债券\" + 0.003*\"说\" + 0.003*\"公司\" + 0.003*\"做\"\')
(1, \'0.081*\"基金\" + 0.013*\"公司\" + 0.011*\"投资\" + 0.008*\"行业\" + 0.007*\"中国\" + 0.007*\"市场\" + 0.007*\"中\" + 0.007*\"亿元\" + 0.006*\"规模\" + 0.005*\"新\"\')
(2, \'0.013*\"功能\" + 0.009*\"采用\" + 0.008*\"机身\" + 0.007*\"设计\" + 0.007*\"支持\" + 0.007*\"中\" + 0.005*\"玩家\" + 0.005*\"拍摄\" + 0.005*\"拥有\" + 0.005*\"倍\"\')
(3, \'0.007*\"中\" + 0.006*\"佣金\" + 0.006*\"企业\" + 0.004*\"考\" + 0.004*\"万家\" + 0.003*\"市场\" + 0.003*\"单词\" + 0.003*\"橱柜\" + 0.003*\"说\" + 0.003*\"行业\"\')
(4, \'0.012*\"拍摄\" + 0.007*\"中\" + 0.007*\"万\" + 0.006*\"镜头\" + 0.005*\"搭载\" + 0.005*\"英寸\" + 0.005*\"高清\" + 0.005*\"约\" + 0.004*\"拥有\" + 0.004*\"元\"\')
(5, \'0.007*\"中\" + 0.004*\"拍摄\" + 0.004*\"说\" + 0.003*\"英语\" + 0.002*\"时间\" + 0.002*\"柯达\" + 0.002*\"中国\" + 0.002*\"国泰\" + 0.002*\"市场\" + 0.002*\"电影\"\')
(6, \'0.024*\"考试\" + 0.010*\"相机\" + 0.008*\"套装\" + 0.007*\"拍摄\" + 0.005*\"万\" + 0.005*\"玩家\" + 0.005*\"中\" + 0.004*\"英寸\" + 0.004*\"索尼\" + 0.004*\"四级\"\')
(7, \'0.019*\"赎回\" + 0.007*\"基金\" + 0.007*\"净\" + 0.006*\"中\" + 0.004*\"市场\" + 0.004*\"资产\" + 0.004*\"收益\" + 0.003*\"中国\" + 0.003*\"债券\" + 0.003*\"说\"\')
(8, \'0.010*\"基金\" + 0.010*\"中\" + 0.006*\"公司\" + 0.005*\"产品\" + 0.005*\"市场\" + 0.004*\"元\" + 0.004*\"中国\" + 0.004*\"投资\" + 0.004*\"信息\" + 0.004*\"考试\"\')
(9, \'0.085*\"基金\" + 0.016*\"市场\" + 0.014*\"公司\" + 0.013*\"投资\" + 0.012*\"股票\" + 0.011*\"分红\" + 0.008*\"中\" + 0.007*\"一季度\" + 0.006*\"经理\" + 0.006*\"收益\"\')

第三步:抽取新闻的主题

我们还可以利用训练好的LDA,得到一条新闻的主题分布,也就是一条新闻属于各主题的可能性的概率分布。

找了三条新闻,分别是体育,娱乐和科技新闻:

体育    马晓旭意外受伤让国奥警惕 无奈大雨格外青睐殷家军记者傅亚雨沈阳报道 来到沈阳,国奥队依然没有摆脱雨水的困扰 ...

娱乐    尚雯婕筹备回沪献演□晨报记者 郭翔鹤 北京摄影报道 3月在北京举行了自己的首唱“尚佳分享·尚雯婕2008北京演唱会”后 ...

科技    摩托罗拉:GPON在FTTH中比EPON更有优势作 者:鲁义轩2009年,在国内光进铜退的火热趋势下,摩托罗拉携其在...

然后同样进行分词、ID化,通过lda.get_document_topics(corpus_test) 这个函数得到每条新闻的主题分布。得到新闻的主题分布之后,通过计算余弦距离,应该也可以进行文本相似度比较。

从结果中可以看到体育新闻的第6个主题的权重最大:(5, 0.60399055),可惜从第6个主题的单词分布来看,貌似这是个娱乐主题。

娱乐新闻的主题分布中,第5个主题的权重最大:(4, 0.46593386),而科技新闻的主题分布中,第3个主题的权重最大:(2, 0.38577113)。

\"\"\"抽取新闻的主题\"\"\"
# 用来测试的三条新闻,分别为体育、娱乐和科技新闻    
file_test = \"./cnews.test.txt\"
news_test = open(file_test, \'r\', encoding=\'UTF-8\')
    
test = []
# 处理成正确的输入格式       
for line in news_test:
    line = line.split(\'\\t\')[1]
    line = re.sub(r\'[^\\u4e00-\\u9fa5]+\',\'\',line)
    line_seg = seg_depart(line.strip())
    line_seg = [word.strip() for word in line_seg.split(\' \')]
    test.append(line_seg)    
    
# 新闻ID化    
corpus_test = [dictionary.doc2bow(text) for text in test]
# 得到每条新闻的主题分布
topics_test = lda.get_document_topics(corpus_test)  
labels = [\'体育\',\'娱乐\',\'科技\']
for i in range(3):
    print(\'这条\'+labels[i]+\'新闻的主题分布为:\\n\')
    print(topics_test[i],\'\\n\')

fr.close()
news_test.close()
这条体育新闻的主题分布为:

[(2, 0.022305986), (3, 0.20627314), (4, 0.039145608), (5, 0.60399055), (7, 0.1253269)] 

这条娱乐新闻的主题分布为:

[(3, 0.06871579), (4, 0.46593386), (7, 0.23081028), (8, 0.23132402)] 

这条科技新闻的主题分布为:

[(2, 0.38577113), (5, 0.14801453), (6, 0.09730849), (7, 0.36559567)] 

 

二、希拉里邮件门主题抽取

在美国大选期间,希拉里的邮件被泄露出来了,有6000多封邮件,我们可以用LDA主题模型对这些邮件的进行主题抽取,得到每个主题的单词分布,和每封邮件的主题分布。

还可以利用训练好模型,得到新邮件的主题分布。

步骤和以上的案例差不多,只是不需要进行分词。

第一步:用正则表达式清洗数据,并去除停用词

#!/usr/bin/python
# -*- coding:utf-8 -*-

import numpy as np
import pandas as pd
import re

from gensim import corpora, models, similarities
import gensim

\"\"\"第一步:用正则表达式清洗数据,并去除停用词\"\"\"
df = pd.read_csv(\"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 = \'\'
    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_text = df[\'ExtractedBodyText\']
docs = docs_text.apply(lambda s: clean_email_text(s))  

# 得到所有邮件的内容
doclist = docs.values
print(\"一共有\",len(doclist),\"封邮件。\\n\")
print(\"第1封邮件未清洗前的内容为: \\n\",docs_text.iloc[0],\'\\n\')

# 去除停用词,处理成gensim需要的输入格式
stopwords = [word.strip() for word in open(\'./stopwords.txt\',\'r\').readlines()]
# 每一封邮件都有星期和月份,这里也把他们过滤掉
weeks = [\'monday\',\'mon\',\'tuesday\',\'tues\',\'wednesday\',\'wed\',\'thursday\',\'thur\',\'friday\',\'fri\',\'saturday\',\'sat\',\'sunday\',\'sun\']
months = [\'jan\',\'january\',\'feb\',\'february\',\'mar\',\'march\',\'apr\',\'april\',\'may\',\'jun\',\'june\',\'jul\',\\
          \'july\',\'aug\',\'august\',\'sept\',\'september\',\'oct\',\'october\',\'nov\',\'november\',\'dec\',\'december\']
stoplist = stopwords+weeks+months+[\'am\',\'pm\']
texts = [[word for word in doc.lower().split() if word not in stoplist] for doc in doclist]

texts = [[word for word in doc.lower().split() if word not in stoplist] for doc in doclist]
print(\"第1封邮件去除停用词并处理成gensim需要的格式为:\\n\",texts[0],\'\\n\')
一共有 6742 封邮件。

第1封邮件未清洗前的内容为: 
 B6
Thursday, March 3, 2011 9:45 PM
H: Latest How Syria is aiding Qaddafi and more... Sid
hrc memo syria aiding libya 030311.docx; hrc memo syria aiding libya 030311.docx
March 3, 2011
For: Hillary 

第1封邮件去除停用词并处理成gensim需要的格式为:
 [\'latest\', \'syria\', \'aiding\', \'qaddafi\', \'sid\', \'hrc\', \'memo\', \'syria\', \'aiding\', \'libya\', \'docx\', \'hrc\', \'memo\', \'syria\', \'aiding\', \'libya\', \'docx\', \'hillary\'] 

第二步:构建语料库,训练LDA模型

这个英文的stopwordlist感觉不太行,从最终得到的单词分布来看,us、would这种词居然还有。这些单词看得眼睛都花了,不容看出来主题是啥。

我们看第8个主题的单词分布,里面的词有:state,obama,president,government,估计这个主题与当前总统有关。

(7, \'0.008*\"us\" + 0.008*\"new\" + 0.007*\"would\" + 0.005*\"state\" + 0.005*\"obama\" + 0.004*\"one\" + 0.004*\"said\" + 0.004*\"president\" + 0.003*\"first\" + 0.003*\"government\"\'), 

\"\"\"第二步:构建语料库,将文本ID化\"\"\"
dictionary = corpora.Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]
# 将每一篇邮件ID化
print(\"第1封邮件ID化后的结果为:\\n\",corpus[0],\'\\n\')

\"\"\"训练LDA模型\"\"\"
lda = gensim.models.ldamodel.LdaModel(corpus=corpus, id2word=dictionary, num_topics=20)
# 第10个主题的单词分布,取权重最高的前10个词
print(lda.print_topic(9, topn=10))
# 所有主题的单词分布
print(lda.print_topics(num_topics=20, num_words=10))
第1封邮件ID化后的结果为:
 [(0, 3), (1, 2), (2, 1), (3, 2), (4, 1), (5, 2), (6, 2), (7, 1), (8, 1), (9, 3)] 

[(0, \'0.008*\"us\" + 0.008*\"state\" + 0.006*\"doc\" + 0.006*\"afghan\" + 0.005*\"taliban\" + 0.005*\"said\" + 0.003*\"department\" + 0.003*\"strategic\" + 0.003*\"diplomacy\" + 0.003*\"afghanistan\"\'),
(1, \'0.019*\"pls\" + 0.014*\"call\" + 0.013*\"cheryl\" + 0.013*\"print\" + 0.012*\"fw\" + 0.011*\"mills\" + 0.010*\"state\" + 0.010*\"sullivan\" + 0.009*\"secretary\" + 0.008*\"huma\"\'),
(2, \'0.012*\"get\" + 0.010*\"see\" + 0.009*\"call\" + 0.008*\"good\" + 0.008*\"im\" + 0.007*\"thx\" + 0.007*\"know\" + 0.007*\"think\" + 0.007*\"today\" + 0.007*\"like\"\'),
(3, \'0.069*\"fyi\" + 0.007*\"sbwhoeop\" + 0.006*\"sid\" + 0.005*\"waldorf\" + 0.005*\"talk\" + 0.004*\"organizing\" + 0.004*\"fw\" + 0.004*\"abedin\" + 0.004*\"agree\" + 0.004*\"huma\"\'),
(4, \'0.004*\"ri\" + 0.003*\"phil\" + 0.003*\"yeah\" + 0.003*\"consulted\" + 0.003*\"arrange\" + 0.003*\"mayors\" + 0.003*\"cloture\" + 0.003*\"windows\" + 0.002*\"denis\" + 0.002*\"miliband\"\'),
(5, \'0.007*\"us\" + 0.006*\"people\" + 0.006*\"would\" + 0.006*\"one\" + 0.006*\"american\" + 0.005*\"israel\" + 0.005*\"said\" + 0.004*\"government\" + 0.004*\"united\" + 0.004*\"also\"\'),
(6, \'0.012*\"yes\" + 0.009*\"tomorrow\" + 0.007*\"boehner\" + 0.006*\"kurdistan\" + 0.006*\"still\" + 0.005*\"message\" + 0.005*\"talk\" + 0.005*\"call\" + 0.004*\"ops\" + 0.004*\"would\"\'),
(7, \'0.008*\"us\" + 0.008*\"new\" + 0.007*\"would\" + 0.005*\"state\" + 0.005*\"obama\" + 0.004*\"one\" + 0.004*\"said\" + 0.004*\"president\" + 0.003*\"first\" + 0.003*\"government\"\'),
(8, \'0.008*\"president\" + 0.008*\"obama\" + 0.007*\"said\" + 0.006*\"white\" + 0.005*\"house\" + 0.005*\"state\" + 0.005*\"percent\" + 0.005*\"ok\" + 0.005*\"new\" + 0.005*\"one\"\'),
(9, \'0.024*\"office\" + 0.017*\"secretarys\" + 0.013*\"meeting\" + 0.012*\"room\" + 0.009*\"state\" + 0.009*\"time\" + 0.008*\"department\" + 0.008*\"call\" + 0.007*\"treaty\" + 0.007*\"arrive\"\')]

第三步:查看邮件的主题分布

查看了第一封邮件的主题分布,然后推测了希拉里两条推特的主题。

\"\"\"第三步:查看某封邮件所属的主题\"\"\"
print(\"第1封邮件的大致内容为:\\n\",texts[0],\'\\n\')
topic = lda.get_document_topics(corpus[0])
print(\"第1封邮件的主题分布为:\\n\",topic,\'\\n\')

# 希拉里发的两条推特
# 给大伙翻译一下这两句:
# 这是选举的一天!数以百万计的美国人投了希拉里的票。加入他们吧,确定你投给谁。
# 希望今天每个人都能度过一个安乐的感恩节,和家人朋友共度美好时光——来自希拉里的问候。

twitter = [\"It\'s Election Day! Millions of Americans have cast their votes for Hillary—join them and confirm where you vote \",
       \"Hoping everyone has a safe & Happy Thanksgiving today, & quality time with family & friends. -H\"]

text_twitter = [clean_email_text(s) for s in twitter]
text_twitter = [[word for word in text.lower().split() if word not in stoplist] for text in text_twitter]
corpus_twitter = [dictionary.doc2bow(text) for text in text_twitter]
topics_twitter = lda.get_document_topics(corpus_twitter)
print(\"这两条推特的主题分布分别为:\\n\",topics_twitter[0] ,\'\\n\',topics_twitter[1])
第1封邮件的大致内容为:
 [\'latest\', \'syria\', \'aiding\', \'qaddafi\', \'sid\', \'hrc\', \'memo\', \'syria\', \'aiding\', \'libya\', \'docx\', \'hrc\', \'memo\', \'syria\', \'aiding\', \'libya\', \'docx\', \'hillary\'] 

第1封邮件的主题分布为:
 [(7, 0.9499477)] 

这两条推特的主题分布分别为:
 [(0, 0.23324193), (15, 0.6667277)] 
 [(0, 0.34214944), (7, 0.23708023), (9, 0.34343135)]

 

参考资料:

1、李航:《统计学习方法》(第二版)

2、某培训班资料

给TA打赏
共{{data.count}}人
人已打赏
随笔日记

Go语言调度器之盗取goroutine(17)

2020-11-9 4:55:36

随笔日记

5G元年的世界电信日,我们的生活将如何被改变?

2020-11-9 4:55:38

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索