Skip to content

[NLP] 文字探勘中的 TF-IDF 技術

TF-IDF (Term Frequency – Inverse Document Frequency) 是在文字探勘、自然語言處理當中相當著名的一種文字加權方法,能夠反映出『詞彙』對於『文件』的重要性。跟著名的 Word2Vec 同樣能夠將文字轉換成向量,以供電腦進行計算。

以下,我會逐一介紹 TF-IDF 的原理、公式、以及該如何透過程式實作 TF-IDF。


TF-IDF 原理

正如前言所說,TF-IDF 常用於資訊檢索的加權,而所謂的『詞彙對於文件的重要性』可以理解成一個詞彙對於某一特定文件的權重 —— 那麼問題來了,我們該如何定義所謂的『重要性』呢?

TF-IDF 做了以下這樣的假設:

  • 一個『詞彙』越常出現在一篇『文件』中,這個『詞彙』越重要
  • 一個『詞彙』越常出現在多篇『文件』中,這個『詞彙』越不重要

舉例來說,假設我們今天有一大堆的『美國旅遊介紹文章』,而『』和『玉米』在 A 文章中都是高頻詞,但是『』經常出現在其他文章中、而『玉米』則只出現在 A 文章以及其他少數幾篇文章中。

那們我們就會透過 TF-IDF 給予的權重認定,『玉米』對於 A 文章來說是個相當重要的詞彙,能夠很好地讓 A 文章與其他文章做出區分。相對地,『』這個詞對於 A 文章來說就一點兒也不重要,因為這是大多數的文章都會出現的詞彙。

以上是 TF-IDF 基本原理,以下就來比較詳細地介紹 TF-IDF 的公式。


TF-IDF 公式

TF-IDF 通常會分成 TF 以及 IDF 兩個部份來討論:

TF 就是所謂的『詞頻』,是一個詞彙在一篇文件中的出現頻率,計算方法為『詞彙在該文件中出現次數 / 該文件中所有詞彙數』。公式如下:

分子 n<i,j> 為詞 t<i> 在文件 d<j> 中出現次數。
分母則為文件 d<j> 所有詞彙數量。

IDF 則是所謂的『逆向文件頻率』,計算方法為『log( 總文件數量 / 包含該詞彙的文件數量 )』,跟上方的原理講的是同一個道理,旨在確認某一詞彙是否會出現在許多文件中。

公式如下:

log 為取 10 為底的對數函數。
分子為所有文件數量。
分母為包含該詞彙的文件數量。
不過考慮到分母可能為 0 的情況,通常分母會再額外加 1。

然後所謂的 TF-IDF,就是將 TF 值乘上 IDF 值。


實作 TF-IDF 程式

在開始實作之前,要事先說明 Scikit-Learn 其實已經有寫好的套件了,很多時候,除非要學習或是特別做些調整,否則多半都建議直接呼叫別人寫好的工具,避免重複造輪子。

以下的範例程式會使用到 Scikit-Learn 以及其他套件,若環境中尚未安裝可以使用以下指令安裝:

pip3 install matplotlib
pip3 install pandas
pip3 install scikit-learn


然後我們便來使用 Scikit-Learn 幫我們計算 TF-IDF 吧!測試的 4 個文件為我隨手寫下的。

# coding: utf-8
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer


# Documents
doc_0 = 'Today is a nice day'
doc_1 = 'Today is a bad day'
doc_2 = 'Today I want to play all day'
doc_3 = 'I went to play all day yesterday'
doc_all = [doc_0, doc_1, doc_2, doc_3]


# TF-IDF
vectorizer = TfidfVectorizer(smooth_idf=True)
tfidf = vectorizer.fit_transform(doc_all)
result = pd.DataFrame(tfidf.toarray(), columns=vectorizer.get_feature_names())
print('Scikit-Learn:')
print(result)


Output:

以下,則是我按照公式直接寫的 TF-IDF 計算程式:

# coding: utf-8
import math
import pandas as pd
from sklearn.preprocessing import normalize


# Documents
doc_0 = 'Today is a nice day'
doc_1 = 'Today is a bad day'
doc_2 = 'Today I want to play all day'
doc_3 = 'I went to play all day yesterday'
doc_all = [doc_0, doc_1, doc_2, doc_3]
doc_all = [[word.lower() for word in doc.split() if len(word) >= 2] for doc in doc_all]


# TF
tf = dict()
for n in range(len(doc_all)):
    for word in doc_all[n]:
        if word not in tf: tf[word] = [0 for _ in doc_all]
        tf[word][n] = sum([1 for term in doc_all[n] if term == word])/len(doc_all[n])


# IDF
total_D = len(doc_all)
idf = dict()
for doc in doc_all:
    for word in doc:
        if word not in idf:
            word_idf = math.log10(total_D/sum([1 for doc in doc_all if word in doc])+1)
            idf[word] = word_idf


# TF-IDF
sorted_word = sorted(set([word for word in tf]))
tfidf = list()
for word in sorted_word:
    value = tf[word]
    value = [v*idf[word] for v in value]
    tfidf.append(value)

tfidf = normalize(tfidf, norm='l2')
results = dict()
for n in range(len(sorted_word)):
    results[sorted_word[n]] = tfidf[n]


print(pd.DataFrame(results).transpose())



Output:

會有不一樣的地方相當正常。

在 Scikit-Learn 中,TF 和 IDF 的公式計算都不是經典的,而是變體的 —— 實際上在 Scikit-Learn 中還有許多可以調整的參數細項,至於哪種在任務中好用,就得實際測試之後才能得知了。


References


Read More

Leave a Reply