【中文分词预处理之N最短路径法小结(转)】
本文算法来自《基于N-最短路径方法的中文词语粗分模型》(张华平、刘群,中文信息学报,16卷5期)。凡有不解处,当参考原文。
汉语之魅力在于整齐而富有音律美。不像英文,单词间长短不一,字与字之间还用空格隔开。话虽如此,可计算机处理起来,天然的空格有助于计算机迅速识别单词间边界。而中文,美则美矣,却让机器颇为困惑。所以,中文分词就自然而然的成了一切中文信息处理的必要前提。但是,由于中文往往会出现歧义、人(地)名、译名等多种计算机难于处理的语言现象,所以一个最为妥善的办法便是先进行预处理,将所有可能的分词结果一并给出。然後让後续的程序来进一步处理这些结果。此预处理过程便是本文讨论的重点。
通常思维能够想到的最直接的分词方案是对着词典挨个找。比如:老师说明天下午休息。我们通过跟词典比较,能够找到以下词:
老师 / 说 / 明天 / 下午 / 休息
老师 / 说明 / 天下 / 午休 / 息
那么我们的程序如何实现这样一个想法呢?这并不简单。这里我们不对词典结构做具体展开,仅仅讨论如何在有词典的情况下找出最佳的前几个分词可能性。这里说的最佳是指,在所有最终给出的N个(比如说10个)最终分词方案中,要尽可能地含有正确的分词方案。这是一个很自然的要求,如果你预处理的结果集中没有正确的答案,那後续的处理只能是错上加错。这就是所谓召回率的意思。
我们说过,汉语很美,其实古汉语更美。他总是力图使用最少的字来表达最丰富的含义。现代汉语传承了古汉语的这一特点(虽然他比古汉语要醜很多)。所以,你会看到,一个中文句子,总是词越少越好(那些蹩脚翻译家从那些从句套从句的英语句子翻译出来的也是从句套从句的中文除外!不过,当然,这些句子也能用我们这里的方法分词就是。)这就是这个算法寻求最短路径的依据所在。对于一个给定的句子,他试图找出具有最少个数词语的分词结果。
上面这段文字换一种说法就是,我们要找出第一个字到最後一个字之间的最短词语路径。这有点拗口,什么叫词语路径?我们通常说的路径是指从一个点到另一个点,而我们这里说的词语路径,意思无非就是指从一个词到另一个词。理解我这里说的意思便好,叫什么无所谓。至此为止,一个基于图搜索的方案已经浮出水面,不过具体的点的意思可能和刚才所说的并不完全一致。在这个图中,点是词(能够从字典中找到的)之间的边界,而相邻点之间的路径实质上就是词首到词尾连成的一个通路。
还是以前面的例子为例。我们首先认为每一个字都可能是一个词。所以,前面那句话共九个字,所以我们要有十个点。我们用更为学术的方法来标记。用c[i]来表示各个字,i从1到9。这样c[1]=老,c[2]=师,如此等等。再用v[j]来表示点,j从0到9,并且v[j - 1]到v[j]之间的路径表示字c[j]。注意,我们还需要定义每条路径的长度作为其权重。简单地,我们设每条路径的长度都是1。这个原因很简单,1表示这个词词典里是有的,它是一个可能的方案。而对于那些词典里没有的词,它的长度是0,也就是,没有这条路径。这样,我们就部分完成了这个图的建立。
但是,这个图还没有建好。我们还需要真正地把所有可能的词找出来,并且把它们的路径连起来(注意到,到目前为止,我们的图上只有相邻的点之间有通路,任意其他点之间还没有通路,等待我们建立)。比如,上面的例子中,老师是一个词,那么v[0]和v[2]两个点之间应该有一条连线。如此一来,我们说,这个图真正建成功了。
当然,你完全可以质疑,为什么要把每条连线的权重都设为1。这个质疑是很合理的,因为没有任何理由不能把各路径的值设为小数。另外一个更有力的质疑理由是:如果句子的长度增长,那么我们将得到的结果集中可能会是一个很大的集合。虽然我们只找排名前N的结果,但具有相同最短路径长度的结果我们视为排名一样,并且他们都挤在一起。比如,如果有两个长度一样的分词方案都是排名第二,那么他们并列第二名,并且不影响下面的成为第三名。如果句子长度过长,并且每条连线的权重都是1,那么每一个名次上都有很多种分词方案堆积在一起,结果我们就得到一大堆结果。就比如,2分成两个正整数之和只能是1+1,可如果是100分成两个正整数之和呢?我们会得到一堆方案。所以,我们需要为每条路径设置不同的权重。至于如何设权重,以及具体寻找排名前N的最短路径,我姑且留待下回分享。
前一篇我们已经根据词典建立了一个图,但是一个缺陷就是,我们主观地设图上各个路径的长度都是1,这不是一个好的选择。一个很自然的想法是,我们应当将一条路径(其实也就是一个词)的权重(或者说长度)与这个词在这里应该成为一个词的概率联系起来。比如说:今天下雨。我们看这样一种分词方案:
(今)(天下)(雨)
这一条通路其实就是V[0] ---> V[1] ---> V[3] ---> V[4]。我们现在要考察V[1] ---> V[3] 这条路径的权重,这实际上应该与(天下)这个词在这里成为一个词的概率(这样的问题对于人脑而言的确过于小儿科,概率直接就是0。但是计算机并没办法知道这一点)联系在一起。说到概率,我们自然想到应当有一个充分大的语料库,然後考察各个词出现的频率。这些都对,但是,事情其实还很复杂,因为(天下)是否成为一个词,他关系到的不仅是(天)和(下)两个字,他直接关系到整个句子的分词。所以我们不能仅仅根据(天下)一词出现的概率来给V[1] ---> V[3]这条路径设置权重。
我们还是按照通用的逻辑来描述这个事情,但这涉及一点数学基础。接着前面的例子,我们的问题归根到底就是得到字符串“今天下雨”被分解成(今)(天下)(雨)的概率。或者更学术一点,我们的问题就是已知一个字符串C,本来这个字符串是一个词串,就是由一个一个词组成的这么一串,但是我们现在已经没有了这个词串的分解,就是说,这个词串W已经退化成了原字符串C,我们的任务就是找到这个W,使得概率P(W | C)最大。因为概率最大意思就是这种分词方案是最可能的分词方案。按照Bayes公式:P(W | C) = P(W) * P(C | W) / P(C)。由于对所有分词方案而言,P(C)都是一样的,所以我们不予考虑。又由于从词串W恢复到字符串C的方式是唯一的,所以P(C | W) = 1。于是,我们只剩下P(W),我们就是要求他的最大概率。如果我们假设词与词之间是独立的,那么P(W)就等于各个词w[j]的概率P(w[j])的乘积。
我们再来集中讨论P(w[j])的计算方法。我们这里用的方法叫n-grams,千万别被这个名字吓到,其实他很简单。当然,如果你愿意通过查看原文献来了解更多的细节,那么你可以参考《Foundations of Statistical Natrual Language Processing》一书的6.2节。这个算法的核心思想就是,当前这个词他能够成为一个词,只与在他前面的n个词相关。继续前面的例子,如果我们使用的是2-grams(也叫bigrams),那么以(雨)举例。这里(雨)究竟该不该以一个单独的词出现,他的概率就等于在语料库中在(今)(天下)之後找到(雨)的概率。这里有必要对语料库稍作介绍。我们的语料库不仅有着充分大的语句资源,而且这些句子都是分词好了的,并且还经过了词性标注。所以我们寻找前面的概率才成为可能。如果我们用更学术的方法写出来,就是P(w[j]) = P(w[j] | w[j - n + 1], . . . , w[j - 1])。这个表达方式可能会让人困惑,因为前面一个P和後面一个P的含义是不一样的:前面的表示w[j]这个词分的成功率,後面的P表示这些情况在语料库中出现的比例。进一步地,因为我们做过条件独立假设,所以,P(w[j] | w[j - n + 1], . . . , w[j - 1]) = P(w[j - n + 1] , . . . , w[j] | w[j - n + 1], . . . , w[j - 1]) = P(w[j - n + 1] , . . . , w[j]) / P(w[j - n + 1] , . . . , w[j - 1])。所以,(雨)在这里成为一个词,他的概率等于语料库中(今)(天下)(雨)出现的几率除以(今)(天下)出现的几率。
当然,明眼人一看就看出来了。我们这里面临一个问题就是,如果n很大,非常有可能的是,这n个词串在语料库中一次都没出现过,这样其值是0,而这个0值因为乘法关系被传递到最终的结果中。那岂不是很糟糕?这的确是一个问题。在n-最短路径中文分词算法中,作者采用了分子分母同时加1的方法来做一个参数平滑处理。更多的参数平滑处理方法可以参考黄建中、王肖雷:Katz平滑算法在中文分词系统中的应用(计算机工程,30卷)。
还有最後一点:我们这里用的是乘法,而最後各个路径的和我们需要的是加法,我们需要将乘法转换为加法。这个很简单,做对数操作就可以实现。所以,最後,一条路径的权重应该是ln(((P(w[j - n + 1] , . . . , w[j]) + 1) / (P(w[j - n + 1] , . . . , w[j - 1]) + 1))。不过这与N-最短路径的原文并不符合,这是比较奇怪的地方。但包括这篇原文引用的原文献以及其他文献中都是我们这里给出的式子,所以我这里暂且这么写,但不代表这个就是正确的。总之,这样一个系统需要自己来实现一下,在自己实现的过程中,我们可以更清楚地了解各个细节。
至于Dijkstra算法求最短路径,这个比较简单。我姑且留作下回分享。
现在,我们只剩下最後一步:求出一个图中两个点的最短N个路径。教科书上已经有非常成熟的算法求解单向图上两点之间的最短路径,这就是Dijkstra法。这个算法的想法是两步走:
第一步:计算各点到起始点的最短距离。这个思路就是先设各点到起始点的最短距离是无穷,当然,起始点本身是0。然後,从最接近起始点的点开个逐个计算。每计算一个点时,就是计算这个点的所有前面一个紧邻的点加上该点与目前的这个点的距离,在所有这些求和值中找一个最小值。这个最小值就是该点距离起始点的最短距离。当然,很有可能在计算一个点时,这个点已经被赋了一个值。这个时候就把这个值与那些求和值一起比较,找出一个最小值,这个过程称为松弛技术。这个算法的思路是很自然的。不过,在教科书上,他有个不太优雅的名字:贪心算法。意思就是,每次都做最优的一个选择,最後的问题解也必然是最优的。这有个前提:就是最优问题必须具备最优子问题,并且子问题不会相互影响。更详细的介绍请参考教科书。
第二步:从终点逐点回溯。这个过程很简单,我们在松弛时记录下每个点的前溯点即可。《算法导论》上给出了这些操作能够得到最短路径的详细证明。有兴趣的朋友可以阅读一下。
以上步骤是求出一条最短路径,但我们现在的目标是找出N条最短路径。这在操作上要麻烦一点,但想法还是很简单的,同样是上面的思路,只是每个点我们都找出前N个(而不是前1个)最短距离,并存储N个前溯点。如此一来,我们的整个算法就设计完工了。
推荐阅读
- 人工智能|hugginface-introduction 案例介绍
- 深度学习|2019年CS224N课程笔记-Lecture 17:Multitask Learning
- 深度学习|2018年度总结和2019年度计划
- BERT微调做中文文本分类
- 【学习笔记】自然语言处理实践(新闻文本分类)- 基于深度学习的文本分类Bert
- 【学习笔记】自然语言处理实践(新闻文本分类)- 基于深度学习的文本分类Word2Vec
- 自然语言处理|答案选择|语义匹配任务目前表现最好的几个模型
- 深度学习|NLP重铸篇之BERT如何微调文本分类
- NLP实践-Task1