毕业论文案例-LDA主题模型实现文本聚类

引言

这是一篇以我毕业论文为背景的博客。由于对数据分析非常感兴趣,所以选择这个主题。在论文的撰写中,我参考了大量相关的CSDN博客。

时光匆匆,在毕业之际,我愿意分享论文过程的体会和心得,给各位兄弟姐妹们做个参考。

(以下内容知网查重过,勿搬运)

LDA主题模型的预备知识

LDA主题模型本质是文本聚类算法,属于文档生成模型(利用样本的相似性)。它常用于对文本数据的主题挖掘中,比如对用户评论的挖掘,人才招聘需求信息的挖掘等。它是NLP中一个常用的算法。

(1)多项式分布 Multinomial Distribution

多项式分布是二项分布的推广。最常见的二项分布比如抛硬币的实验,实验结果只有两个,即正面和反面,且每个结果发生概率为1/2。现在我们假设掷骰子的实验,6个面分别对应6个不同的点数,每个点数出现的概率都是1/6,这就是一个典型的多项分布。但这是很理想的情况,现在我们把它推广至n个情况,且每个面出现的概率是不确定,我们就可以认为各结果出现的概率服从一定的分布,概率密度函数如下:
P { X 1 = n 1 , X 2 = n 2 , … , X r = n r } = n ! n 1 ! n 2 ! … n r ! P 1 n 1 P 2 n 2 … P r n r P \lbrace X_1=n_1,X_2=n_2, \ldots ,X_r=n_r \rbrace ={n! \over n_1!n_2! \ldots n_r!} P{ ^{n_1}_1 }P{ ^{n_2}_2 } \ldots P{ ^{n_r}_r } P{X1=n1,X2=n2,,Xr=nr}=n1!n2!nr!n!P1n1P2n2Prnr
其中, n 1 + n 2 + … + n r = n n_1+n_2+ \ldots+n_r=n n1+n2++nr=n
P 1 + P 2 + … + P r = 1 P_1+P_2+ \ldots+P_r=1 P1+P2++Pr=1
本文假定主题分布和词分布均服从与多项式分布。这是由于:一篇文章都有一定数量的主题,且能够以一定的概率去选择某个主题。换句话说,在全部的主题数中(文章包含的和未包含的),文章所对应的主题概率大部分都为0,只有有限个主题的概率不为0,我们将每篇文章对应的主题概率看作一行,所有文章排列组合起来,可以得到一个巨大的稀疏矩阵,这与多项式的分布比较相似。所以,我们用多项式分布来拟合主题分布和词分布。

(2)狄利克雷分布 Dirichlet Distribution

狄利克雷分布是贝塔分布推广至多变量的情形,其概率密度函数如下:
在这里插入图片描述

其中,向量α是狄利克雷分布的参数;B(α)表示 Dirichlet分布的归一化常数。

(3)共轭分布 Conjugate Distribution

共轭分布式贝叶斯统计中的一个常用概念,当后验分布的概率密度函数与先验分布的概率密度函数具有相同的形式时,我们称它们为一组共轭分布。比如贝塔分布和二项分布是一组共轭分布,本文所用的狄利克雷分布和多项式分布是一组共轭先验分布。

(4)吉普斯采样 Gibbs Sampling

吉布斯采样是一种简单且广泛适用的马尔可夫链蒙特卡洛(MCMC)算法,可以看作是Metropolis-Hastings算法的一个特例。吉布斯采样适用于联合分布未明确知道或难以直接抽样但每个变量的条件分布是已知的并且很容易(或者至少更容易)从中抽样的情况。具体来说,它交替的固定某一维度,然后通过其他维度的值来抽样该维度的值。本文的LDA主题模型是一个文档生成模型,它就是通过吉普斯采样来生成的。

LDA主题模型的代码过程

所用样本是跟研究主题相关的新闻评论。故以下处理均是针对中文文本进行的。原始数据文本如下:
在这里插入图片描述

(1)文本预处理

这一部分较常规,包括结巴库分词,添加停用词。

#这里定义停用词列表
def stopwordslist(filepath):
    stopwords = [line.strip() for line in open(filepath, 'r', encoding='utf-8').readlines()]
    return stopwords

# 对句子进行分词
def seg_sentence(sentence):
    sentence = re.sub(u'[0-9\.]+', u'', sentence)
    
    jb.add_word('今年')		# 这里是加入用户自定义的词来补充jieba词典。
    jb.add_word('成为')	    # 想要删除的词语就先把它加上然后放进停用词表
    jb.add_word('通过')
    jb.add_word('我们')
    jb.add_word('自己')
    jb.add_word('以来')
    jb.add_word('没有')
    jb.add_word('天津')
    jb.add_word('电商')
    jb.add_word('西安')
    
    sentence_seged = jb.cut(sentence.strip())
    stopwords = pd.read_csv("./stopwords.txt",index_col=False,quoting=3,sep=" ",names=['stopword'],encoding='UTF-8')
    stopwords = stopwordslist('./stopwords.txt')  # 这里加载停用词的路径
    outstr = ''
    for word in sentence_seged:
        if word not in stopwords and word.__len__()>1:
            if word != '\t':
                outstr += word
                outstr += " "
    return outstr

#这里要注意编码问题 inputs即是读入原文本,outputs即是新建一个文本,然后将处理好的文本放入
inputs = open('./text.txt','r',encoding='gb18030')


outputs = open('./text03.txt','w',encoding='gb18030')
for line in inputs:
    line_seg = seg_sentence(line)  # 这里的返回值是字符串
    outputs.write(line_seg + '\n')
outputs.close()
inputs.close()

于是,预处理完毕后的文本就被存放在outputs文本里,下一步操作直接导入outputs即可。预处理后的文本如下:
在这里插入图片描述

(2)建模和可视化

建立LDA主题模型的代码有很多,这里给出本文所用的:

#这里先导入常用库
from gensim import corpora
from gensim.models import LdaModel
from gensim.corpora import Dictionary
import codecs

train = []

fp=codecs.open(r'F:\Desktop\comments_chuli\text03.txt','r',encoding='gb18030')

data=fp.read()
fp.close()

'''
for line in data: 
    if line != '':
        line = line.split(" ")
        train.append([w for w in line])
'''
#以下几行将train列表的每个元素都生成一个列表 形成列表嵌套
train0=data.split(" ")
train=[]
for i in range(len(train0)):
    train1=[]
    train1.append(train0[i])
    train.append(train1)


dictionary = corpora.Dictionary(train)
dictionary.filter_extremes(no_below=2, no_above=0.1)

corpus = [dictionary.doc2bow(text) for text in train]

lda = LdaModel(corpus=corpus, id2word=dictionary, num_topics=10, passes=50)
#corpus是词袋,dictionary是词典
# num_topics:主题数目
# passes:训练伦次
# num_words:每个主题下输出的term的数目
#corpus指语料词典

#以下为模型的输出结果,即是每个主题下的20个特征词
#可以适当注释,节省运行时间

for topic in lda.print_topics(num_words = 20):
    termNumber = topic[0]
    print(topic[0], ':', sep='')
    listOfTerms = topic[1].split('+')
    for term in listOfTerms:
        listItems = term.split('*')
        print('  ', listItems[1], '(', listItems[0], ')', sep='')
  
    
#--------------以下是代码可视化部分------------------------------

#将可视化结果保存到lda001的网页上
import gensim
from gensim import models
import pyLDAvis.gensim_models
pyLDAvis.enable_notebook()
def lda_vis():
    #dictionary = gensim.corpora.Dictionary.load('lda.dict')
    #corpus = gensim.corpora.MmCorpus('lda.mm')
    #lda = models.ldamodel.LdaModel.load('lda.lda')
    vis = pyLDAvis.gensim_models.prepare(lda, corpus, dictionary)
    pyLDAvis.save_html(vis, r'F:\Desktop\comments_chuli\lda001.html')
    return 0
 
 
if __name__ == '__main__':
    lda_vis()

根据模型运行出的结果,即每个主题下的20个词语,能够倒推出来它是属于哪一类的主题,从而实现主题的挖掘。这里我截取主题数为5时,模型运行结果如下,下图是每个主题下的特征词,括号里是各特征词的权重:
在这里插入图片描述
这里需要注意的是:

(1)由于LDA模型是一个文档生成模型,需要不断取样生成主题和词语,并多次重复,建模过程比较复杂,运算时间比较长。因此,根据情况对部分代码加上注释减少它的运行时间;

(2)特别需要注意的是,用词袋模型处理数据类型一定是一个列表嵌套,对应的,上文中的train是一个列表嵌套。(这里大坑,我花了不少时间)

(3)模型优化

LDA主题模型本质上是一个文本聚类模型,并且它与K-means聚类算法一样,是需要我们手动输入聚类个数的。

当聚类个数过多,可能出现过度拟合的现象;

而聚类个数过少,可能使得不同的主题聚为一类,聚类效果不明显;

因此,对于聚类算法来说,确定合适的主题数至关重要。

LDA主题模型中,对于主题数的选取有以下五种:

A、困惑度(perplexity)

由Ble提出,主要内容是对于训练后的模型构建似然函数并求其最大值,值越大表明提取主题的质量最优,此时的困惑度最小,它可以理解为训练后的模型对于一篇文档C有哪些主题是不确定的,所以困惑度的大小与模型质量成反比

B、一致性(coherence)

由LauJH(2010)提出的,它是一种基于主题相关性的定量评价,主要内容是一致性的大小与模型质量成正比

C、可视化聚类效果

类似Excel中的气泡图,气泡的大小代表主题在文档出现的频率(权重),气泡之间的距离代表主题间的相关性。因此,气泡的间隔越远,说明模型的聚类效果越好。

D、距离度量

由曹娟(2008)提出来的,利用主题间的相似度来选择最优LDA模型的方法,通过计算主题向量的余弦距离和KL距离来计算主题的相似度,主题平均相似度最小时,模型效果最好。

E、主题方差

由关鹏(2016)提出的,用主题方差衡量主题空间的整体差异性和稳定性,当方差越大时,主题间的区分度越大,说明模型的效果越好。

本文采用前三种,以下是相关的代码:

#-------------计算困惑度-------------------------------------
import codecs
import gensim
from gensim import corpora, models
import matplotlib.pyplot as plt
import matplotlib
from nltk.tokenize import RegexpTokenizer
from nltk.stem.porter import PorterStemmer



fp = codecs.open('./text03.txt','r',encoding='gb18030')
data=fp.read()
fp.close()

#以下几行将train列表的每个元素都生成一个列表 形成列表嵌套
train0=data.split(" ")
train=[]
for i in range(len(train0)):
    train1=[]
    train1.append(train0[i])
    train.append(train1)

dictionary = corpora.Dictionary(train)  # 构建 document-term matrix
corpus = [dictionary.doc2bow(text) for text in train]
Lda = gensim.models.ldamodel.LdaModel
    
def perplexity(num_topics):
    ldamodel = Lda(corpus, num_topics=num_topics, id2word = dictionary, passes=50)  #passes为迭代次数,次数越多越精准
    print(ldamodel.print_topics(num_topics=num_topics, num_words=7))  #num_words为每个主题下的词语数量
    print(ldamodel.log_perplexity(corpus))
    return ldamodel.log_perplexity(corpus)
 
# 绘制困惑度折线图
x = range(1,10)  #主题范围数量
y = [perplexity(i) for i in x]
plt.plot(x, y)
plt.xlabel('主题数目')
plt.ylabel('困惑度大小')
plt.rcParams['font.sans-serif']=['SimHei']
matplotlib.rcParams['axes.unicode_minus']=False
plt.title('主题-困惑度变化情况')
plt.show()

#----------------计算一致性-------------------------------
#计算coherence

from gensim.models.coherencemodel import CoherenceModel
 
def coherence(num_topics):
    ldamodel = Lda(corpus, num_topics=num_topics, id2word = dictionary, passes=30,random_state = 1)
    #print(ldamodel.print_topics(num_topics=num_topics, num_words=7))
    ldacm = CoherenceModel(model=ldamodel, texts=train, dictionary=dictionary, coherence='c_v')
    #print(ldacm.get_coherence())
    return ldacm.get_coherence()

x = range(1,10)
# z = [perplexity(i) for i in x]  #如果想用困惑度就选这个
y = [coherence(i) for i in x]
plt.plot(x, y)
plt.xlabel('主题数目')
plt.ylabel('一致性大小')
plt.rcParams['font.sans-serif']=['SimHei']
matplotlib.rcParams['axes.unicode_minus']=False
plt.title('主题-一致性变化情况')
plt.show()

困惑度和模型质量成反比,一致性与模型质量成正比。困惑度和一致性具体的含义可查阅相关论文,这里只给出代码的实现方法。

根据困惑度和一致性可以初步确定主题数目的范围,再辅以可视化聚类效果,能够确定合适的主题数目.

以下是可视化聚类的效果图:
分别是K=10,5,4
Alt
在这里插入图片描述
在这里插入图片描述
从上述三幅图对比可得,当主题数为10时,主题线条过细,模型过度拟合;当主题数为5时,主题2、3仍有部分相交;而当主题数为4是,各个主题相隔较远,聚类效果明显,且在主题无相交的条件下,模型的困惑度达到最小。故本文应设定主题数为4。

LDA主题模型的优点和不足

LDA主题模型的优点是相对于传统的主题聚类算法来说。比如PLSA算法,它认为文档-主题、主题-词语的分布均是一个确定值。通俗的讲,首先以一定的概率选择某个主题,再在这个主题下以一定的概率选择某个词语,不断的重复这两步,直至生成整篇文章。

而LDA主题模型则被称为是贝叶斯版本下的PLSA。LDA模型采用了贝叶斯学派的思想,把一切参数都看作随机变量,都服从一定的概率先验分布。因此,它为主题分布和词分布都加上了两个狄利克雷的先验分布。

LDA主题模型的结构流程图具体可参考网上,很常见,这里就不再给出。

由于LDA模型采用了贝叶斯学派的思想,在小样本以及可靠性统计的情况下,运行结果相比于经典统计学派更具优势;

但是LDA模型也有它自身的局限性:比如说进行文本向量化的时候,用的是词袋模型。词袋模型只考虑了词数,未考虑词序;比如说用矩阵来描述词语,当文档和词语增加,会形成一个巨大的稀疏矩阵,可能造成维度灾难等;没有考虑一些更先进的方法,如用上下文的语义来预测下文的词语等。

需要本文数据和代码,可在本文评论区留言或者私信我。

希望对各位兄弟姐妹们有所帮助!