利用TF-IDF與余弦相似性自動提取關鍵詞

1 TF-IDF算法
2 代碼實現
3 余弦相似性
4 代碼實現

1 TF-IDF算法

舉個例子

假定現在有一篇長文《中國的蜜蜂養殖》,我們準備用計算機提取它的關鍵詞。
一個容易想到的思路,就是找到出現次數最多的詞。如果某個詞很重要,它應該在這篇文章中多次出現。于是,我們進行"詞頻"(Term Frequency,縮寫為TF)統計。
結果你肯定猜到了,出現次數最多的詞是----"的"、"是"、"在"----這一類最常用的詞。它們叫做"停用詞"(stop words),表示對找到結果毫無幫助、必須過濾掉的詞。
假設我們把它們都過濾掉了,只考慮剩下的有實際意義的詞。這樣又會遇到了另一個問題,我們可能發現"中國"、"蜜蜂"、"養殖"這三個詞的出現次數一樣多。這是不是意味著,作為關鍵詞,它們的重要性是一樣的?
顯然不是這樣。因為"中國"是很常見的詞,相對而言,"蜜蜂"和"養殖"不那么常見。如果這三個詞在一篇文章的出現次數一樣多,有理由認為,"蜜蜂"和"養殖"的重要程度要大于"中國",也就是說,在關鍵詞排序上面,"蜜蜂"和"養殖"應該排在"中國"的前面。
所以,我們需要一個重要性調整系數,衡量一個詞是不是常見詞。如果某個詞比較少見,但是它在這篇文章中多次出現,那么它很可能就反映了這篇文章的特性,正是我們所需要的關鍵詞。
用統計學語言表達,就是在詞頻的基礎上,要對每個詞分配一個"重要性"權重。最常見的詞("的"、"是"、"在")給予最小的權重,較常見的詞("中國")給予較小的權重,較少見的詞("蜜蜂"、"養殖")給予較大的權重。這個權重叫做"逆文檔頻率"(Inverse Document Frequency,縮寫為IDF),它的大小與一個詞的常見程度成反比。
知道了"詞頻"(TF)和"逆文檔頻率"(IDF)以后,將這兩個值相乘,就得到了一個詞的TF-IDF值。某個詞對文章的重要性越高,它的TF-IDF值就越大。所以,排在最前面的幾個詞,就是這篇文章的關鍵詞。

下面就是這個算法的細節。

  • 第一步,計算詞頻。


    image.png

    考慮到文章有長短之分,為了便于不同文章的比較,進行"詞頻"標準化。


    image.png

    或者
    image.png
  • 第二步,計算逆文檔頻率。
    這時,需要一個語料庫(corpus),用來模擬語言的使用環境。


    image.png

    如果一個詞越常見,那么分母就越大,逆文檔頻率就越小越接近0。分母之所以要加1,是為了避免分母為0(即所有文檔都不包含該詞)。log表示對得到的值取對數。

  • 第三步,計算TF-IDF。


    image.png

可以看到,TF-IDF與一個詞在文檔中的出現次數成正比,與該詞在整個語言中的出現次數成反比。所以,自動提取關鍵詞的算法就很清楚了,就是計算出文檔的每個詞的TF-IDF值,然后按降序排列,取排在最前面的幾個詞。

TF-IDF算法的優點是簡單快速,結果比較符合實際情況。缺點是,單純以"詞頻"衡量一個詞的重要性,不夠全面,有時重要的詞可能出現次數并不多。而且,這種算法無法體現詞的位置信息,出現位置靠前的詞與出現位置靠后的詞,都被視為重要性相同,這是不正確的。

2 代碼實現

實現思路:

  • 通過中文分詞器分詞
  • 統計詞頻
  • 統計擬文檔頻率
  • 計算結果,排序輸出

通過中文分詞器分詞

/**
     * 調用IKSegmenter切詞,智能切詞
     * @param 讀取的文本內容
     * @return returnStr 切詞結果,末尾加上""
     * */ 
    private static String segStr(String text) throws IOException{  
        String returnStr = "";  
        IKSegmenter ikSegmenter = new IKSegmenter(new StringReader(text), true);
        Lexeme lexeme;
        while ((lexeme = ikSegmenter.next()) != null) {
            returnStr += lexeme.getLexemeText()+" ";
        }         
        return returnStr;  
    }  

統計詞頻TF

public static HashMap<String, Double> tf(String[] cutWordResult) {  
        HashMap<String, Double> tf = new HashMap<String, Double>();// 正規化  
        int wordNum = cutWordResult.length;  
        int wordtf = 0;  
        for (int i = 0; i < wordNum; i++) {  
            wordtf = 0;  
            for (int j = 0; j < wordNum; j++) {  
                if (cutWordResult[i] != " " && i != j) {  
                    if (cutWordResult[i].equals(cutWordResult[j])) {  
                        cutWordResult[j] = " ";  
                        wordtf++;  
                    }  
                }  
            }  
            if (cutWordResult[i] != " ") {  
                tf.put(cutWordResult[i], (new Double(++wordtf)) / wordNum);  
                cutWordResult[i] = " ";  
            }  
        }  
        return tf;  
    }

計算IDF

public static Map<String, Double> idf(String dir) throws FileNotFoundException, UnsupportedEncodingException,  
    IOException {  
        // 公式IDF=log((1+|D|)/|Dt|),其中|D|表示文檔總數,|Dt|表示包含關鍵詞t的文檔數量。  
        Map<String, Double> idf = new HashMap<String, Double>();  
        List<String> located = new ArrayList<String>();  

        float Dt = 1;  
        float D = allTheNormalTF.size();// 文檔總數  
        List<String> key = fileList;// 存儲各個文檔名的List  
        Map<String, HashMap<String, Integer>> tfInIdf = allTheNormalTF;// 存儲各個文檔tf的Map  

        for (int i = 0; i < D; i++) {  
            HashMap<String, Integer> temp = tfInIdf.get(key.get(i));  
            for (String word : temp.keySet()) {  
                Dt = 1;  
                if (!(located.contains(word))) {  
                    for (int k = 0; k < D; k++) {  
                        if (k != i) {  
                            HashMap<String, Integer> temp2 = tfInIdf.get(key.get(k));  
                            if (temp2.keySet().contains(word)) {  
                                located.add(word);  
                                Dt = Dt + 1;  
                                continue;  
                            }  
                        }  
                    }  
                    idf.put(word, (Double) Math.log((1.0 + D) / Dt));  
                }  
            }  
        }  
        return idf;  
    }

計算TF-IDF = TF*IDF

public static Map<String, HashMap<String, Double>> tfidf(String RootURL_exSelectedWeb) throws IOException {  
        //Map<String, Float> singelFile = new TreeMap<String,Float>();
        Map<String, Double> idf = TfIdf.idf(RootURL_exSelectedWeb);  
        Map<String, HashMap<String, Double>> tf = TfIdf.tfOfAll(RootURL_exSelectedWeb);
        Map<String, HashMap<String, Double>> tfidf = new TreeMap<String,HashMap<String,Double>>();;

        for (String file : tf.keySet()) {//1 獲取tf的鍵(文件名) 
            HashMap<String, Double> singelFile = tf.get(file);//2獲取tf的值 (詞語,詞頻)
            for (String word : singelFile.keySet()) {//3 獲取詞語,通過詞語遍歷整個 文檔詞語,并逐一計算TF-IDF 
                singelFile.put(word, (idf.get(word)) * singelFile.get(word));
            }
            tfidf.put(file,singelFile);
        }

        return tfidf;  
    }  

    /**
     * 將詞頻和文件名稱聯系起來
     * @param dir
     * @return allTheTf(文件名稱,詞語,正規化詞頻)
     * @throws IOException
     */
    public static Map<String, HashMap<String, Double>> tfOfAll(String dir) throws IOException {  
        List<String> fileList = TfIdf.readDirs(dir);  
        for (String file : fileList) {  
            HashMap<String, Double> dict = new HashMap<String, Double>();  
            dict = TfIdf.tf(TfIdf.cutWord(file));  
            allTheTf.put(file, dict);  
        }  
        return allTheTf;  
    } 

3 余弦相似性

有些時候,除了找到關鍵詞,我們還希望找到與原文章相似的其他文章。
為了找出相似的文章,需要用到"余弦相似性"(cosine similiarity)。下面,我舉一個例子來說明,什么是"余弦相似性"。

為了簡單起見,我們先從句子著手。

句子A:我喜歡看電視,不喜歡看電影。
句子B:我不喜歡看電視,也不喜歡看電影。

請問怎樣才能計算上面兩句話的相似程度?

基本思路是:如果這兩句話的用詞越相似,它們的內容就應該越相似。因此,可以從詞頻入手,計算它們的相似程度。

  • 第一步,分詞。

句子A:我/喜歡/看/電視,不/喜歡/看/電影。
句子B:我/不/喜歡/看/電視,也/不/喜歡/看/電影。

  • 第二步,列出所有的詞。

我,喜歡,看,電視,電影,不,也。

  • 第三步,計算詞頻。

句子A:我 1,喜歡 2,看 2,電視 1,電影 1,不 1,也 0。
句子B:我 1,喜歡 2,看 2,電視 1,電影 1,不 2,也 1。

  • 第四步,寫出詞頻向量。

句子A:[1, 2, 2, 1, 1, 1, 0]
句子B:[1, 2, 2, 1, 1, 2, 1]

到這里,問題就變成了如何計算這兩個向量的相似程度。

我們可以把它們想象成空間中的兩條線段,都是從原點([0, 0, ...])出發,指向不同的方向。兩條線段之間形成一個夾角,如果夾角為0度,意味著方向相同、線段重合;如果夾角為90度,意味著形成直角,方向完全不相似;如果夾角為180度,意味著方向正好相反。
因此,我們可以通過夾角的大小,來判斷向量的相似程度。夾角越小,就代表越相似。

image.png

以二維空間為例,上圖的a和b是兩個向量,我們要計算它們的夾角θ。余弦定理告訴我們,可以用下面的公式求得:


image.png

image.png

假定a向量是[x1, y1],b向量是[x2, y2],那么可以將余弦定理改寫成下面的形式:


image.png

image.png

數學家已經證明,余弦的這種計算方法對n維向量也成立。假定A和B是兩個n維向量,A是 [A1, A2, ..., An] ,B是 [B1, B2, ..., Bn] ,則A與B的夾角θ的余弦等于:
image.png

使用這個公式,我們就可以得到,句子A與句子B的夾角的余弦。


image.png

余弦值越接近1,就表明夾角越接近0度,也就是兩個向量越相似,這就叫"余弦相似性"。所以,上面的句子A和句子B是很相似的,事實上它們的夾角大約為20.3度。

由此,我們就得到了"找出相似文章"的一種算法:

(1)使用TF-IDF算法,找出兩篇文章的關鍵詞;
(2)每篇文章各取出若干個關鍵詞(比如20個),合并成一個集合,計算每篇文章對于這個集合中的詞的詞頻(為了避免文章長度的差異,可以使用相對詞頻);
(3)生成兩篇文章各自的詞頻向量;
(4)計算兩個向量的余弦相似度,值越大就表示越相似。

"余弦相似度"是一種非常有用的算法,只要是計算兩個向量的相似程度,都可以采用它。

4 代碼實現

   private double computeSimTest_COS(Map<String, Double> testWordTFMap,  
            Map<String, Double> trainWordTFMap) {
        double mul = 0, reslut=0, aMul=0, bMul=0,amulPow=0,bmulPow=0;
        ArrayList<Double> aVector = new ArrayList<Double>();
        ArrayList<Double> bVector = new ArrayList<Double>(); 
        Set<Map.Entry<String, Double>> testWordTFMapSet = testWordTFMap.entrySet();//除K文檔外所有文檔
        Set<Map.Entry<String, Double>> testTrainWordSet = trainWordTFMap.entrySet();//初始k文檔
        //分別遍歷兩個文本向量,取出其權值
        for(Iterator<Map.Entry<String, Double>> it = testWordTFMapSet.iterator(); it.hasNext();){  
            Map.Entry<String, Double> me = it.next();    
            aVector.add(me.getValue());
        }
        for(Iterator<Map.Entry<String, Double>> it = testTrainWordSet.iterator(); it.hasNext();){
            Map.Entry<String, Double> me = it.next();
            bVector.add(me.getValue());
        }
        for(int i=0;i<aVector.size();i++){
            aMul=aVector.get(i);
            bMul=bVector.get(i);
            mul+=aMul*bMul;
            amulPow+=aMul*aMul;
            bmulPow+=bMul*bMul;
        }
        reslut=mul/(Math.sqrt(amulPow)*Math.sqrt(bmulPow));
        return reslut ;
    }

END

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容