本文始發於個人公眾號:TechFlow,原創不易,求個關注


今天的文章和大家聊聊文字分析當中的一個簡單但又大名鼎鼎的演算法——TF-idf。說起來這個演算法是自然語言處理領域的重要演算法,但是因為它太有名了,以至於雖然我不是從事NLP領域的,但在面試的時候仍然被問過好幾次,可見這個演算法的重要性。

好在演算法本身並不困難,雖然從名字上看疑惑重重,但是一旦理解了其中的原理,一切都水到渠成,再也不怕面試的時候想不起來了。廢話不多說,我們進入正題。


演算法原理


TF-idf名字的中間用分隔號進行了分割,並且TF和idf都不像是人名,所以它其實是表明了這個演算法是由TF和idf兩個部分構成的。我們先來看TF的部分。


TF的解釋

TF的英文全稱是Term Frequency,Frequency很好理解就是頻次、頻率。而這個Term硬翻譯是項的意思,聯絡上下文,它其實是指的文本當中的單詞或者短語。所以結合起來,Term Frequency就是短語的頻率。其實如果你明白了它的意思,剩下的光憑猜測都可以猜測出一個大概。

它的意思很樸素,就是字面意思,即一個單詞在本文當中的重要性和它出現的頻率有關。

這個觀點很直觀,比如我們在網頁搜尋”TechFlow“,出來的網站當中通篇連一個”TechFlow“都沒有,顯然這次搜尋的質量很差。如果一個網站當中包含的”TechFlow“很多,那說明很有可能搜尋正確,這個網站就是我們想要的。

除此之外,它還可以反映單詞的重要程度。如果在同一個文本當中,一個Term的出現頻率比另一個大,那麼一般情況下,顯然它的重要程度也更大。

據說早期的搜尋引擎就是用的這個策略,它衡量使用者搜尋的關鍵詞在各個網頁文本當中出現的頻率。傾向於將出現頻率高的網頁排在前面,由於排名靠前的網頁能夠獲得大量的流量。所以由於利益的驅動,後來越來越多的網頁傾向於在內容當中嵌入更多的搜尋熱詞,以此來獲得更高的排名和更多的流量。相信大家也都有過類似的體會,當我們使用搜索引擎輸入某個關鍵詞,搜尋出來的網頁號稱有相關的匹配,但是當我們真正點選進去卻什麼也沒有發現,或者是滿屏的廣告。

在早期的網際網路當中存在大量這樣的網頁,它們以囊括更多的搜尋熱詞為生。以此還衍生出了一個技術工種——SEO,即search engine optimization搜尋引擎優化,專門用各種手段來替各大網頁優化搜尋引擎當中的排名。

很快搜索引擎的工程師也都發現了這個問題,也正是為了解決這個問題,才引入了IDF的概念。


IDF的概念


IDF的英文是Inverse Document Frequency,即逆文件頻率。這個概念很難翻譯,也很難直白地解釋,所以往往我們還是使用它的英文縮寫。它表達的意思也很簡單,就是越廣泛存在的Term越不重要,也就是Term的重要性和出現的廣泛性成反比。

舉個例子,最常用的”的“,”了“,”是的“這些單詞肯定廣泛出現在各個文章當中,而像是“搜尋”,“機器學習”這些短語會出現的文章可能就要少得多。顯然對於搜尋引擎或者是一些其他模型而言,這些出現更少的單詞的參考意義更大,因為往往意味著更加精準的導向。所以IDF可以簡單理解成出現廣泛程度的倒數,它的定義也很簡單:

\[\displaystyle idf_i=\log\frac{|D|}{1 + |\{j:t_i \in d_j \}|}\]

其中\(|D|\)是所有文件的數量,\(t_i\)是第i個短語,\(|\{j:t_i \in d_j \}|\)表示包含第i個短語的文件的數量。為了防止它為0,我們為它加上一個常數1。同樣,我們也可以寫出TF的公式:

\[TF(t) = \frac{TF_i}{TN_t}\]

分母的\(TN_t\)表示文章t當中包含的所有Term的數量,分子\(TF_i\)表示\(Term_i\)在文件中的數量。

我們回顧一下這兩個概念可以發現,TF衡量的是短語和文件的關係,而idf衡量的是短語和所有文件的關係。也就是說前者衡量的是短語對於某一個具體文件的重要性,而idf衡量的是短語對於所有文件的重要性。這兩者有點像是區域性和整體的關係,我們將兩者相乘就可以得到一個Term相容兩者最終得到的重要性,也就是說TF-idf是用來計算短語在某個文件中重要性的演算法。

TF-idf的演算法也很簡單,我們直接將TF和idf計算得到的取值相乘即可。

演算法的原理理解了之後,我們可以自己動手寫一個計算TF-idf的演算法,並不複雜,整個過程不超過40行:

class TFIdfCalculator:

    # 初始化方法
    def __init__(self, text=[]):
        # 自定義的文字預處理,包括停用詞過濾和分詞,歸一化等
        self.preprocessor = SimpleTextPreprocessing()
        # 防止使用者只傳了單條文字,做相容處理
        if isinstance(text, list):
            rows = self.preprocessor.preprocess(text)
        else:
            rows = self.preprocessor.preprocess([text])

        self.count_list = []
        # 使用Counter來計算詞頻
        for row in rows:
            self.count_list.append(Counter(row))

    # fit介面,初始化工作
    def fit(self, text):
        self.__init__(text)

    # 計算詞頻,即單詞出現次數除以總詞數
    # 用在初始化之後
    def tf(self, word, count):
        return count[word] / sum(count.values())

    # 計算包含單詞的文字數量
    def num_containing(self, word):
        return sum(1 for count in self.count_list if word in count)

    # 計算idf,即log(文件數除以出現次數+1)
    def idf(self, word):
        return math.log(len(self.count_list) / (1 + self.num_containing(word)))

    # 計算tfidf,即tf*idf
    def tf_idf(self, word, count_id):
        if isinstance(count_id, int) and count_id < len(self.count_list):
            return self.tf(word, self.count_list[count_id]) * self.idf(word)
        else:
            return 0.0

其中SimpleTextPreprocessing是我自己開發的一個進行文字預處理的類,包括分詞、去除停用詞以及詞性歸一化等基本操作。這些內容在之前樸素貝葉斯分類的文章當中曾經提到過,感興趣的同學可以點選下方的連結進行檢視。

機器學習基礎——樸素貝葉斯做文字分類程式碼實戰

我們來實驗一下程式碼:

tfidf = TFIdfCalculator()
tfidf.fit(['go until jurong', 'point craze go', 'cine there got amore', 'cine point until'])
print(tfidf.tf_idf('jurong', 0))
print(tfidf.tf_idf('go', 0))

我們自己建立了一些無意義的文字進行呼叫,我們計算第一條文本當中go和jurong單詞的重要程度。根據TFidf的定義,go出現在了第一條和第二條文本當中,它出現的次數更多,所以它的idf更小,並且兩者在第一條文本當中出現的詞頻一致,所以應該jurong的TFidf更大。

最後的結果也符合我們預期,jurong的TFidf是0.345,而go的TFidf是0.143。


深度思考


TFidf的原理我們都理解了,程式碼也寫出來了,看似圓滿了,但其實有一個關鍵的點被我們忽略了。有一點很奇怪,為什麼我們計算idf的時候需要對擬文字頻率這個值求log呢?雖然從結果上來看求了log之後的結果看起來更加正常,並且分佈也更加合理。但這是結果不是原因,而從原理上來說,這個log出現的原因是什麼呢?

其實在TFidf這個理論出現的早期,並沒有人想過這個問題,可以說是誤打誤撞。後來有大神從夏農資訊理論的角度給與瞭解釋,這一切才完美的自圓其說。

在之前關於交叉熵的推導文章當中,我們曾經討論過,如果存在一個事件A,它包含的資訊量是\(-\log(P(A))\),即它發生概率的對數。也就是說發生概率越小的事件,它的資訊量越大。這個log的出現是有玄機的,資訊理論的本質是將資訊量化。資訊量化的結果是bit,也就是二進位制位。我們都知道一個二進位制位能夠表示0和1兩個數字,代表了2分量的資訊。隨著bit的增多,我們能表示的資訊量也在增大,但是資訊量不是線性增長的,而是指數增長的。

舉個簡單又經典的例子,32支球隊挺近了世界盃,這其中只有一支球隊能夠獲勝。假設最終獲勝的是法國隊、西班牙隊,我們知道訊息的時候並不會驚訝。而如果獲勝的是日本隊,估計所有人會大吃一驚。這背後的原因就和資訊量有關,我們都知道雖然從表面上來看32支球隊是平等的,哪一支都有獲勝的可能,但是實際上各個球隊獲勝的概率是不同的。

假設法國隊、西班牙這種勁旅獲勝的概率是1/4,\(-\log(\frac{1}{4})=2\)那麼我們只需要2個bit就可以表示。假設日本隊獲勝的概率是1/128,那麼我們需要7個bit才能表示,顯然後者的資訊量大得多。

到這裡,大家也就明白了,我們取對數的本質是計算資訊量對應的bit的數量。bit的數量是線性的,資訊量是指數級的,也就是說我們將一個指數級的資訊量轉化成了線性的bit。對於大多數模型而言,線性的特徵更加容易擬合,這也是TFidf效果出色的本質原因。

最後,我們從資訊理論的角度解釋一下idf,假設網際網路世界當中所有的文件有\(2^{30}\)。現在使用者搜尋中美貿易戰,其中包含中國和美國的文件數量都是\(2^{14}\),那麼中國和美國這兩個詞包含的資訊量就是\(\log(\frac{2^{30}}{2^{14}})=16\),而如果包含貿易戰這個詞的文件數量只有\(2^6\),那麼貿易戰這個詞包含的資訊量就是\(\log(\frac{2^{30}}{2^6})=24\),那麼顯然,貿易戰這個詞的資訊量要比中國和美國大得多,那麼它在文件排序當中起到的作用也就應該更大。

如果你能從資訊理論的角度對TFidf的原理進行解釋,而不只是簡單地瞭解原理,那我覺得這個知識點才是真正掌握了,那麼當你在面試當中遇到自然也就能遊刃有餘了。

今天的文章就是這些,如果覺得有所收穫,請順手掃碼點個關注吧,你們的舉手之勞對我來說很重要。

相關文章