本文介绍一些常用的无监督关键词提取算法:TF-IDF,TextRank,主题模型算法
一、TF-IDF算法
即词频-逆文档频次算法,其基本思想是想要找到这样的词:它在一篇文档中出现的频次高(TF),即说明这篇文档很有可能围绕这个词进行说明;但是并不在多篇文档中出现(IDF),即说明这个词对文档的区分能力强。
tf(word)=(word在文档中出现的次数)/(该文档总词数),tf是就单篇文档来说的;
idf(i)=log(D/1+D(i)),idf是就多篇文档之间来说的,该式表达的含义是,D为总文档数,D(i)为出现词i的文档数量。
tf-idf值为上述相乘,在一篇文章中,计算每个词的tf-idf值,值最大的最适合作为这篇文章的关键词,关键词的数量通常不止一个,所以可以将tf-idf值由大到小排列,取前n个作为关键词。
如果是jieba自带的提取关键字,代码如下:
import jieba from jieba import analyse # 引入接口 filename = "C:/Users/Administrator/Desktop/stop_words/news/1.txt" # 进行关键词抽取 content = open(filename, encoding='utf-8').read() keywords = analyse.extract_tags(content,topK=10, withWeight=True, allowPOS=[]) # 第一个参数:待提取关键字文本 # 第二个参数:返回关键词的数量,重要性从高到低排序 # 第三个参数:是否同时返回每个关键词的权重 # 第四个参数:词性过滤,为空表示不过滤,若提供则仅返回符合词性要求的关键词 for keyword in keywords: print (keyword[0],keyword[1])
结果为:
jieba自带的抽取关键词调用方法可以只需要输入我们要抽取的文档即可,但是tfidf需要我们输入语料库,即多篇文档,才可以计算idf值
考虑停用词表和自定义词典的话,还有如下的代码可添加:
# 加载自定义idf词典 analyse.set_idf_path('./source/idf.txt.big') # 加载停用词 analyse.set_stop_words('./source/stop_words.utf8')
tfidf主要是用来弥补纯向量化的不足,我们将下面4个短文本做了词频统计:
corpus=["I come to China to travel",
"This is a car polupar in China",
"I love tea and Apple ",
"The work is to write some papers in science"]
不考虑停用词,处理后得到的词向量如下:
[[0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 2 1 0 0] [0 0 1 1 0 1 1 0 0 1 0 0 0 0 1 0 0 0 0] [1 1 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0] [0 0 0 0 0 1 1 0 1 0 1 1 0 1 0 1 0 1 1]]
如果我们直接将统计词频后的19维特征做为文本分类的输入,会发现有一些问题。比如第一个文本,我们发现"come","China"和“Travel”各出现1次,而“to“出现了两次。似乎看起来这个文本与”to“这个特征更关系紧密。但是实际上”to“是一个非常普遍的词,几乎所有的文本都会用到,因此虽然它的词频为2,但是重要性却比词频为1的"China"和“Travel”要低的多。如果我们的向量化特征仅仅用词频表示就无法反应这一点。因此我们需要进一步的预处理来反应文本的这个特征,而这个预处理就是TF-IDF。
import math import jieba import jieba.posseg as psg from gensim import corpora,models from jieba import analyse import functools #训练步骤: #步骤1,加载文档数据集,加载停用词表 #步骤2,分词,过滤停用词 #步骤3,训练模型 #停用词表加载 def get_stopword_list(path): stopword_list=[sw.replace(' ',' ') for sw in open(path,encoding='UTF-8').readlines()] return stopword_list #分词 def seg_to_list(sentence,pos=False): if not pos:#如果不进行词性标注 seg_list=jieba.cut(sentence) else: seg_list=psg.cut(sentence) return seg_list #去除干扰词 def word_filter(seg_list,pos=False): stopword_list=get_stopword_list('C:/Users/Administrator/Desktop/stop_words/stop_words.txt') filter_list=[] #对于分词结果中的每个词,如果不进行词性过滤,则所有词性标为n,然后检查这个词,如果它不在停用词表中且长度至少为2,就把它添加到分词结果中去。 for seg in seg_list: if not pos: word=seg flag='n' else: word=seg.word flag=seg.flag if not flag.startswith('n'): continue if not word in stopword_list and len(word)>1: filter_list.append(word) return filter_list #加载数据集,这个数据集是用来给目标文本计算idf值用的 def load_data(pos=False,corpus_path='E:/课程数据/learning-nlp-master/learning-nlp-master/chapter-5/corpus.txt'): doc_list=[] #原始数据集是一个文件,文件中的每一行是一个文本 for line in open(corpus_path,encoding='UTF-8'): content=line.strip() seg_list=seg_to_list(content,pos) filter_list=word_filter(seg_list,pos) doc_list.append(filter_list) return doc_list#这个结果是一个由多个子列表组成的大列表,子列表是一个文本中的过滤后的分词结果 #用来对加载进来的全部数据集计算idf值 def train_idf(doc_list): idf_dic={} tt_count=len(doc_list)#文档总数 #对于一篇文档中的每个词,计算其出现的文档数 for doc in doc_list: for word in set(doc):#已经对一篇文档进行元组处理, idf_dic[word]=idf_dic.get(word,0.0)+1 #对于每一个词,计算它的idf值 for k,v in idf_dic.items(): idf_dic[k]=math.log(tt_count/(1.0+v)) default_idf=math.log(tt_count/(1.0)) return idf_dic,default_idf #TFIDF类 class TfIdf(object): def __init__(self,idf_dic,default_idf,word_list,keyword_num):#需要输入idf值,过滤后的分词结果,关键词数量 self.word_list=word_list self.idf_dic,self.default_idf=idf_dic,default_idf self.tf_dic=self.get_tf_dic() self.keyword_num=keyword_num def get_tf_dic(self):#计算tf值,就是统计文档中该词的词频 tf_dic={} for word in self.word_list: tf_dic[word]=tf_dic.get(word,0.0)+1 tt_count=len(self.word_list) for k,v in tf_dic.items(): tf_dic[k]=float(v)/tt_count return tf_dic def get_tfidf(self):#计算tfidf值 tfidf_dic={} for word in self.word_list: idf=self.idf_dic.get(word,self.default_idf)#这里是前面根据加载的数据集计算得idf值 tf=self.tf_dic.get(word,0)这是上一个get_tf_dic函数算出来的tf值 tfidf=tf*idf tfidf_dic[word]=tfidf for k,v in sorted(tfidf_dic.items(),key=functools.cmp_to_key(cmp),reverse=True)[:self.keyword_num]: print(k+"/",end=' ') #封装tfidf def tfidf_extract(word_list,pos=False,keyword_num=10): doc_list=load_data(pos)#加载数据集 idf_dic,default_idf=train_idf(doc_list)#加载进来的数据集在这里计算idf值 tfidf_model=TfIdf(idf_dic,default_idf,word_list,keyword_num)#在类中计算tf值,利用上一行代码的idf值,相乘计算tfidf值以及关键词数量 tfidf_model.get_tfidf() #对新文档进行关键词提取步骤: #步骤1,对新文档进行分词,过滤干扰词 #步骤2,根据训练好的算法进行分词 if __name__=='__main__': text='6月19日,《2012年度中国爱心城市公益活动新闻发布会》在北京举行'+'中华社会救助基金会理事长许嘉璐到会讲话。基金会高级顾问朱发忠,全国老龄'+'办副主任朱勇,民政局社会救助司助理巡视员周萍,中华社会救助基金会副理事长耿志远,'+'重庆市民政局巡视员谭明政。靖江市人大常委会主任陈健倩,以及10余个省、市、自治区民政局' pos=False seg_list=seg_to_list(text,pos)#的得到分词结果 filter_list=word_filter(seg_list,pos)#输入分词结果,得到去干扰词后的结果 tfidf_extract(filter_list)
结果为:
二、TextRank
其他的算法都是基于一个现成的语料库,如TF-IDF需要统计每个词在语料库的多少个文档中出现过(回答了前面的问题),主题模型的关键词提取算法则是要通过对大规模文档的学习,来发现隐含的主题。但是TextRank可以不需要语料库,仅对单篇文章进行分析就可以提取该文档的关键词。
TextRank算法在关键词提取中的思想是,存在一个窗口,每个窗口中所有的词都有链接关系,当词j与词i有链接关系,且方向为j流向i时,词i的得分就由词j的分数(初始化为1)/词j的词出度来确定,对每个词进行得分计算,最后选择得分最高的n个词就可以作为文档的关键词。那么这么计算得到的关键词为什么就是关键词呢,因为一个词越重要,就说明它的父节点很多,即多次链接它,还有它如果被一个分数越高的父词所链接,也能表明它很重要。词i将所有父节点贡献给它的分数合计就是词i自身的得分,具体公式如下:
代码如下:
from jieba import analyse # 引入TextRank关键词抽取接口 filename = "C:/Users/Administrator/Desktop/stop_words/news/1.txt" # 进行关键词抽取 content = open(filename, encoding='utf-8').read() # 第一个参数:待提取关键字文本 # 第二个参数:返回关键词的数量,重要性从高到低排序 # 第三个参数:是否同时返回每个关键词的权重 # 第四个参数:词性过滤 # textrank keywords = analyse.textrank(content, topK=10, withWeight=True, allowPOS=['us','n']) for item in keywords: # 分别为关键词和相应的权重 print(item[0], item[1])
结果为:
三、主题模型
前两种算法是基于统计的算法,直接根据词和文档的关系对关键词进行抽取,对其中的语义信息没办法进行利用。主题模型认为在词和文档之间没有直接的联系,它们之间是由主题串联起来的,每个文档都有1个或多个主题,每个主题都有对应的词分布。LDA的目的就是要识别主题,即把文档—词汇矩阵变成文档—主题矩阵(分布)和主题—词汇矩阵(分布)。
由上图可以看出,原先的word-document中间多了topic进行串联。
核心数学公式为:
上式表示的含义为:文档-词汇概率=主题-词汇概率×文档-主题概率,实际上就可以理解为【想要抽取给定j文档中的词i,就先抽取给定j文档的k主题,再抽取k主题的词i即可】,就相当于,我要找到第三小学的小明同学,我就先在众多小学中找到第三小学,再找到小明同学的年级,再从年级里找到小明同学,比全校同学在一起筛选要快得多。
在一个已知的数据集中,p(wi|dj)是已知的,主题模型就是根据这个已知的信息,来分别计算主题的词分布p(wi|tk)和文档的主题分布p(tk|dj),具体得到这些概率的方法有LSA/LDA,LSA是采用SVD的方法进行分解,而LDA是通过贝叶斯方法。
四、LDA方法
大致步骤为:
- 随机初始化,即对当前文档中的每个词,随机赋予一个topic
- 对该文档的每个词,重新采用topic,知道采样收敛,这里的采样方法可以是吉布斯采样等方法
- 统计文档中的topic分布即为预估结果
详细步骤说明:
- 文档集合D,D中每个文档d看作一个单词序列<w1,w2,...,wn>,wi表示第i个单词,设具体文档d有n个单词。D中涉及的所有不同单词组成一个大集合VOCABULARY(简称VOC)。
- 对每个D中的文档d,对应到不同Topic的概率θd<pt1,...,ptk>,其中,pti表示d属于第i个topic的概率p(t|d)。计算方法pti=nti/n,其中nti表示d中属于第i个topic的词的数目,n是d中所有词的总数。比如一篇文章一共有100个单词,其中有10个词是医学类词语,那么这篇文章属于医学类主题文档的概率为1/10,这就是文档的主题分布。
- 对每个T中的topict,生成不同单词的概率φt<pw1,...,pwm>,其中,pwi表示t生成VOC中第i个单词的概率p(w|t),比如主题为医学,两个词分别是发烧和花生,则医学主题中出现发烧的概率与出现花生的概率明显有差异。计算方法pwi=Nwi/N,其中Nwi表示对应到topict的VOC中第i个单词的数目,N表示所有对应到topic的单词总数。
- 已知p(wi|dj),根据上面的公式,p(w|t)利用φt计算得到,p(t|d)利用θd计算得到
- 实际上,利用当前的θd和φt,我们可以为一个文档中的一个单词计算它对应任意一个Topic时的p(w|d),根据上文说明,可以知道topic的不同,会影响p(t|d)和p(w|t)的结果,进而影响它们的乘积p(w|d)。我们计算在每一个topic下的p(w|d)值,简单的想可以选择最大的一个,即argmax[t]p(w|d),就可以知道最大p(w|d)对应的主题t是哪一个。然后,更新主题,如果这个更新改变了这个单词初始化时所对应的Topic,就会反过来影响θd和φt,这个过程周而复始,直到收敛。
import math import jieba import jieba.posseg as psg from gensim import corpora,models from jieba import analyse import functools #训练步骤: #步骤1,加载文档数据集,加载停用词表 #步骤2,分词,过滤停用词 #步骤3,训练模型 #停用词表加载 def get_stopword_list(path): stopword_list=[sw.replace(' ',' ') for sw in open(path,encoding='UTF-8').readlines()] return stopword_list #分词 def seg_to_list(sentence,pos=False): if not pos:#如果不进行词性标注 seg_list=jieba.cut(sentence) else: seg_list=psg.cut(sentence) return seg_list #去除干扰词 def word_filter(seg_list,pos=False): stopword_list=get_stopword_list('C:/Users/Administrator/Desktop/stop_words/stop_words.txt') filter_list=[] #对于分词结果中的每个词,如果不进行词性过滤,则所有词性标为n,然后检查这个词,如果它不在停用词表中且长度至少为2,就把它添加到分词结果中去。 for seg in seg_list: if not pos: word=seg flag='n' else: word=seg.word flag=seg.flag if not flag.startswith('n'): continue if not word in stopword_list and len(word)>1: filter_list.append(word) return filter_list #加载数据集 def load_data(pos=False,corpus_path='E:/课程数据/learning-nlp-master/learning-nlp-master/chapter-5/corpus.txt'): doc_list=[] #原始数据集是一个文件,文件中的每一行是一个文本 for line in open(corpus_path,encoding='UTF-8'): content=line.strip() seg_list=seg_to_list(content,pos) filter_list=word_filter(seg_list,pos) doc_list.append(filter_list) return doc_list#这个结果是一个由多个子列表组成的大列表,子列表是一个文本中的过滤后的分词结果 def cmp(e1, e2): import numpy as np res = np.sign(e1[1] - e2[1]) if res != 0: return res else: a = e1[0] + e2[0] b = e2[0] + e1[0] if a > b: return 1 elif a == b: return 0 else: return -1 #TFIDF类 class TopicModel(object): def __init__(self,doc_list,keyword_num,model='LSI',num_topics=4):#需要输入过滤后的分词结果,关键词数量,具体模型(LSI or LDA),主题数量 self.dictionary=corpora.Dictionary(doc_list)#得到一个大小和doc_list一样的空字典 corpus=[self.dictionary.doc2bow(doc) for doc in doc_list]#使用词袋模型对分词结果中的每一个词进行向量化,结果是列表 self.tfidf_model=models.TfidfModel(corpus)#对于每个词,都用tfidf进行加权,得到加权后的向量表示 self.corpus_tfidf=self.tfidf_model[corpus] self.keyword_num=keyword_num self.num_topics=num_topics if model=='LSI': self.model=self.train_lsi() else: self.model=self.train_lda() #得到数据集的主题-词分布 word_dic=self.word_dictionary(doc_list) self.wordtopic_dic=self.get_wordtopic(word_dic) def train_lsi(self): lsi=models.LsiModel(self.corpus_tfidf,id2word=self.dictionary,num_topics=self.num_topics) return lsi def train_lda(self): lda=models.LdaModel(self.corpus_tfidf,id2word=self.dictionary,num_topics=self.num_topics) return lda def word_dictionary(self, doc_list):## 词空间构建方法和向量化方法 dictionary = [] for doc in doc_list: dictionary.extend(doc) dictionary = list(set(dictionary)) return dictionary def get_wordtopic(self,word_dic): wordtopic_dic={} for word in word_dic: single_list=[word] wordcorpus=self.tfidf_model[self.dictionary.doc2bow(single_list)] wordtopic=self.model[wordcorpus] wordtopic_dic[word]=wordtopic return wordtopic_dic def get_simword(self,word_list): sentcorpus=self.tfidf_model[self.dictionary.doc2bow(word_list)] senttopic=self.model[sentcorpus] def calsim(l1,l2): a,b,c=0.0,0.0,0.0 for t1,t2 in zip(l1,l2): x1=t1[1] x2=t2[1] a+=x1*x1 b+=x1*x1 c+=x2*x2 sim=a/math.sqrt(b*c) if not (b*c)==0.0 else 0.0 return sim ## 计算输入文本和每个词的主题分布相似度 sim_dic={} for k,v in self.wordtopic_dic.items(): if k not in word_list: continue sim=calsim(v,senttopic) sim_dic[k]=sim for k,v in sorted(sim_dic.items(),key=functools.cmp_to_key(cmp),reverse=True)[:self.keyword_num]: print(k+'/ ',end='') print() def topic_extract(word_list, model, pos=False, keyword_num=10): doc_list = load_data(pos) topic_model = TopicModel(doc_list, keyword_num, model=model) topic_model.get_simword(word_list) #对新文档进行关键词提取步骤: #步骤1,对新文档进行分词,过滤干扰词 #步骤2,根据训练好的算法进行分词 if __name__=='__main__': text='6月19日,《2012年度中国爱心城市公益活动新闻发布会》在北京举行'+'中华社会救助基金会理事长许嘉璐到会讲话。基金会高级顾问朱发忠,全国老龄'+'办副主任朱勇,民政局社会救助司助理巡视员周萍,中华社会救助基金会副理事长耿志远,'+'重庆市民政局巡视员谭明政。靖江市人大常委会主任陈健倩,以及10余个省、市、自治区民政局'+'领导及四十多家媒体参加了发布会。中华社会救助基金会秘书长时正新介绍本年度“中国爱心城' + '市”公益活动将以“爱心城市宣传、孤老关爱救助项目及第二届中国爱心城市大会”为主要内容,重庆市' + '、呼和浩特市、长沙市、太原市、蚌埠市、南昌市、汕头市、沧州市、晋江市及遵化市将会积极参加' + '这一公益活动。�中国雅虎副总编张银生和凤凰网城市频道总监赵耀分别以各自媒体优势介绍了活动' + '的宣传方案。会上,中华社会救助基金会与“第二届中国爱心城市大会”承办方晋江市签约,许嘉璐理' + '事长接受晋江市参与“百万孤老关爱行动”向国家重点扶贫地区捐赠的价值400万元的款物。晋江市人大' + '常委会主任陈健倩介绍了大会的筹备情况。' pos=True seg_list=seg_to_list(text,pos)#的得到分词结果 filter_list=word_filter(seg_list,pos)#输入分词结果,得到去干扰词后的结果 topic_extract(filter_list,'LDA',pos)
结果为: