【NLP】【三】jieba原始碼分析之關鍵字提取(TF-IDF/TextRank)
【一】綜述
利用jieba進行關鍵字提取時,有兩種介面。一個基於TF-IDF演算法,一個基於TextRank演算法。TF-IDF演算法,完全基於詞頻統計來計算詞的權重,然後排序,在返回TopK個詞作為關鍵字。TextRank相對於TF-IDF,基本思路一致,也是基於統計的思想,只不過其計算詞的權重時,還考慮了詞的上下文(通過視窗滑動來實現),而且計算詞的權重時,也考慮了相關聯絡詞的影響。可以說,TextRank實際上是依據位置與詞頻來計算詞的權重的。下面,結合基於jieba原始碼,來分別解釋兩種演算法的實現。
【二】TF-IDF
1. 原理解析
假設,共有N篇文件,分別用 d1,d2,d3,,,,,,,dn來表示。
TF = 某個詞在di篇文章中出現的次數/di篇文章的總詞數 = count(W in di)/ count(di)。因此,TF計算的是單個詞在單個文件中出現的詞頻。
IDF = 總的文件數 / 出現詞W的文件數 。 IDF其實反映了詞W在文件之間的區別度。如果W在僅在一篇文件中出現,則說明可以使用W將該文件與其他文件區別開來。即IDF可以反映W的獨特性 。
TF*IDF,可以得到詞的重要性。比如: 北京和西安在同一篇文件中的詞頻均為20%,那如何估計北京是該文的關鍵字,還是西安呢?如果同時有10篇文章均提到了北京,恰好只有這篇文章提到了西安,則西安作為這篇文章的關鍵字更為合理。
2. idf.txt
jieba有統計好的idf值,在 jieba/analyse/idf.txt中。
勞動防護 13.900677652
生化學 13.900677652
奧薩貝爾 13.900677652
考察隊員 13.900677652
崗上 11.5027823792
倒車檔 12.2912397395
3. idf.txt 載入
程式碼在 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]
4. 利用tfidf演算法提取關鍵字的介面:extract_tags
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
"""
# 判斷提取出哪些詞性的關鍵字
if allowPOS:
allowPOS = frozenset(allowPOS)
# 如果需要提取指定詞性的關鍵字,則先進行詞性分割
words = self.postokenizer.cut(sentence)
else:
# 如果提取所有詞性的關鍵字,則使用精確分詞
words = self.tokenizer.cut(sentence)
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())
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
# 對詞頻做個排序,獲取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
5. jieba實現tf-idf總結
1):idf的值時通過語料庫統計得到的,所以,實際使用時,可能需要依據使用環境,替換為使用對應的語料庫統計得到的idf值。
2):需要從分詞結果中去除停用詞。
3):如果指定了僅提取指定詞性的關鍵詞,則詞性分割非常重要,詞性分割中準確程度,影響關鍵字的提取。
【三】TextRank
1. 演算法原理介紹
TextRank採用圖的思想,將文件中的詞表示成一張無向有權圖,詞為圖的節點,詞之間的聯絡緊密程度體現為圖的邊的權值。計算詞的權重等價於計算圖中節點的權重。提取關鍵字,等價於找出圖中權重排名TopK的節點。
如上圖所示:有A B C D E五個詞,詞之間的關係使用邊連線起來,詞之間連線的次數作為邊的權值。比如:A和C一起出現了2次,則其邊的權重為2,A與B/C/E都有聯絡,而D僅與B有聯絡。
所以說,TextRank背後體現的思想為:與其他詞關聯性強的詞,越重要。通俗一點就是:圍著誰轉,誰就重要。就像大家基本都會圍著領導轉一樣。
2. 圖的構建
圖的構建分為兩部分:
1):確認圖的節點之間的聯絡
2):確認邊的權值
jieba是如何做的呢?
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
3. TextRank演算法的實現
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
【四】TF-IDF與TextRank演算法的比較
1. 從演算法原理上來看,基礎都是詞頻統計,只是TD-IDF通過IDF來調整詞頻的權值,而TextRank通過上下文的連線數來調整詞頻的權值。TextRank通過滑動視窗的方式,來實現詞的位置對詞的權值的影響。
2. TD-IDF計算簡單,執行效能更好。