文本主题抽取:用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=10)# 所有主题的单词分布 print(lda.print_topics(num_topics=10, 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.0111170085), (1, 0.011118207), (2, 0.01111913), (3, 0.011116115), (4, 0.89994085), (5, 0.011116263), (6, 0.011116605), (7, 0.011117295), (8, 0.01111973), (9, 0.0111187985)] [(4, 0.9181052)]
参考资料:
1、李航:《统计学习方法》(第二版)
2、某培训班资料