1. 程式人生 > >中文分詞——知更鳥分詞(RS)設計與實現

中文分詞——知更鳥分詞(RS)設計與實現

內容提要

  本文設計了一種帶逆向回退策略的正向最大匹配。

分詞概述

  英文文字的處理相對簡單,每一個單詞之間有空格或標點符號隔開。如果不考慮短語,僅以單詞作為唯一的語義單元的話,處理英文單詞切分相對簡單,只需要分類多有單詞,去除標點符號。中文自然語言處理首先要解決的難題就是中文分詞技術。
  中文分詞(Chinese Word Segmentation) 指的是將一個漢字序列切分成一個個單獨的詞。分詞就是將連續的字序列按照一定的規範重新組合成詞序列的過程。我們知道,

演算法分類

  現有的分詞演算法可分為三大類:基於字串匹配的分詞方法、基於理解的分詞方法和基於統計的分詞方法。按照是否與詞性標註過程相結合,又可以分為單純分詞方法和分詞與標註相結合的一體化方法。
字元匹配
  這種方法又叫做機械分詞方法,它是按照一定的策略將待分析的漢字串與一個“充分大的”機器詞典中的詞條進行配,若在詞典中找到某個字串,則匹配成功(識別出一個詞)。按照掃描方向的不同,串匹配分詞方法可以分為正向匹配和逆向匹配;按照不同長度優先匹配的情況,可以分為最大(最長)匹配和最小(最短)匹配。
常用的幾種機械分詞方法如下:
  1)正向最大匹配法(由左到右的方向);
  2)逆向最大匹配法(由右到左的方向);
  3)最少切分(使每一句中切出的詞數最小);
  4)雙向最大匹配法(進行由左到右、由右到左兩次掃描)
理解法


  這種分詞方法是通過讓計算機模擬人對句子的理解,達到識別詞的效果。其基本思想就是在分詞的同時進行句法、語義分析,利用句法資訊和語義資訊來處理歧義現象。它通常包括三個部分:分詞子系統、句法語義子系統、總控部分。在總控部分的協調下,分詞子系統可以獲得有關詞、句子等的句法和語義資訊來對分詞歧義進行判斷,即它模擬了人對句子的理解過程。這種分詞方法需要使用大量的語言知識和資訊。由於漢語語言知識的籠統、複雜性,難以將各種語言資訊組織成機器可直接讀取的形式,因此目前基於理解的分詞系統還處在試驗階段。
統計法
  從形式上看,詞是穩定的字的組合,因此在上下文中,相鄰的字同時出現的次數越多,就越有可能構成一個詞。因此字與字相鄰共現的頻率或概率能夠較好的反映成詞的可信度。可以對語料中相鄰共現的各個字的組合的頻度進行統計,計算它們的互現資訊。定義兩個字的互現資訊,計算兩個漢字X、Y的相鄰共現概率。互現資訊體現了漢字之間結合關係的緊密程度。當緊密程度高於某一個閾值時,便可認為此字組可能構成了一個詞。這種方法只需對語料中的字組頻度進行統計,不需要切分詞典,因而又叫做無詞典分詞法或統計取詞方法。但這種方法也有一定的侷限性,會經常抽出一些共現頻度高、但並不是詞的常用字組,例如“這一”、“之一”、“有的”、“我的”、“許多的”等,並且對常用詞的識別精度差,時空開銷大。實際應用的統計分詞系統都要使用一部基本的分詞詞典(常用詞詞典)進行串匹配分詞,同時使用統計方法識別一些新的詞,即將串頻統計和串匹配結合起來,既發揮匹配分詞切分速度快、效率高的特點,又利用了無詞典分詞結合上下文識別生詞、自動消除歧義的優點。

常見專案

極易分詞
  MMAnalyzer極易中文分片語件,由中科院提供的中文極易分詞器。比較完善的中文分詞器。
  支援英文、數字、中文(簡體)混合分詞。
  常用的數量和人名的匹配。
  超過22萬詞的詞庫整理。
  實現正向最大匹配演算法。

IK
  IKAnalyzer是一個開源的,基於java語言開發的輕量級的中文分詞工具包。從2006年12月推出1.0版開始,IKAnalyzer已經推出了3個大版本。最初,它是以開源專案Luence為應用主體的,結合詞典分詞和文法分析演算法的中文分片語件。新版本的IKAnalyzer3.0則發展為面向Java的公用分片語件,獨立於Lucene專案,同時提供了對Lucene的預設優化實現。
Paoding
  Paoding(庖丁解牛分詞)基於Java的開源中文分片語件,提供lucene和solr 介面,具有極 高效率和 高擴充套件性。引入隱喻,採用完全的面向物件設計,構思先進。
  高效率:在PIII 1G記憶體個人機器上,1秒可準確分詞 100萬漢字。
  採用基於不限制個數的詞典檔案對文章進行有效切分,使能夠將對詞彙分類定義。
  能夠對未知的詞彙進行合理解析。
  僅支援Java語言。
MMSEG4J
  MMSEG4J基於Java的開源中文分片語件,提供lucene和solr 介面:
  1.mmseg4j 用 Chih-Hao Tsai 的 MMSeg 演算法實現的中文分詞器,並實現 lucene 的 analyzer 和 solr 的TokenizerFactory 以方便在Lucene和Solr中使用。
  2.MMSeg 演算法有兩種分詞方法:Simple和Complex,都是基於正向最大匹配。Complex 加了四個規則過慮。官方說:詞語的正確識別率達到了 98.41%。mmseg4j 已經實現了這兩種分詞演算法。
smallseg
  開源的,基於DFA的輕量級的中文分詞工具包。
  特點:可自定義詞典、切割後返回登入詞列表和未登入詞列表、有一定的新詞識別能力。

知更鳥分詞實現

  中文分詞既然作為自然語言處理的基本工具,那麼我們就來實現一個基於字元匹配的中文分詞,用筆者的英文名(Robin)給她起個名字,就叫做——知更鳥分詞器(RS/Robin-Segmenter)

演算法描述

  基本實現演算法:
  (1)正向最大匹配;
  (2)逆向最大匹配;
  (3)帶逆向回退策略的正向最大匹配(重點)。
  支援以下功能:

  • 新詞發現、新詞標註;
  • 字元轉換、格式統一;
  • 自定義分割符;
  • 特殊符號保留;
  • 檢索分詞模式;

資料結構

  基於字典字元匹配匹配的分詞演算法,重點是字典資料結構的設計。好的字典資料結構能夠極大提升匹配檢索的效率。
  採用基本容器HashMap實現的一種變種字典樹(Trie)結構。其基本的思想如圖。

Trie樹型結構之正向最大匹配過程

  假設詞典:中國 | 中南海 | 人民 | 人民幣 | 人情 | 銀行 | 中國人民 | 中國人民銀行 | 中國人壽保險 | ……
  例如:“中國人民銀行行長易綱。”這樣一條文字的匹配過程如圖示紅線路徑。分詞結果應該示“中國人民銀行 行長 易綱”。
  由於中文詞首字分佈比較均勻,並且查詢首字的概率遠大於其他非首字,根節點採用map陣列方式,陣列下標為字元的Unicode碼減去最小中文漢字 Unicode碼。陣列維度為漢字的Unicode碼範圍。

程式碼實現

Unicode工具類:

package com.robin.segment.robinseg;

import com.robin.config.ConfigUtil;
import com.robin.file.FileUtil;
import java.util.HashSet;
import java.util.Set;

/**
 * <DT><B>描述:</B></DT>
 * <DD>Unicode 編碼工具類</DD>
 *
 * @version Version1.0
 * @author Robin
 * @version <I> V1.0 Date:2018-01-28</I>
 * @author  <I> E-mail:[email protected]</I>
 */
public class Unicode {

    /** 量詞配置路徑 */
    private static final String QUANTIFIER_PATH;
    /** 量詞集合 */
    private static Set<Character> quantifierSet;
    /** 中文數詞路徑 */
    private static final String NUMERAL_PATH;
    /** 中文數詞集合 */
    private static Set<Character> numeralSet;
    /** 詞典檔案編碼 */
    private static final String DIC_ENCODING;
    /** 空字元 Unicode */
    public static final int NULL = 0x0;
    /** 半形空格 Unicode */
    public static final int DBC_BLANK = 0x20;
    /** 半形句點 Unicode */
    public static final int DBC_POINT = 0x2E;
    /** 半形0 Unicode */
    public static final int DBC_0 = 0x30;
    /** 半形9 Unicode */
    public static final int DBC_9 = 0x39;
    /** 半形A Unicode */
    public static final int DBC_A = 0x41;
    /** 半形Z Unicode */
    public static final int DBC_Z = 0x5A;
    /** 半形a Unicode */
    public static final int DBC_LA = 0x61;
    /** 半形z Unicode */
    public static final int DBC_LZ = 0x7A;
    /** 普通最小中文漢字 Unicode */
    public static final int MIN_CHINESE = 0x4E00;
    /** 普通最大中文漢字 Unicode */
    public static final int MAX_CHINESE = 0x9FA5;
    /** 全形空格 Unicode */
    public static final int SBC_BLANK = 0x3000;//0xFF00也是全形空格
    /** 關注全形下限 Unicode */
    public static final int SBC_LOW_LIMIT = 0xFF00;
    /** 關注全形上限 Unicode */
    public static final int SBC_UP_LIMIT = 0xFF5E;
    /** 全形句點 Unicode */
    public static final int SBC_POINT = 0xFF0E;
    /** 全形0 Unicode */
    public static final int SBC_0 = 0xFF10;
    /** 全形9 Unicode */
    public static final int SBC_9 = 0xFF19;
    /** 全形A Unicode */
    public static final int SBC_A = 0xFF21;
    /** 全形Z Unicode */
    public static final int SBC_Z = 0xFF3A;
    /** 全形a Unicode */
    public static final int SBC_LA = 0xFF41;
    /** 全形z Unicode */
    public static final int SBC_LZ = 0xFF5A;
    /** 半形全形數字或字母 Unicode 編碼偏移 */
    public static final int OFFSET_DBC_SBC = 0xFEE0;
    /** 英文大小寫字母 Unicode 編碼偏移 */
    public static final int OFFSET_UP_LOW = 0x20;

    static {
        QUANTIFIER_PATH = ConfigUtil.getConfig("dic.quantifier");
        NUMERAL_PATH = ConfigUtil.getConfig("dic.numeral");
        DIC_ENCODING = ConfigUtil.getConfig("dic.encoding");
        initQantifierSet();
        initNumeralSet();
    }

    /**
     * Unicode 字元型別
     */
    public enum CharType {
        /** 控制符 */
        CONTROL,
        /** 半形數字 */
        DBC_DIGIT,
        /** 半形大寫字母 */
        DBC_UPPER_CASE,
        /** 半形小寫字母 */
        DBC_LOWER_CASE,
        /** 中文 */
        COMMON_CHINESE,
        /** 全形字元 */
        SBC_CHAR,
        /** 全形字母 */
        SBC_CASE,
        /** 全形數字 */
        SBC_DIGIT,
        /** 全形大寫字母 */
        SBC_UPPER_CASE,
        /** 全形小寫字母 */
        SBC_LOWER_CASE,
        /** 小數點 */
        DECIMAL_POINT,
        /** 數字字尾 */
        DECIMAL_SUFFIX,
        /** 百分比符號 */
        PERCENT_CHAR,
        /** 空白符 */
        BLANK_CHAR,
        /** 其他 */
        OTHER_CHAR,
    }

    /**
     * 通過字元判斷其型別
     *
     * @param ch Unicode字元
     * @return 字元型別
     */
    public static CharType getCharType(char ch) {
        int value = ch;
        //Unicode 普通中文漢字區
        if ((value >= MIN_CHINESE) && (value <= MAX_CHINESE)) {
            return CharType.COMMON_CHINESE;
        }
        //空白符、控制字元、半形空格0x20、全形空格0x3000
        if ((value <= DBC_BLANK) || (value == SBC_BLANK)) {
            return CharType.BLANK_CHAR;
        }
        //半形數字0-9:0x30-0x39
        if ((value >= DBC_0) && (value <= DBC_9)) {
            return CharType.DBC_DIGIT;
        }
        //半形字母a-z:0x61-0x7A
        if ((value >= DBC_LA) && (value <= DBC_LZ)) {
            return CharType.DBC_LOWER_CASE;
        }
        //半形字母A-Z:0x41-0x5A
        if ((value >= DBC_A) && (value <= DBC_Z)) {
            return CharType.DBC_UPPER_CASE;
        }
        //小數點:半形0x2E
        if (value == DBC_POINT) {
            return CharType.DECIMAL_POINT;
        }
        //數字字尾
        if ((value == (int) ('%'))
                || (value == (int) ('$'))
                || (value == (int) ('¥'))) {
            return CharType.DECIMAL_SUFFIX;
        }
        //關注的全形區:0xFF00-0xFF5E  與半形相距FEE0
        if ((value >= SBC_LOW_LIMIT) && (value <= SBC_UP_LIMIT)) {
            return CharType.SBC_CHAR;
        }
        //說明:全形其他子區可以不判斷了,為擴充套件保留
        //全形數字0-9:0xFF10-0xFF19  與半形相距FEE0
        if ((value >= SBC_0) && (value <= SBC_9)) {
            return CharType.SBC_CHAR;
        }
        //全形字母A-Z:0xFF21-0xFF3A  與半形相距FEE0
        if ((value >= SBC_A) && (value <= SBC_Z)) {
            return CharType.SBC_CHAR;
        }
        //全形字母a-z:0xFF41-0xFF5A  與半形相距FEE0
        if ((value >= SBC_LA) && (value <= SBC_LZ)) {
            return CharType.SBC_CHAR;
        }
        //其他字元
        return CharType.OTHER_CHAR;
    }

    /**
     * 判斷字元否是數字型別字元。
     *
     * @param ch 字元
     * @return boolean true-數字型別,fasle-非數字型別
     */
    static boolean isDecimalType(char ch) {

        if (Unicode.isDecimal(ch)) {
            return true;
        }

        if (Unicode.isPoint(ch)) {
            return true;
        }
        return Unicode.isDecimalSuffix(ch);
    }

    /**
     * 判斷字元否是數字。
     *
     * @param ch 字元
     * @return boolean true-數字,fasle-非數字
     */
    static boolean isDecimal(char ch) {
        int unicode = ch;
        //半形或全形數字
        return ((unicode >= DBC_0) && (unicode <= DBC_9))
                || (unicode >= SBC_0) && (unicode <= SBC_9);
    }

    /**
     * 判斷字元是否是數字字尾。
     *
     * @param ch 字元
     * @return boolean true-數字字尾,false-非數字字尾
     */
    static boolean isDecimalSuffix(char ch) {
        int unicode = ch;
        //數字字尾
        return (unicode == (int) ('%'))
                || (unicode == (int) ('$'))
                || (unicode == (int) ('¥'));
    }

    /**
     * 判斷字元是否是小數點。
     *
     * @param ch 字元
     * @return boolean true-小數點,fasle-非小數點
     */
    static boolean isPoint(char ch) {
        int unicode = ch;
        //半形或全形數字
        return (unicode == DBC_POINT) || (unicode == SBC_POINT);
    }

    /**
     * 初始化中文數字集合
     */
    private static void initNumeralSet() {
        numeralSet = new HashSet<>();
        String numerals = FileUtil.readText(NUMERAL_PATH, DIC_ENCODING);
        char[] symbolArr = numerals.toCharArray();
        for (int i = 0; i < symbolArr.length; i++) {
            numeralSet.add(symbolArr[i]);
        }
    }

    /**
     * 判斷一個字元是否是中文數字
     *
     * @param ch 輸入字元
     * @return 是否是中文數字
     */
    static boolean isNumeral(char ch) {
        int unicode = ch;
        if ((unicode < Unicode.MIN_CHINESE) || (unicode > Unicode.MAX_CHINESE)) {
            return false;
        }
        return numeralSet.contains(ch);
    }

    /**
     * 初始化單量詞集合
     */
    private static void initQantifierSet() {
        quantifierSet = new HashSet<>();
        String quantifiers = FileUtil.readText(QUANTIFIER_PATH, DIC_ENCODING);
        char[] symbolArr = quantifiers.toCharArray();
        for (int i = 0; i < symbolArr.length; i++) {
            quantifierSet.add(symbolArr[i]);
        }
    }

    /**
     * 判斷一個字元是否是中文量詞
     *
     * @param ch 輸入字元
     * @return 是否是量詞
     */
    static boolean isQuantifier(char ch) {
        int unicode = ch;
        if ((unicode < Unicode.MIN_CHINESE) || (unicode > Unicode.MAX_CHINESE)) {
            return false;
        }
        return quantifierSet.contains(ch);
    }
}

分詞器引數配置:

package com.robin.segment.robinseg;

/**
 * <DT><B>描述:</B></DT>
 * <DD>Robin分詞器引數配置類</DD>
 *
 * @version 1.0
 * @author Robin
 * @version <I> V1.0 Date:2018-01-30</I>
 * @author  <I> E-mail:[email protected]</I>
 */
public class SegmentArgs {

    /** 詞結束符 */
    static final char END_MARK = Unicode.NULL;
    /** 分詞方法 */
    SegAlgorithm segMethod = SegAlgorithm.FORWARD;
    /** 符號標誌 */
    boolean cleanSymbolFlag = true;
    /** 新詞標註標誌 */
    boolean markNewWordFlag = false;
    /** 大小寫轉換 */
    boolean downCasingFlag = true;
    /** 分詞合併模式-字典中未出現的孤立子合併 */
    boolean mergePatternFlag = true;
    /** 分詞檢索模式 */
    boolean retrievalPatternFlag = false;
    /** 分隔符 */
    String separator = " ";
    /** 詞標符號 */
    private static final String WORD_MARK = "⊙";
    /** 新詞標記 */
    static final String NEW_WORD_MARK = "[" + WORD_MARK + "新詞]";
    /** 拼接詞標記 */
    static final String SPLICE_WORD_MARK = "[" + WORD_MARK + "拼接詞]";
    /** 混合詞標記 */
    static final String CN_EN_MIX_MARK = "[" + WORD_MARK + "混合詞]";

    /**
     * Robin分詞方法列舉型別
     */
    public enum SegAlgorithm {

        /** 帶回退策略的正向最大匹配 */
        FORWARD,
        /** 簡單反向最大匹配 */
        REVERSE
    }

    /**
     * 詞的類別
     */
    enum WordClass {

        STANDARD,
        /** 英文詞,自動識別 */
        ENGLISH,
        /** 數量詞,包括自動識別的中文數量詞 */
        QUANTIFIER,
        /** 新詞,識別的含有中文字元的新詞 */
        NEW_WORD,
        /** 中英文混合詞 */
        CN_EN_MIX,
        /** 拼接新詞,標準詞與孤立字元拼接出來的新詞 */
        SPLICE_WORD,
        /** 孤立中文字元 */
        ISOLATED_CN,
        /** 其他,上述之外 */
        OTHER
    }

    /**
     * 構造方法
     */
    public SegmentArgs() {
    }

    /**
     * 重置預設配置引數
     */
    public void resetDefaultArgs() {
        this.segMethod = SegAlgorithm.FORWARD;
        this.cleanSymbolFlag = true;
        this.markNewWordFlag = false;
        this.downCasingFlag = true;
        this.mergePatternFlag = true;
        this.retrievalPatternFlag = false;
    }

    /**
     * 設定分詞分隔符
     *
     * @param separator 分隔符
     */
    void setSeparator(String separator) {
        this.separator = separator;
    }

    /**
     * 設定刪除符號標誌
     *
     * @param flag 是否刪除
     */
    public void setCleanSymbolFlag(boolean flag) {
        this.cleanSymbolFlag = flag;
    }

    /**
     * 設定是否檢索型分詞
     *
     * @param flag 是否檢索型分詞
     */
    public void setRetrievalPatternFlag(boolean flag) {
        this.retrievalPatternFlag = flag;
    }

    /**
     * 設定拼接模式標誌
     *
     * @param flag 是否拼接模式
     */
    public void setMergePatternFlag(boolean flag) {
        this.mergePatternFlag = flag;
    }

    /**
     * 設定新詞標註開關
     *
     * @param flag 是否標註
     */
    public void setMarkNewWordFlag(boolean flag) {
        this.markNewWordFlag = flag;
    }

    /**
     * 設定分詞演算法 目前支援正向最大匹配、反向最大匹配 分詞演算法修改需要重新載入詞典
     *
     * @param segAlgorithm 分詞演算法
     */
    public void setSegAlgorithm(SegAlgorithm segAlgorithm) {
        this.segMethod = segAlgorithm;
    }

    /**
     * 設定是否大-小寫字母轉換
     *
     * @param flag 是否轉換
     */
    public void setDowncasingFlag(boolean flag) {
        this.downCasingFlag = flag;
    }
}

詞典:

package com.robin.segment.robinseg;

import com.robin.config.ConfigUtil;
import com.robin.log.RobinLogger;
import com.robin.file.FileUtil;
import com.robin.segment.robinseg.Unicode.CharType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * <DT><B>描述:</B></DT>
 * <DD>詞典類</DD>
 *
 * @version Version1.0
 * @author Robin
 * @version <I> V1.0 Date:2018-01-28</I>
 * @author  <I> E-mail:[email protected]</I>
 */
class Dictionary {

    private static final Logger LOGGER = RobinLogger.getLogger();