基于规则的分词是一种机械分词方法,需要不断维护和更新词典,在切分语句时,将语句的每个字符串与词表中的每个次进行逐一匹配,找到则切分,找不到则不予切分。
按照匹配方法来划分,主要有正向最大匹配、逆向最大匹配以及双向最大匹配。
1. 正向最大匹配
正向最大匹配(Maximum Match,MM):
- 从左向右取待切分汉语句的m个字符作为匹配字段,m为机器词典中最长词条的字符数。
- 查找机器词典并进行匹配。若匹配成功,则将这个匹配字段作为一个词切分出来。 若匹配不成功,则将这个匹配字段的最后一个字去掉,剩下的字符串作为新的匹配字段,进行再次匹配,重复以上过程,直到切分出所有词为止。
比如我们现在有个词典,最长词的长度为5,词典中存在“南京市长”“长江大桥”和 “大桥”3个词。现采用正向最大匹配对句子“南京市长江大桥”进行分词,那么首先从句 子中取出前5个字“南京市长江”,发现词典中没有该词,于是缩小长度,取前4个字“南 京市长”,词典中存在该词,于是该词被确认切分。再将剩下的“江大桥”按照同样方式 切分,得到“江”“大桥”,最终分为“南京市长”“江”“大桥”3个词。显然,这种结果不是我们所希望的。
正向最大匹配法示例代码如下:
class MaximumMatch:
"""正向最大匹配的中文分词器"""
def __init__(self):
self.window_size = 3 # 字典中最长词条的字符数
def cut(self, text):
global piece
result = []
index = 0 # 前指针
text_length = len(text)
dictionary = {'研究', '研究生', '声明', '起源'}
while text_length > index:
for r_index in range(self.window_size+index, index, -1): # 后指针
piece = text[index: r_index]
if piece in dictionary:
index = r_index - 1
break
index = index + 1
result.append(piece)
return result
if __name__ == '__main__':
text = '研究生命的起源'
tokenizer = MaximumMatch()
print(tokenizer.cut(text))
输出结果:
['研究生', '命', '的', '起源']
2. 逆向最大匹配
逆向最大匹配简称为RMM法。RMM法的基本原理与MM法大致相同,不同的是分词切分的方向与MM法相反。
逆向最大匹配法从被处理文档的末端开始匹配扫描,每次取最末端的m个字符(m为词典中最长词数作为匹配字段,若匹配失败,则去掉匹配字段最前面的一个字,继续匹配。相应地,它使用的分词词典是逆序词典,其中的每个词条都将按逆序方式存放。在实际处理时,先将文档进行倒排处理,生成逆序文档。然后,根据逆序词典,对逆序文档用正向最大匹配法处理即可。
由于汉语中偏正结构较多,若从后向前匹配,可以适当提高精确度。所以,逆向最大匹配法比正向最大匹配法的误差要小。统计结果表明,单纯使用正向最大匹配的错误率为1/169,单纯使用逆向最大匹配的错误率为1/25。比如之前的“南京市长江大桥”,按照逆向最大匹配,最终得到“南京市”“长江大桥”的分词结果。当然,如此切分并不代表完全正确、可能有个叫“江大桥”的“南京市长”也说不定。
逆向最大匹配法示例代码如下:
class ReverseMaximumMatch:
"""逆向最大匹配"""
def __init__(self):
self.window_size = 3
def cut(self, text):
result = []
right = len(text) # 右指针
dic = {'研究', '研究生', '生命', '命', '的', '起源'}
global piece
while right > 0:
for left in range(right - self.window_size, right): # 固定右指针,左指针逐渐右移
piece = text[left: right] # 切片
if piece in dic: # 当命中时
right = left + 1 # 命中更新
break
right = right - 1 # 自然更新
result.append(piece)
result.reverse()
return result
if __name__ == '__main__':
text = '研究生命的起源'
rmm_tokenizer = ReverseMaximumMatch()
print(rmm_tokenizer.cut(text))
输出结果:
['研究', '生命', '的', '起源']
3. 双向最大匹配
双向最大匹配法是将正向最大匹配法得到的分词结果和逆向最大匹配法得到的结果进行比较,然后按照最大匹配原则,选取词数切分最少的作为结果。据SunM.s.和 Benjamin K…研究表明,对于中文中90.0%左右的句子,正向最大匹配和逆向最大匹配的切分结果完全重合且正确,只有大概9.0%的句子采用两种切分方法得到的结果不一样,但其中必有一个是正确的(歧义检测成功),只有不到1.0%的句子,或者正向最大匹配和逆向最大匹配的切分结果虽重合却都是错的,或者正向最大匹配和逆向最大匹配的切分结果不同但两个都不对(歧义检测失败)。这正是双向最大匹配法在实用中文信息处理系统中得以广泛使用的原因所在。
前面列举的“南京市长江大桥”采用双向最大匹配法进行切分,中间产生“南京市/江/大桥”和“南京市/长江大桥”两种结果,最终选取词数较少的“南京市/长江大桥”这一结果。
双向最大匹配的规则如下所示:
- 如果正反向分词结果词数不同,则取分词数量较少的那个结果(上例:“南京市江/大桥”的分词数量为3,而“南京市/长江大桥”的分词数量为2,所以返回分词数量为2的结果)
- 如果分词结果词数相同,则:
① 分词结果相同,就说明没有歧义,可返回任意一个结果。
② 分词结果不同,返回其中单字较少的那个。比如前文示例代码中,正向最大匹配返回的结果为“[‘研究生’,‘命’,‘的起源’]”,其中单字个数为2个;而逆向最大匹配返回的结果为“[研究’,生命’, ‘的’,‘起源’]",其中单字个数为1。所以返回的是逆向最大匹配的结果。
代码如下:
class BidirectionalMaximumMatch:
"""双向最大匹配"""
def _count_single_char(self, world_list: List[str]):
"""
统计单字成词的个数
"""
return sum(1 for word in world_list if len(word) == 1)
def cut(self, text: str):
mm = MaximumMatch()
rmm = ReverseMaximumMatch()
f = mm.cut(text)
b = rmm.cut(text)
if len(f) < len(b):
return f
elif len(f) > len(b):
return b
else:
return b if self._count_single_char(f) >= self._count_single_char(b) else f
if __name__ == '__main__':
text = '研究生命的起源'
bmm = BidirectionalMaximumMatch()
print(bmm.cut(text))
输出结果:
['研究', '生命', '的', '起源']
基于规则的分词一般都较为简单高效,但是词典的维护面临很庞大的工作量。在网络发达的今天,网络新词层出不穷,很难通过词典覆盖所有词。另外,词典分词也无法区分歧义以及无法召回新词。
在实际项目中,我们是否会考虑使用规则分词?
虽然使用规则分词的分词准确率看上去非常高,但是规则分词有几个特别大的问题:
① 不断维护词典是非常烦琐的,新词总是层出不穷,人工维护费时费力;
② 随着词典中条目数的增加,执行效率变得越来越低;
③ 无法解决歧义问题。
所以在这里不建议采用规则分词法。