1. 程式人生 > >hanlp關鍵詞提取演算法TextRank

hanlp關鍵詞提取演算法TextRank

技術交流qq群: 659201069

  TextRank是在Google的PageRank演算法啟發下,針對文本里的句子設計的權重演算法,目標是自動摘要。它利用投票的原理,讓每一個單詞給它的鄰居(術語稱視窗)投贊成票,票的權重取決於自己的票數。這是一個“先有雞還是先有蛋”的悖論,PageRank採用矩陣迭代收斂的方式解決了這個悖論。引用自http://www.hankcs.com/nlp/textrank-algorithm-to-extract-the-keywords-java-implementation.html。本博文通過hanlp關鍵詞提取的一個Demo,並通過圖解的方式來講解TextRank的演算法。

//長句子
        String content = "程式設計師(英文Programmer)是從事程式開發、維護的專業人員。" +
                "一般將程式設計師分為程式設計人員和程式編碼人員," +
                "但兩者的界限並不非常清楚,特別是在中國。" +
                "軟體從業人員分為初級程式設計師、高階程式設計師、系統" +
                "分析員和專案經理四大類。";

最後提取的關鍵詞是:[程式設計師, 程式, 分為, 人員, 軟體]

  下面來分析為什麼會提取出這5個關鍵詞

第一步:分詞

  把content 通過一個的分詞演算法進行分詞,這裡採用的是Viterbi演算法也就是HMM演算法,具體請參與我的另篇文章https://blog.csdn.net/zhaojianting/article/details/78194317。分詞後(當然首先應把停用詞、標點、副詞之類的去除)的結果是:
  
[程式設計師, 英文, Programmer, 從事, 程式, 開發, 維護, 專業, 人員, 程式設計師, 分為, 程式, 設計, 人員, 程式, 編碼, 人員, 界限, 並不, 非常, 清楚, 特別是在, 中國, 軟體, 從業人員, 分為, 程式設計師, 高階, 程式設計師, 系統分析員, 專案經理, 四大]

第二步:構造視窗

  hanlp的實現程式碼如下:
  

 Map<String, Set<String>> words = new TreeMap<String, Set<String>>();
        Queue<String> que = new LinkedList<String>();
        for (String w : wordList)
        {
            if (!words.containsKey(w))
            {
                words.put(w, new TreeSet<String>());
            }
            // 複雜度O(n-1)
            if (que.size() >= 5)
            {
                que.poll();
            }
            for (String qWord : que)
            {
                if (w.equals(qWord))
                {
                    continue;
                }
                //既然是鄰居,那麼關係是相互的,遍歷一遍即可
                words.get(w).add(qWord);
                words.get(qWord).add(w);
            }
            que.offer(w);
        }

  這個程式碼的功能是為分個詞構造視窗,這個詞前後各四個詞就是這個詞的視窗,如詞分詞後一個詞出現了多次,像[程式設計師],那就是把每次出現取一次視窗,然後把各次結果合併去重,最後結果是:程式設計師=[Programmer, 專業, 中國, 人員, 從業人員, 從事, 分為, 四大, 開發, 程式, 系統分析員, 維護, 英文, 設計, 軟體, 專案經理, 高階]。最後形成的視窗:
  

Map<String, Set<String>> words = 

{Programmer=[從事, 開發, 程式, 程式設計師, 維護, 英文], 專業=[人員, 從事, 分為, 開發, 程式, 程式設計師, 維護], 中國=[從業人員, 分為, 並不, 清楚, 特別是在, 程式設計師, 軟體, 非常], 人員=[專業, 分為, 並不, 開發, 清楚, 界限, 程式, 程式設計師, 維護, 編碼, 設計, 非常], 從業人員=[中國, 分為, 清楚, 特別是在, 程式設計師, 軟體, 高階], 從事=[Programmer, 專業, 開發, 程式, 程式設計師, 維護, 英文], 分為=[專業, 中國, 人員, 從業人員, 特別是在, 程式, 程式設計師, 系統分析員, 維護, 設計, 軟體, 高階], 四大=[程式設計師, 系統分析員, 專案經理, 高階], 並不=[中國, 人員, 清楚, 特別是在, 界限, 程式, 編碼, 非常], 開發=[Programmer, 專業, 人員, 從事, 程式, 程式設計師, 維護, 英文], 清楚=[中國, 人員, 從業人員, 並不, 特別是在, 界限, 軟體, 非常], 特別是在=[中國, 從業人員, 分為, 並不, 清楚, 界限, 軟體, 非常], 界限=[人員, 並不, 清楚, 特別是在, 程式, 編碼, 非常], 程式=[Programmer, 專業, 人員, 從事, 分為, 並不, 開發, 界限, 程式設計師, 維護, 編碼, 英文, 設計], 程式設計師=[Programmer, 專業, 中國, 人員, 從業人員, 從事, 分為, 四大, 開發, 程式, 系統分析員, 維護, 英文, 設計, 軟體, 專案經理, 高階], 系統分析員=[分為, 四大, 程式設計師, 專案經理, 高階], 維護=[Programmer, 專業, 人員, 從事, 分為, 開發, 程式, 程式設計師], 編碼=[人員, 並不, 界限, 程式, 設計, 非常], 英文=[Programmer, 從事, 開發, 程式, 程式設計師], 設計=[人員, 分為, 程式, 程式設計師, 編碼], 軟體=[中國, 從業人員, 分為, 清楚, 特別是在, 程式設計師, 非常, 高階], 非常=[中國, 人員, 並不, 清楚, 特別是在, 界限, 編碼, 軟體], 專案經理=[四大, 程式設計師, 系統分析員, 高階], 高階=[從業人員, 分為, 四大, 程式設計師, 系統分析員, 軟體, 專案經理]}

第三步:迭代投票

  每個詞最後的投票得分由這個詞的視窗進行多次迭代投票決定,迭代的結束條件就是大於最大迭代次數這裡是200次,或者兩輪之前某個詞的權重小於某一值這裡是0.001f。看下程式碼:
  

Map<String, Float> score = new HashMap<String, Float>();
        //依據TF來設定初值
        for (Map.Entry<String, Set<String>> entry : words.entrySet()){ 
            score.put(entry.getKey(),sigMoid(entry.getValue().size()));
        }
        System.out.println(score);
        for (int i = 0; i < max_iter; ++i)
        {
            Map<String, Float> m = new HashMap<String, Float>();
            float max_diff = 0;
            for (Map.Entry<String, Set<String>> entry : words.entrySet())
            {
                String key = entry.getKey();
                Set<String> value = entry.getValue();
                m.put(key, 1 - d);
                for (String element : value)
                {
                    int size = words.get(element).size();
                    if (key.equals(element) || size == 0) continue;
                    m.put(key, m.get(key) + d / size * (score.get(element) == null ? 0 : score.get(element)));
                }
                max_diff = Math.max(max_diff, Math.abs(m.get(key) - (score.get(key) == null ? 0 : score.get(key))));
            }
            score = m;
            if (max_diff <= min_diff) break;
        }

        System.out.println(score);
        return score;
    }

  投票的原理拿Programmer=[從事, 開發, 程式, 程式設計師, 維護, 英文],這個詞來說明,Programmer最後的得分是由[從事, 開發, 程式, 程式設計師, 維護, 英文],這6個詞依次投票決定的,每個詞投出去的分數是和他本身的權重相關的。
  

1、投票開始前每個詞初始化了一個權重,score.put(entry.getKey(),sigMoid(entry.getValue().size())),這個權重是0到1之間,公式是

//value是每個詞視窗的大小
    public static float sigMoid(float value) {
        return (float)(1d/(1d+Math.exp(-value)));
    } 

這個函式的公式和影象如下,因為value一定是大於0的,所以sigMod值屬於(0,1)
這裡寫圖片描述

初始化後的分詞是:{特別是在=0.99966466, 程式設計師=0.99999994, 編碼=0.99752736, 四大=0.98201376, 英文=0.9933072, 非常=0.99966466, 界限=0.99908894, 系統分析員=0.9933072, 從業人員=0.99908894, 程式=0.99999774, 專業=0.99908894, 專案經理=0.98201376, 設計=0.9933072, 從事=0.99908894, Programmer=0.99752736, 軟體=0.99966466, 人員=0.99999386, 清楚=0.99966466, 中國=0.99966466, 開發=0.99966466, 並不=0.99966466, 高階=0.99908894, 分為=0.99999386, 維護=0.99966466}

  進行迭代投票,第一輪投票,[Programmer, 專業, 中國, 人員, 從業人員, 從事, 分為, 四大, 開發, 程式, 系統分析員, 維護, 英文, 設計, 軟體, 專案經理, 高階]依給次*程式設計師*投票,得分如下:
  
  [Programmer][程式設計師]投票後,[]程式設計師]的得分:
這裡寫圖片描述  

[專業][程式設計師]投票

這裡寫圖片描述

 這樣[Programmer, 專業, 中國, 人員, 從業人員, 從事, 分為, 四大, 開發, 程式, 系統分析員, 維護, 英文, 設計, 軟體, 專案經理, 高階]依次給[程式設計師]投票,投完票後,再給其它的詞進行投票,本輪結束後,判斷是否達到最大迭代次數200或兩輪之間分數差值小於0.001,如果滿足則結束,否則繼續進行迭代。
 最後的投票得分是:{特別是在=1.0015739, 程式設計師=2.0620303, 編碼=0.78676623, 四大=0.6312981, 英文=0.6835063, 非常=1.0018439, 界限=0.88890904, 系統分析員=0.74232763, 從業人員=0.8993066, 程式=1.554001, 專業=0.88107216, 專案經理=0.6312981, 設計=0.6702926, 從事=0.9027207, Programmer=0.7930236, 軟體=1.0078223, 人員=1.4288887, 清楚=0.9998723, 中國=0.99726284, 開發=1.0065585, 並不=0.9968608, 高階=0.9673803, 分為=1.4548829, 維護=0.9946941},分數最高的關鍵詞就是要提取的關鍵詞