jieba源碼研讀筆記(十七) - 關鍵詞提取之TF-IDF
前言
在前篇介紹了jieba/analyse/tfidf.py
的架構,本篇將介紹該檔案中的TFIDF
類別。
TFIDF
類別的extract_tags
函數負責實現核心算法。透過allowPOS
這個參數可以選擇要使用一般的tokenizer或是用於詞性標注的tokenizer。
TF-IDF算法
參考維基百科中的tf-idf頁面:
TF代表的是term frequency,即文檔中各詞彙出現的頻率。
IDF代表的是inverse document frequency,代表詞彙在各文檔出現頻率倒數的對數值(以10為底)。
而TF-IDF值則是上述兩項的乘積。
TF-IDF值是在各詞彙及各文檔間計算的。如果詞彙i在文檔j中的TF-IDF值越大,則代表詞彙i在文檔j中越重要。
初始化
class TFIDF(KeywordExtractor):
def __init__(self, idf_path=None):
#定義兩種tokenizer,分別在兩種模式下使用
self.tokenizer = jieba.dt
self.postokenizer = jieba.posseg.dt
#self.STOP_WORDS繼承自KeywordExtractor類別
self.stop_words = self.STOP_WORDS.copy()
#DEFAULT_IDF為全局變數
#如果有傳參數到IDFLoader建構子內,
#那麼它就會自動呼叫set_new_path函數,
#來將idf_freq, median_idf這兩個屬性設定好
self.idf_loader = IDFLoader(idf_path or DEFAULT_IDF)
self.idf_freq, self.median_idf = self.idf_loader.get_idf()
set_idf_path函數
如果使用者想要換一個新的idf檔案,可以直接使用set_idf_path
函數。
它會調用IDFLoader
類別的set_new_path
函數,讀取idf.txt
這個文檔,並設定TFIDF
物件的idf_freq
及median_idf
這兩個屬性。
class TFIDF(KeywordExtractor):
# ...
def set_idf_path(self, idf_path):
new_abs_path = _get_abs_path(idf_path)
if not os.path.isfile(new_abs_path):
raise Exception("jieba: file does not exist: " + new_abs_path)
self.idf_loader.set_new_path(new_abs_path)
self.idf_freq, self.median_idf = self.idf_loader.get_idf()
extract_tags函數
jieba文檔中關於extract_tags
參數的說明:
class TFIDF(KeywordExtractor):
# ...
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:
# 參考[Python frozenset()](https://www.programiz.com/python-programming/methods/built-in/frozenset)
# The frozenset() method returns an immutable frozenset object
# initialized with elements from the given iterable.
allowPOS = frozenset(allowPOS)
# words為generator of pair(pair類別定義於jieba/posseg/__init__.py檔)
# 其中pair類別的物件具有word及flag(即詞性)兩個屬性
words = self.postokenizer.cut(sentence)
else:
# words為generator of str
words = self.tokenizer.cut(sentence)
# 計算詞頻(即TF,term frequency)
freq = {}
for w in words:
if allowPOS:
if w.flag not in allowPOS: # 僅選取存在於allowPOS中詞性的詞
continue
elif not withFlag: # 僅回傳詞彙本身
w = w.word
# 在allowPOS及withFlag皆為True的情況下,從w中取出詞彙本身,設為wc
# 如果不符上述情況,則直接將wc設為w
wc = w.word if allowPOS and withFlag else w
if len(wc.strip()) < 2 or wc.lower() in self.stop_words:
#略過長度小於等於1的詞及停用詞?
continue
freq[w] = freq.get(w, 0.0) + 1.0
# 所有詞頻的總和
total = sum(freq.values())
# 將詞頻(TF)乘上逆向文件頻率(即IDF,inverse document frequency)
for k in freq:
kw = k.word if allowPOS and withFlag else k
# 如果idf_freq字典中未記錄該詞,則以idf的中位數替代
freq[k] *= self.idf_freq.get(kw, self.median_idf) / total
# 現在freq變為詞彙出現機率乘上IDF
if withWeight:
# 回傳詞彙本身及其TF-IDF
# itemgetter(1)的參數是鍵值對(因為是sorted(freq.items()))
# 它回傳tuple的第1個元素(index從0開始),即字典的值
# 所以sorted會依value來排序
# reverse=True:由大至小排列
tags = sorted(freq.items(), key=itemgetter(1), reverse=True)
else:
# 僅回傳詞彙本身
# freq.__getitem__的參數是字典的鍵(因為是sorted(freq))
# 它回傳的是字典的值,所達到的效用是sort by value
tags = sorted(freq, key=freq.__getitem__, reverse=True)
if topK:
# 僅回傳前topK個
return tags[:topK]
else:
return tags
總結一下,extract_tags
函數會先用tokenizer
或postokenizer
分詞後,再計算各詞的詞頻。
得到詞頻後,再與idf_freq
這個字典中相對應的詞做運算,得到每個詞的TF-IDF值。
最後依據withWeight
及topK
這兩個參數來對結果做後處理再回傳。
參考連結
維基百科中的tf-idf頁面
jieba源碼研讀筆記(八) - 分詞函數入口cut及tokenizer函數
jieba源碼研讀筆記(十四) - 詞性標注函數入口