jieba库中基于TextRank算法的关键词抽取——源代码分析(⼀)
2021SC@SDUSC
2021SC
⽂章⽬录
2021SC@SDUSC
前⾔
在⼀篇⽂章中已经提到从这篇⽂章开始会对jieba库中的源代码实现进⾏分析,⾸先从TextRank算法开始进⾏,具体算法内容及源代码分析如下:
⼀、TextRank算法是什么?
jieba库中⽤于关键词提取的算法主要有两种,⼀种是TF-IDF算法,⼀种是TextRank算法。其中Text Rank算法是基于⽤于解决⽹页排名的PageRank算法。
TextRank算法思想通过词之间的相邻关系构建⽹络,然后⽤PageRank迭代计算每个节点的rank值,排序r
ank值即可得到关键词。
算法可以这样理解:
1.每个单词有⼀个rank值,rank值越⾼代表其重要性越⾼,即关键词
2.如果⼀个单词出现在很多单词后⾯的话,那么说明这个单词⽐较重要,其rank值升⾼
3.⼀个TextRank值很⾼的单词后⾯跟着的⼀个单词,那么这个单词的TextRank值会相应地因此⽽提⾼
也可以表⽰成如下公式(在PageRank的基础上):
S(vi)表⽰TextRank值,j、i在这⾥表⽰任意两个单词,Wij表⽰的是边的权重;
公式表⽰的主要意思就是:⼀个单词i的权重取决于与在i前⾯的各个点j组成的 (j,i) 这条边的权重,以及 j这个点到其他其他边的权重之和。
具体实现过程可以概括为以下三点:
1.将待抽取关键词的⽂本进⾏分词
2.以固定窗⼝⼤⼩(默认为5,通过span属性调整),词之间的共现关系,构建图
3.计算图中节点的PageRank,注意是⽆向带权图
⼆、具体实现
打开textrank.py可以看到⾥⾯主要有两个类UndirectWeightedGraph和TextRank,其实分别对应于textrank实现过程中的⼆、三步,第⼀步则是使⽤了jieba.cut⽅法,在textrank⽅法中使⽤。
类TextRank的初始化与pairfilter⽅法的定义
⾸先分析TextRank类,类中先进⾏初始化
def__init__(self):
#初始化时,默认加载分词函数tokenizer = jieba.dt以及词性标注⼯具jieba.posseg.dt,停⽤词stop_words = self.py(),#词性过滤集合pos_filt = frozenset(('ns', 'n', 'vn', 'v')),窗⼝span = 5,(("ns", "n", "vn", "v"))表⽰词性为地名、名词、动名词、动词。
self.stop_words = self.py()#停⽤词stop_words
self.pos_filt =frozenset(('ns','n','vn','v'))#词性过滤集合,"ns", "n", "vn", "v"分别表⽰词性为地名、名词、动名词、动词
self.span =5#窗⼝⼤⼩,与第⼆步中关键词与上下⽂关键词的联系有关
之后定义函数⽅法pairfilter⽤于过滤关键词不在初始化中规定的词性,和词的长度⼩于2不符合条件的关键词。
def pairfilter(self, wp):
return(wp.flag in self.pos_filt and len(wp.word.strip())>=2
and wp.word.lower()not in self.stop_words)
TextRank中分词的具体实现是对每个句⼦进⾏分词和词性标注处理,过滤掉除指定词性外的其他单词,过滤掉出现在停⽤词表的单词,过滤掉长度⼩于2的单词
⾸先我们可以看到TextRank算法的源代码如下(部分):
def textrank(self, sentence, topK=20, withWeight=False, allowPOS=('ns','n','vn','v'), withFlag=False):
"""
Extract keywords from sentence using TextRank algorithm.
Parameter:
- topK: return how many top keywords. `None` for all possible words.
- withWeight: if True, return a list of (word, weight);
if False, return a list of words.
- allowPOS: the allowed POS list eg. ['ns', 'n', 'vn', 'v'].
if the POS of w is not in this list, it will be filtered.
- withFlag: if True, return a list of pair(word, weight) like posseg.cut
if False, return a list of words
"""
self.pos_filt =frozenset(allowPOS)
g = UndirectWeightedGraph()
cm = defaultdict(int)
words =kenizer.cut(sentence))
⾸先是定义函数,然后是有关参数的介绍,然后是正式代码:
self.pos_filt =frozenset(allowPOS)
g = UndirectWeightedGraph()#定义⽆向有权图
cm = defaultdict(int)#定义共现词典
words =kenizer.cut(sentence))#分词
那么可以看到有关实现TextRank算法第⼀步——对待抽取关键词的的⽂本进⾏分词的源代码为:
words =kenizer.cut(sentence))
#这句⽤于分词
在这⾥就需要先暂停对textrank.py中具体代码的分析,先进⾏对jieba.cut,也就是textrank算法实现过程第⼀步的分析。
⽤于分词的jieba.cut
jieba.cut是⽤于分词的⽅法,其接受四个输⼊参数: 需要分词的字符串;cut_all 参数⽤来控制是否采⽤全模式;HMM 参数⽤来控制是否使⽤ HMM 模型;use_paddle 参数⽤来控制是否使⽤paddle模式下的分词模式,paddle模式采⽤延迟加载⽅式,通过enable_paddle接⼝安装paddlepaddle-tiny,并且import相关代码。
在_init_.py中可以到jieba.cut⽅法的实现代码:
def cut(self, sentence, cut_all=False, HMM=True, use_paddle=False): """
The main function that segments an entire sentence that contains
Chinese characters into separated words.
Parameter:
- sentence: The str(unicode) to be segmented.
- cut_all: Model type. True for full pattern, False for accurate pattern.            - HMM: Whether to use the Hidden Markov Model.
"""
is_paddle_installed = check_paddle_install['is_paddle_installed']
sentence = strdecode(sentence)
if use_paddle and is_paddle_installed:
# if sentence is null, it will raise core exception in paddle.
if sentence is None or len(sentence)==0:
return
import jieba.lac_small.predict as predict
results = _sent(sentence)
for sent in results:
if sent is None:
continue
yield sent
return
re_han = re_han_default
re_skip = re_skip_default
if cut_all:
cut_block = self.__cut_all
elif HMM:
cut_block = self.__cut_DAG
else:
cut_block = self.__cut_DAG_NO_HMM
blocks = re_han.split(sentence)
for blk in blocks:
if not blk:
continue
if re_han.match(blk):
for word in cut_block(blk):
yield word
else:
tmp = re_skip.split(blk)
for x in tmp:
if re_skip.match(x):
yield x
elif not cut_all:
for xx in x:
yield xx
else:
yield x
代码⽚段较长,我们可以逐步分析。
⾸先看到代码中的解释部分:
"""
The main function that segments an entire sentence that contains
Chinese characters into separated words.
Parameter:
- sentence: The str(unicode) to be segmented.
- cut_all: Model type. True for full pattern, False for accurate pattern.
- HMM: Whether to use the Hidden Markov Model.
"""
#其翻译如下:
'''
jieba分词主函数,返回generator
参数:
- sentence: 待切分⽂本.
- cut_all: 切分模式. True 全模式, False 精确模式.
- HMM: 是否使⽤隐式马尔科夫.
weight是什么词性'''
其中HMM即⽤于新词发现的隐式马尔科夫模型。
简单了解各变量含义之后对代码进⾏逐⾏分析
⾸先是第⼀部分:
is_paddle_installed = check_paddle_install['is_paddle_installed']
sentence = strdecode(sentence)
if use_paddle and is_paddle_installed:
# if sentence is null, it will raise core exception in paddle.
if sentence is None or len(sentence)==0:
return
import jieba.lac_small.predict as predict
results = _sent(sentence)
for sent in results:
if sent is None:
continue
yield sent
return
第⼀⾏中is_paddle_installed判断是否使⽤paddle模式下的分词模式,paddle模式最主要的特点是采⽤延迟加载⽅式。
第⼆⾏的strdecode是将sentence解码为Unicode。
若jieba 采⽤延迟加载,import jieba 和 jieba.Tokenizer()(⽤于新建⾃定义分词器,可⽤于同时使⽤不同词典) 不会⽴即触发词典的加载,⼀旦有必要才开始加载词典构建前缀字典。如果你想⼿⼯初始 jieba,也可以⼿动初始化,if结构中便是对使⽤paddle模式下分词的控制。
分析之后的代码可以看到与未使⽤paddle模式相⽐,paddle模式下,只使⽤了⼀层for循环,最后返回yield sent。
那么什么是yield呢?
⼀个带有 yield 的函数就是⼀个 generator,它和普通函数不同,⽣成⼀个 generator 看起来像函数调⽤,但不会执⾏任何函数代码,直到对其调⽤ next()(在 for 循环中会⾃动调⽤ next())才开始执⾏。虽然执⾏流程仍按函数的流程执⾏,但每执⾏到⼀个 yield 语句就会中断,并返回⼀个迭代值,下次执⾏时从 yield 的下⼀个语句继续执⾏。看起来就好像⼀个函数在正常执⾏的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。
yield 的好处是显⽽易见的,把⼀个函数改写为⼀个 generator 就获得了迭代能⼒,⽐起⽤类的实例保存状态来计算下⼀个 next() 的值,不仅代码简洁,⽽且执⾏流程异常清晰。
那么再通看jieba.cut全部代码可知:
jieba.cut返回的是⼀个可迭代的generator,可以使⽤ for 循环来获得分词后得到的每⼀个词语。
总结
以上就是今天要讲的内容,本⽂仅仅主要介绍了TextRank算法的第⼀步分词中的主要⽅法jieba.cut的paddle模式和其相关的yield⽅法,那么下篇博客会继续对textrank算法实现第⼀步的具体⽅法——jieba.cut源代码进⾏详细研究.