关键词是指能反映文本主题或者意思的词语,如论文中的Keyword字段。
关键词提取是文本挖掘领域一个很重要的部分,通过对文本提取的关键词可以窥探整个文本的主题思想,进一步应用于文本的推荐或文本的搜索。
常用的关键词提取算法:TF-IDF算法、TextRank算法
利用jieba进行关键字提取时,有两种接口。一个基于TF-IDF算法,一个基于TextRank算法。
TF-IDF算法,完全基于词频统计来计算词的权重,然后排序,再返回TopK个词作为关键字。
TextRank相对于TF-IDF,基本思路一致,也是基于统计的思想,只不过其计算词的权重时,还考虑了词的上下文(通过窗口滑动来实现),而且计算词的权重时,也考虑了相关联系词的影响。可以说,TextRank实际上是依据位置与词频来计算词的权重的。
下面,结合基于jieba源码,来分别解释两种算法的实现。
TF-IDF是关键词提取最基本、最简单易懂的方法。判断一个词再一篇文章中是否重要,一个最容易想到的衡量指标就是词频,重要的词往往在文章中出现的频率也非常高;但另一方面,不是出现次数越多的词就一定重要,因为有些词在各种文章中都频繁出现(例如:我们),那它的重要性肯定不如哪些只在某篇文章中频繁出现的词重要性强。从统计学的角度,就是给予那些不常见的词以较大的权重,而减少常见词的权重,最终得分较高的词语即为关键词。
TF和IDF计算公式如下:
词 频 ( T F ) = 某 个 词 在 文 章 中 出 现 的 次 数 文 章 的 总 词 数 词频(TF)=\frac{某个词在文章中出现的次数}{文章的总词数} 词频(TF)=文章的总词数某个词在文章中出现的次数
逆 文 档 频 率 ( I D F ) = l o g ( 语 料 库 的 文 档 总 数 包 含 该 词 的 文 档 数 + 1 ) 逆文档频率(IDF)=log(\frac{语料库的文档总数}{包含该词的文档数+1}) 逆文档频率(IDF)=log(包含该词的文档数+1语料库的文档总数)
一个词IDF值的计算是根据语料库得出的,如果一个词在语料库中越常见,那么分母就越大,IDF就越小越接近0。分母之所以要加1,是为了避免分母为0(即所有文档都不包含该词)。该词在语料库中越常见,IDF值越小。
最终得到TF-IDF值:
T F − I D F = 词 频 ( T F ) × 逆 文 档 频 率 ( I D F ) TF-IDF = 词频(TF)×逆文档频率(IDF) TF−IDF=词频(TF)×逆文档频率(IDF)
可以看出TF-IDF与一个词在文档中的出现次数成正比,与该词在整个语料库中出现次数成反比。一个词的TF-IDF值非常高,说明这个词比较少见,但是它在这篇文章中多次出现,那么这个词就非常可能是我们需要的关键词。
以文章《中国的蜜蜂养殖》为例,“蜜蜂”和“养殖”两个词的TF-IDF值都非常高,作为这篇文章的关键词实际上看也是非常合适的。另外“中国”这个词虽然在文章中的词频并不低“蜜蜂”和“养殖”低,但因为它在整个语料库中经常出现,导致IDF值非常低,所以不会作为文章的关键词。
import jieba.analyse
import jieba.analyse as analyse
import pandas as pd
df = pd.read_csv("./origin_data/technology_news.csv", encoding='utf-8')
df = df.dropna()
lines=df.content.values.tolist()
content = "".join(lines)
keywords = analyse.extract_tags(content, topK=30, withWeight=False, allowPOS=())
print(keywords)
[‘用户’, ‘2016’, ‘互联网’, ‘手机’, ‘平台’, ‘人工智能’, ‘百度’, ‘2017’, ‘智能’, ‘技术’, ‘数据’, ‘360’, ‘服务’, ‘直播’, ‘产品’, ‘企业’, ‘安全’, ‘视频’, ‘移动’, ‘应用’, ‘网络’, ‘行业’, ‘游戏’, ‘机器人’, ‘电商’, ‘内容’, ‘中国’, ‘领域’, ‘通过’, ‘发展’]
jieba有统计好的idf值,在 jieba/analyse/idf.txt中。
劳动防护 13.900677652
生化学 13.900677652
奥萨贝尔 13.900677652
考察队员 13.900677652
岗上 11.5027823792
倒车档 12.2912397395
代码在 jieba/analyse/tfidf.py
class IDFLoader(object):
def __init__(self, idf_path=None):
self.path = ""
self.idf_freq = {}
# 初始化idf的中位数值
self.median_idf = 0.0
if idf_path:
# 解析idf.txt
self.set_new_path(idf_path)
def set_new_path(self, new_idf_path):
if self.path != new_idf_path:
self.path = new_idf_path
content = open(new_idf_path, 'rb').read().decode('utf-8')
self.idf_freq = {}
# 解析 idf.txt,拿到词与idf的对应值,key = word,value = idf
for line in content.splitlines():
word, freq = line.strip().split(' ')
self.idf_freq[word] = float(freq)
# 取idf的中位数
self.median_idf = sorted(
self.idf_freq.values())[len(self.idf_freq) // 2]
def extract_tags(self, sentence, topK=20, withWeight=False, allowPOS=(), withFlag=False):
"""
Extract keywords from sentence using TF-IDF 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','nr'].
if the POS of w is not in this list,it will be filtered.
- withFlag: only work with allowPOS is not empty.
if True, return a list of pair(word, weight) like posseg.cut
if False, return a list of words
"""
# (1)中文分词
# 判断提取出哪些词性的关键字
if allowPOS:
allowPOS = frozenset(allowPOS)
# 如果需要提取指定词性的关键字,则先进行词性分割
words = self.postokenizer.cut(sentence)
else:
# 如果提取所有词性的关键字,则使用精确分词
words = self.tokenizer.cut(sentence)
# (2)计算词频TF
freq = {}
# 按照分词结果,统计词频
for w in words:
if allowPOS:
if w.flag not in allowPOS:
continue
elif not withFlag:
w = w.word
wc = w.word if allowPOS and withFlag else w
# 该词不能是停用词
if len(wc.strip()) < 2 or wc.lower() in self.stop_words:
continue
#统计该词出现的次数
freq[w] = freq.get(w, 0.0) + 1.0
# 计算总的词数目
total = sum(freq.values())
# (3)计算IDF
for k in freq:
kw = k.word if allowPOS and withFlag else k
# 依据tf-idf公式进行tf-idf值,作为词的权重。其中,idf是jieba通过语料库统计得到的
freq[k] *= self.idf_freq.get(kw, self.median_idf) / total
# (4)排序得到关键词集合
# 对词频做个排序,获取TopK的词
if withWeight:
tags = sorted(freq.items(), key=itemgetter(1), reverse=True)
else:
tags = sorted(freq, key=freq.__getitem__, reverse=True)
if topK:
return tags[:topK]
else:
return tags
TextRank采用图的思想,将文档中的词表示成一张无向有权图,词为图的节点,词之间的联系紧密程度体现为图的边的权值。计算词的权重等价于计算图中节点的权重。提取关键字,等价于找出图中权重排名TopK的节点。
如上图所示:有A B C D E五个词,词之间的关系使用边连接起来,词之间连接的次数作为边的权值。比如:A和C一起出现了2次,则其边的权重为2,A与B/C/E都有联系,而D仅与B有联系。
所以TextRank背后体现的思想为:与其他词关联性强的词,越重要。通俗一点就是:围着谁转,谁就重要。就像大家基本都会围着领导转一样。
图的构建分为两部分:
1)确认图的节点之间的联系
2)确认边的权值
基本思想:
算法论文: TextRank: Bringing Order into Texts
import jieba.analyse as analyse
import pandas as pd
df = pd.read_csv("./origin_data/military_news.csv", encoding='utf-8')
df = df.dropna()
lines=df.content.values.tolist()
content = "".join(lines)
print(" ".join(analyse.textrank(content, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'))))
print("---------------------我是分割线----------------")
print(" ".join(analyse.textrank(content, topK=20, withWeight=False, allowPOS=('ns', 'n'))))
中国 海军 训练 美国 部队 进行 官兵 航母 作战 任务 能力 军事 发展 工作 国家 问题 建设 导弹 编队 记者
---------------------我是分割线----------------
中国 海军 美国 部队 官兵 航母 军事 国家 任务 能力 导弹 技术 问题 日本 军队 编队 装备 系统 记者 战略
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 = tuple(self.tokenizer.cut(sentence))
# 遍历分词结果
for i, wp in enumerate(words):
# 词wp如果满足关键词备选条件,则加入图中
if self.pairfilter(wp):
# span为滑动窗口,即词的上下文,借此来实现此的共现,完成词之间的连接。
for j in xrange(i + 1, i + self.span):
if j >= len(words):
break
# 后向词也要满足备选词条件
if not self.pairfilter(words[j]):
continue
if allowPOS and withFlag:
# 共现词作为图一条边的两个节点,共现词出现的次数,作为边的权值
cm[(wp, words[j])] += 1
else:
cm[(wp.word, words[j].word)] += 1
# 将 备选词和与该词连接的词加入到graph中,即完成graph的构造
for terms, w in cm.items():
g.addEdge(terms[0], terms[1], w)
# 调用graph的rank接口,完成TextRank算法的计算,即计算出各节点的权重
nodes_rank = g.rank()
if withWeight:
# 对graph中的阶段的权重进行排序
tags = sorted(nodes_rank.items(), key=itemgetter(1), reverse=True)
else:
tags = sorted(nodes_rank, key=nodes_rank.__getitem__, reverse=True)
if topK:
return tags[:topK]
else:
return tags
def rank(self):
ws = defaultdict(float)
outSum = defaultdict(float)
# 计算初始化节点的weight值
wsdef = 1.0 / (len(self.graph) or 1.0)
# 初始化各个节点的weight值,并计算各个节点的出度数目
for n, out in self.graph.items():
ws[n] = wsdef
outSum[n] = sum((e[2] for e in out), 0.0)
# this line for build stable iteration
sorted_keys = sorted(self.graph.keys())
# 循环迭代10,迭代计算出各个节点的weight值
for x in xrange(10): # 10 iters
for n in sorted_keys:
s = 0
# 依据TextRank公式计算weight
for e in self.graph[n]:
s += e[2] / outSum[e[1]] * ws[e[1]]
ws[n] = (1 - self.d) + self.d * s
(min_rank, max_rank) = (sys.float_info[0], sys.float_info[3])
for w in itervalues(ws):
if w < min_rank:
min_rank = w
if w > max_rank:
max_rank = w
for n, w in ws.items():
# to unify the weights, don't *100.
ws[n] = (w - min_rank / 10.0) / (max_rank - min_rank / 10.0)
return ws
从算法原理上来看,基础都是词频统计,只是TD-IDF通过IDF来调整词频的权值,而TextRank通过上下文的连接数来调整词频的权值。TextRank通过滑动窗口的方式,来实现词的位置对词的权值的影响。
TD-IDF计算简单,运行性能更好。
参考网址:
使用python的jieba库中的TF-IDF算法进行关键词提取
【NLP】【三】jieba源码分析之关键字提取(TF-IDF/TextRank)