LDA主題模型三連擊-入門/理論/代碼

本文將從三個方面介紹LDA主題模型——總體概況、數學推導、動手實現。
關於LDA的文章網上已經有不少了,大多都是從經典的《LDA 數學八卦》中引出來的,原創性不太多。
本文將用盡可能少的公式,跳過不須要的證實,將最核心須要學習的部分與你們分享,展現出直觀的理解和基本的數學思想,避免數學八卦中過於詳細的推導。最後用python 進行實現。python

概況

第一部分,包括如下四部分。算法

  • 爲何須要
  • LDA是什麼
  • LDA的應用
  • LDA的使用

爲何須要

挖掘隱含語義信息。一個經典的例子是app

「喬布斯離咱們而去了。」
「蘋果價格會不會降?」dom

上面這兩個句子沒有共同出現的單詞,但這兩個句子是類似的,若是按傳統的方法判斷這兩個句子確定不類似。
因此在判斷文檔相關性的時候須要考慮到文檔的語義,而語義挖掘的利器是主題模型,LDA就是其中一種比較有效的模型。函數


LDA是什麼

LDA主題模型,首先是在文本分類領域提出來的,它的本意是挖掘文本中的隱藏主題。它將文本看做是詞袋模型(文章中的詞之間沒有關聯)產生的過程當作 先選一堆主題,再在主題中選擇詞,以此構建了一篇文章。
\(d\)是文章
\(z_1 ... z_n\)是主題
\(w\) 是單詞
\(\theta_{mk}\)是文檔選擇主題的機率。
\(\varphi_{kt}\)是主題選擇詞的機率。
這裏寫圖片描述
這裏新手比較困惑的一點是選來選取,變量是什麼?
你能夠這樣理解,先不要管狄利克雷分佈,明確是從topic分佈上選取topic,獲得各topic的機率,而後再去另外一個詞的分佈上選取剛纔獲得topic對應的詞。學習

這裏寫圖片描述

這裏寫圖片描述
注意:此時不用想這兩個分佈怎麼來的,只要把這個過程能想明白便可。LDA產生文檔的過程。
選主題分佈->選主題spa

設置狄利克雷分佈參數α->生成主題分佈: 
設置狄利克雷分佈參數β->生成主題的詞分佈:
生成主題分佈->選取主題t: 
生成主題t的詞分佈->生成詞:
選取主題t->生成主題t的詞分佈:

LDA的應用

經過隱含語義找到關聯項。
類似文檔發現;
推薦商品;將該商品歸屬的主題下其餘商品推薦給用戶
主題評分;分析文檔主題傾向,看哪一個主題比重大.net


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)

數學原理

經過上節內容,在工程上基本能夠用起來了。可是你們都是有追求的,不只知足使用。這節簡單介紹背後的數學原理。只會將核心部分的數學知識拿出來,不會面面俱到(我以爲這部分理解就足夠了)
(詳盡內容推薦去看《數學八卦》)3d

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

相關文章
相關標籤/搜索