NLP:TextRank 与 TF-IDF,原理与库使用,关键词提取

文章目录

  • TextRank
    • PageRank
    • TextRank关键词抽取
    • TextRank摘要生成
    • TextRank4ZH库的使用
  • TF-IDF
    • 原理
    • sklearn库中TF-IDF的使用
  • jieba分词用于TextRank和TF-IDF

TextRank

PageRank

  textrank借鉴大名鼎鼎的pagerank,pagerank是Google在搜索引擎中计算网页重要性的算法,这是一个基于有向图的迭代算法。在图中,每个节点表示一个网页,如果网页 V j V_j Vj有一个链接连到网页 V i V_i Vi,则在构建图时,也有一条边从 V j V_j Vj连到 V i V_i Vi。网页重要性的迭代公式如下:

S ( V i ) = ( 1 − d ) + d × Σ j ∈ I n ( V i ) 1 ∣ O u t ( V j ) ∣ × S ( V j ) S(V_i)=(1-d)+d\times \Sigma_{j\in In(V_i)}\frac{1}{|Out(V_j)|}\times S(V_j) S(Vi)=(1d)+d×ΣjIn(Vi)Out(Vj)1×S(Vj)

  其中, S ( V ) S(V) S(V)表示网页 V V V的重要性, d d d是阻尼系数,一般取0.85, I n ( V ) In(V) In(V) O u t ( V ) Out(V) Out(V)分别表示节点 V V V的入度和出度。不难看出来,上式的大体意思就是说,一个网页的重要性由接入到这个网页的其它网页的重要性决定,同时,接入的其它网页的重要性也对自己的出度取平均,在一定程度上削弱了某些具有大量外链的网页的影响。通过网页间的相互连接构建成有向图之后,对每个网页初始化一个重要性,一般可以初始化为1,然后不断的迭代。在每轮迭代中,上式左边的输出是迭代后网页 V V V的重要性,等号右边用到的临近节点的重要性全是该轮迭代前的。

  考虑到要迭代很多轮次,且上式右边每次迭代只有 S ( V j ) S(V_j) S(Vj)是不一样的,所以我们可以用矩阵模拟上面的运算,考虑下面这个简单的有向图

NLP:TextRank 与 TF-IDF,原理与库使用,关键词提取_第1张图片

邻接矩阵表述如下:

A B C
A 0 0 0
B 1 0 0
C 1 0 0

考虑到迭代公式等号右边的接入网页重要性要对出度取平均,只需对邻接矩阵执行行归一化,当然对当前例子邻接矩阵行归一化后没有任何变化,记归一化邻接矩阵用 M M M表示,初始的各网页重要性为列向量 P 0 = [ 1 , 1 , 1 ] T P_0=[1,1,1]^{T} P0=[1,1,1]T,经过第一轮迭代后,网页重要性输出为 P 1 P_1 P1

P 1 = ( 1 − d ) + d ∗ M T ∗ P 0 P_1=(1-d)+d*M^T*P_0 P1=(1d)+dMTP0

同理,迭代公式也表示为:

P n = ( 1 − d ) + d ∗ M T ∗ P n − 1 P_n=(1-d)+d*M^T*P_{n-1} Pn=(1d)+dMTPn1

经过一百次迭代后,输出 P 100 = [ 1.4595 , 0.7703 , 0.7703 ] T P_{100}=[1.4595,0.7703,0.7703]^T P100=[1.4595,0.7703,0.7703]T,A网页更重要一些,这确实也符合我们的直观感受。

TextRank关键词抽取

  textrank进行关键词抽取同上面的原理基本相同,图的节点表示成单词即可,重点是考虑边怎么连接,这里设置一个观察窗口window,window可以设置为2,且认为一个句子中相邻的window个单词,两两之间有边,注意这里的边是双向边。也就是说,对于一个文本,testrank会依据标点符号分割成句子,对每一个句子,进行分词,滤停用词,保留指定词性的词,例如名词、动词和形容词(倾斜字体表示该步骤可以省略);考虑分词间的联系,每个分词的左边window-1个词和右边window-1个词和自己有边相连,注意分词不能跨句子相连,有些blog会说,两个节点之间存在边仅当它们对应的词汇在长度为window的窗口中共现,两种理解本质上是相同的。

  构建好图之后,不断迭代到收敛就可以了。在TextRank4ZH库中,构建好图之后的迭代操作,引用了networkx库中的pagerank。

   testrank还可以进行关键词合并,在得到最重要的T个关键词后,在原始文本中进行标记,若形成相邻词组,则组合成多词关键词。例如,文本中有句子“Matlab code for plotting ambiguity function”,如果“Matlab”和“code”均属于候选关键词,则组合成“Matlab code”加入关键词序列,当然在考虑关键词的前后顺序时,如果在抽取关键词时进行了过滤停用词和保留指定词性的词的操作,需要进行恢复,也就是考虑句子最初的顺序

TextRank摘要生成

  在关键词抽取中,textrank把节点由网页换成了词,在摘要生成任务中,textrank把节点换成了句子,那么如何考虑节点之间的边,也就是句子之间的联系呢,经验来看,相邻的两个句子不一定有相似的语意。textrank通过计算句子的相似度来考虑句子的联系,论文中,相似度计算公式如下:

S i m ( S i , S j ) = ∣ { w k ∣ w k ∈ S i & w k ∈ S j } ∣ l o g ( ∣ S i ∣ ) + l o g ( S j ∣ ) Sim(S_i, S_j)=\frac{\Big|\{w_k|w_k\in S_i \& w_k \in S_j\}\Big|}{log(|S_i|)+log(S_j|)} Sim(Si,Sj)=log(Si)+log(Sj){wkwkSi&wkSj}

其中, S i m ( S i , S j ) Sim(S_i,S_j) Sim(Si,Sj)表示句子 S i S_i Si和句子 S j S_j Sj之间的相关性, w k w_k wk表示第 k k k个单词, ∣ S i ∣ |S_i| Si表示句子 S i S_i Si中的单词数。当然也可以尝试考虑其它的相似度度量方式。

  然后用相似度作为边的权值,引入pagerank的迭代公式中,修改如下:

W S ( V i ) = ( 1 − d ) + d × Σ V j ∈ I n ( V i ) w j i Σ V k ∈ O u t ( V j ) w j k W S ( V j ) WS(V_i)=(1-d)+d\times \Sigma_{V_j \in In(V_i)}\frac{w_{ji}}{\Sigma_{V_k\in Out(V_j)}w_{jk}}WS(V_j) WS(Vi)=(1d)+d×ΣVjIn(Vi)ΣVkOut(Vj)wjkwjiWS(Vj)

其中, w j i w_{ji} wji表示节点 V j V_j Vj到节点 V i V_i Vi的边的权重,可以看出来,这里所有的节点相互之间均有带权边相连,没有window的概念。

TextRank4ZH库的使用

  textrank在使用中,如果是用的python语言,可以调用TextRank4ZH库方便的使用,该库可以提取关键词,提取关键短语和进行摘要生成。所谓关键短语,就是参照关键词提取,提取出若干关键词,若原文本中存在若干个关键词相邻的情况,那么这些关键词可以构成一个关键词组,例如,在一篇介绍支持向量机的文章中,可以找到关键词支持、向量、机,通过关键词组提取,可以得到支持向量机。

  我们用下面的文本进行测试:

text = ['#嗨,早安#【雾霾天该吃的8种清肺食物】北京的雾霾已经持续了5天,不仅要注意口罩的选择和使用,还要注意饮食。雾霾干燥的天气会对肺脏造成损伤,那么这么严重的雾霾天气我们该吃什么好呢?戳图了解↓↓8种食物助你养肺。'
, '转:【古有邯郸学步,今有“邯郸差评”】邯郸涉县一张姓男子在该县新医院营业性餐厅消费后,去网上发帖评论称其“质差、价贵、量少”,被当地警方以“发布虚假信息扰乱公共秩序”为由拘留。“邯郸差评”必将成为人类21世纪法治史上的笑话。'
, '【调查丨网传央行“放水”为误读,多家银行表示三季度信贷政策没有转向】网传央行即将“开闸放水”,对银行进行窗口指导,要求各行马上寻求储备项目,包括此前受限的房贷也不再设限,要加大信贷投放。针对此,21采访调查的国有大行、股份制银行均表示,当前信贷投放计划并未改变>>|调查丨网传...']
# 提取关键字
from textrank4zh import TextRank4Keyword, TextRank4Sentence
import pandas as pd
import numpy as np

print( '关键词:' )
output = []
for i in range(len(text)):
    t = text[i]
    tr4w = TextRank4Keyword()
    tr4w.analyze(text=t, lower=True, window=2) # 一个句子中相邻的window个单词,两两之间认为有边,注意如果window小于2,会自动设为2
    tmp = {}
    for item in tr4w.get_keywords(num=5, word_min_len=1):  # 取长度大于等于1的,重要性最高的五个关键词
        if item.word != None:
            tmp[item.word] = item.weight
    output.append(tmp)
print(output)


# 输出:
"""
关键词:
[{'食物': 0.06877496866675559, 
'注意': 0.06642571238463141, 
'图': 0.06304253501497754, 
'雾': 0.0502533228198287,
'养': 0.0502533228198287},
 {'邯郸': 0.05700443557038662, 
 '营业性': 0.05071271929824561, 
 '餐厅': 0.05071271929824561, 
 '扰乱': 0.05071271929824561, 
 '公共秩序': 0.05071271929824561}, 
 {'银行': 0.06437964191495385, 
 '央行': 0.047035522222080364, 
 '信贷': 0.04645060306488038, 
 '储备': 0.035730365928703024, 
 '受限': 0.035730365928703024}]
"""
# 提取关键短语
from textrank4zh import TextRank4Keyword, TextRank4Sentence
import pandas as pd
import numpy as np

print( '关键短语:' )
output = []
for i in range(len(text)):
    t = text[i]
    tr4w = TextRank4Keyword()
    tr4w.analyze(text=t, lower=True, window=2) # 一个句子中相邻的window个单词,两两之间认为有边,注意如果window小于2,会自动设为2
    tmp = []
    for item in tr4w.get_keyphrases(keywords_num=5, min_occur_num= 1):  # 获取 keywords_num 个关键词构造的可能出现的短语,要求这个短语在原文本中至少出现的次数为min_occur_num
        tmp.append(item)
    output.append(tmp)
print(output)

# 输出
"""
关键短语:
[[], ['营业性餐厅', '扰乱公共秩序'], []]  # 只输出能够合并的短语,不能合并的关键词没有输出
"""

  提取摘要的方式类似,这里就不在赘述,详细使用方法可以参考github。

TF-IDF

原理

  Textrank提取关键词不依赖与其它文档,换句话说,就算只有一句话,Textrank也能提取出关键词,而TF-IDF需要借助其他文档才可以实现。TF-IDF(Term Frequency-Inverse Document Frequency,词频-逆文档频率)是一种十分简单且有效的关键词提取方法,几乎不涉及高深的理论,只用的一些统计知识。TF-IDF的计算十分简单,先计算TF,在计算IDF,两者相乘即可。

  TF常见的计算如下:

T F i j = n i j Σ k n k j TF_{ij}=\frac{n_{ij}}{\Sigma_{k}n_{kj}} TFij=Σknkjnij

T F i j = n i j n c j TF_{ij}=\frac{n_{ij}}{n_{cj}} TFij=ncjnij

这里介绍了两种计算TF的方法。其中, n i j n_{ij} nij表示词 i i i在文档 j j j中的出现次数, Σ k n k j \Sigma_{k}n_{kj} Σknkj是文档 j j j的总词数, n c j n_{cj} ncj是文档 j j j中出现频率最高的词的出现次数。

  IDF计算如下

I D F i = l o g ( N N i ) IDF_i = log(\frac{N}{N_i}) IDFi=log(NiN)

I D F i = l o g ( N N i + 1 ) IDF_i = log(\frac{N}{N_i+1}) IDFi=log(Ni+1N)

I D F i = l o g ( N + 1 N i + 1 ) + 1 IDF_i = log(\frac{N+1}{N_i+1})+1 IDFi=log(Ni+1N+1)+1

I D F i = l o g ( N N i ) + 1 IDF_i = log(\frac{N}{N_i})+1 IDFi=log(NiN)+1

这里介绍了四种计算IDF的方法,第一种较为少见;第二种是标准的式子;如果在sklearn.feature_extraction.text.TfidfTransformer中,默认smooth_idf=True,则是利用第三种,避免了分子分母为0的情况;如果设置smooth_idf=False,则是利用第四种。其中, N N N表示文档总数, N i N_i Ni表示包含词 i i i的文档数。

  TF很好理解,TF越高,表示词在文档中出现频率越高,对文章越重要,但是经常存在一些词在很多文档中都会出现,即使滤掉了常见的停用词,也还是会有这样的词。因此,利用IDF做一个权值修正,IDF越高,表示词在所有文档中出现的频率低,那么该词越有可能表现该文档。TF与IDF分别从单个文档和文档集的角度,利用频率计算词对文档的重要性,它们的乘积就是TF-IDF。

  TF-IDF仍有缺陷,单纯以"词频"衡量一个词的重要性,不够全面,有时重要的词可能出现次数并不多。而且,这种算法无法体现词的位置信息,出现位置靠前的词与出现位置靠后的词,都被视为重要性相同,这是不正确的。(一种解决方法是,对全文的第一段和每一段的第一句话,给予较大的权重。)

sklearn库中TF-IDF的使用

  sklearn中包好两个涉及TF-IDF的类,分别是TfidfTransformer和TfidfVectorizer。TfidfTransformer需要搭配CountVectorizer使用,CountVectorizer用于统计文档-词频 词数(token counts)矩阵,TfidfTransformer对文档-词数矩阵求TF-IDF。由于TF-IDF使用过于频繁,因此sklearn中集成了TfidfTransformer和CountVectorizer的功能,得到TfidfVectorizer类。

As tf–idf is very often used for text features, there is also another class called TfidfVectorizer that combines all the options of CountVectorizer and TfidfTransformer in a single model.

  我们看一下TfidfTransformer与CountVectorizer的搭配使用。

from sklearn.feature_extraction.text import TfidfTransformer,CountVectorizer

corpus = [  
    'This is the first document.',  
    'This is the second second document.',  
    'And the third one.',  
    'Is this the first document?',  
]  
# 将文本中的词语转换为文档-词数矩阵  
vectorizer = CountVectorizer()  # TfidfVectorizer()  
X = vectorizer.fit_transform(corpus)  
word = vectorizer.get_feature_names()  

# 计算TF-IDF
transformer = TfidfTransformer()
tfidf = transformer.fit_transform(X)

print('获取词袋中所有文本关键词:')
print(word  )
print('查看词数统计结果:')
print(X.toarray())
print('查看TF-IDF结果:')
print(tfidf.toarray())


# 输出
"""
获取词袋中所有文本关键词:
['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
查看词数统计结果:
[[0 1 1 1 0 0 1 0 1]
 [0 1 0 1 0 2 1 0 1]
 [1 0 0 0 1 0 1 1 0]
 [0 1 1 1 0 0 1 0 1]]
查看TF-IDF结果:
[[0.         0.43877674 0.54197657 0.43877674 0.         0.
  0.35872874 0.         0.43877674]
 [0.         0.27230147 0.         0.27230147 0.         0.85322574
  0.22262429 0.         0.27230147]
 [0.55280532 0.         0.         0.         0.55280532 0.
  0.28847675 0.55280532 0.        ]
 [0.         0.43877674 0.54197657 0.43877674 0.         0.
  0.35872874 0.         0.43877674]]
"""

  具体推一下计算过程,首先我们利用CountVectorizer计算出来了文档-词数矩阵,四行表示四个文档,9列表示9个词。第二个单词’document’在3个文档中出现过,因此其IDF计算如下:

I D F d o c u m e n t = l o g ( 4 + 1 3 + 1 ) + 1 = 1.2231435513142097 IDF_{document}=log(\frac{4+1}{3+1})+1=1.2231435513142097 IDFdocument=log(3+14+1)+1=1.2231435513142097

考虑第二个文档, T F d o c u m e n t TF_{document} TFdocument计算如下,这里仅仅用了词数,没有除以总词数,到最后会统一归一化:

T F d o c u m e n t = 1 TF_{document}=1 TFdocument=1

所以

T F d o c u m e n t ∗ I D F d o c u m e n t = 1.2231435513142097 TF_{document}*IDF_{document}=1.2231435513142097 TFdocumentIDFdocument=1.2231435513142097

对第二个文档的所有词计算TT*IDF后得到

t = [0, 1, 0, 1, 0, 2, 1, 0, 1]
d = np.asarray([1,3,2,3,1,1,4,1,3])+1
result = (np.log(5/d)+1)*t

r e s u l t = [ 0.    , 1.22314355 ,    0.    , 1.22314355 ,    0.    , 3.83258146    , 1.    , 0.    , 1.22314355 ] result=[0.~ ~, 1.22314355, ~~0. ~~ , 1.22314355, ~~0. ~~ , 3.83258146~~, 1. ~~ , 0. ~~ , 1.22314355] result=[0.  ,1.22314355,  0.  ,1.22314355,  0.  ,3.83258146  ,1.  ,0.  ,1.22314355]

经过L2 归一化后,得到最终的TF-IDF结果:

from sklearn.preprocessing import normalize
print(normalize(result.reshape(1,-1), norm='l2', axis=1))

[ [ 0. ,    0.27230147 ,    0. ,    0.27230147 ,    0. ,    0.85322574 ,    0.22262429 ,    0. ,    0.27230147 ] ] [[0. , ~~ 0.27230147,~~ 0., ~~ 0.27230147,~~ 0., ~~ 0.85322574,~~ 0.22262429,~~ 0., ~~ 0.27230147]] [[0.,  0.27230147,  0.,  0.27230147,  0.,  0.85322574,  0.22262429,  0.,  0.27230147]]

这个结果和上面TfidfTransformer的计算一样。

  下面看一下TfidfVectorizer类。

from sklearn.feature_extraction.text import TfidfVectorizer

corpus = [  
    'This is the first document.',  
    'This is the second second document.',  
    'And the third one.',  
    'Is this the first document?',  
]  

vectorizer = TfidfVectorizer()    
X = vectorizer.fit_transform(corpus)  
word = vectorizer.get_feature_names() 

print('获取词袋中所有文本关键词:')
print(word  )
print('查看TF-IDF结果:')
print(X.toarray())


# 输出:
"""
获取词袋中所有文本关键词:
['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
查看TF-IDF结果:
[[0.         0.43877674 0.54197657 0.43877674 0.         0.
  0.35872874 0.         0.43877674]
 [0.         0.27230147 0.         0.27230147 0.         0.85322574
  0.22262429 0.         0.27230147]
 [0.55280532 0.         0.         0.         0.55280532 0.
  0.28847675 0.55280532 0.        ]
 [0.         0.43877674 0.54197657 0.43877674 0.         0.
  0.35872874 0.         0.43877674]]
"""

  可以看到,TfidfVectorizer类的计算结果,同TfidfTransformer和CountVectorizer并列使用的结果相同。

jieba分词用于TextRank和TF-IDF

  jieba分词集成了textrank和tf-idf两种关键词提取方式,封装层次更高,使用更简单,但是可供选择的参数更少(当然可以改源码参数),下面举例来看:

import jieba
import jieba.analyse

corpus = ['#嗨,早安#【雾霾天该吃的8种清肺食物】北京的雾霾已经持续了5天,不仅要注意口罩的选择和使用,还要注意饮食。雾霾干燥的天气会对肺脏造成损伤,那么这么严重的雾霾天气我们该吃什么好呢?戳图了解↓↓8种食物助你养肺。'
, '转:【古有邯郸学步,今有“邯郸差评”】邯郸涉县一张姓男子在该县新医院营业性餐厅消费后,去网上发帖评论称其“质差、价贵、量少”,被当地警方以“发布虚假信息扰乱公共秩序”为由拘留。“邯郸差评”必将成为人类21世纪法治史上的笑话。'
, '【调查丨网传央行“放水”为误读,多家银行表示三季度信贷政策没有转向】网传央行即将“开闸放水”,对银行进行窗口指导,要求各行马上寻求储备项目,包括此前受限的房贷也不再设限,要加大信贷投放。针对此,21采访调查的国有大行、股份制银行均表示,当前信贷投放计划并未改变>>|调查丨网传...']

  目前,jieba分词使用textrank只能提取关键词,窗口(span)默认是5。

tags_textrank = []
for txt in corpus:
    tags_textrank.append(jieba.analyse.textrank(txt, topK=10, withWeight=True))
print(tags_textrank)

# 输出
"""
[[('注意', 1.0),
  ('使用', 0.9939653740290371),
  ('造成', 0.9644932611967312),
  ('肺脏', 0.9613030701189164),
  ('食物', 0.8083092604518259),
  ('清肺', 0.8047160649355167),
  ('北京', 0.800715532823081),
  ('选择', 0.7534390338077387),
  ('口罩', 0.7481765264003833),
  ('天气', 0.6465854695796378)],
 [('发布', 1.0),
  ('法治', 0.971624976314444),
  ('质差', 0.888419599863792),
  ('医院', 0.8832133652311739),
  ('评论', 0.8824439178461055),
  ('邯郸', 0.858919815946459),
  ('男子', 0.7698391144928507),
  ('成为', 0.7014653231951647),
  ('人类', 0.6987159730433887),
  ('世纪', 0.6957365770257145)],
 [('银行', 1.0),
  ('表示', 0.9545146460556392),
  ('信贷', 0.7712884620666232),
  ('投放', 0.6582689421628634),
  ('没有', 0.5312159753004774),
  ('信贷政策', 0.5297466921566801),
  ('项目', 0.5251082797754265),
  ('包括', 0.5206186990178326),
  ('寻求', 0.5062623051224235),
  ('储备', 0.5030110482157112)]]
"""

  jieba分词使用TF-IDF提取关键词,所使用的IDF为jieba分词自己维护的文档库计算出来的,当然也可以自定义文档库。

tags_tfidf = []
for txt in corpus:
    tags_tfidf.append(jieba.analyse.extract_tags(txt, topK=10, withWeight=True))
print(tags_tfidf)

# 输出
"""
[[('天气', 0.4614245674707143),
  ('食物', 0.45168255150714287),
  ('天该', 0.42695598224642856),
  ('早安', 0.4077061072214286),
  ('清肺', 0.39322521050357145),
  ('注意', 0.3810904205921429),
  ('肺脏', 0.3787443137857143),
  ('口罩', 0.3197113486285715),
  ('损伤', 0.2654635330667857),
  ('干燥', 0.2500340908817857)],
 [('邯郸', 0.663667545646923),
  ('差评', 0.6130650001487179),
  ('古有', 0.30653250007435895),
  ('邯郸学步', 0.30653250007435895),
  ('今有', 0.30653250007435895),
  ('称其', 0.30653250007435895),
  ('质差', 0.30653250007435895),
  ('价贵', 0.30653250007435895),
  ('量少', 0.30653250007435895),
  ('21', 0.30653250007435895)],
 [('网传', 0.6641537501611111),
  ('gt', 0.4427691667740741),
  ('调查', 0.30471405305277777),
  ('投放', 0.2597831855351852),
  ('银行', 0.25946870101611114),
  ('21', 0.22138458338703704),
  ('...', 0.22138458338703704),
  ('开闸放水', 0.21891177982037036),
  ('信贷', 0.21553747682407406),
  ('央行', 0.21384681226851854)]]
"""

参考文献:

使用TextRank算法为文本生成关键字和摘要:https://www.letiantian.me/2014-12-01-text-rank/

letiantian/TextRank4ZH:https://github.com/letiantian/TextRank4ZH

textrank算法原理与提取关键词、自动提取摘要PYTHON:http://www.imooc.com/article/41987?block_id=tuijian_wz

TextRank学习笔记:https://www.jianshu.com/p/ffaee5708866

通俗理解TF-IDF:https://www.jianshu.com/p/0d7b5c226f39

NLP关键字提取之TF-IDF算法:https://blog.csdn.net/qq_20989105/article/details/82685162

你可能感兴趣的:(NLP)