您可以自由地共享和演绎本文稿, 只需遵守开源协议[CC By 4.0]
如果这篇文章帮助到你, 可以请我喝一杯咖啡~
与编译过程类似地,想要让计算机理解自然语言,首先要将句子分为不同的词素。但与编译过程不同的是,自然语言首先不是结构化的,其次自然语言的词汇量要远大于计算机高级语言,再者自然语言存在着广泛的二义性,这就给句子的切分带来了难度。
而且,中文在语言上属于分析语,对它进行分词处理的难度要远大于对英文(屈折语)、日语(黏着语)等等的其他语言:英文的语言性质(带有空格)决定了它基本不需要分词,而日语中每个词素基本上都自带一个决定它词性的后缀,相对地,中文就没有这样的性质,因此对它的分词格外困难。
我们今天首先考察几种比较朴素直观的算法:虽然它们的效果都不是很好。
首先,使用基于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))
输出是
['教授', '和', '研究生', '在', '共同', '研究', '一个', '项', '目的', '细节']
情况很不好。因此学术界想到,可以对这两个算法进行一个结合:
双向最长匹配
基于语言学的分析:“汉语中的单字词数量远小于多字词”,我们提出了一种新的方法:同时执行最长正向和最长逆向,基于下述的原则来考虑保留哪一个结果:
- 若正向逆向产出的两个结果的分词数不同,则优先选用词数最少的
- 若产出的两个结果词数相等,则优先选择单字词最少的一项
- 如果上述两个指标完全一致,选择逆向产出
那么我们可以很轻松地写出这一个算法:
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
这个算法又可以解决一部分问题,但,自然语言不是这么简单的东西。有一些句子的正向和逆向匹配全都是有误的,如欢迎新老师生前来就餐
,而有一些句子通过上边的三条规则不能划分为最优的解决方案。这就是通过硬编程描述的规则系统对于自然语言的无力,因此我们还是需要使用机器学习。