中文分词一撸到底:分词的各种打开方式

302 阅读5分钟

1,什么是中文分词

分词,就是将连续的字序列按照一定的规范,重新组合成词序列的过程。 e.x: 字序列:掘金社区是一个中文技术社区。 词序列:掘金/社区/是/一个/中文/技术/社区。

2,为什么需要中文分词

在英文的语境中,文章的基本组成成分是单词(word),单词本身具有独立的含义和属性(nature),因此,英文虽然也存在分词,但是由于语言本身特性,分词并不是句子分析必要环节。

但是在中文语境下,脱离上下文的单个字,很难明确其含义。比如:“中国人”中的“中”字,单从字上看是方位词,但是“中国人”是个名词。因此,只有明确地将句子分成由词组成的序列,才能明确句子的含义。

3,如何进行中文分词

按照分词的底层依据,可以将分词方法分为:基于词典的分词、基于规则的分词、基于语言模型的统计分词、基于nlp算法的分词。其中,由于本人并非从事算法开发工作,因此“基于NLP算法”的分词,不在此文章中详细讲解。

3.1 基于词典的分词

顾名思义,基于词典的分词就是在分词词典的基础上,对中文文本进行切分。按照分词次序和匹配规则的不同,可以划分为:前向最大匹配、前向最小匹配、后向最大匹配、后向最小匹配、双向最大匹配、双向全切分。

假定我们现有词典:[北京, 北京大学,天安门,大学,学校],待分词句子为:北京大学校门到北京天安门多远。对不同的分词方式,分词具体如下:

3.1.1 前向最大匹配

前向最大匹配算法流程为:

  1. 从左向右取待切分汉语句的m个字符作为匹配字段,m为大机器词典中最长词条个数。
  2. 查找大机器词典并进行匹配。若匹配成功,则将这个匹配字段作为一个词切分出来。
  3. 若匹配不成功,则将字符串的最后一个字符去掉,直到匹配成功或者切分为单字为止。

前向最大匹配是最直观最符合自然阅读顺序的分词方式,一般作为 词典分词 的主要分词分词方式,前向最大分词实现如下:

/**
 * 前向最大分词
 * @param sentence 待分词句子
 * @param dict  分词词典(注意,此处仅仅用set作为词典示意,实际使用时推荐使用前缀树或者双数组前缀树进行压缩)
 * @param maxWordLength  分词的最大长度
 * @return  分词结果
 */
public List<String> segForwardMax(String sentence, Set<String> dict, int maxWordLength) {
    List<String> segResult = new ArrayList<>();
    for (int i = 0; i < sentence.length(); i++) {
        for (int j = Math.min(i + maxWordLength, sentence.length()); j > i; j--) {
            String tmpWord = sentence.substring(i, j);
            if (dict.contains(tmpWord)) {  // hit the dict
                segResult.add(tmpWord);
                i = j - 1;
                break;
            } else if (j == i + 1) {  // not hit the dict, use a char as a word
                segResult.add(tmpWord);
            }
        }
    }
    return segResult;
}

假定我们现有词典:[北京,北京大学,天安门,大学,学校], 则句子:"北京大学校门到北京天安门多远" 分词结果如下:

[北京大学, 校, 门, 到, 北京, 天安门, 多, 远]

3.1.2 前向最小匹配

在前向最大分词的基础上,在某些情形下,我们并不期望永远匹配最长的字典词条,因此引入了前向最小分词,强行最小分词流程如下:

  1. 从左向右取待切分汉语句的2个字符作为匹配字段。
  2. 查找大机器词典并进行匹配。若匹配成功,则将这个匹配字段作为一个词切分出来。
  3. 若匹配不成功,则增加一个右侧字符,直到匹配成功或者字符串长度=m为止(m为最长词长度)。

前向最小分词实现如下:

/**
 * 前向最小分词
 * @param sentence 待分词句子
 * @param dict  分词词典(注意,此处仅仅用set作为词典示意,实际使用时推荐使用前缀树或者双数组前缀树进行压缩)
 * @param maxWordLength  分词的最大长度
 * @return  分词结果
 */
public List<String> segForwardMin(String sentence, Set<String> dict, int maxWordLength) {
    List<String> segResult = new ArrayList<>();
    for (int i = 0; i < sentence.length(); i++) {
        boolean hit = false;
        for (int j = i + 2; j <= Math.min(i + maxWordLength, sentence.length()); j++) {
            String tmpWord = sentence.substring(i, j);
            if (dict.contains(tmpWord)) {  // hit the dict
                segResult.add(tmpWord);
                i = j-1;
                hit = true;
                break;
            }
        }
        if (!hit) {
            segResult.add(sentence.substring(i, i + 1));
        }
    }
    return segResult;
}

相同的,对现有词典:[北京,北京大学,天安门,大学,学校], 则句子:"北京大学校门到北京天安门多远" 分词结果如下:

[北京, 大学, 校, 门, 到, 北京, 天安门, 多, 远]

除了上述的前向最大匹配、前向最小匹配外,还有后向最大匹配后向最小匹配,这两种匹配方式仅仅是对句子的遍历方式由从前到后改为从后到前,此处不再赘述。

3.1.3 全匹配词网

在现实环境中,单纯的一种匹配方式可能无法很好的覆盖多种应用场景,因此在实际应用中,往往将所有与词典匹配的结果都分出来,这种分词方法叫做全匹配分词;全匹配分词保留分词交叉部分,从而形成一张词典的逻辑网状结构,这种分词网状结构称为词网。 词网并不是分词的最终结果,但是在基于语言模型的分词中,全匹配词网是最基础的分词结构,是语言模型成立的基石。

3.2 消歧

通过词网,如果需要得到最终分词结果,则需要处理各路分词之间的歧义,这种词典歧义消解的方法,叫做分词消歧。 常见的分词消歧的方法分为:基于规则的消歧,基于语言模型的分词消歧。 基于规则的消歧:是指通过强制规则,将存在歧义的结果归一化的处理方式,比如:长词优先消歧(即最大前向匹配)、短词优先消歧(即最小前向匹配)、最短路径消歧。还有根据词重要度进行消歧。 词重要度消歧示例代码如下:

/**
 * 合并两个qp识别结果
 * @param tokenListHigh 高级别的结果
 * @param tokenListLow 低级别的结果
 * @return 合并之后的结果
 */
public static List<TokenBase> merge(List<TokenBase> tokenListHigh, List<TokenBase> tokenListLow) {
    List<TokenBase> result = new ArrayList<>();
    for (int high = 0, low = 0; high < tokenListHigh.size(); high++) {
        TokenBase tokenHigh = tokenListHigh.get(high);
        while (low < tokenListLow.size() && tokenListLow.get(low).getOffset() < tokenHigh.getOffset()) {
            low++;
        }
        if (low == tokenListLow.size()) { // lowList到结尾,把所有highList追加进去
            result.addAll(tokenListHigh.subList(high, tokenListHigh.size()));
            break;
        }
        TokenBase tokenLow = tokenListLow.get(low);
        if (tokenLow.getOffset() == tokenHigh.getOffset() &&  // token偏移相同,且级别更高,直接覆盖
                tokenLow.getEnd() == tokenHigh.getEnd() &&
                tokenLow.getPriority() > tokenHigh.getPriority()) {
            result.add(tokenLow);
        } else {
            int lowEnd = low;
            while (lowEnd < tokenListLow.size() && tokenListLow.get(lowEnd).getEnd() < tokenHigh.getEnd()) {
                lowEnd++;
            }
            if (lowEnd > low && lowEnd < tokenListLow.size() &&
                    tokenListLow.get(lowEnd).getEnd() == tokenHigh.getEnd()) {  // high 队列里面的一个词,对应low队列里面的多个词,使用low队列,尽可能地把词切分开
                result.addAll(tokenListLow.subList(low, lowEnd + 1));
                low = lowEnd + 1;
            } else {
                result.add(tokenHigh);
            }
        }
    }
    return result;
}

测试代码


@Test
public void testDisambiguate() {
    List<TokenBase> oneList = Lists.newArrayList();
    oneList.add(new TokenBase("武汉市", 0, 3, 10));
    oneList.add(new TokenBase("长江", 0, 3, 16));
    oneList.add(new TokenBase("大桥", 0, 3, 10));

    List<TokenBase> twoList = Lists.newArrayList();
    twoList.add(new TokenBase("武汉", 0, 3, 9));
    twoList.add(new TokenBase("市长", 0, 3, 12));
    twoList.add(new TokenBase("江大桥", 0, 3, 1));

    System.out.println(Disambiguate.merge(oneList, twoList).stream().map(TokenBase::getWord).collect(Collectors.joining(" ")));
}
测试结果: 武汉市 长江 大桥

基于语言模型的分词消歧:是指通过语言模型,计算词网上每一条边的权重w,再通过计算图的最短路径,获取最佳的分词结果。基于语言模型的的分词消歧可以参考《数学之美》中系列一&二。

3.2 维特比分词

维特比(viterbi)算法是一种在图中快速寻找最可能产生观测序列的动态规划算法。在基于语言模型的分词中,维特比常用于快速查找词网最短路径,加快分词速度,优化分词效率。

对于如下图所指示词网: image.png

从开始节点S出发,对于节点B1,存在S->A1->B1 ,S->A2->B1 ,S->A3->B1 ,已知三条路径的权重为W(A2B1)<W(A1B1)<W(A3B1),如果最短路径经过B1,那么权重高的S->A1->B1 ,S->A3->B1 一定不存在最短路径上,因此可以剪枝处理,得到备选路径S->A2->B1 。

同理,对B2,C3路径进行剪枝处理,剪枝后结果如图(一轮剪枝)所示。

对于节点C1,存在路径S->A2->B1->C1 ,S->A3->B2->C1,S->A3->B3->C1,对路径权重,按照权重从小到达做剪枝,得到剪枝后备路径S->A2->B1→C1。

同理,对C2,C3路径进行剪枝处理,剪枝后结果如图(二轮剪枝)所示。

对于终点E,比较从C1、C2、C3进入的权重大小,从而遴选出最佳的维特比路径:S->A3->B3->C3。

git地址:gitee.com/aslante/seg…