得知李航老师的《统计学习方法》出了第二版,我第一时间就买了。看了这本书的目录,非常高兴,好家伙,居然把主题模型都写了,还有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、某培训班资料

05-18 02:32