【自然语言处理】中文分词技术(一)

1.1 中文分词简介

“词 "这个概念一直是汉语语言学界纠缠不清而又绕不开的问题。“词是什么”(词的抽象定义)和" 什么是词”(词的具体界定),这两个基本问题迄今为止也未能有一个权威、明确的表述,更无法拿出令大众认同的词表来。主要难点在于汉语结构与印欧体系语种差异甚大,对词的构成边界方面很难进行界定。比如,在英语中,单词本身就是 "词" 的表达,一篇英文文章就是 "单词" 加分隔符(空格)来表示的,而在汉语中,词以字为基本单位,但是一篇文章的语义表达却仍然是以词来划分的。因此,在处理中文文本时,需要进行分词处理,将句子转化为词的表示。这个切词处理过程就是中文分词,它通过计算机自动识别出句子的词,在词间加入间隔标记符,分隔出各个词汇。整个过程看似简单,然而实践起来却很复杂,主要的困难在于分词歧义。以 NLP 分词的经典语句举例,“结婚的和尚未结婚的”,应该分词为 "结婚 / 的 / 和 / 尚未 / 结婚 / 的",还是 "结婚 / 的 / 和尚 / 未 / 结婚 / 的"?这个由人来判定都是问题,机器就更难处理了。此外,像未登录词、分词粒度粗细等都是影响分词效果的重要因素。

自中文自动分词被提出以来,历经将近 30 年的探索,提出了很多方法,可主要归纳为 "规则分词"、"统计分词" 和 "混合分词(规则 + 统计)" 这三个主要流派。规则分词是最早兴起的方法,主要是通过人工设立词库,按照一定方式进行匹配切分,其实现简单高效,但对新词很难进行处理。随后统计机器学习技术的兴起,应用于分词任务上后,就有了统计分词,能够较好应对新词发现等特殊场景。然而实践中,单纯的统计分词也有缺陷,那就是太过于依赖语料的质量,因此实践中多是采用这两种方法的结合,即混合分词。

下面将详细介绍这些方法的代表性算法。

1.2 规则分词

基于规则的分词是一种机械分词方法,主要是通过维护词典,在切分语句时,将语句的每个字符串与词表中的词进行逐一匹配,找到则切分,否则不予切分。

按照匹配切分的方式,主要有正向最大匹配法、逆向最大匹配法以及双向最大匹配法三种方法。

1.2.1 正向最大匹配法

正向最大匹配(Maximum Match Method,MM 法)的基本思想为:假定分词词典中的最长词有 i 个汉字字符,则用被处理文档的当前字串中的前 i 个字作为匹配字段,查找字典。若字典中存在这样的一个 i 字词,则匹配成功,匹配字段被作为一个词切分出来。如果词典中找不到这样的一个 i 字词,则匹配失败,将匹配字段中的最后一个字去掉,对剩下的字串重新进行匹配处理。如此进行下去,直到匹配成功,即切分出一个词或剩余字串的长度为零为止。这样就完成了一轮匹配,然后取下一个 i 字字串进行匹配处理,直到文档被扫描完为止。

比如我们现在有个词典,最长词的长度为 5,词典中存在 "南京市长" 和 "长江大桥" 两个词。现采用正向最大匹配对句子 "南京市长江大桥" 进行分词,那么首先从句子中取出前五个字 "南京市长江",发现词典中没有该词,于是缩小长度,取前 4 个字 "南京市长",词典中存在该词,于是该词被确认切分。再将剩下的 "江大桥" 按照同样方式切分,得到 "江" “大桥”,最终分为 "南京市长" “江” "大桥"3 个词。显然,这种结果还不是我们想要的。示例代码如下:

class MM:
    def __init__(self):
        self.window_size = 3
        self.dic = ['研究', '研究生', '生命', '命', '的', '起源']

    def cut(self, text):
        result = []
        index = 0
        text_length = len(text)
        while text_length > index:
            for size in range(self.window_size+index, index, -1):
                piece = text[index:size]
                if piece in self.dic:
                    index = size - 1
                    break
            index += 1
            result.append(piece+'----')  # 没有符合匹配的结果也会将第一个字切分
        return result

if __name__ == "__main__":
    text = '研究生命的起源'
    tokenizer = MM()
    print(tokenizer.cut(text))

分词的结果如下(这个结果并不能让人满意):
['研究生----', '命----', '的----', '起源----']

1.2.2 逆向最大匹配法

逆向最大匹配法(Reverse Maximum Match Method,RMM 法)的基本原理与 MM 法相同,不同的是分词切分的方向与 MM 法相反。逆向最大匹配法从被处理文档的末端开始匹配扫描,每次取最末端的 i 个字符(i 为词典中最长词数)作为匹配字段,若匹配失败,则去掉匹配字段最前面的一个字,继续匹配。一种做法是使用逆序的分词词典,其中的每个词条都将按逆序方式存放。在实际处理时,先将文档进行倒排处理,生成逆序文档。然后,根据逆序词典,对逆序文档用正向最大匹配法处理。另一种做法是为逆向最大匹配法专门编写一个逆向最大匹配算法。

由于汉语中偏正结构较多,若从后向前匹配,可以适当提高精确度。所以,逆向最大匹配法比正向最大匹配法的误差要小。统计结果表明,单纯使用正向最大匹配的错误率为 1/169,单纯使用逆向最大匹配的错误率为 1/245。比如之前的 "南京市长江大桥",按照逆向最大匹配,最终得到 "南京市" “长江大桥”。当然,如此切分并不代表完全正确,可能有个叫 "江大桥" 的 "南京市长" 也说不定。示例代码如下:

class RMM:
    def __init__(self):
        self.window_size = 3
        self.dic = ['研究', '研究生', '生命', '命', '的', '起源']

    def cut(self, text):
        result = []
        index = len(text)
        while index > 0:
            for size in range(index-self.window_size, index):
                piece = text[size:index]
                if piece in self.dic:
                    index = size + 1
                    break
            index -= 1
            result.append(piece+'----')  # 没有符合匹配的结果也会将第一个字切分
        result.reverse()
        return result

if __name__ == "__main__":
    text = '研究生命的起源'
    tokenizer = RMM()
    print(tokenizer.cut(text))

分词的结果如下(这个结果就很靠谱了):
['研究----', '生命----', '的----', '起源----']

1.2.3 双向最大匹配法

双向最大匹配法(Bi-direction Matching method)是将正向最大匹配法得到的分词结果和逆向最大匹配法得到的结果进行比较,然后按照最大匹配原则,选取词数切分最少的作为结果。据 SunM.S. 和 Benjamin K.T.(1995)的研究表明,中文中 90.0% 左右的句子,正向最大匹配法和逆向最大匹配法完全重合且正确,只有大概 9.0% 的句子两种切分方法得到的结果不一样,但其中必有一个是正确的(歧义检测成功),只有不到 1.0% 的句子,使用正向最大匹配法和逆向最大匹配法的切分虽重合却是错的,或者正向最大匹配法和逆向最大匹配法切分不同但两个都不对(歧义检测失败)。这正是双向最大匹配法在实用中文信息处理系统中得以广泛使用的原因。

前面举例的 "南京市长江大桥",采用该方法,中间产生 "南京市 / 长江 / 大桥" 和 "南京市 / 长江大桥" 两种结果,最终选取词数较少的 "南京市 / 长江大桥" 这一结果。

双向最大匹配的规则是:

  1. 如果正反向分词结果词数不同,则取分词数量较少的那个(上例:"南京市 / 长江 / 大桥" 的分词数量为 3 而 "南京市 / 长江大桥" 的分词数量为 2,所以返回分词数量为 2 的)。
  2. 如果分词结果词数相同:
    • 分词结果相同,就说明没有歧义,可返回任意一个。
    • 分词结果不同,返回其中单字较少的那个。比如:上述示例代码中,正向最大匹配返回的结果为 "[‘研究生—-’, ‘命—-’, ‘的—-’, ‘起源—-’]“,其中单字个数为 2 个;而逆向最大匹配返回的结果为”[‘研究—-’, ‘生命—-’, ‘的—-’, ‘起源—-’]",其中单字个数为 1 个。所以返回的是逆向最大匹配的结果。

示例代码如下:

class BM:
    def __init__(self):
        self.window_size = 3
        self.dic = ['研究', '研究生', '生命', '命', '的', '起源']

    def mm_cut(self, text):
        result = []
        index = 0
        text_length = len(text)
        while text_length > index:
            for size in range(self.window_size+index, index, -1):
                piece = text[index:size]
                if piece in self.dic:
                    index = size - 1
                    break
            index += 1
            result.append(piece+'----')  # 没有符合匹配的结果也会将第一个字切分
        return result

    def rmm_cut(self, text):
        result = []
        index = len(text)
        while index > 0:
            for size in range(index-self.window_size, index):
                piece = text[size:index]
                if piece in self.dic:
                    index = size + 1
                    break
            index -= 1
            result.append(piece+'----')  # 没有符合匹配的结果也会将第一个字切分
        result.reverse()
        return result

    def cut(self, text):
        result1 = self.mm_cut(text)
        result2 = self.rmm_cut(text)
        if len(result1) > len(result2):  # 正反向分词结果词数不同
            return result2
        elif len(result2) > len(result1):
            return result1
        else:
            if result1 == result2:
                return result1
            else:
                if [len(i) for i in result1].count(5) < [len(i) for i in result2].count(5):
                    return result1 
                else:
                    return result2

if __name__ == "__main__":
    text = '研究生命的起源'
    tokenizer = BM()
    print(tokenizer.cut(text))

1.2.4 总结

基于规则的分词,一般都较为简单高效,但是词典的维护是一个很庞大的工程。在网络发达的今天,网络新词层出不穷,很难通过词典覆盖到所有词。