自然语言处理 中文词法分析 01 切分算法 木灵的炼金工作室

与编译过程类似地,想要让计算机理解自然语言,首先要将句子分为不同的词素。但与编译过程不同的是,自然语言首先不是结构化的,其次自然语言的词汇量要远大于计算机高级语言,再者自然语言存在着广泛的二义性,这就给句子的切分带来了难度。
而且,中文在语言上属于分析语,对它进行分词处理的难度要远大于对英文(屈折语)、日语(黏着语)等等的其他语言:英文的语言性质(带有空格)决定了它基本不需要分词,而日语中每个词素基本上都自带一个决定它词性的后缀,相对地,中文就没有这样的性质,因此对它的分词格外困难。
我们今天首先考察几种比较朴素直观的算法:虽然它们的效果都不是很好。

首先,使用基于conda的python3.8.3。我们使用HanLP自然语言处理库,以jpype库的startJVM方法导入HanLP的Java函数:

startJVM(getDefaultJVMPath(), 
	"-Djava.class.path=D:/Miniconda3/Lib/site-packages/pyhanlp/static/hanlp-1.7.8.jar;D:/Miniconda3/Lib/site-packages/pyhanlpstatic", 
	"-Xms1g", 
	"-Xmx1g") 

以如下的方法导入HanLP的mini词典并将其存入一个python字典数据结构中

def loadDictionary():
    IOUtil = JClass('com.hankcs.hanlp.corpus.io.IOUtil')
    path = HanLP.Config.CoreDictionaryPath.replace('.txt', '.mini.txt')
    dic = IOUtil.loadDictionary([path])
    return set(dic.keySet())

那么,我们现在拥有了绝大多数汉语词汇的字面信息,我们可以使用简单的模式匹配算法来划分其中的词汇。

首先我们来看一个非常朴素的算法:它枚举串中所有在词典中的词汇并返回一个列表,这个算法叫:

完全切分

这个算法的实现非常直观:

def fullySegment(text, dic):
    wordList = []
    for i in range(len(text)):
        for j in range(i + 1, len(text) + 1):
            word = text[i : j]
            if word in dic:
                wordList.append(word)
    return wordList

对于一个输入(这个输入请使用UTF-8编码),我们打印它的输出:

text = '你那指尖跃动的电光是我此生不变的信仰' #好中二我死了
dictionary = loadDictionary()
print(fullySegment(text, dictionary))

得到输出

['你', '那', '指', '指尖', '尖', '跃', '跃动', '动', '的', '电', '电光', '光', '光是', '是', '我', '此', '此生', '生', '不', '变', '的', '信', '信仰', '仰']

但是这种完全切分并不属于分词,我们需要的是事实上的词语序列:这就需要一种选择:因为我们就上边的算法发现,词典中有很多单个汉字的条目,因此一个词信仰会被分割为信 仰信仰,而这两种分词方式明显是互斥的。因此我们需要做出一种选择:考虑到汉语中越长的语汇表达的语义越精确,因此我们选用最长的分词:

最长匹配

实现上述的算法:

def forwardLongestSegment(text, dic):
    wordList = []
    i = 0
    while i < len(text):
        longest_word = text[i]
        for j in range(i + 1, len(text) + 1):
            word = text[i : j]
            if word in dic:
                if len(word) > len(longest_word):
                    longest_word = word
        wordList.append(longest_word)
        i += len(longest_word)
    return wordList

这个算法叫做正向最长匹配
输入上述内容

text = '你那指尖跃动的电光是我此生不变的信仰'
dictionary = loadDictionary()
print(forwardLongestSegment(text, dictionary))

得到结果

['你', '那', '指尖', '跃动', '的', '电光', '是', '我', '此生', '不', '变', '的', '信仰']

看起来还不错,但是我们再来看一个例子

text = '教授和研究生在共同研究生命的起源'
dictionary = loadDictionary()
print(forwardLongestSegment(text, dictionary))

得到结果

['教授', '和', '研究生', '在', '共同', '研究生', '命', '的', '起源']

由于我们判断优先级的依据仅有词汇的长度,那么无论什么情况,“研究生”是要优于“研究”“生”的,这就导致了词法分析出现误差。
那么我们能不能换一个角度,反着来呢?

def backwardLongestSegment(text, dic):
    wordList = []
    i = len(text) - 1
    while i >= 0:
        longest_word = text[i]
        for j in range(0, i):
            word = text[j : i + 1]
            if word in dic:
                if len(word) > len(longest_word):
                    longest_word = word
                    break
        wordList.insert(0, longest_word)
        i -= len(longest_word)
    return wordList

我们这次从后往前分词。这个算法叫逆向最长匹配。

继续上边的输入:得到

['教授', '和', '研究生', '在', '共同', '研究', '生命', '的', '起源']

看似解决了问题,但是这一组呢?

text = '教授和研究生在共同研究一个项目的细节'
print(backwardLongestSegment(text, dictionary))

输出是

['教授', '和', '研究生', '在', '共同', '研究', '一个', '项', '目的', '细节']

情况很不好。因此学术界想到,可以对这两个算法进行一个结合:

双向最长匹配

基于语言学的分析:“汉语中的单字词数量远小于多字词”,我们提出了一种新的方法:同时执行最长正向和最长逆向,基于下述的原则来考虑保留哪一个结果:

  1. 若正向逆向产出的两个结果的分词数不同,则优先选用词数最少的
  2. 若产出的两个结果词数相等,则优先选择单字词最少的一项
  3. 如果上述两个指标完全一致,选择逆向产出

那么我们可以很轻松地写出这一个算法:

def doubleLongestSegment(text, dic):
    forward = forwardLongestSegment(text,dic)
    backward = backwardLongestSegment(text, dic)
    if len(forward) > len(backward):
        return backward
    elif len(forward) < len(backward):
        return forward
    else:
        fwdSingle, bwdSingle = 0, 0
        for word in forward:
            if len(word) == 1:
                fwdSingle += 1
        for word in backward:
            if len(word) == 1:
                bwdSingle += 1
        if fwdSingle >= bwdSingle:
            return backward
        else:
            return forward

这个算法又可以解决一部分问题,但,自然语言不是这么简单的东西。有一些句子的正向和逆向匹配全都是有误的,如欢迎新老师生前来就餐,而有一些句子通过上边的三条规则不能划分为最优的解决方案。这就是通过硬编程描述的规则系统对于自然语言的无力,因此我们还是需要使用机器学习。


Copyright AmachiInori 2017-2021. All Right Reserved.
Powered By Jekyll.
amachi.com.cn