1,什么是中文分词
分词,就是将连续的字序列按照一定的规范,重新组合成词序列的过程。 e.x: 字序列:掘金社区是一个中文技术社区。 词序列:掘金/社区/是/一个/中文/技术/社区。
2,为什么需要中文分词
在英文的语境中,文章的基本组成成分是单词(word),单词本身具有独立的含义和属性(nature),因此,英文虽然也存在分词,但是由于语言本身特性,分词并不是句子分析必要环节。
但是在中文语境下,脱离上下文的单个字,很难明确其含义。比如:“中国人”中的“中”字,单从字上看是方位词,但是“中国人”是个名词。因此,只有明确地将句子分成由词组成的序列,才能明确句子的含义。
3,如何进行中文分词
按照分词的底层依据,可以将分词方法分为:基于词典的分词、基于规则的分词、基于语言模型的统计分词、基于nlp算法的分词。其中,由于本人并非从事算法开发工作,因此“基于NLP算法”的分词,不在此文章中详细讲解。
3.1 基于词典的分词
顾名思义,基于词典的分词就是在分词词典的基础上,对中文文本进行切分。按照分词次序和匹配规则的不同,可以划分为:前向最大匹配、前向最小匹配、后向最大匹配、后向最小匹配、双向最大匹配、双向全切分。
假定我们现有词典:[北京, 北京大学,天安门,大学,学校],待分词句子为:北京大学校门到北京天安门多远。对不同的分词方式,分词具体如下:
3.1.1 前向最大匹配
前向最大匹配算法流程为:
- 从左向右取待切分汉语句的m个字符作为匹配字段,m为大机器词典中最长词条个数。
- 查找大机器词典并进行匹配。若匹配成功,则将这个匹配字段作为一个词切分出来。
- 若匹配不成功,则将字符串的最后一个字符去掉,直到匹配成功或者切分为单字为止。
前向最大匹配是最直观、最符合自然阅读顺序的分词方式,一般作为 词典分词 的主要分词分词方式,前向最大分词实现如下:
/**
* 前向最大分词
* @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 前向最小匹配
在前向最大分词的基础上,在某些情形下,我们并不期望永远匹配最长的字典词条,因此引入了前向最小分词,强行最小分词流程如下:
- 从左向右取待切分汉语句的2个字符作为匹配字段。
- 查找大机器词典并进行匹配。若匹配成功,则将这个匹配字段作为一个词切分出来。
- 若匹配不成功,则增加一个右侧字符,直到匹配成功或者字符串长度=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)算法是一种在图中快速寻找最可能产生观测序列的动态规划算法。在基于语言模型的分词中,维特比常用于快速查找词网最短路径,加快分词速度,优化分词效率。
对于如下图所指示词网:
从开始节点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…