LDA(Latent dirichlet allocation)是有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參數的算法偽代碼如下:
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([ [0 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()
本文來自於:
謝謝博主