1. 程式人生 > >TF-IDF入門與例項

TF-IDF入門與例項

我們對文件分析的時候,通常需要提取關鍵詞,中文分詞可以使用jieba分詞,英文通過空格和特殊字元分割即可。那麼分割之後是不是出現頻率越高這些詞就能越好代表這篇文章描述的內容呢?答案是否定的,比如英文中常見的詞a、an等,中文中常見的“的”、“你”等等。有一些詞可以通過過濾stop Word詞表去掉,但是對於領域文件分析就會遇到更復雜的情況,比如需要把100份文件分到不同的領域,提取每個領域的關鍵詞;或者需要提取每個文件具有代表性的描述關鍵詞之後,進行文件歸類。這個時候僅僅統計單詞出現的頻率就無法區別文件描述的內容了,這個時候我們還需要考慮這些詞是否在其他文件裡出現的頻率,這樣才能保證這些詞具有本文件有代表性的同時能夠區分其他文件,即TF-IDF的思想。

1. TF-IDF基本介紹

TF-IDF (全稱:Term Frequency - Inverse Document Frequency)在文字挖掘及資訊檢索中非常常用的加權統計方法。通常用來評估單詞或者單字對於一份文件在整個文件集或者語料庫中的重要程度。字詞的重要程度與單詞在文件中出現的頻率成正比,與單詞在其他文件中出現的頻率成反比。

當一個單詞在當前這篇文章裡出現的頻率比較高,但是在其他文章中出現的頻率比較低,則我們認為這個單詞具有很強的區分能力,比較適合做分類。

公式為:TF-IDF = TF * IDF

(1)TF(Term Frequency)計算方法

假設在文件d中,總得單詞數為size(d),單詞w出現的次數為count(w, d),則單詞w在d中的頻率為:

tf(w, d) = count(w, d) / size(d)

這裡為什麼要對出現的次數除去文件總的單詞數呢,這裡其實是在做歸一化,是為了保證資料結果相對於其他文件的公平性,比如長文件裡統計的某個單詞的出現次數很大概率會比短文件高,但是其並不一定能代表文件。

(2)IDF (Inverse Document Frequency)計算方法

假設在文件集D總文件數為size(D),而w在文件集的count(w, D)個文件出現次數,則單詞逆向檔案頻率為:

idf(w, D) = log(size(D) / count(w, D))

這裡大家可能會考慮count(w, D)作為除數會不會為0的情況,寫程式實現的時候要注意的。出現在文件裡的詞才會去算這個單詞的TF-IDF,那麼這個單詞至少出現在文件集的其中一個文件中,所以至少為1。

(3)一個Query與文件的匹配度計算方法

TF-IDF最常用的場景就是在搜尋引擎中進行匹配候選文件列表。那麼使用者一般輸入的是一個查詢(Query),其中包含多個單詞,經過查詢解析系統(Query Parser),會產生多個關鍵詞(Search Teram),中文用結巴,英文用空格即可。之後我們要算哪些文件與這些關鍵詞更匹配,之後根據匹配度進行排序(Ranking system),再到使用者體驗系統(User Experience)展現頁面。簡單的搜尋引擎流程就是上面的邏輯,當然很多細節我們避開不談,我們看看計算匹配度用TF-IDF怎麼得到。

假設一個查詢q中n個單詞分別為w[1]、w[2]、...、w[n],其中w[i]在文件d中相對於文件集D的TF-IDF為tf-idf(w[i], d),則這個查詢在文件d中的TF-IDF為:

tf-idf(q, d) = sum { tf-idf(w[i], d) | i=1..n } = sum { tf(w[i], d) * idf(w[i], D) | i=1..n }

通常在計算Query的權重之前,我們傾向於提前把文件集中所有關鍵詞的TF-IDF都計算出來並儲存。之後根據Query獲取文件計算時,可以直接計算Query的TF-IDF,而不用每次都計算每個詞的TF-IDF。

 

2. Python 簡單例子

讓我看下用python如何計算文件的TF-IDF的例子。下面實現了對於raw text文件計算TF-IDF之後,然後可以實現根據關鍵字搜尋top n相關文件的例子,簡單模擬搜尋引擎的小功能。(當然搜尋引擎的架構還是比較複雜的,找個時間可以聊一聊。現在很多聊天工具基本也都是:語音識別+語音分析+搜尋引擎,可見搜尋是未來的一個趨勢,也是簡化我們生活的方式。5年前做搜尋引擎的時候,大家就在討論未來的搜尋引擎是什麼,大家更多考慮基於語音的搜尋,Google、Apple和微軟領先推出了語音助手搜尋工具:Google Assistant、Siri、Cortana,緊接著國內就瘋狂的刷語音助手。現在搜尋引擎已經從傳統的網頁搜尋引擎轉換為了語音助手。)

例子比較簡單,大家看程式碼註釋就行了,不過多的講解了。用高階語言寫程式的原則:能用但中函式名和變數名錶達清楚的,就少加註釋。

#!/usr/bin/python
# encoding: UTF-8
import re
import os
import sys
from sklearn import feature_extraction
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer

class DocumentMg:
    def __init__(self, orig_file_dir, seg_file_dir, tfidf_file_dir):
        print('Document manager is initializing...')
        self.orig_file_dir = orig_file_dir
        self.seg_file_dir = seg_file_dir
        self.tfidf_file_dir = tfidf_file_dir

        if not os.path.exists(self.seg_file_dir):
            os.mkdir(self.seg_file_dir)
        if not os.path.exists(tfidf_file_dir):
            os.mkdir(tfidf_file_dir)

    # get all file in the directory
    def get_file_list(self, dir):
        file_list = []
        files = os.listdir(dir)
        for f in files:
            if f[0] == '.' or f[0] == '..':
                pass
            else:
                file_list.append(f)
        return file_list

    # keep English, digital and ' '
    def clean_eng_data(self, data):
        comp = re.compile("[^ ^a-z^A-Z^0-9]")
        return comp.sub('', data)

    # keep Chinese, English and digital and '.'
    def clean_chinese_data(self, data):
        comp = re.compile("[^\u4e00-\u9fa5^.^a-z^A-Z^0-9]")
        return comp.sub('', data)

    # split file content as word list
    def split_to_word(self, file_name):
        fi = open(self.orig_file_dir + '/' + file_name, 'r+', encoding='UTF-8')
        data = fi.read()
        fi.close()

        clean_data = self.clean_eng_data(data)
        seg_list = clean_data.split(' ')
        result = []
        for seg in seg_list:
            if (seg != ' ' and seg != '\n' and seg != '\r\n'):
                result.append(seg)

        fo = open(self.seg_file_dir + '/' + file_name, 'w+')
        fo.write(' '.join(result))
        fo.close()

    # compute tfidf and save to file
    def compute_tfidf(self):
        all_file_list = self.get_file_list(self.orig_file_dir)
        for f in all_file_list:
            self.split_to_word(f)

        corpus = []
        for fname in all_file_list:
            file_path = self.seg_file_dir + '/' + fname
            f = open(file_path, 'r+')
            content = f.read()
            f.close()
            corpus.append(content)

        vectorizer = CountVectorizer()
        transformer = TfidfTransformer()
        tfidf = transformer.fit_transform(vectorizer.fit_transform(corpus))
        word = vectorizer.get_feature_names()
        weight = tfidf.toarray()

        for i in range(len(weight)):
            tfidf_file = self.tfidf_file_dir + '/' + all_file_list[i]
            print (u'-------------Writing tf-idf result in the file ', tfidf_file, '----------------')
            f = open(tfidf_file, 'w+')
            for j in range(len(word)):
                f.write(word[j] + " " + str(weight[i][j]) + '\n')
            f.close()

    # init search engine
    def init_search_engine(self):
        self.global_weight = {}
        all_file_list = self.get_file_list(self.tfidf_file_dir)
        # if file not exist, we need regenerate. It will work on first time
        if len(all_file_list) == 0:
            doc_mg.compute_tfidf()
            all_file_list = self.get_file_list(self.tfidf_file_dir)
        for fname in all_file_list:
            file_path = self.tfidf_file_dir + '/' + fname
            f = open(file_path, 'r+')
            line = f.readline()
            file_weight = {}
            while line:
                parts = line.split(' ')
                if len(parts) != 2:
                    continue
                file_weight[parts[0]] = float(parts[1])
                line = f.readline()
            self.global_weight[fname] = file_weight
            
            f.close()

    # preprocess query
    def preprocess_query(self, query):
        clean_data = self.clean_eng_data(query)
        seg_list = clean_data.split(' ')
        result = []
        for seg in seg_list:
            if (seg != ' ' and seg != '\n' and seg != '\r\n'):
                result.append(seg)
        return result

    # search releated top n file, you can try to use min-heap to implement it.
    # but here we will use limited insertion
    def search_related_files(self, query, top_num):
        keywords = self.preprocess_query(query)
        top_docs = []
        for fname in self.global_weight:
            # calculate document weight
            fweight = 0
            for word in keywords:
                if word in self.global_weight[fname].keys():
                    fweight += self.global_weight[fname][word]

            # instert document weight
            idx = 0
            for idx in range(len(top_docs)):
                if fweight < top_docs[idx]['fweight']:
                    break
            top_docs.insert(idx, {'fname': fname, 'fweight': fweight})
            # remove exceed document weight
            if len(top_docs) > top_num:
                top_docs.remove(top_docs[0])

        return top_docs

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print('Usage: python tf-idf.py <orign file path>')
        sys.exit()
    orig_file_dir = sys.argv[1] # the folder to store your plain text files
    seg_file_dir = './tfidf_data/seg_file' # the folder to store split text files
    tfidf_file_dir = './tfidf_data/tfidf_file' # the folder to store tfidf result
    # initialize document manager
    doc_mg = DocumentMg(orig_file_dir, seg_file_dir, tfidf_file_dir)
    # initialzie search engine
    doc_mg.init_search_engine()
    query = 'understand cable bar'
    # search query and get top documents with weight
    doc_list = doc_mg.search_related_files(query, 2)
    print('query is: ', query)
    print('result is: ')
    print(doc_list)