文字主題抽取:用gensim訓練LDA模型

Luv_GEM發表於2019-05-17

得知李航老師的《統計學習方法》出了第二版,我第一時間就買了。看了這本書的目錄,非常高興,好傢伙,居然把主題模型都寫了,還有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、某培訓班資料

相關文章