1. 程式人生 > >LDA主題模型原理解析與python實現

LDA主題模型原理解析與python實現

LDA(Latent dirichlet allocation)[1]是有Blei於2003年提出的三層貝葉斯主題模型,通過無監督的學習方法發現文字中隱含的主題資訊,目的是要以無指導學習的方法從文字中發現隱含的語義維度-即“Topic”或者“Concept”。隱性語義分析的實質是要利用文字中詞項(term)的共現特徵來發現文字的Topic結構,這種方法不需要任何關於文字的背景知識。文字的隱性語義表示可以對“一詞多義”和“一義多詞”的語言現象進行建模,這使得搜尋引擎系統得到的搜尋結果與使用者的query在語義層次上match,而不是僅僅只是在詞彙層次上出現交集。文件、主題以及詞可以表示為下圖:


LDA引數:

K為主題個數,M為文件總數,是第m個文件的單詞總數。 是每個Topic下詞的多項分佈的Dirichlet先驗引數,   是每個文件下Topic的多項分佈的Dirichlet先驗引數。是第m個文件中第n個詞的主題,是m個文件中的第n個詞。剩下來的兩個隱含變數分別表示第m個文件下的Topic分佈和第k個Topic下詞的分佈,前者是k維(k為Topic總數)向量,後者是v維向量(v為詞典中term總數)。

LDA生成過程:

所謂生成模型,就是說,我們認為一篇文章的每個詞都是通過“以一定概率選擇了某個主題,並從這個主題中以一定概率選擇某個詞語”這樣一個過程得到。文件到主題服從多項式分佈,主題到詞服從多項式分佈。每一篇文件代表了一些主題所構成的一個概率分佈,而每一個主題又代表了很多單詞所構成的一個概率分佈。



Gibbs Sampling學習LDA:

Gibbs Sampling 是Markov-Chain Monte Carlo演算法的一個特例。這個演算法的執行方式是每次選取概率向量的一個維度,給定其他維度的變數值Sample當前維度的值。不斷迭代,直到收斂輸出待估計的引數。初始時隨機給文字中的每個單詞分配主題,然後統計每個主題z下出現term t的數量以及每個文件m下出現主題z中的詞的數量,每一輪計算,即排除當前詞的主題分配,根據其他所有詞的主題分配估計當前詞分配各個主題的概率。當得到當前詞屬於所有主題z的概率分佈後,根據這個概率分佈為該詞sample一個新的主題。然後用同樣的方法不斷更新下一個詞的主題,直到發現每個文件下Topic分佈

和每個Topic下詞的分佈收斂,演算法停止,輸出待估計的引數,最終每個單詞的主題也同時得出。實際應用中會設定最大迭代次數。每一次計算的公式稱為Gibbs updating rule.下面我們來推導LDA的聯合分佈和Gibbs updating rule。

用Gibbs Sampling 學習LDA引數的演算法虛擬碼如下:


python實現:

#-*- coding:utf-8 -*-
import logging
import logging.config
import ConfigParser
import numpy as np
import random
import codecs
import os

from collections import OrderedDict
#獲取當前路徑
path = os.getcwd()
#匯入日誌配置檔案
logging.config.fileConfig("logging.conf")
#建立日誌物件
logger = logging.getLogger()
# loggerInfo = logging.getLogger("TimeInfoLogger")
# Consolelogger = logging.getLogger("ConsoleLogger")

#匯入配置檔案
conf = ConfigParser.ConfigParser()
conf.read("setting.conf") 
#檔案路徑
trainfile = os.path.join(path,os.path.normpath(conf.get("filepath", "trainfile")))
wordidmapfile = os.path.join(path,os.path.normpath(conf.get("filepath","wordidmapfile")))
thetafile = os.path.join(path,os.path.normpath(conf.get("filepath","thetafile")))
phifile = os.path.join(path,os.path.normpath(conf.get("filepath","phifile")))
paramfile = os.path.join(path,os.path.normpath(conf.get("filepath","paramfile")))
topNfile = os.path.join(path,os.path.normpath(conf.get("filepath","topNfile")))
tassginfile = os.path.join(path,os.path.normpath(conf.get("filepath","tassginfile")))
#模型初始引數
K = int(conf.get("model_args","K"))
alpha = float(conf.get("model_args","alpha"))
beta = float(conf.get("model_args","beta"))
iter_times = int(conf.get("model_args","iter_times"))
top_words_num = int(conf.get("model_args","top_words_num"))
class Document(object):
    def __init__(self):
        self.words = []
        self.length = 0
#把整個文件及真的單詞構成vocabulary(不允許重複)
class DataPreProcessing(object):
    def __init__(self):
        self.docs_count = 0
        self.words_count = 0
        #儲存每個文件d的資訊(單詞序列,以及length)
        self.docs = []
        #建立vocabulary表,照片文件的單詞
        self.word2id = OrderedDict()
    def cachewordidmap(self):
        with codecs.open(wordidmapfile, 'w','utf-8') as f:
            for word,id in self.word2id.items():
                f.write(word +"\t"+str(id)+"\n")
class LDAModel(object):
    def __init__(self,dpre):
        self.dpre = dpre #獲取預處理引數
        #
        #模型引數
        #聚類個數K,迭代次數iter_times,每個類特徵詞個數top_words_num,超引數α(alpha) β(beta)
        #
        self.K = K
        self.beta = beta
        self.alpha = alpha
        self.iter_times = iter_times
        self.top_words_num = top_words_num 
        #
        #檔案變數
        #分好詞的檔案trainfile
        #詞對應id檔案wordidmapfile
        #文章-主題分佈檔案thetafile
        #詞-主題分佈檔案phifile
        #每個主題topN詞檔案topNfile
        #最後分派結果檔案tassginfile
        #模型訓練選擇的引數檔案paramfile
        #
        self.wordidmapfile = wordidmapfile
        self.trainfile = trainfile
        self.thetafile = thetafile
        self.phifile = phifile
        self.topNfile = topNfile
        self.tassginfile = tassginfile
        self.paramfile = paramfile
        # p,概率向量 double型別,儲存取樣的臨時變數
        # nw,詞word在主題topic上的分佈
        # nwsum,每各topic的詞的總數
        # nd,每個doc中各個topic的詞的總數
        # ndsum,每各doc中詞的總數
        self.p = np.zeros(self.K)
        # nw,詞word在主題topic上的分佈
        self.nw = np.zeros((self.dpre.words_count,self.K),dtype="int")
        # nwsum,每各topic的詞的總數
        self.nwsum = np.zeros(self.K,dtype="int")
        # nd,每個doc中各個topic的詞的總數
        self.nd = np.zeros((self.dpre.docs_count,self.K),dtype="int")
        # ndsum,每各doc中詞的總數
        self.ndsum = np.zeros(dpre.docs_count,dtype="int")
        self.Z = np.array([ [0 for y in xrange(dpre.docs[x].length)] for x in xrange(dpre.docs_count)])        # M*doc.size(),文件中詞的主題分佈

        #隨機先分配型別,為每個文件中的各個單詞分配主題
        for x in xrange(len(self.Z)):
            self.ndsum[x] = self.dpre.docs[x].length
            for y in xrange(self.dpre.docs[x].length):
                topic = random.randint(0,self.K-1)#隨機取一個主題
                self.Z[x][y] = topic#文件中詞的主題分佈
                self.nw[self.dpre.docs[x].words[y]][topic] += 1
                self.nd[x][topic] += 1
                self.nwsum[topic] += 1

        self.theta = np.array([ [0.0 for y in xrange(self.K)] for x in xrange(self.dpre.docs_count) ])
        self.phi = np.array([ [ 0.0 for y in xrange(self.dpre.words_count) ] for x in xrange(self.K)]) 
    def sampling(self,i,j):
        #換主題
        topic = self.Z[i][j]
        #只是單詞的編號,都是從0開始word就是等於j
        word = self.dpre.docs[i].words[j]
        #if word==j:
        #    print 'true'
        self.nw[word][topic] -= 1
        self.nd[i][topic] -= 1
        self.nwsum[topic] -= 1
        self.ndsum[i] -= 1

        Vbeta = self.dpre.words_count * self.beta
        Kalpha = self.K * self.alpha
        self.p = (self.nw[word] + self.beta)/(self.nwsum + Vbeta) * \
                 (self.nd[i] + self.alpha) / (self.ndsum[i] + Kalpha)

        #隨機更新主題的嗎
        # for k in xrange(1,self.K):
        #     self.p[k] += self.p[k-1]
        # u = random.uniform(0,self.p[self.K-1])
        # for topic in xrange(self.K):
        #     if self.p[topic]>u:
        #         break

        #按這個更新主題更好理解,這個效果還不錯
        p = np.squeeze(np.asarray(self.p/np.sum(self.p)))
        topic = np.argmax(np.random.multinomial(1, p))

        self.nw[word][topic] +=1
        self.nwsum[topic] +=1
        self.nd[i][topic] +=1
        self.ndsum[i] +=1
        return topic
    def est(self):
        # Consolelogger.info(u"迭代次數為%s 次" % self.iter_times)
        for x in xrange(self.iter_times):
            for i in xrange(self.dpre.docs_count):
                for j in xrange(self.dpre.docs[i].length):
                    topic = self.sampling(i,j)
                    self.Z[i][j] = topic
        logger.info(u"迭代完成。")
        logger.debug(u"計算文章-主題分佈")
        self._theta()
        logger.debug(u"計算詞-主題分佈")
        self._phi()
        logger.debug(u"儲存模型")
        self.save()
    def _theta(self):
        for i in xrange(self.dpre.docs_count):#遍歷文件的個數詞
            self.theta[i] = (self.nd[i]+self.alpha)/(self.ndsum[i]+self.K * self.alpha)
    def _phi(self):
        for i in xrange(self.K):
            self.phi[i] = (self.nw.T[i] + self.beta)/(self.nwsum[i]+self.dpre.words_count * self.beta)
    def save(self):
        # 儲存theta文章-主題分佈
        logger.info(u"文章-主題分佈已儲存到%s" % self.thetafile)
        with codecs.open(self.thetafile,'w') as f:
            for x in xrange(self.dpre.docs_count):
                for y in xrange(self.K):
                    f.write(str(self.theta[x][y]) + '\t')
                f.write('\n')
        # 儲存phi詞-主題分佈
        logger.info(u"詞-主題分佈已儲存到%s" % self.phifile)
        with codecs.open(self.phifile,'w') as f:
            for x in xrange(self.K):
                for y in xrange(self.dpre.words_count):
                    f.write(str(self.phi[x][y]) + '\t')
                f.write('\n')
        # 儲存引數設定
        logger.info(u"引數設定已儲存到%s" % self.paramfile)
        with codecs.open(self.paramfile,'w','utf-8') as f:
            f.write('K=' + str(self.K) + '\n')
            f.write('alpha=' + str(self.alpha) + '\n')
            f.write('beta=' + str(self.beta) + '\n')
            f.write(u'迭代次數  iter_times=' + str(self.iter_times) + '\n')
            f.write(u'每個類的高頻詞顯示個數  top_words_num=' + str(self.top_words_num) + '\n')
        # 儲存每個主題topic的詞
        logger.info(u"主題topN詞已儲存到%s" % self.topNfile)

        with codecs.open(self.topNfile,'w','utf-8') as f:
            self.top_words_num = min(self.top_words_num,self.dpre.words_count)
            for x in xrange(self.K):
                f.write(u'第' + str(x) + u'類:' + '\n')
                twords = []
                twords = [(n,self.phi[x][n]) for n in xrange(self.dpre.words_count)]
                twords.sort(key = lambda i:i[1], reverse= True)
                for y in xrange(self.top_words_num):
                    word = OrderedDict({value:key for key, value in self.dpre.word2id.items()})[twords[y][0]]
                    f.write('\t'*2+ word +'\t' + str(twords[y][1])+ '\n')
        # 儲存最後退出時,文章的詞分派的主題的結果
        logger.info(u"文章-詞-主題分派結果已儲存到%s" % self.tassginfile)
        with codecs.open(self.tassginfile,'w') as f:
            for x in xrange(self.dpre.docs_count):
                for y in xrange(self.dpre.docs[x].length):
                    f.write(str(self.dpre.docs[x].words[y])+':'+str(self.Z[x][y])+ '\t')
                f.write('\n')
        logger.info(u"模型訓練完成。")
# 資料預處理,即:生成d()單詞序列,以及詞彙表
def preprocessing():
    logger.info(u'載入資料......')
    with codecs.open(trainfile, 'r','utf-8') as f:
        docs = f.readlines()
    logger.debug(u"載入完成,準備生成字典物件和統計文字資料...")
    # 大的文件集
    dpre = DataPreProcessing()
    items_idx = 0
    for line in docs:
        if line != "":
            tmp = line.strip().split()
            # 生成一個文件物件:包含單詞序列(w1,w2,w3,,,,,wn)可以重複的
            doc = Document()
            for item in tmp:
                if dpre.word2id.has_key(item):# 已有的話,只是當前文件追加
                    doc.words.append(dpre.word2id[item])
                else:  # 沒有的話,要更新vocabulary中的單詞詞典及wordidmap
                    dpre.word2id[item] = items_idx
                    doc.words.append(items_idx)
                    items_idx += 1
            doc.length = len(tmp)
            dpre.docs.append(doc)
        else:
            pass
    dpre.docs_count = len(dpre.docs) # 文件數
    dpre.words_count = len(dpre.word2id) # 詞彙數
    logger.info(u"共有%s個文件" % dpre.docs_count)
    dpre.cachewordidmap()
    logger.info(u"詞與序號對應關係已儲存到%s" % wordidmapfile)
    return dpre
def run():
    # 處理文件集,及計算文件數,以及vocabulary詞的總個數,以及每個文件的單詞序列
    dpre = preprocessing()
    lda = LDAModel(dpre)
    lda.est()
if __name__ == '__main__':
    run()
    


參考資料:

lda八卦