LDA主題模型三連擊-入門/理論/代碼
本文將從三個方面介紹LDA主題模型——整體概況、數學推導、動手實現。
關於LDA的文章網上已經有很多了,大多都是從經典的《LDA 數學八卦》中引出來的,原創性不太多。
本文將用盡量少的公式,跳過不需要的證明,將最核心需要學習的部分與大家分享,展示出直觀的理解和基本的數學思想,避免數學八卦中過於詳細的推導。最後用python 進行實現。
[TOC]
概況
第一部分,包括以下四部分。
- 為什麽需要
- LDA是什麽
- LDA的應用
- LDA的使用
為什麽需要
挖掘隱含語義信息。一個經典的例子是
“喬布斯離我們而去了。”
“蘋果價格會不會降?”
上面這兩個句子沒有共同出現的單詞,但這兩個句子是相似的,如果按傳統的方法判斷這兩個句子肯定不相似。
所以在判斷文檔相關性的時候需要考慮到文檔的語義,而語義挖掘的利器是主題模型,LDA就是其中一種比較有效的模型。
LDA是什麽
LDA主題模型,首先是在文本分類領域提出來的,它的本意是挖掘文本中的隱藏主題。它將文本看作是詞袋模型(文章中的詞之間沒有關聯)產生的過程看成 先選一堆主題,再在主題中選擇詞,以此構建了一篇文章。
\(d\)是文章
\(z_1 ... z_n\)是主題
\(w\) 是單詞
\(\theta_{mk}\)是文檔選擇主題的概率。
\(\varphi_{kt}\)是主題選擇詞的概率。
這裏新手比較困惑的一點是選來選取,變量是什麽?
你可以這樣理解,先不要管狄利克雷分布,明確是從topic分布上選取topic,得到各topic的概率,然後再去另一個詞的分布上選取剛才得到topic對應的詞。
註意:此時不用想這兩個分布怎麽來的,只要把這個過程能想明白即可。LDA產生文檔的過程。
選主題分布->選主題
設置狄利克雷分布參數α->生成主題分布:
設置狄利克雷分布參數β->生成主題的詞分布:
生成主題分布->選取主題t:
生成主題t的詞分布->生成詞:
選取主題t->生成主題t的詞分布:
LDA的應用
通過隱含語義找到關聯項。
相似文檔發現;
推薦商品;將該商品歸屬的主題下其他商品推薦給用戶
主題評分;分析文檔主題傾向,看哪個主題比重大
gensim應用
import jieba
import gensim
def load_stop_words(file_path):
stop_words = []
with open(file_path,encoding=‘utf8‘) as f:
for word in f:
stop_words.append(word.strip())
return stop_words
def pre_process(data):
# jieba 分詞
cut_list = list(map(lambda x: ‘/‘.join(jieba.cut(x,cut_all=True)).split(‘/‘), data))
# 加載停用詞 去除 "的 了 啊 "等
stop_words = load_stop_words(‘stop_words.txt‘)
final_word_list = []
for cut in cut_list:
# 去除掉空字符和停用詞
final_word_list.append(list(filter(lambda x: x != ‘‘ and x not in stop_words, cut)))
print(final_word_list)
word_count_dict = gensim.corpora.Dictionary(final_word_list)
# 轉成詞袋模型 每篇文章由詞字典中的序號構成
bag_of_words_corpus = [word_count_dict.doc2bow(pdoc) for pdoc in final_word_list]
print(bag_of_words_corpus)
#返回 詞袋庫 詞典
return bag_of_words_corpus, word_count_dict
def train_lda(bag_of_words_corpus, word_count_dict):
# 生成lda model
lda_model = gensim.models.LdaModel(bag_of_words_corpus, num_topics=10, id2word=word_count_dict)
return lda_model
# 新聞地址 http://news.xinhuanet.com/world/2017-12/08/c_1122082791.htm
train_data = [u"中方對我們的建交國同臺灣開展正常經貿和民間往來不持異議,但堅決反對我們的建交國同臺灣發生任何形式的官方往來或簽署任何帶有主權意涵的協定或合作文件",
u"灣與菲律賓簽署了投資保障協定等7項合作文件。菲律賓是臺灣推動“新南向”政策中首個和臺灣簽署投資保障協定的國家。",
u"中方堅決反對建交國同臺灣發生任何形式的官方往來或簽署任何帶有主權意涵的協定或合作文件,已就此向菲方提出交涉"]
processed_train_data = pre_process(train_data)
lda_model = train_lda(*processed_train_data)
lda_model.print_topics(10)
數學原理
通過上節內容,在工程上基本可以用起來了。但是大家都是有追求的,不僅滿足使用。這節簡單介紹背後的數學原理。只會將核心部分的數學知識拿出來,不會面面俱到(我覺得這部分理解就足夠了)
(詳盡內容推薦去看《數學八卦》)
LDA認為各個主題的概率和各個主題下單詞的概率不是固定不變的(比如通過設定3個主題的抽取概率為0.3 0.4 0.3 就一直這麽用),而是由先驗和樣本共同通過貝葉斯計算得到的一個分布,同時還會依據不斷新增加的樣本進行調整。pLSA(LDA的前身) 看待分布情況就是固定的,求完就求完了,而LDA看待分布情況是 不斷依據先驗和樣本調整。
預備知識
下面我們來介紹一下貝葉斯公式
\(P(θ|X) = \frac{P(X|θ)P(θ)}{P(X)}\)
其中
後驗概率 \(P(θ|X)\) 就是說在觀察到X個樣本情況下,θ的概率
先驗概率 \(P(θ)\) 人們歷史經驗,比如硬幣正反概率0.5 骰子每個面是1/6
似然函數 \(P(X|θ)\) 在\(θ\)下,觀察到X個樣本的概率
貝葉斯估計簡單來說
先驗分布 + 數據的知識 = 後驗分布(嚴格的數學推導請看數學八卦)
\begin{equation}
Beta(p|\alpha,\beta) + Count(m_1,m_2) = Beta(p|\alpha+m_1,\beta+m_2)
\end{equation}
對於選主題,選單詞這個過程,LDA將其主題,單詞的分布看作是兩個後驗概率來求解。因為這兩個過程每次的結果都和骰子類似,有多種情況,因此是一個多項式分布對應抽樣分布\(P(\theta)\),對於多項式為抽樣分布來說,狄利克雷分布是它的共軛分布。
先驗分布反映了某種先驗信息,後驗分布既反映了先驗分布提供的信息,又反映了樣本提供的信息。若先驗分布和抽樣分布決定的後驗分布與先驗分布是同類型分布,則稱先驗分布為抽樣分布的共軛分布。當先驗分布與抽樣分布共軛時,後驗分布與先驗分布屬於同一種類型,這意味著先驗信息和樣本信息提供的信息具有一定的同一性。
- Beta的共軛分布是伯努利分布;
- 多項式分布的共軛分布是狄利克雷分布;
- 高斯分布的共軛分布是高斯分布。
那麽狄利克雷分布什麽樣子?
先介紹\(\Gamma\)函數和\(B\)函數
\[\Gamma(x)=\int_0^{\infty}t^{x-1}e^{-t}dt \\
B(m,n) = \frac{\Gamma(m)\Gamma(n)}{\Gamma(m+n)}\]
狄利克雷分布為下圖,其中\(\alpha_1 ... \alpha_n\)就是每個類型的偽先驗(按照歷史經驗和常識,比如骰子每個面都出現10次)
抽取模型
介紹完了基礎的數學知識,現在來看下如何得到LDA模型。
因為LDA是詞袋模型,各個主題,各個詞之間並沒有關聯,因此我們對於M篇文章,K個主題,可以兩次抽取,第一次抽取M個 topics 生成概率,第二次獲取K個主題的詞生成概率
主題生成概率
\(\vec{\mathbf{z}}\)是topic主題向量
\(\vec{\alpha}\)是在訓練時指定的參數
根據貝葉斯參數估計,可以得到主題的分布概率如下
\(\begin{align} p(\vec{\mathbf{z}} |\vec{\alpha}) & = \prod_{m=1}^M p(\vec{z}_m |\vec{\alpha}) \notag \\ &= \prod_{m=1}^M \frac{\Delta(\vec{n}_m+\vec{\alpha})}{\Delta(\vec{\alpha})}\quad\quad (*) \end{align}\)
詞生成概率
\(p(\vec{\mathbf{w}}|\vec{\mathbf{z}},\vec{\mathbf{\beta}})\) 是在指定的主題\(z\)和給定的參數\(\beta\)下詞生成概率 \(z\) 就是上一步得到的主題
\(\begin{align} p(\overrightarrow{\mathbf{w}} |\overrightarrow{\mathbf{z}},\overrightarrow{\beta}) &= p(\overrightarrow{\mathbf{w}}‘ |\overrightarrow{\mathbf{z}}‘,\overrightarrow{\beta}) \notag \\ &= \prod_{k=1}^K p(\overrightarrow{w}_{(k)} | \overrightarrow{z}_{(k)}, \overrightarrow{\beta}) \notag \\ &= \prod_{k=1}^K \frac{\Delta(\overrightarrow{n}_k+\overrightarrow{\beta})}{\Delta(\overrightarrow{\beta})} \quad\quad (**) \end{align}\)
模型
綜合主題模型和詞模型得到下面公式,就是LDA模型的分布
在\(\alpha,\beta\)參數下,依據抽取主題->抽取詞過程可以得到下面的分布,這個分布就是LDA模型的分布
\(\begin{align} p(\overrightarrow{\mathbf{w}},\overrightarrow{\mathbf{z}} |\overrightarrow{\alpha}, \overrightarrow{\beta}) &= p(\overrightarrow{\mathbf{w}} |\overrightarrow{\mathbf{z}}, \overrightarrow{\beta}) p(\overrightarrow{\mathbf{z}} |\overrightarrow{\alpha}) \notag \\ &= \prod_{k=1}^K \frac{\Delta(\overrightarrow{n}_k+\overrightarrow{\beta})}{\Delta(\overrightarrow{\beta})} \prod_{m=1}^M \frac{\Delta(\overrightarrow{n}_m+\overrightarrow{\alpha})}{\Delta(\overrightarrow{\alpha})} \quad\quad (***) \end{align}\)
樣本生成
雖然我們得到了模型的分布,但是如何獲取到符合這個分布的樣本(一個具體的實例)?
這裏就涉及到采樣的知識了,也就是馬爾科夫/Gibbs等知識。
簡單來說,馬爾科夫鏈中當前狀態僅與前一個狀態有關,而與其他狀態無關
同時對於大部分有轉移矩陣P的馬氏鏈(非周期),從任何一個狀態轉移,最終都會收斂到一個狀態
在這裏的思路是,構造一個馬氏鏈讓其的轉移概率等於我們需要的LDA分布。
Gibbs就是一個效率很高的滿足這樣方式的一個算法。
這塊的推導公式可以看數學八卦,不過理解到對其采樣即可。
關於Gibbs采樣大家可以參考
計算流程為
具體Gibbs采樣方程為
\(\begin{equation} p(z_i = k|\overrightarrow{\mathbf{z}}_{\neg i}, \overrightarrow{\mathbf{w}}) \propto \frac{n_{m,\neg i}^{(k)} + \alpha_k}{\sum_{k=1}^K (n_{m,\neg i}^{(k)} + \alpha_k)} \cdot \frac{n_{k,\neg i}^{(t)} + \beta_t}{\sum_{t=1}^V (n_{k,\neg i}^{(t)} + \beta_t)} \end{equation}\)
有了公式後,算法流程為
代碼編寫
運行代碼前先設置一下train_file.txt 文件,安裝numpy
得到了Gibbs計算公式後,對於每個單詞來說(每個單詞對應一個主題,就是一個(topic,word)元組) 通過每次去除該詞topic和詞本身在分布中的計數得到了條件分布\(P(z=k,w=t|z_{\neg k},w_{\neg t})\)
然後計算得到本次的topic,在放入計算矩陣doc_word_topic中。
以下為代碼,註釋詳盡
#-*- coding:utf-8 -*-
import random
import codecs
import os
import numpy as np
from collections import OrderedDict
import sys
print(sys.stdin.encoding)
train_file = ‘train_file.txt‘
bag_word_file = ‘word2id.txt‘
# save file
# doc-topic
phi_file = ‘phi_file.txt‘
# word-topic
theta_file = ‘theta_file.txt‘
############################
alpha = 0.1
beta = 0.1
topic_num = 10
iter_times = 100
##########################
class Document(object):
def __init__(self):
self.words = []
self.length = 0
class DataDict(object):
def __init__(self):
self.docs_count = 0
self.words_count = 0
self.docs = []
self.word2id = OrderedDict()
def add_word(self, word):
if word not in self.word2id:
self.word2id[word] = self.words_count
self.words_count += 1
return self.word2id[word]
def add_doc(self, doc):
self.docs.append(doc)
self.docs_count += 1
def save_word2id(self, file):
with codecs.open(file, ‘w‘,‘utf-8‘) as f:
for word,id in self.word2id.items():
f.write(word +"\t"+str(id)+"\n")
class DataClean(object):
def __init__(self, train_file):
self.train_file = train_file
self.data_dict = DataDict()
‘‘‘
input: text-word matrix
‘‘‘
def process_each_doc(self):
for text in self.texts:
doc = Document()
for word in text:
word_id = self.data_dict.add_word(word)
doc.words.append(word_id)
doc.length = len(doc.words)
self.data_dict.add_doc(doc)
def clean(self):
with codecs.open(self.train_file, ‘r‘,‘utf-8‘) as f:
self.texts = f.readlines()
self.texts = list(map(lambda x: x.strip().split(), self.texts))
assert type(self.texts[0]) == list , ‘wrong data format, texts should be two dimension‘
self.process_each_doc()
class LDAModel(object):
def __init__(self, data_dict):
self.data_dict = data_dict
#
# 模型參數
# 主題數topic_num
# 叠代次數iter_times,
# 每個類特征詞個數top_words_num
# 超參數alpha beta
#
self.beta = beta
self.alpha = alpha
self.topic_num = topic_num
self.iter_times = iter_times
# p,概率向量 臨時變量
self.p = np.zeros(self.topic_num)
# word-topic_num: word-topic matrix 一個word在不同topic的數量
# topic_word_sum: 每個topic包含的word數量
# doc_topic_num: doc-topic matrix 一篇文檔在不同topic的數量
# doc_word_sum: 每篇文檔的詞數
self.word_topic_num = np.zeros((self.data_dict.words_count, self.topic_num),dtype="int")
self.topic_word_sum = np.zeros(self.topic_num,dtype="int")
self.doc_topic_num = np.zeros((self.data_dict.docs_count, self.topic_num),dtype="int")
self.doc_word_sum = np.zeros(data_dict.docs_count,dtype="int")
# doc_word_topic 每篇文章每個詞的類別 size: len(docs),len(doc)
# theta 文章->類的概率分布 size: len(docs), topic_num
# phi 類->詞的概率分布 size: topic_num, len(doc)
self.doc_word_topic = np.array([[0 for y in range(data_dict.docs[x].length)] for x in range(data_dict.docs_count)])
self.theta = np.array([[0.0 for y in range(self.topic_num)] for x in range(self.data_dict.docs_count)])
self.phi = np.array([[0.0 for y in range(self.data_dict.words_count)] for x in range(self.topic_num)])
#隨機分配類型
for doc_idx in range(len(self.doc_word_topic)):
for word_idx in range(self.data_dict.docs[doc_idx].length):
topic = random.randint(0,self.topic_num - 1)
self.doc_word_topic[doc_idx][word_idx] = topic
# 對應矩陣topic內容增加
word = self.data_dict.docs[doc_idx].words[word_idx]
self.word_topic_num[word][topic] += 1
self.doc_topic_num[doc_idx][topic] += 1
self.doc_word_sum[doc_idx] += 1
self.topic_word_sum[topic] += 1
def sampling(self, doc_idx, word_idx):
topic = self.doc_word_topic[doc_idx][word_idx]
word = self.data_dict.docs[doc_idx].words[word_idx]
# Gibbs 采樣,是去除上一次原本情況的采樣
self.word_topic_num[word][topic] -= 1
self.doc_topic_num[doc_idx][topic] -= 1
self.topic_word_sum[topic] -= 1
self.doc_word_sum[doc_idx] -= 1
# 構造計算公式
Vbeta = self.data_dict.words_count * self.beta
Kalpha = self.topic_num * self.alpha
self.p = (self.word_topic_num[word] + self.beta) / (self.topic_word_sum + Vbeta) * (self.doc_topic_num[doc_idx] + self.alpha) / (self.doc_word_sum[doc_idx] + Kalpha)
for k in range(1,self.topic_num):
self.p[k] += self.p[k-1]
# 選取滿足本次抽樣的topic
u = random.uniform(0,self.p[self.topic_num - 1])
for topic in range(self.topic_num):
if self.p[topic] > u:
break
# 將新topic加回去
self.word_topic_num[word][topic] += 1
self.doc_topic_num[doc_idx][topic] += 1
self.topic_word_sum[topic] += 1
self.doc_word_sum[doc_idx] += 1
return topic
def _theta(self):
for i in range(self.data_dict.docs_count):
self.theta[i] = (self.doc_topic_num[i]+self.alpha)/ (self.doc_word_sum[i]+self.topic_num * self.alpha)
def _phi(self):
for i in range(self.topic_num):
self.phi[i] = (self.word_topic_num.T[i] + self.beta)/ (self.topic_word_sum[i]+self.data_dict.words_count * self.beta)
def train_lda(self):
for x in range(self.iter_times):
for i in range(self.data_dict.docs_count):
for j in range(self.data_dict.docs[i].length):
topic = self.sampling(i,j)
self.doc_word_topic[i][j] = topic
print("叠代完成。")
print("計算文章-主題分布")
self._theta()
print("計算詞-主題分布")
self._phi()
def main():
data_clean = DataClean(train_file)
data_clean.clean()
data_dict = data_clean.data_dict
data_dict.save_word2id(bag_word_file)
lda = LDAModel(data_dict)
lda.train_lda()
if __name__ == ‘__main__‘:
main()
其他參考
1
2
3
LDA主題模型三連擊-入門/理論/代碼