使用VSM及LSI分别对人民日报标注语料库(PFR)进行文章相似度分析
阅读数:7,280前言
人民日报语料库包含有1980年至1998年所刊登的文章,已经分词完毕且标注完所有词性。需要该文本可通过下方链接下载
链接:https://pan.baidu.com/s/1DfYr0BiYZLHMN6mMHfA8nw 密码:1bdp
VSM模型介绍
VSM称作向量空间模型。向量空间模型在程序编写上简单易懂同时拥有良好的性能,因此是计算文本相似度时最常用的一种模型。从直观上来看,VSM的整个过程可以分为以下几个主要步骤:
1. 首先对每一篇文章进行分词,获得分词后的向量
2. 汇总所有文章中的词语并计算每个词语出现的次数
3. 根据每个词语出现的次数以及每篇文章的词向量,计算每篇文章内部所有词的TFIDF值。(关于TFIDF值不作过多叙述,如需了解可以查看该链接:https://zhuanlan.zhihu.com/p/31197209)
4 通过文章之间的TFIDF向量计算向量与向量之间的夹角。夹角越小,说明相似度越高。
仅仅通过步骤的描述可能太过干涩。直观上来说,我们把每篇文章都做成了一个向量,比如在二维中,我们通过对文章A进行计算,通过一些计算方法得出了它对应的向量d。然后又将文章B通过计算转换成了向量q。那么我们在某种程度上认为,这两个向量的夹角越小,两篇文章就越相似。(可以这么想,当q向量完全与d向量重合时,两篇文章即为同一篇文章,因为只有文章相同,算出来的向量才会相同,所以夹角小说明文章相似度高)。这就是VSM的原理。
那么这里就关系到怎么讲文章转换成相应的夹角。这里涉及到了步骤1。我们对整篇文章没法分析,那我们可以把里面的词语一个一个单独拉出来。如果两篇文章都用了很多相同的词语,那么我们有理由相信它们的相似度很高。这里有人会问,那万一使用同义词呢?比如说喜爱和喜欢。实际上在这里确实没法分辨,在程序中会将这两个词认为是不相关的。(在解决上可以通过查阅同义词典等方式)。
比如某一篇文章只有一句话,内容是“我现在在写博客”,那么会转换为[‘我’, ‘现在’, ‘在’, ‘写博客’]
那么分词结束了,可是在运算过程中总不能带着这些词语跑来跑去算,这种向量也没法在数学上表示。我们最好可以将向量转换为[0, 0, 1, 1, 0, 1, 0, 1]这种方式,其中每一位代表一个特定的词语,比如说第一位是‘现在’,那么向量中该位为1说明这个向量表示的文章中包含了‘现在’这个词。如果是0就说明没有。在[0, 0, 1, 1, 0, 1, 0, 1]中我们可以认为第2、3、5、7位所代表的词语在该文章中出现了,其余的都没有出现。这样多方便。
但是又一个问题出现了,这样子我是不是得去找本汉语词典,把所有的词语都在向量里头占一个位子,这样我出现了什么词语,我才能在向量中找到相应的位子去置1?可是要是这么做了,这个向量也太长了,计算机肯定没法计算。我们可以转换一下思维,比如说我们要比较相似度的文本一共是A、B、C、D四篇,那么我们从文章里扒出来的词语肯定不会超出这四篇文章所包含的词语。我们只需要对所有的四篇文章进行分词,然后把所有采集到的词语汇总起来就可以了,让这些词语在向量中挨个占位,向量的设置就肯定够用了,也不至于长到要包含所有的中文词语。
现在向量已经能用数字来表示啦,可是现在有个问题。我分析的是人民日报的语料,那么我估计里头‘政府’、‘会议’这些词会比较常见,可能大部分文章里都有这些词。那么程序很有可能会认为他们是相似的。在世界上所有的文章中去这么判断是没有错的,人民日报内部的文章肯定相似。那么在人民日报内部我要分析文章和文章之间的相似度,这可能就是一种干扰项了,就像医院内部分析相似度时,‘治疗’可能就是一种通用词造成的干扰项。类似的还有计算机专业的‘计算机’、会计方向的‘钱’这些词等。我们有一些方法可以解决这些事,比如说我计算所有文章中出现该词的次数。比如说A、B、C、D四篇文章中出现了8次‘会议’,那么我完全可以将这个词剔除掉不参与分析。这种方式是可行的,但是在某些极端的情况下,例如A文章中出现了4次‘会议’,B文章中出现了4次‘会议’,其它两篇没有该词,那很明显这是一个判断A、B相似的非常好的条件,可是被我们排除了。第二种方式就是TFIDF,它可以通过一些简单的计算算出所有文章中出现的每个词语相对的权重。权重越小说明普遍写越高,价值也就越小。具体的原理可以在本文前面些的链接里自己看一下。在实际使用中,我们不是只用第一种方法也不是只用第二种,通常会先使用第一种方法筛除数量明显过于大的,例如在1000篇文章中某词语的频率达到1000次或者2000次以上是筛除,之后再通过TFIDF计算剩余词语的权重。
此时向量也已经有了,那么可以直接计算夹角了,通过下面的式子的出来的是cosθ。
夹角的大小可以有很多种方式去度量。这里使用cos,主要是因为当θ较大时,说明两向量相似度较低,这是cosθ值较小,反之相似时,cos较大。且cos能保证输出值在0-1之间,这样对于我们来说,我们可以很直观地看输出值来知道文档之间的相似度。值越接近1也就表示越相似。
VSM代码
引用组件:
import pandas as pd from gensim import corpora, models, similarities import time #以下三行是由于在运行中会出现一些警告 加上以后就可以消除了 import warnings warnings.filterwarnings(action='ignore',category=UserWarning,module='gensim') warnings.filterwarnings(action='ignore',category=FutureWarning,module='gensim')
下面是代码部分:
文件加载部分:
def loadData(fileName): wordArr = [] #存放词语 wordBagDict = {} #存档所有出现过的词语的字典 fr = open(fileName) #获取所有出现过的词语 存入字典中并计算使用频率 for line in fr.readlines(): lineArr = line.strip().split() for i in range(1, len(lineArr)): if lineArr[i] not in wordBagDict: wordBagDict[lineArr[i]] = 1 else: wordBagDict[lineArr[i]] += 1 print(len(wordBagDict)) #剩余词语数量:62031 #删除只出现过一次的词 for word in list(wordBagDict.keys()): if wordBagDict[word] < 2: del wordBagDict[word] print(len(wordBagDict)) #剩余词语数量:33003 #删除一些无用词 stopWord = [x[0] for x in wordBagDict.items() if ('/w' or '/y' or '/u' or '/c') in x[0]] for i in range(len(stopWord)): del wordBagDict[stopWord[i]] print(len(wordBagDict)) #剩余词语数量:32966 fr = open(fileName) curArr = [] for line in fr.readlines(): lineArr = line.strip().split() if lineArr != []: for word in lineArr: # print(word) if word in wordBagDict.keys(): curArr.append(word) # curArr[wordBagDict[word][1]] = 1 else: if curArr != []: #由于不同天份之间的文章隔两个空格 所以这里还需要检测一次 wordArr.append(curArr) curArr = [] return wordArr
输出矩阵保存部分:
def saveAsCVS(fileName, dataSet): fileNameStr = fileName + '.csv' result_csv = pd.DataFrame(dataSet) result_csv.to_csv(fileNameStr)
VSM计算部分:
def VSM(texts): #创建词袋的doc2bow模型 dictionary = corpora.Dictionary(texts) #将所有文本包含的词汇内容转换为字典 corpus = [dictionary.doc2bow(text) for text in texts] #建立每个词袋的doc2bow模型 #计算tf-idf值,并将词汇的频率替换为tf-idf值 tfidf = models.TfidfModel(corpus) corpus_tfidf = tfidf[corpus] #计算相似矩阵 similarity = similarities.Similarity('SVM_Test', corpus_tfidf, num_features=99999) # similarity.num_best = 30 # 最相近的30篇文档 #计算所有文档相似值 corpusAll = [dictionary.doc2bow(text) for text in texts[:]] corpus_tfidf = tfidf[corpusAll] result = similarity[corpus_tfidf] return result
main函数部分:
if __name__ == '__main__': start = time.time() #导入文件 texts = loadData('199801_clear_1.txt') #计算所有文本与其他文本的相似度 result = VSM(texts) # 保存 saveAsCVS('VSM_result', result) end = time.time() print(end - start)
运行结果:
人民日报语料库共三千多篇文章,输出矩阵行和列代表每篇文章,内部元素为相似度。首先最直观可以看到[0,,0]、[1,1]、…[n, n], 因为这是自己和自身进行比对的相似度,这些点的值要么是1,要么是无限接近于1,从这一反面来看,模型是正确的。
在输出矩阵中看到第22篇与第23篇相似度为0.782(图中没有),说明两文在主体内容或关键字上应该有一些共同点,将两篇文本进行一下人为比对。
第23篇有关俄罗斯的经济问题,第22篇有关和南非之间的发展以及与台湾之间的一些关系,文中主要从经济的角度来阐述。两篇文章的共同点是都在经济的方向上进行了多方位的阐述,具有一定相似度,验证了算法对于相似性检测具有一定的作用。
该输出文件下载链接:
链接:https://pan.baidu.com/s/11tYIL7NGQPWWY2cCGpjs8w 密码:354q
LSI模型介绍
LSI的准备步骤与VSM一致,也是将文章进行分词并转换为对应TFIDF词向量。唯一不同的是VSM使用向量在空间内的夹角作为相似度的度量单位,而LSI使用奇异值分解(SVD)的方式,使用的缘由就要从向量夹角的缺点说起了。
前文在VSM中说起在制作向量的时候,所有文本包含的单词构成一个向量空间的长度,那么如果是几十万篇文章呢?那包含的词语可能在千万级别,也可能更大。如果我们给每一个文章所指的向量都分配这么长的长度,显然是不利于计算。此外如果向量长度达倒百万级别,一篇文章的总词数可能才几百或者几千,那么向量中绝大多数的位置都是0,所有文章向量组成的矩阵也就极其稀疏。
所以这里就要引出SVD了,SVD认为一个极其稀疏的矩阵一定能够通过某种方式将其转换为非稀疏矩阵,此外由于稀疏矩阵的大多数位置是0,并没有存储信息。所以只要找到合适的方式,不光能缩小矩阵大小,减少稀疏度,此外也不会丢失很多的信息。
上式即为SVD公式,A就是前文所说的稀疏矩阵,它通过SVD变换编程三个矩阵相乘的方式,简单来说每个矩阵内部的元素都是原始矩阵A的一些特征,且大小小于矩阵A。因此计算结束后只需要对小型矩阵进行分析即可。由于该部分涉及较多公式(虽然公式形式都很简单),写出来也是从别人那里摘点公式之类的,所以直接给个链接吧,有兴趣的可以自行查看。
https://blog.csdn.net/xiaocong1990/article/details/54909126/
有人可能会问,其他介绍SVD的博客说了SVD的原理,明白了,但是怎么使用呢?没说在程序中怎么进行SVD分解啊。有很多的包可以在这里被使用,我使用的是gensim,可以很方便地对SVD进行计算。
由于LSI和VSM只有最后这部分不相同,所以说完以后,咱们直接上程序吧。
LSI代码:
引用组件:
import time import pandas as pd from gensim import corpora, models, similarities #以下三行是由于在运行中会出现一些警告 加上以后就可以消除了 import warnings warnings.filterwarnings(action='ignore',category=UserWarning,module='gensim') warnings.filterwarnings(action='ignore',category=FutureWarning,module='gensim')
文件加载部分:
def loadData(fileName): wordArr = [] #存放词语 wordBagDict = {} #存档所有出现过的词语的字典 fr = open(fileName) #获取所有出现过的词语 存入字典中并计算使用频率 for line in fr.readlines(): lineArr = line.strip().split() for i in range(1, len(lineArr)): if lineArr[i] not in wordBagDict: wordBagDict[lineArr[i]] = 1 else: wordBagDict[lineArr[i]] += 1 print(len(wordBagDict)) #剩余词语数量:62031 #删除只出现过一次的词 for word in list(wordBagDict.keys()): if wordBagDict[word] < 2: del wordBagDict[word] print(len(wordBagDict)) #剩余词语数量:33003 #删除一些无用词 stopWord = [x[0] for x in wordBagDict.items() if ('/w' or '/y' or '/u' or '/c') in x[0]] for i in range(len(stopWord)): del wordBagDict[stopWord[i]] print(len(wordBagDict)) #剩余词语数量:32966 fr = open(fileName) curArr = [] for line in fr.readlines(): lineArr = line.strip().split() if lineArr != []: for word in lineArr: # print(word) if word in wordBagDict.keys(): curArr.append(word) # curArr[wordBagDict[word][1]] = 1 else: if curArr != []: #由于不同天份之间的文章隔两个空格 所以这里还需要检测一次 wordArr.append(curArr) curArr = [] return wordArr
输出矩阵保存部分:
def saveAsCVS(fileName, dataSet): fileNameStr = fileName + '.csv' result_csv = pd.DataFrame(dataSet) result_csv.to_csv(fileNameStr)
LSI计算部分:
def LSI(texts): # 用于返回的相似度矩阵 returnArr = [] # 创建词袋的doc2bow模型 dictionary = corpora.Dictionary(texts) # 将所有文本包含的词汇内容转换为字典 corpus = [dictionary.doc2bow(text) for text in texts] # 建立每个词袋的doc2bow模型 # 计算tf-idf值,并将词汇的频率替换为tf-idf值 tfidf = models.TfidfModel(corpus) corpus_tfidf = tfidf[corpus] #构建LSI模型,设置主题数为2 lsi = models.LsiModel(corpus_tfidf, id2word=dictionary, num_topics=2) # lsi.print_topics(2) lsi_vector = lsi[corpus_tfidf] #建立相似矩阵,用于后续步骤计算相似度 Similar = similarities.MatrixSimilarity(lsi_vector) #对于每一个样本计算它与其他样本的相似度 for i in range(len(texts)): #将本文转换为词袋向量 vec_bow = dictionary.doc2bow(texts[i]) #将词袋向量转换为tfidf值 vec_tfidf = tfidf[vec_bow] #将tfidf向量转换为lsi向量 vec_lsi = lsi[vec_tfidf] #计算相似度 sims = Similar[vec_lsi] # print(sims) #将该文本与其他样本的相似度存入列表 returnArr.append(sims) return returnArr
main函数部分:
if __name__ == '__main__': start = time.time() #载入文件 texts = ct.loadData('199801_clear_1.txt') #计算所有文本与其他文本的相似度 result = LSI(texts) #保存 ct.saveAsCVS('LSI_result', result) end = time.time() print(end - start) #运行时长
运行结果:
LSI最直观地是可以看到在自身和自身的相似度匹配上做得很好,大部分都可直接检测为1。在数值上好像普遍要比VSM的要高一些,直接检验一下就知道对不对了。结果中显示文章0和文章2相似度很高,达到0.99,那么看一下。
好像不太对,文章0说的是国家主席的讲话,内容是过去一年的总结,而文章2写的是一场新年音乐会,而相似度却显示0.99。怎么回事呢。
这里引出了LSI模型中主题数的概念,前文提到过LSI模型中通过SVD奇异值分解的方式来缩小原始的稀疏矩阵,降低数据维度。那么降低到几维,这个目前没有一种较为统一的方法,一般使用试凑的方式来决定降低后的维数是多少,这就是主题数。前文代码中我将主题数设置为2,那么通过奇异值分解后每篇文章使用的向量都是二维的,维数过小可能造成了一些程度上的错误判断,因此我将主题数修改为4。
#构建LSI模型,设置主题数为4 lsi = models.LsiModel(corpus_tfidf, id2word=dictionary, num_topics=4)
运行结果:
可以看到文章0和2的相似度降为了0.73,于此同时发现在主题数4的情况下,文章0和1相似度很高,达到了0.81。直接看文章比对一下:
可以看到一个时新年讲话,一个是元旦献词,不管是主题还是内容都大致相同,说明主题数为4在一定程度上性能优于2。但是也不能就直接确定主题数4就是正确参数,需要多次试验不同参数然后进行检验,只有这样才能保证LSI的性能。
输出文件:
链接:https://pan.baidu.com/s/1ySc8asxd9ouw8EW47o3W-w 密码:wm46
参考博客:
https://blog.csdn.net/felomeng/article/details/4024078
https://blog.csdn.net/u010105243/article/details/53352155
https://www.cnblogs.com/pinard/p/6805861.html
https://www.jianshu.com/p/edf666d3995f
https://zhuanlan.zhihu.com/p/31197209
目前为止有一条评论