笔者最近在研究机器学习相关的算法,正好学到了LDA(主题模型),所以就网上的一段主题模型的相关代码做一下分析。
以笔者的想法来看,主题模型类似于聚类,例如对一堆文档进行聚类,猜测文档有K个主题,则经过一连串的计算,将这堆文档划分为K类。话不多说,先上代码(注:本代码转载自 https://www.cnblogs.com/fujian-code/p/7637571.html,笔者额外加了一些注释,本段代码基于python3):
#-*- 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")
# 1000个文档 * 假设第一个文档 1024个词汇 第二个文档 1133个词汇 第三个文档 1458个词汇
self.Z = np.array([ [0 for y in range(dpre.docs[x].length)] for x in range(dpre.docs_count)]) # M*doc.size(),文档中词的主题分布
#随机先分配类型,为每个文档中的各个单词分配主题
for x in range(len(self.Z)):
self.ndsum[x] = self.dpre.docs[x].length
for y in range(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['文档一']['娱乐']
self.nd[x][topic] += 1
#topic['新闻'] 最后假设有5123个词
self.nwsum[topic] += 1
#theta 代表 1000个文档 * 10个topic 都是0.0
self.theta = np.array([ [0.0 for y in range(self.K)] for x in range(self.dpre.docs_count) ])
#phi 代表 10个主题 * 100000个单词
self.phi = np.array([ [ 0.0 for y in range(self.dpre.words_count) ] for x in range(self.K)])
def sampling(self,i,j):
#换主题 第i个文档 第j个词汇 所属的topic
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
# 1000个文档 10个主题, 总共300000个词汇
# nw 300000 * 10
# nwsum 1 * 10 [30000,30000,30000,30000,30000,30000,30000,30000,30000,30000] 代表每个主题 30000个词汇
# nd 1000 * 10 第i个文档,第j个主题下,共有多少单词
# ndsum 1 * 1000 [300,300,300,.......] 总共有1000个300 每个文档平均300个词汇
self.p = (self.nw[word] + self.beta)/(self.nwsum + Vbeta) * \
(self.nd[i] + self.alpha) / (self.ndsum[i] + Kalpha)
#随机更新主题的吗
# for k in range(1,self.K):
# self.p[k] += self.p[k-1]
# u = random.uniform(0,self.p[self.K-1])
# for topic in range(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 range(self.iter_times):
for i in range(self.dpre.docs_count):
for j in range(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 range(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 range(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 range(self.dpre.docs_count):
for y in range(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 range(self.K):
for y in range(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 range(self.K):
f.write(u'第' + str(x) + u'类:' + '\n')
twords = []
twords = [(n,self.phi[x][n]) for n in range(self.dpre.words_count)]
twords.sort(key = lambda i:i[1], reverse= True)
for y in range(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 range(self.dpre.docs_count):
for y in range(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 item in dpre.word2id:# 已有的话,只是当前文档追加
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()
假设有1000个文档,总共300000万个词汇,平均每个文档300个词汇
代码的逻辑大致为:
1. 从当前文件夹下加载配置文件setting.conf,
2. 读取训练集文件trainfile,trainfile包括1000行,每行代表1个文档,且做好分词,笔者的训练文档并没有像笔者说的那样每个 文档300个词汇,300个词汇只是做个假设,方便分析而已
3. 初始化文档的相关信息,计算文档数目(docs_count),词汇数目(words_count),词汇和序号对应字典表(word2id),以及单独的序号list(words)
4. 初始化参数,各个参数含义如下:
K:主题个数
alpha:平滑参数,防止有些主题下,单词数目为0,则计算出来的概率为0
beta:平滑参数,防止有文档没有单词,则计算出来的概率为0
iter_times:训练数据时的循环次数上限
top_words_num:最后要展示的每个类的高频词个数
其它不同文件的作用,已在代码中说明
5. 初始化相关矩阵,各个矩阵大致含义如下:
p:1行10列的矩阵(因为假设有10个主题),概率向量 double类型,存储采样的临时变量
nw:300000行10列的矩阵(因为假设有300000个单词),词word在主题topic上的分布,假设nw[0][0]=3,代表第一个词,在第一个主题下有出现了3次
nwsum:1行10列的矩阵,每个主题的词的总数
nd:1000行10列的矩阵,每个文档对应主题下的词的数目
ndsum:1行1000列的矩阵,每个文档的词数
Z:1000行,每行的列数不固定,因为实际情况下,每个文档长短不一,1000个词对应1000个列
theta:1000行10列的矩阵,代表每个文档属于某个主题的概率
phi:10行300000列的矩阵,代表某个词属于某个主题的概率
6. 循环1000个文档,并循环每个文档的单词,初始时,为每个文档随机分配一个主题,并统计各个矩阵
7. 然后对文档进行采样,计算每个文档中的每个词所属的主题
采样的过程如下:
for iter_times:参数里面设置的循环次数
for docs: 循环每个文档
for word:循环每个具体文档的词
topic = sampling(i,j) 第i个文档的第j个单词,从新计算其所属主题
Z[i][j] = topic 将主题赋给相应的矩阵元素
其中sampling的步骤如下:
a.先取得(i,j)对应的单词的主题,将该主题对应的统计频次,各自减1,
b.计算中间平滑参数,Vbeta和Kalpha,这里的Vbeta和Kalpha笔者并不是特别了解
c.进行新的概率的计算,根据这个概率按照多项式分布计算出一个新的主题
d.将这个新的主题的统计频次各加1
e.返回该主题
其中c步骤的公式,笔者大致理解如下图所示:
LDA主题模型代码的讲解大致就这样,笔者首次发博客,有很多不懂的,还请各位多多指教