好久之前,我用过TFIDF作过行业关键词提取。TFIDF仅仅从词的统计信息出发,而没有充分考虑词之间的语义信息。如今本文将介绍一种考虑了相邻词的语义关系、基于图排序的关键词提取算法TextRank。html
TextRank由Mihalcea与Tarau于EMNLP'04 [1]提出来,其思想很是简单:经过词之间的相邻关系构建网络,而后用PageRank迭代计算每一个节点的rank值,排序rank值便可获得关键词。PageRank原本是用来解决网页排名的问题,网页之间的连接关系即为图的边,迭代计算公式以下:python
\[ PR(V_i) = (1-d) + d * \sum_{j \in In(V_i)} \frac{1}{|Out(V_j)|}PR(V_j) \]git
其中,\(PR(V_i)\)表示结点\(V_i\)的rank值,\(In(V_i)\)表示结点\(V_i\)的前驱结点集合,\(Out(V_j)\)表示结点\(V_j\)的后继结点集合,\(d\)为damping factor用于作平滑。github
网页之间的连接关系能够用图表示,那么怎么把一个句子(能够看做词的序列)构建成图呢?TextRank将某一个词与其前面的N个词、以及后面的N个词均具备图相邻关系(相似于N-gram语法模型)。具体实现:设置一个长度为N的滑动窗口,全部在这个窗口以内的词都视做词结点的相邻结点;则TextRank构建的词图为无向图。下图给出了由一个文档构建的词图(去掉了停用词并按词性作了筛选):算法
考虑到不一样词对可能有不一样的共现(co-occurrence),TextRank将共现做为无向图边的权值。那么,TextRank的迭代计算公式以下:json
\[ WS(V_i) = (1-d) + d * \sum_{j \in In(V_i)} \frac{w_{ji}}{\sum_{V_k \in Out(V_j)} w_{jk}} WS(V_j) \]网络
接下来将评估TextRank在关键词提取任务上的准确率、召回率与F1-Measure,并与TFIDF作对比;准确率计算公式以下:app
\[ Precision = \frac{1}{N} \sum_{i=0}^{N-1} \frac{\left|P_i \cap T_i\right|}{\left|P_i\right|} \]测试
其中,\(N\)为文档数量,\(P_i\)为文档\(i\)所提取出的关键词,\(T_i\)为文档的标注关键词。召回率与F1的计算公式以下:lua
\[ Recall = \frac{1}{N} \sum_{i=0}^{N-1} \frac{\left|P_i \cap T_i\right|}{\left|T_i\right|} \]
\[ F1 = \frac{2*Precision*Recall}{Precision + Recall} \]
测试集是由刘知远老师提供的网易新闻标注数据集,共有13702篇文档。Jieba完整地实现了关键词提取TFIDF与TextRank算法,基于Jieba-0.39的评估实验代码以下:
import jieba.analyse import json import codecs def precision_recall_fscore_support(y_true, y_pred): """ evaluate macro precision, recall and f1-score. """ doc_num = len(y_true) p_macro = 0.0 r_macro = 0.0 for i in range(doc_num): tp = 0 true_len = len(y_true[i]) pred_len = len(y_pred[i]) for w in y_pred[i]: if w in y_true[i]: tp += 1 p = 1.0 if pred_len == 0 else tp / pred_len r = 1.0 if true_len == 0 else tp / true_len p_macro += p r_macro += r p_macro /= doc_num r_macro /= doc_num return p_macro, r_macro, 2 * p_macro * r_macro / (p_macro + r_macro) file_path = 'data/163_chinese_news_dataset_2011.dat' with codecs.open(file_path, 'r', 'utf-8') as fr: y_true = [] y_pred = [] for line in fr.readlines(): d = json.loads(line) content = d['content'] true_key_words = [w for w in set(d['tags'])] y_true.append(true_key_words) # for w in true_key_words: # jieba.add_word(w) key_word_pos = ['x', 'ns', 'n', 'vn', 'v', 'l', 'j', 'nr', 'nrt', 'nt', 'nz', 'nrfg', 'm', 'i', 'an', 'f', 't', 'b', 'a', 'd', 'q', 's', 'z'] extract_key_words = jieba.analyse.extract_tags(content, topK=2, allowPOS=key_word_pos) # trank = jieba.analyse.TextRank() # trank.span = 5 # extract_key_words = trank.textrank(content, topK=2, allowPOS=key_word_pos) y_pred.append(extract_key_words) prf = precision_recall_fscore_support(y_true, y_pred) print('precision: {}'.format(prf[0])) print('recall: {}'.format(prf[1])) print('F1: {}'.format(prf[2]))
其中,每一个文档提取的关键词数为2,并按词性作过滤;span表示TextRank算法中的滑动窗口的大小。评估结果以下:
方法 | Precision | Recall | F1-Measure |
---|---|---|---|
TFIDF | 0.2697 | 0.2256 | 0.2457 |
TextRank span=5 | 0.2608 | 0.2150 | 0.2357 |
TextRank span=7 | 0.2614 | 0.2155 | 0.2363 |
若是将标注关键词添加到自定义词典,则评估结果以下:
方法 | Precision | Recall | F1-Measure |
---|---|---|---|
TFIDF | 0.3145 | 0.2713 | 0.2913 |
TextRank span=5 | 0.2887 | 0.2442 | 0.2646 |
TextRank span=7 | 0.2903 | 0.2455 | 0.2660 |
直观感觉下关键词提取结果(添加了自定义词典):
// TFIDF, TextRank, labelled ['文强', '陈洪刚'] ['文强', '陈洪刚'] {'文强', '重庆'} ['内贾德', '伊朗'] ['伊朗', '内贾德'] {'制裁', '世博', '伊朗'} ['调控', '王珏林'] ['调控', '楼市'] {'楼市', '调控'} ['罗平县', '男子'] ['男子', '罗平县'] {'被砍', '副局长', '情感纠葛'} ['佟某', '黄玉'] ['佟某', '黄现忠'] {'盲井', '伪造矿难'} ['女生', '聚众淫乱'] ['女生', '聚众淫乱'] {'聚众淫乱', '东莞', '不雅视频'} ['马英九', '和平协议'] ['马英九', '推动'] {'国台办', '马英九', '和平协议'} ['东帝汶', '巡逻艇'] ['东帝汶', '中国'] {'东帝汶', '军舰', '澳大利亚'} ['墨西哥', '警方'] ['墨西哥', '袭击'] {'枪手', '墨西哥', '打死'}
从上述两组实验结果,能够发现:
此外,因为TextRank涉及到构建词图及迭代计算,因此提取速度较慢。
[1] Rada, Mihalcea, and Paul Tarau. "TextRank: Bringing Order into Texts." empirical methods in natural language processing (2004): 404-411.