中文分词:最大匹配算法

(一)引言

分词是自然语言处理中非常常见的操作,也是必不可少的文本数据预处理步骤。各国语言的表达方式和书写方式截然不同,因此分词的方式和难度也不同。英文分词是最简单的,因为每个单词已经用空格自动分词了,比如"I like Chinese" 这个句子已经被分成了三个单词。当然,英文分词也是有难点的,比如单词大小写所代表的含义不同以及各种符号的用法,这里暂不讨论。中文是汉字为基本书写单位,词语甚至句子之间并没有明显的区分标记,并且不同的词组合容易产生歧义。比如:“结婚的和尚未结婚的”,计算机很难判断是分成“结婚/的/和/尚未/结婚/的”还是“结婚/的/和尚/未/结婚/的”。因此,中文分词是一项非常具有挑战性的工作。
现今,中文分词方法一般被分为三类:
(1)基于字典的分词方法
(2)基于统计的分词方法
(3)基于机器学习的分词方法
本篇文章主要介绍最直接粗暴的方法----基于词典的分词方法。

(二)正向最大匹配算法

顾名思义,正向最大匹配是从左到右扫描字符串,在一个给定的词典中寻找词的最大匹配。先看一个例子:
给定一个句子:“他是研究生物化学的”。
给定一个词典:["他","是","研究","研究生","生物","物化","化学","学","的"]。
思路:先获得词典中词的最大长度m,这个例子中m为3;给字符串初始位置一个指针pi,即在“他”的位置;从当前指针起取m个字作为词(也可以直接到字符串末尾作为词,但是效率低),即“他是研”;选出的词如果在词典中,就在词的后面进行划分,然后指针移动到这个词后面的一个字,如果选出的词不在词典中,就把选出的词长度减一(即m-1),“他是研”就变成了“他是”,然后在进行此步骤的操作。
算法流程:
输入:字符串 s
过程:

  1. 令指针 pi 指向 s 的初始位置
  2. repeat
  3. 计算当前指针 pi 到字串末端的字数(即未被切分字串的长度) n
  4. 令 m=词典中最长单词的字数,如果 n<m, 令 m=n
  5. 从当前 pi 起取 m 个汉字作为词 wi
  6. **if  wi **在词典中
  7. then 在 wi 后添加一个切分标志,根据 wi 的长度修改指针 pi
  8. ** else**
  9. 将 wi 从右端去掉一个字
  10. until pi 指向字串末端
    输出: 添加切分标志后的字符串 s

示例代码:

text = "他是研究生物化学的"
Dict = ["他","是","研究","研究生","生物","物化","化学","学","的"]

def forword_Match(text, Dict):
    '''前向最大匹配'''
    word_list = []
    pi = 0    #初始位置
    #找出字典中的最长的词的长度
    m = max([len(word) for word in Dict])
    while pi != len(text):
        n = len(text[pi:])    #当前指针到字符串末尾的长度
        if n < m:
            m = n
        for index in range(m,0,-1):      #从当前 pi 起取 m 个汉字作为词 
            if text[pi:pi+index] in Dict:
                word_list.append(text[pi:pi+index])
                pi = pi + index         # 根据词的长度修改指针pi
                break
    print('/'.join(word_list))
    
forword_Match(text, Dict)    
 ## 输出:  他/是/研究生/物化/学/的

(三)逆向最大匹配算法

可以想到,逆向最大匹配是从右到左扫描字符串,在一个给定的词典中寻找词的最大匹配。先看一个例子:
给定一个句子:“他是研究生物化学的”。
给定一个词典:["他","是","研究","研究生","生物","物化","化学","学","的"]。
思路:先获得词典中词的最大长度m,这个例子中m为3;给字符串末尾位置一个指针pi,即在“的”的位置;从当前指针向左取m个字作为词(也可以直接到字符串开头作为词,但是效率低),即“化学的”;选出的词如果在词典中,就在词的前面进行划分,然后指针移动到这个词前面的一个字,如果选出的词不在词典中,就把选出的词长度减一(即m-1),“化学的”就变成了“学的”,然后在进行此步骤的操作。
算法流程:
输入:字符串 s
过程:

  1. 令指针 pi 指向 s 的末尾位置
  2. repeat
  3. 计算当前指针 pi 到字串开头的字数(即未被切分字串的长度) n
  4. 令 m=词典中最长单词的字数,如果 n<m, 令 m=n
  5. 从当前 pi 起取往左取m个汉字作为词 wi
  6. **if  wi **在词典中
  7. then 在 wi 前面添加一个切分标志,根据 wi 的长度修改指针 pi
  8. ** else**
  9. 将 wi 从左端去掉一个字
  10. until pi 指向字串开头
    输出: 添加切分标志后的字符串 s

示例代码:

text = "他是研究生物化学的"
Dict = ["他","是","研究","研究生","生物","物化","化学","学","的"]

def back_Match(text, Dict):
    '''逆向最大匹配'''
    word_list = []
    pi = len(text) - 1
    m = max(len(word) for word in Dict)
    while pi >= 0:
        n = len(text[0:pi+1])
        if n < m:
            m = n
        for index in range(m-1,-1,-1):
            if text[pi-index:pi+1] in Dict:
                word_list.append(text[pi-index:pi+1])
                pi = pi - index -1
                break

    print('/'.join(word_list[::-1]))

back_Match(text, Dict)
## 输出:  他/是/研究/生物/化学/的

至此就完成了基于词典的中文分词方法,但是这种方法过度依赖于词典。如果词典质量不高(比如容量小、记录不全等)会影响分类效果,还有一些会产生歧义的句子也不适合用这种方法。接下来我们会一起学习基于统计的分词方法......