最近看了不少关于主题模型的东西,要说起主题模型,现在最火的当然是LDA, LDA全称是Latent Dirichlet Allocation(隐狄利克雷分布), 而不是Linear Discriminant Analysis, 相信大家很多都对lda的理解感到痛苦不已,因为里面涉及到的数学推导实在是太多了,从gamma函数,beta分布,狄利克雷分布,马尔可夫蒙特卡洛模型,看到都觉得反胃,不过今天,我们不从这些来说,就跟大家分析一下怎么从工程上去实现整个LDA

实现之前还是来说一下LDA的起源:

先上一张来自Blei大师之手的图,简单的说一下吧, theta代表文档-主题分布,在工程上可以理解为一个矩阵,如果整个文档语料库包含的词是|W|,包含的文档数是|D|,那么矩阵的大小就是|D| * |W|,直观的来说,这个矩阵中存储的值theta[d][z]表示的是文档d中被分派给主题z的词的个数,更具体的,我们可以认为它就是p(z|d)

主题模型是一种生成模型,什么是生成模型呢,比如我们在构思一篇文档:(1)我们要选择文章的主题,一个主题中可能有多个词; (2)我们现在就要从这个主题中选择我们想要的词;第一个部分的概率就是p(z|d),表示在给定文档d,出现主题z的概率;

举一个例子(例子来源于 Rich jin的LDA数学八卦):

我们平时在构造一篇自然语言处理的文章时,可能会有40%的概率谈论语言学,30%的概率谈论概率统计,20%的概率谈论计算机,还有10%谈论其他主题;选定了主题之后,我们执行第二部,选词,那正常情况下,我们是怎么选词的呢?

  • 说到语言学:我们一般会想到 语法,句子,乔姆斯基,据法分析,主语这些词
  • 谈到概率统计,我们也很容易想到词:概率,模型,均值,方差,证明,独立,马尔可夫链
  • 说到计算机,我们也能联想到 内存,硬盘,编程,二进制,对象,算法,复杂度这些词
为什么能联想到这些词,因为在我们的认知下,这些词出现的频率比较高,换句话说,我们接触的这些词比较多,比较频繁,所以我们能在确定主题之后很快的从这些词中选择一个
这些是前话,现在来开始说一下工程上怎么实现吧,我自己是用python来写的,在这里跟大家分享一下
首先是LDA模型的类定义:
class LDAModel:
alpha = float      #超参数alpha
beta = float       #超参数beta
D = int            #文档数目
K = int            #主题个数
W = int            #词的个数
NumberOfIterations = int   #迭代次数
SaveStep = int     #存储的步数
Dictionary = object  #整个语料的词典
Z = object         # D * doc.size()大小的矩阵,Z[i][j]表示第i文档的第j个词背分配的主题
W = object         # D * doc.size()大小的矩阵, W[i][j]表示第i文档的第j个词
IDListSet = object  # D * doc.size()大小的矩阵, IDListSet[i][j]表示第i篇文档的第j个词在词典中的编号
nw = object        # W * K 大小的矩阵, nw[w][z]表示词w被分配到主题z的次数
nd = object        # D * K 大小的矩阵,nd[d][z]文档d中被分配为主题z的词的个数
nwsum = object     # K * 1 大小的向量,nwsum[z]表示主题z中包含的词的个数
ndsum = object     # D * 1 大小的向量,ndsum[d]表示文档d中包含的词的个数
theta = object     # D * K 大小的矩阵,p(z|d) = theta[d][z]
phi = object       # K * V 大小的矩阵,p(w|z) = phi[z][w]

具体我就不说这些成员的意思,注释上都有,

首先是构造函数,这里要说明的是,在工程上,alpha一般取50/K, beta一般取0.01,吉布斯抽样的迭代次数一般为1000次、
    def __init__(self, alpha, beta, NumberOfIterations, SaveStep, K):self.alpha = alphaself.beta = betaself.NumberOfIterations = NumberOfIterationsself.SaveStep = SaveStepself.K = K#初始化大小为K * 1的向量,初始值为0self.nwsum = ListUtil.Initial(self.K)

有一些列表工具类的方法我已经打包了,先列出来

#ListUtil.py
import string
def Normalize(list, smoother=0.0):"""对向量list进行归一化处理,得到每个元素出现的概率:param list: 向量:param smoother: 平滑值,缺省值为0; 为了防止0概率的出现"""sum = Sum(list)K = len(list)newlist = []if sum > 0:newlist = [float((item + smoother) / (sum + K * smoother)) for item in list]return newlistdef Sum(list):"""计算list中所有元素的和"""res = 0for item in list:res += itemreturn resdef Initial(size, data=0):"""生成一个大小为size, 所有元素都为data的列表:param size: 列表大小:param data: 列表元素"""list = []for i in xrange(size):list.append(data)return listdef InitialMat(M, N, data=0):"""初始化大小为M * N的矩阵,所有元素初始化为data:param M::param N::param data: 矩阵元素"""mat = []for i in xrange(M):row = Initial(N, data)mat.append(row)return matdef InitialEmptyMat(rows):"""初始化一个空的matrix:param rows:"""mat = []for i in xrange(rows):tmp = []   #代表每一个文档包含的词,初始化为空mat.append(tmp)return matdef toString(list):"""将list中的元素拼接成字符串方便用作文件操作:param list: 列表元素"""listStr = ""count = 0for ele in list:if type(ele) == int:eleStr = str(ele)elif type(ele) == float:#浮点数转换为字符串,保留8位小数eleStr = str("%.10f"%ele)elif type(ele) == str or type(ele) == unicode:eleStr = eleif count != len(list) - 1:eleStr += " "count += 1listStr += eleStrlistStr += "\n"return listStrdef StringToFloatList(SS):"""string 转换为float:param SS: 从文件中读取的字符串"""res = [string.atof(item) for item in SS.split(" ")]return resdef AssignList(LL):"""将LL中的值拷贝到另一个list中:param LL: 字符串"""newLL = []for ele in LL:newLL.append(ele)return newLLdef FindMax(LL):"""返回列表LL中最大的元素"""LL.sort()return LL[len(LL) - 1]

好,接着我们刚才的定义

现在可以开始初始化模型了,代码如下:
def ModelInit(self, filename):"""读取文档,文本预处理,构造词典,构造语料库"""Docs = LoadData.LoadDataFromFile(os.getcwd() + "/" + filename)self.D = len(Docs)print "Load ", self.D, " docs from the file"#读取停用词表StopWordList = LoadData.LoadStopWords()#对输入文本进行预处理:去标点符号,去停用词,词干化,然后每篇文档生成一个词的列表WordListSet = [Preprocess.PreprocessText(doc, StopWordList) for doc in Docs if type(doc) != unicode]#通过词表集构造词典self.Dictionary = Preprocess.ConstructDictionary(WordListSet)self.W = len(self.Dictionary)print "Total number of words is: ", self.Wprint "Begin to save the dictionary..."self.SaveDictionary()print "Done!!"#IDListSet 大小 D * doc.size()print "Begin to map the word to ID"self.IDListSet = []for wdl in WordListSet:IdList = Preprocess.Word2Id(wdl, self.Dictionary)self.IDListSet.append(IdList)print "Done!!"#ndsum[d] 文档d中包含的词的个数self.ndsum = ListUtil.Initial(self.D)#初始化一个 D * K的矩阵self.theta = ListUtil.InitialMat(self.D, self.K, 0.0)self.phi = ListUtil.InitialMat(self.K, self.W, 0.0)#nd[d][z] 文档d中被分配给主题z的词数self.nd = ListUtil.InitialMat(self.D, self.K, 0)#nw[w][z] 主题z中包含的词w的个数self.nw = ListUtil.InitialMat(self.W, self.K, 0)#Z[d][w] 文档d的第w个词的主题self.Z = []print "Begin to initialize the LDA model..."#初始化计数向量和计数矩阵self.RandomAssignTopic()print "Topic assignment done!!"

首先是从文件中读取文档,LoadData同样是我定义的工具类;然后用辅助类Preprocess去完成文本的预处理(包括去标点符号,去停用词,词干化,构造词典等等),初始化完成之后,再为每个词赋一个初始的topic,你可能要问,LDA中文档主题的选择应该要服从狄利克雷先验分布,但是为什么可以随机赋值,其实这要从马尔可夫链开始说起了,简单的来说,马尔可夫链就是对象的一系列状态的集合,并且对象的当前状态仅仅跟它的上一个状态有关,来看一个具体的例子

这个图反应的是一个人的收入跟它父母收入高低的联系,1代表下层,2代表中层,3代表上层;上面矩阵的第一行就表示,如果父代是上层阶级,那么他的子代有0.65的可能性仍然是上层阶级,有0.28的概率称为中层阶级,有0.07的概率成为下层阶级;上面的矩阵我们就定义为传递概率矩阵P,如果我们假设当代人属于这3个阶级的概率分别是x(0,1) x(0,2) x(0,3),那么他们的下一代的阶级分布x(1) = x(0) * P,同样,再下一代的阶级分布x(2) = x(1) * P,其他的以此类推,下面来看一个例子:
我们假设初始代的阶级分布x(0) = [0.21 0.68 0.11],那么通过结算可以得到以下结果
我们再换一个初始的阶级分布,假设现在x(0) = [0.75 0.15 0.1],计算结果如下:
通过这两个例子,我们发现无论初始的状态怎么样,最后的阶级分布都能够收敛,并且收敛到同一个分布,这就是马尔可夫链的厉害之处,虽然我到现在仍然无法理解这其中的奥秘,但是把他应用到工程中还是可以的
马尔可夫把这个结论总结成了一个定理(不知道怎么打公式,直接上图了):
这里的P就是我们上面所说的传递概率矩阵,最后的pi就是收敛之后的阶层分布,非周期性马氏链我也不太清楚是什么一回事儿,反正你要记住,我们日常生活中接触的大都是非抽泣性马氏链
正是因为马氏链这种收敛性,所以我们才能够在工程上为文档中的词随机分派主题,因为不管怎样,它最后都能够熟练到一个平稳分布,只是说收敛的快慢问题
现在来贴一下这两个工具类
#Preprocess.py
import string
import nltk
from gensim import corporadef PreprocessText(text, StopWordList):"""预处理一篇文本:剔除标点符号,词干化,去停用词:param text: 传入的文本,类型为字符串:param StopWordList: 停用词表"""WordList = DelPunctuation(text)StemmeredWordList = Stemmer(WordList)FilteredWordList = FilterStopWords(StemmeredWordList, StopWordList)return <span style="font-family: arial, 宋体, sans-serif;">FilteredWordList</span>
def DelPunctuation(text):"""剔除文本中的标点符号:param text:需要剔除标点符号的文本,类型为字符串return:返回文本中的词的序列"""delset = string.punctuation#将标点符号转换为空格newText = text.encode('utf8').translate(None, delset)#文本中的词的列表WordList = [word for word in newText.split(" ") if word != '' and word != ' ']return WordListdef FilterStopWords(WordList, StopWordList):"""返回去停用词后的词表:param WordList::param StopWordList:"""FilteredWordList = filter(lambda x: x.lower() not in StopWordList, WordList)return FilteredWordListdef Stemmer(WordList):"""对文档的词表进行词干化:param WordList:"""stemmer = nltk.LancasterStemmer()StemmeredWordList = [stemmer.stem(w) for w in WordList]return StemmeredWordListdef ConstructDictionary(WordListSet):"""根据输入文档集texts构造词典:rtype : object:param WordListSet: 文档集对应的词表,WordListSet[i]表示第i篇文档中的词"""print "Begin to construct the dictionary"res = corpora.Dictionary(WordListSet)print "Total number of words is: ", len(res)return resdef Word2Id(WordList, Dictionary):"""将词表转换为词典dictionary中的ID:param WordList:"""IDList = []for word in WordList:#遍历字典查找目标项for k, v in Dictionary.items():if v == word:IDList.append(k)return IDList

在文本与处理时,用到了nltk这个强大的自然语言处理的库,程序中使用其中的LancasterStemmer()进行词干化;然后也用到了gensim库,在这个类中,主要是用corpora来构造训练文档集的词典

在贴一下LoadData的代码
#LoadData.py
import os
import stringdef LoadDataFromFile(path):""":param path:短文本存放路径"""#转换为绝对路径fp = open(path, 'r')Docs = []for line in fp:#去掉结尾换行符ll = line.strip('\n').strip('\r')Docs.append(ll)fp.close()print "Done, load ", len(Docs), " docs from the file"return Docsdef LoadStopWords():"""从指定路径读取停用词表return:停用词列表"""path = os.getcwd()path += "/StopWords.txt"fp = open(path, 'r')#获取停用词列表StopWordsList = [line.strip('\n') for line in fp]fp.close()return StopWordsListdef LoadDictionary():"""从指定路径加载训练词典"""path = os.getcwd() + "/dictionary.txt"fp = open(path, 'r')Dictionary = dict()for line in fp:elements = line.strip('\n').split(" ")#词的idk = string.atoi(elements[0])#词本身v = elements[1]Dictionary[k] = vfp.close()return Dictionary

这个类我就不多解释了,学过python的小伙伴应该都能看懂,只是涉及文件操作的路径名你们可以自己diy,我用的是我自己电脑上的文件名

继续解析LDAModel类
初始化模型之后,我们就要开始训练模型参数了(后面的是重点哟):
刚才忘记贴随机分派主题的代码了:
def RandomAssignTopic(self):"""随机为文档中的词分配主题更新计数向量ndsum, nwsum, 计数矩阵nd, nw的值"""for d in xrange(self.D):DocSize = len(self.IDListSet[d])row = ListUtil.Initial(DocSize)self.Z.append(row)for w in xrange(DocSize):#从主题编号0-K-1中随机抽取一个topic = Sample.UniSample(self.K)#获取词的IDwid = self.IDListSet[d][w]self.Z[d][w] = topic#被分派给topic的词w的数目自增1self.nw[wid][topic] += 1#文档d中被分配给主题topic的词的个数self.nd[d][topic] += 1#主题topic中包含的总的词数self.nwsum[topic] += 1self.ndsum[d] = DocSize

lda的训练过程主要就是吉布斯抽样的过程,具体的来说,吉布斯抽样就是将抽样的一个词w从当前的分布中抽出,然后通过抽出这个词之后的主题分布theta和词的分布phi,来计算这个词被分派到其他主题的概率,先上代码

    def sampling(self, d, w):
"""
Gibbs Sampling为当前词重新分配主题
:param d: 文档编号
:param w: 词在文档中的编号
"""
topic = self.Z[d][w]
#对应位置上的词的ID
wid = self.IDListSet[d][w]
self.nw[wid][topic] -= 1
self.nd[d][topic] -= 1
self.nwsum[topic] -= 1
self.ndsum[d] -= 1
#p为马尔可夫链传递概率,p[z]表示当前词被分配到主题z的概率
p = self.ComputeTransProb(d, w)
#从多项分布中抽取新的主题
newtopic = Sample.MultSample(p)
self.nw[wid][newtopic] += 1
self.nd[d][newtopic] += 1
self.nwsum[newtopic] += 1
self.ndsum[d] += 1
return newtopic
def ComputeTransProb(self, d, w):
"""
对第d篇文档的第w个词
计算Gibbs Sampling过程中的传递概率
:param d: 文档编号
:param w: 词在文档中的编号
"""
#用于平滑
Wbeta = self.W * self.beta
Kalpha = self.K * self.alpha
#第d篇文档,第w个词对应的id
wid = self.IDListSet[d][w]
p = ListUtil.Initial(self.K, 0.0)
for k in xrange(self.K):
#p[k] = p(w|k)*p(k|d)   k为主题
p[k] = (float(self.nw[wid][k]) + self.beta) / (float(self.nwsum[k]) + Wbeta) * (float(self.nd[d][k]) + self.alpha) / (float(self.ndsum[d]) + Kalpha)
return p

其实上面的计算法则就是p(z(i)=k|z', w, alpha,beta) = p(z'(i)=k|d)*p(w|z'(i)=k),z'就是代表将当前词w剔除之后的主题分布,z(i)对应当前词w的主题,这里就跟文章开头的生成模型的原理呼应上了,我们先以一定的概率选择主题(p(topic|doc)),然后在从主题包含的词中抽取相应的词(p(word|topic)),吉布斯抽样也是沿着doc->topic->word这样的方向进行的,给一张图大家可能更好理解,theta[m][k]表示第m篇文档,第k个主题出现的概率;phi[k][t]表示主题k中,词t出现的概率

计算传递概率之后,我们再从多项分布中抽取相应的主题(这个函数对应MultSample函数,下面为整个Sample的代码)
def UniSample(K):"""产生从O到K-1的整数:param K: 主题个数"""return RandomNumber.RandInt(0, K - 1)def MultSample(ProbList):"""从多项分布ProbList中采样, ProbList表示剔除当前词之后的主题分布:param ProbList: 多项分布"""size = len(ProbList)for i in xrange(1, size):ProbList[i] += ProbList[i - 1]#随机产生一个[0,1)的小数u = RandomNumber.RandFloat()res = 0for k in xrange(size):if ProbList[k] >= u * ProbList[size - 1]:#抽样结果res = kbreak#res为抽样后的主题编号return res

其实吉布斯抽样的目的就是为乐得到在 图模型中的theta和phi,那要怎么样计算theta和phi呢?其实很简单的,吉布斯抽样中,doc-topic, topic-word矩阵的计数是变化的,当抽样收敛之后,我们就得到了最后的计数,通过这些计数来计算频率就好了

    def ComputTheta(self):"""计算p(z|d)矩阵size:D * Kp(z|d) = theta[d][z]"""for d in xrange(self.D):for k in xrange(self.K):self.theta[d][k] = (float(self.nd[d][k]) + self.alpha) / (float(self.ndsum[d]) + self.K * self.alpha)def ComputePhi(self):"""计算p(w|z)size:K * Wp(w|z) = phi[z][w]"""for k in xrange(self.K):for w in xrange(self.W):self.phi[k][w] = (self.nw[w][k] + self.beta) / (self.nwsum[k] + self.W * self.beta)

为了防止0概率的出现,我们分别用alpha和beta做了平滑

首先来看phi[k][w],我们用频率计数nw[w][k](主题w中包含的词k的数目)来计算,不幸的是,这个值有可能为0,在一连串的乘式中,0的出现会使其他项毫无意义,所以我们要避免这种情况,怎么避免呢,我们可以假设主题k中事先已经存在了词典中的所有词,然后,再用我们得到的收敛之后的 主题-词计数去更新里面的内容,这样,就可以保证不会出现0概率(因为所有词至少出现一次,同时,主题k中的词数nwsum[k]增加了W),那么phi[k][w]=(nw[w][k] + 1) / (nwsum[k] + W),这就是鼎鼎大名的 “拉普拉斯平滑”,不过工程上,1对于概率的影响还是太大了(比如1/2和2/3),所以,我们吧1换成了更小的beta(一般是0.01),这样对于概率的影响就变得很小了,也就得到了上面的公式,theta[d][k]也同理
整个参数训练的过程如下
    def estimate(self):"""LDA参数估计"""for i in xrange(1, self.NumberOfIterations + 1):for d in xrange(self.D):for w in xrange(len(self.IDListSet[d])):newtopic = self.sampling(d, w)#为当前词分派新主题self.Z[d][w] = newtopicif i % self.SaveStep == 0:#计算当前的迭代结果self.ComputTheta()self.ComputePhi()self.SaveTempRes(i)

LDA模型的参数训练部分就讲到这里,下一篇跟大家分享一下LDA的参数推导

希望看到的亲能帮我指出文中的错误,文笔不好,大家多见谅

LDA的python实现之模型参数训练相关推荐

  1. python保存模型与参数_如何导出python中的模型参数

    模型的保存和读取 1.tensorflow保存和读取模型:tf.train.Saver() .save()#保存模型需要用到save函数 save( sess, save_path, global_s ...

  2. LDA模型参数设置,训练效果较好

    前言:写小论文用到lda主题模型,在网上找了一圈没有找到训练效果较好的模型参数示例.为了写出小论文做了很多次实验,达到了实验中最好的效果,故贴出 代码: from gensim.models impo ...

  3. python训练模型函数参数_keras读取训练好的模型参数并把参数赋值给其它模型详解...

    介绍 本博文中的代码,实现的是加载训练好的模型model_halcon_resenet.h5,并把该模型的参数赋值给两个不同的新的model. 函数式模型 官网上给出的调用一个训练好模型,并输出任意层 ...

  4. python可视化多个机器学习模型在训练集(train set)上交叉验证(cross validation)的AUC值、可视化模型效能

    python可视化多个机器学习模型在训练集(train set)上交叉验证(cross validation)的AUC值.可视化模型效能 # 所有的模型中填写的参数都是通过randomsearchcv ...

  5. tensorflow 模型预训练后的参数restore finetuning

    之前训练的网络中有一部分可以用到一个新的网络中,但是不知道存储的参数如何部分恢复到新的网络中,也了解到有许多网络是通过利用一些现有的网络结构,通过finetuning进行改造实现的,因此了解了一下关于 ...

  6. 【机器学习】隐马尔可夫模型及其三个基本问题(三)模型参数学习算法及python实现

    [机器学习]隐马尔可夫模型及其三个基本问题(三)模型参数学习算法及python实现 一.一些概率与期望值的计算 二.非监督学习方法(Baum-Welch算法) 三.python实现 隐马尔可夫模型参数 ...

  7. 模型参数无法更新的原因:训练、预测中加入了print函数

    模型参数无法更新的问题排查以及解决 注释掉结构的方法 排查出错误 最终排查 进一步排查错误 loss的数值一致??? 进一步排查问题来源:预处理之中的标签处理出现错误!!! 灵感:model.trai ...

  8. Python使用tpot获取最优模型并抽取最优模型模型参数

    Python使用tpot获取最优模型并抽取最优模型模型参数 目录 Python使用tpot获取最优模型并抽取最优模型模型参数 #数据划分

  9. 对抗训练硬核分析:对抗样本与模型参数的关系

    ©PaperWeekly 原创 · 作者|孙裕道 学校|北京邮电大学博士生 研究方向|GAN图像生成.情绪对抗样本生成 引言 对抗训练是防御对抗样本一种有效的方法,但是对于它有效性的边界,一直都是很模 ...

最新文章

  1. CNN阴影去除--DeshadowNet: A Multi-context Embedding Deep Network for Shadow Removal
  2. Swift3.0语言教程获取字符串编码与哈希地址
  3. Java 7:使用NIO.2进行文件过滤-第2部分
  4. 如何使用FinalShell、FileZilla上传网站代码到服务器?这两个都是神器
  5. 查询成绩小于85且是计算机的一项应用,查询练习2
  6. 智能芯片的下一场战争是什么?
  7. ie6的png24问题
  8. 将hta包装为exe发布
  9. asp.net ashx + JQuery Ajax + XML
  10. WEB服务器Nginx WINDOWS最简部署
  11. Java中什么不是线程状态_并发基础(四) java中线程的状态
  12. Astah Professional三维图,网络上轻松上传图表
  13. 博主已开启评论精选什么意思_什么叫独立站?
  14. Some file crunching failed, see logs for details 一种情形的解决办法
  15. Vim编辑器的基本使用(二)末行模式中的命令
  16. java基本类型val_Java的基本数据类型
  17. 微信小程序学习笔记2
  18. STDERR: error: unable to open preload file “/etc/sysctl.d/90-omnibus-gitlab-net.core.somaxconn.conf“
  19. 如何监测耳机/麦克风设备插拔操作
  20. SPSS Modeler18.0数据挖掘软件教程(三):逻辑回归分析

热门文章

  1. Google AdSense广告赚钱之技巧篇!
  2. android studio怎么改软件扫码界面_一文入门Android逆向
  3. C++ Programming Basic acknowledge
  4. AutoAugment中16种数据增强方法可视化直观展示
  5. 纪录大华的智能回调需要用到的事件列表
  6. java调用word模板文件_Java使用模板导出word文档
  7. sqllyog可以连接oracle_sqlyog连接不上数据库
  8. MySQLsqlyog详细内容
  9. BUUCTF MD5
  10. 沈华伟老师图卷积神经网络教学视频笔记