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


文章轉自:

wind_blast

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實現:

 

[python] 
 
  1. #-*- coding:utf-8 -*-  
  2. import logging  
  3. import logging.config  
  4. import ConfigParser  
  5. import numpy as np  
  6. import random  
  7. import codecs  
  8. import os  
  9.   
  10. from collections import OrderedDict  
  11. #獲取當前路徑  
  12. path = os.getcwd()  
  13. #導入日志配置文件  
  14. logging.config.fileConfig("logging.conf")  
  15. #創建日志對象  
  16. logger = logging.getLogger()  
  17. # loggerInfo = logging.getLogger("TimeInfoLogger")  
  18. # Consolelogger = logging.getLogger("ConsoleLogger")  
  19.   
  20. #導入配置文件  
  21. conf = ConfigParser.ConfigParser()  
  22. conf.read("setting.conf")   
  23. #文件路徑  
  24. trainfile = os.path.join(path,os.path.normpath(conf.get("filepath", "trainfile")))  
  25. wordidmapfile = os.path.join(path,os.path.normpath(conf.get("filepath","wordidmapfile")))  
  26. thetafile = os.path.join(path,os.path.normpath(conf.get("filepath","thetafile")))  
  27. phifile = os.path.join(path,os.path.normpath(conf.get("filepath","phifile")))  
  28. paramfile = os.path.join(path,os.path.normpath(conf.get("filepath","paramfile")))  
  29. topNfile = os.path.join(path,os.path.normpath(conf.get("filepath","topNfile")))  
  30. tassginfile = os.path.join(path,os.path.normpath(conf.get("filepath","tassginfile")))  
  31. #模型初始參數  
  32. K = int(conf.get("model_args","K"))  
  33. alpha = float(conf.get("model_args","alpha"))  
  34. beta = float(conf.get("model_args","beta"))  
  35. iter_times = int(conf.get("model_args","iter_times"))  
  36. top_words_num = int(conf.get("model_args","top_words_num"))  
  37. class Document(object):  
  38.     def __init__(self):  
  39.         self.words = []  
  40.         self.length = 0  
  41. #把整個文檔及真的單詞構成vocabulary(不允許重復)  
  42. class DataPreProcessing(object):  
  43.     def __init__(self):  
  44.         self.docs_count = 0  
  45.         self.words_count = 0  
  46.         #保存每個文檔d的信息(單詞序列,以及length)  
  47.         self.docs = []  
  48.         #建立vocabulary表,照片文檔的單詞  
  49.         self.word2id = OrderedDict()  
  50.     def cachewordidmap(self):  
  51.         with codecs.open(wordidmapfile, 'w','utf-8') as f:  
  52.             for word,id in self.word2id.items():  
  53.                 f.write(word +"\t"+str(id)+"\n")  
  54. class LDAModel(object):  
  55.     def __init__(self,dpre):  
  56.         self.dpre = dpre #獲取預處理參數  
  57.         #  
  58.         #模型參數  
  59.         #聚類個數K,迭代次數iter_times,每個類特征詞個數top_words_num,超參數α(alpha) β(beta)  
  60.         #  
  61.         self.K = K  
  62.         self.beta = beta  
  63.         self.alpha = alpha  
  64.         self.iter_times = iter_times  
  65.         self.top_words_num = top_words_num   
  66.         #  
  67.         #文件變量  
  68.         #分好詞的文件trainfile  
  69.         #詞對應id文件wordidmapfile  
  70.         #文章-主題分布文件thetafile  
  71.         #詞-主題分布文件phifile  
  72.         #每個主題topN詞文件topNfile  
  73.         #最后分派結果文件tassginfile  
  74.         #模型訓練選擇的參數文件paramfile  
  75.         #  
  76.         self.wordidmapfile = wordidmapfile  
  77.         self.trainfile = trainfile  
  78.         self.thetafile = thetafile  
  79.         self.phifile = phifile  
  80.         self.topNfile = topNfile  
  81.         self.tassginfile = tassginfile  
  82.         self.paramfile = paramfile  
  83.         # p,概率向量 double類型,存儲采樣的臨時變量  
  84.         # nw,詞word在主題topic上的分布  
  85.         # nwsum,每各topic的詞的總數  
  86.         # nd,每個doc中各個topic的詞的總數  
  87.         # ndsum,每各doc中詞的總數  
  88.         self.p = np.zeros(self.K)  
  89.         # nw,詞word在主題topic上的分布  
  90.         self.nw = np.zeros((self.dpre.words_count,self.K),dtype="int")  
  91.         # nwsum,每各topic的詞的總數  
  92.         self.nwsum = np.zeros(self.K,dtype="int")  
  93.         # nd,每個doc中各個topic的詞的總數  
  94.         self.nd = np.zeros((self.dpre.docs_count,self.K),dtype="int")  
  95.         # ndsum,每各doc中詞的總數  
  96.         self.ndsum = np.zeros(dpre.docs_count,dtype="int")  
  97.         self.Z = np.array([ [for y in xrange(dpre.docs[x].length)] for x in xrange(dpre.docs_count)])        # M*doc.size(),文檔中詞的主題分布  
  98.   
  99.         #隨機先分配類型,為每個文檔中的各個單詞分配主題  
  100.         for x in xrange(len(self.Z)):  
  101.             self.ndsum[x] = self.dpre.docs[x].length  
  102.             for y in xrange(self.dpre.docs[x].length):  
  103.                 topic = random.randint(0,self.K-1)#隨機取一個主題  
  104.                 self.Z[x][y] = topic#文檔中詞的主題分布  
  105.                 self.nw[self.dpre.docs[x].words[y]][topic] += 1  
  106.                 self.nd[x][topic] += 1  
  107.                 self.nwsum[topic] += 1  
  108.   
  109.         self.theta = np.array([ [0.0 for y in xrange(self.K)] for x in xrange(self.dpre.docs_count) ])  
  110.         self.phi = np.array([ [ 0.0 for y in xrange(self.dpre.words_count) ] for x in xrange(self.K)])   
  111.     def sampling(self,i,j):  
  112.         #換主題  
  113.         topic = self.Z[i][j]  
  114.         #只是單詞的編號,都是從0開始word就是等於j  
  115.         word = self.dpre.docs[i].words[j]  
  116.         #if word==j:  
  117.         #    print 'true'  
  118.         self.nw[word][topic] -= 1  
  119.         self.nd[i][topic] -= 1  
  120.         self.nwsum[topic] -= 1  
  121.         self.ndsum[i] -= 1  
  122.   
  123.         Vbeta = self.dpre.words_count * self.beta  
  124.         Kalpha = self.K * self.alpha  
  125.         self.p = (self.nw[word] + self.beta)/(self.nwsum + Vbeta) * \  
  126.                  (self.nd[i] + self.alpha) / (self.ndsum[i] + Kalpha)  
  127.   
  128.         #隨機更新主題的嗎  
  129.         # for k in xrange(1,self.K):  
  130.         #     self.p[k] += self.p[k-1]  
  131.         # u = random.uniform(0,self.p[self.K-1])  
  132.         # for topic in xrange(self.K):  
  133.         #     if self.p[topic]>u:  
  134.         #         break  
  135.   
  136.         #按這個更新主題更好理解,這個效果還不錯  
  137.         p = np.squeeze(np.asarray(self.p/np.sum(self.p)))  
  138.         topic = np.argmax(np.random.multinomial(1, p))  
  139.   
  140.         self.nw[word][topic] +=1  
  141.         self.nwsum[topic] +=1  
  142.         self.nd[i][topic] +=1  
  143.         self.ndsum[i] +=1  
  144.         return topic  
  145.     def est(self):  
  146.         # Consolelogger.info(u"迭代次數為%s 次" % self.iter_times)  
  147.         for x in xrange(self.iter_times):  
  148.             for i in xrange(self.dpre.docs_count):  
  149.                 for j in xrange(self.dpre.docs[i].length):  
  150.                     topic = self.sampling(i,j)  
  151.                     self.Z[i][j] = topic  
  152.         logger.info(u"迭代完成。")  
  153.         logger.debug(u"計算文章-主題分布")  
  154.         self._theta()  
  155.         logger.debug(u"計算詞-主題分布")  
  156.         self._phi()  
  157.         logger.debug(u"保存模型")  
  158.         self.save()  
  159.     def _theta(self):  
  160.         for i in xrange(self.dpre.docs_count):#遍歷文檔的個數詞  
  161.             self.theta[i] = (self.nd[i]+self.alpha)/(self.ndsum[i]+self.K * self.alpha)  
  162.     def _phi(self):  
  163.         for i in xrange(self.K):  
  164.             self.phi[i] = (self.nw.T[i] + self.beta)/(self.nwsum[i]+self.dpre.words_count * self.beta)  
  165.     def save(self):  
  166.         # 保存theta文章-主題分布  
  167.         logger.info(u"文章-主題分布已保存到%s" % self.thetafile)  
  168.         with codecs.open(self.thetafile,'w') as f:  
  169.             for x in xrange(self.dpre.docs_count):  
  170.                 for y in xrange(self.K):  
  171.                     f.write(str(self.theta[x][y]) + '\t')  
  172.                 f.write('\n')  
  173.         # 保存phi詞-主題分布  
  174.         logger.info(u"詞-主題分布已保存到%s" % self.phifile)  
  175.         with codecs.open(self.phifile,'w') as f:  
  176.             for x in xrange(self.K):  
  177.                 for y in xrange(self.dpre.words_count):  
  178.                     f.write(str(self.phi[x][y]) + '\t')  
  179.                 f.write('\n')  
  180.         # 保存參數設置  
  181.         logger.info(u"參數設置已保存到%s" % self.paramfile)  
  182.         with codecs.open(self.paramfile,'w','utf-8') as f:  
  183.             f.write('K=' + str(self.K) + '\n')  
  184.             f.write('alpha=' + str(self.alpha) + '\n')  
  185.             f.write('beta=' + str(self.beta) + '\n')  
  186.             f.write(u'迭代次數  iter_times=' + str(self.iter_times) + '\n')  
  187.             f.write(u'每個類的高頻詞顯示個數  top_words_num=' + str(self.top_words_num) + '\n')  
  188.         # 保存每個主題topic的詞  
  189.         logger.info(u"主題topN詞已保存到%s" % self.topNfile)  
  190.   
  191.         with codecs.open(self.topNfile,'w','utf-8') as f:  
  192.             self.top_words_num = min(self.top_words_num,self.dpre.words_count)  
  193.             for x in xrange(self.K):  
  194.                 f.write(u'第' + str(x) + u'類:' + '\n')  
  195.                 twords = []  
  196.                 twords = [(n,self.phi[x][n]) for n in xrange(self.dpre.words_count)]  
  197.                 twords.sort(key = lambda i:i[1], reverse= True)  
  198.                 for y in xrange(self.top_words_num):  
  199.                     word = OrderedDict({value:key for key, value in self.dpre.word2id.items()})[twords[y][0]]  
  200.                     f.write('\t'*2+ word +'\t' + str(twords[y][1])+ '\n')  
  201.         # 保存最后退出時,文章的詞分派的主題的結果  
  202.         logger.info(u"文章-詞-主題分派結果已保存到%s" % self.tassginfile)  
  203.         with codecs.open(self.tassginfile,'w') as f:  
  204.             for x in xrange(self.dpre.docs_count):  
  205.                 for y in xrange(self.dpre.docs[x].length):  
  206.                     f.write(str(self.dpre.docs[x].words[y])+':'+str(self.Z[x][y])+ '\t')  
  207.                 f.write('\n')  
  208.         logger.info(u"模型訓練完成。")  
  209. # 數據預處理,即:生成d()單詞序列,以及詞匯表  
  210. def preprocessing():  
  211.     logger.info(u'載入數據......')  
  212.     with codecs.open(trainfile, 'r','utf-8') as f:  
  213.         docs = f.readlines()  
  214.     logger.debug(u"載入完成,准備生成字典對象和統計文本數據...")  
  215.     # 大的文檔集  
  216.     dpre = DataPreProcessing()  
  217.     items_idx = 0  
  218.     for line in docs:  
  219.         if line != "":  
  220.             tmp = line.strip().split()  
  221.             # 生成一個文檔對象:包含單詞序列(w1,w2,w3,,,,,wn)可以重復的  
  222.             doc = Document()  
  223.             for item in tmp:  
  224.                 if dpre.word2id.has_key(item):# 已有的話,只是當前文檔追加  
  225.                     doc.words.append(dpre.word2id[item])  
  226.                 else:  # 沒有的話,要更新vocabulary中的單詞詞典及wordidmap  
  227.                     dpre.word2id[item] = items_idx  
  228.                     doc.words.append(items_idx)  
  229.                     items_idx += 1  
  230.             doc.length = len(tmp)  
  231.             dpre.docs.append(doc)  
  232.         else:  
  233.             pass  
  234.     dpre.docs_count = len(dpre.docs) # 文檔數  
  235.     dpre.words_count = len(dpre.word2id) # 詞匯數  
  236.     logger.info(u"共有%s個文檔" % dpre.docs_count)  
  237.     dpre.cachewordidmap()  
  238.     logger.info(u"詞與序號對應關系已保存到%s" % wordidmapfile)  
  239.     return dpre  
  240. def run():  
  241.     # 處理文檔集,及計算文檔數,以及vocabulary詞的總個數,以及每個文檔的單詞序列  
  242.     dpre = preprocessing()  
  243.     lda = LDAModel(dpre)  
  244.     lda.est()  
  245. if __name__ == '__main__':  
  246.     run()  
  247.       
 

參考資料:

lda主題模型

概率語言模型及其變形系列

lda八卦


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM