天天看點

es實戰-使用IK分詞器進行詞頻統計1. IK 分詞統計代碼2. 擷取詞雲 TopN 個詞3. IK 代碼淺析4. 進行詞雲展示5. 總結

本文主要介紹如何通過 IK 分詞器進行詞頻統計。使用分詞器對文章的詞頻進行統計,主要目的是實作如下圖所示的詞雲功能,可以找到文章内的重點詞彙。後續也可以對詞進行詞性标注,實體識别以及對實體的情感分析等功能。

es實戰-使用IK分詞器進行詞頻統計1. IK 分詞統計代碼2. 擷取詞雲 TopN 個詞3. IK 代碼淺析4. 進行詞雲展示5. 總結

詞頻統計服務具體子產品如下:

資料輸入:文本資訊

資料輸出:詞 - 詞頻(TF-IDF等) - 詞性等内容

使用的元件:分詞器、語料庫、詞雲展示元件等

功能點:白名單,黑名單,同義詞等

現存的中文分詞器有 IK、HanLP、jieba 和 NLPIR 等幾種,不同分詞器各有特點,本文使用 IK 實作,因為 ES 一般使用 medcl 等大佬封裝的 IK 分詞器插件作為中文分詞器。

由于 ES 的 IK 分詞器插件深度結合了 ES,僅對文本分詞使用不到 ES 的内容,是以文本采用

申豔超大佬版本的 IK

1. IK 分詞統計代碼

IK 的代碼相對比較簡單,東西不多,将 String 拆分為詞并統計代碼如下:

  1. 單純統計詞頻:
/**
 * 全文本詞頻統計
 *
 * @param content  文本内容
 * @param useSmart 是否使用 smart
 * @return 詞,詞頻
 * @throws IOException
 */
private static Map<String, Integer> countTermFrequency(String content, Boolean useSmart) throws IOException {
    // 輸出結果 Map
    Map<String, Integer> frequencies = new HashMap<>();
    if (StringUtils.isBlank(content)) {
        return frequencies;
    }
    DefaultConfig conf = new DefaultConfig();
    conf.setUseSmart(useSmart);
    // 使用 IKSegmenter 初始化文本資訊并加載詞典
    IKSegmenter ikSegmenter = new IKSegmenter(new StringReader(content), conf);
    Lexeme lexeme;
    while ((lexeme = ikSegmenter.next()) != null) {
        if (lexeme.getLexemeText().length() > 1) {// 過濾單字,也可以過濾其他内容,如數字和單純符号等内容
            final String term = lexeme.getLexemeText();
            // Map 累加操作
            frequencies.compute(term, (k, v) -> {
                if (v == null) {
                    v = 1;
                } else {
                    v += 1;
                }
                return v;
            });
        }
    }
    return frequencies;
}           
  1. 統計詞頻和文檔頻率:
/**
 * 文本清單詞頻和詞文檔頻率統計
 *
 * @param docs     文檔清單
 * @param useSmart 是否使用隻能分詞
 * @return 詞頻清單 詞-[詞頻,文檔頻率]
 * @throws IOException
 */
private static Map<String, Integer[]> countTFDF(List<String> docs, boolean useSmart) throws IOException {
    // 輸出結果 Map
    Map<String, Integer[]> frequencies = new HashMap<>();
    for (String doc : docs) {
        if (StringUtils.isBlank(doc)) {
            continue;
        }
        DefaultConfig conf = new DefaultConfig();
        conf.setUseSmart(useSmart);
        // 使用 IKSegmenter 初始化文本資訊并加載詞典
        IKSegmenter ikSegmenter = new IKSegmenter(new StringReader(doc), conf);
        Lexeme lexeme;
        // 用于文檔頻率統計的 Set
        Set<String> terms = new HashSet<>();
        while ((lexeme = ikSegmenter.next()) != null) {
            if (lexeme.getLexemeText().length() > 1) {
                final String text = lexeme.getLexemeText();
                // 進行詞頻統計
                frequencies.compute(text, (k, v) -> {
                    if (v == null) {
                        v = new Integer[]{1, 0};
                    } else {
                        v[0] += 1;
                    }
                    return v;
                });
                terms.add(text);
            }
        } 
        // 進行文檔頻率統計:無需初始化 Map,統計詞頻後 Map 裡面必有該詞記錄
        for (String term : terms) {
            frequencies.get(term)[1] += 1;
        }
    }
    return frequencies;
}           

2. 擷取詞雲 TopN 個詞

擷取 TopN 個詞用于詞雲展示有多種排序方式,可以直接根據詞頻、文檔頻率或者 TF-IDF 等算法進行排序,本文僅根據詞頻求取 TopN。

M 個數字擷取 TopN 有以下算法:

  • M 小 N 小:快速選擇算法
  • M 大 N 小:小頂堆
  • M 大 N 大:歸并排序

本文采用小頂堆方式實作,對應JAVA中的優先隊列資料結構 PriorityQueue:

/**
 * 按出現次數,從高到低排序取 TopN
 *
 * @param data 詞和排序數字對應的 Map
 * @param TopN 詞雲展示的 TopN
 * @return 前 N 個詞和排序值
 */
private static List<Map.Entry<String, Integer>> order(Map<String, Integer> data, int topN) {
    PriorityQueue<Map.Entry<String, Integer>> priorityQueue = new PriorityQueue<>(data.size(), new Comparator<Map.Entry<String, Integer>>() {
        @Override
        public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
            return o2.getValue().compareTo(o1.getValue());
        }
    });
    for (Map.Entry<String, Integer> entry : data.entrySet()) {
        priorityQueue.add(entry);
    }
    //TODO 目前100詞頻一緻時(機率極低)的處理辦法,if( list(0).value == list(99).value ){xxx}
    List<Map.Entry<String, Integer>> list = new ArrayList<>();
    //統計結果隊列size和topN值取較小值清單
    int size = priorityQueue.size() <= topN ? priorityQueue.size() : topN;
    for (int i = 0; i < size; i++) {
        list.add(priorityQueue.remove());
    }
    return list;
}           

3. IK 代碼淺析

核心主類為

IKSegmenter

,需要關注的點有

dic

包也就是詞典相關内容以及字元處理工具類

CharacterUtil

identifyCharType()

方法,目錄結構如下:

es實戰-使用IK分詞器進行詞頻統計1. IK 分詞統計代碼2. 擷取詞雲 TopN 個詞3. IK 代碼淺析4. 進行詞雲展示5. 總結

IKSegmenter

類結構如下圖,其中 init() 為私有方法,初始化加載詞典采用非懶加載模式,在第一次初始化

IKSegmenter

執行個體時會調用并加載詞典,代碼位于結構圖下方。

es實戰-使用IK分詞器進行詞頻統計1. IK 分詞統計代碼2. 擷取詞雲 TopN 個詞3. IK 代碼淺析4. 進行詞雲展示5. 總結
// IKSegmenter 類構造方法
public IKSegmenter(Reader input, Configuration cfg) {
    this.input = input;
    this.cfg = cfg;
    this.init();
}
// IKSegmenter 類初始化
private void init() {
    //初始化詞典單例
    Dictionary.initial(this.cfg);
    //初始化分詞上下文
    this.context = new AnalyzeContext(this.cfg);
    //加載子分詞器
    this.segmenters = this.loadSegmenters();
    //加載歧義裁決器
    this.arbitrator = new IKArbitrator();
}

// Dictionary 類初始化詞典
public static Dictionary initial(Configuration cfg) {
    if (singleton == null) {
        synchronized (Dictionary.class) {
            if (singleton == null) {
                singleton = new Dictionary(cfg);
                return singleton;
            }
        }
    }
    return singleton;
}           

詞典私有構造方法

Dictionary()

内會加載 IK 自帶的詞典以及擴充詞典,我們也可以把自己線上不變的詞典放到這裡這樣

IKAnalyzer.cfg.xml

中就隻需要配置經常變更詞典即可。

private Dictionary(Configuration cfg) {
    this.cfg = cfg;
    this.loadMainDict();// 主詞典以及擴充詞典
    this.loadmiaozhenDict();// 自定義詞典加載,仿照其他方法即可
    this.loadStopWordDict();// 擴充停詞詞典
    this.loadQuantifierDict();// 量詞詞典
}           

IKSegmenter

類調用

next()

方法擷取下一個詞元時,會調用

CharacterUtil

類中的

identifyCharType()

方法識别字元種類,這裡我們也可以自定義一些字元種類針對處理新興的網絡語言,如@、##等内容:

static int identifyCharType(char input) {
    if (input >= '0' && input <= '9') {
        return CHAR_ARABIC;
    } else if ((input >= 'a' && input <= 'z') || (input >= 'A' && input <= 'Z')) {
        return CHAR_ENGLISH;
    } else {
        Character.UnicodeBlock ub = Character.UnicodeBlock.of(input);
        //caster 增加#為中文字元
        if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
                || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
                || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A ||input=='#') {
            //目前已知的中文字元UTF-8集合
            return CHAR_CHINESE;

        } else if (ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS //全角數字字元和日韓字元
                //韓文字元集
                || ub == Character.UnicodeBlock.HANGUL_SYLLABLES
                || ub == Character.UnicodeBlock.HANGUL_JAMO
                || ub == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO
                //日文字元集
                || ub == Character.UnicodeBlock.HIRAGANA //平假名
                || ub == Character.UnicodeBlock.KATAKANA //片假名
                || ub == Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS) {
            return CHAR_OTHER_CJK;

        }
    }
    //其他的不做處理的字元
    return CHAR_USELESS;
}           

由于 IK 内容不多,建議大家可以從頭捋一遍,包括各個實作

ISegmenter

接口的各個自分詞器等内容。

4. 進行詞雲展示

詞雲展示可以使用 Kibana 自帶的詞雲 Dashboard,或者比較熱門的 WordCloud。自己測試可以使用線上的

微詞雲

快速便捷檢視詞雲效果:導入兩列的 XLS 檔案即可,左側控制欄也可以對形狀字型等進行配置美化。

es實戰-使用IK分詞器進行詞頻統計1. IK 分詞統計代碼2. 擷取詞雲 TopN 個詞3. IK 代碼淺析4. 進行詞雲展示5. 總結

展示效果如下圖所示:

es實戰-使用IK分詞器進行詞頻統計1. IK 分詞統計代碼2. 擷取詞雲 TopN 個詞3. IK 代碼淺析4. 進行詞雲展示5. 總結

5. 總結

本文主要通過 IK 分詞器實作了詞頻統計功能,用于詞雲的展示,不僅僅适用于 ES,任何資料源文檔都可以進行詞頻統計。但是功能比較基礎,感興趣的同學可以實作一下詞排序方式變更(tf/idf)、詞性标注、實體識别和情感分析等功能;IK 分詞器較為局限,需要使用 HanLP(自帶詞性标注)等更進階的分詞器以及 NLP 相關知識來輔助,也可以參考百度 AI 的詞法分析子產品。