概述
分词是NLP任务Pipeline中的重要步骤,一般来说都需要将句子切分成词之后,才能进一步把词进行向量化,最终输出各种各样的数学模型中,从而完成特定的NLP任务。中文不同于英文句子那样天然会用空格分割单词,所以中文句子切成独立的词相对困难,并且中文句子的词是上下文相关的,不同的分词方式会导致同一个句子出现不同含义。例如: 研究所取得的成就
,这个句子切分成研究/所/取得/的/成就
和研究所/取得/的/成就
含义是不一样的,在描述取得成就
这件事上,前者主语是研究
,而后者主语是研究所
。至于哪种切分方式是正确的,则完全取决于上下文,在某个上下文中,如果大概率谈论的是研究所
而非研究
,那么我们则倾向于将句子切分成研究所/取得/的/成就
。从这个角度来看,分词其实就是:将句子按照最合理(概率最大)的方式进行分词。
从序列到图
假设我们已经拥有一个足够大的词典 D D D,和待切分的句子 t \bm t t,从上文的叙述来看,分词的目的是将句子的字序列 t = ( t 1 t 2 t 3 . . . t n ) \bm t=(t_1t_2t_3...t_n) t=(t1t2t3...tn)切分成词序列 s = ( w 1 w 2 w 3 . . . w j ∣ w j ∈ D ) \bm s=(w_1w_2w_3...w_j|w_j \in D) s=(w1w2w3...wj∣wj∈D),并且使得概率 p ( s ) p(\bm s) p(s)最大。于是我们的算法的步骤无外乎三步:
- 找到一个切分序列 s i \bm s_i si
- 计算该切分序列的概率 p ( s i ) p(\bm s_i) p(si)
- 选择概率最高的切分序列作为切分结果
我们且不去想如何计算一个切分序列的概率 p ( s i ) p(\bm s_i) p(si),现在先将注意力集中在如何寻找一个切分序列 s i \bm s_i si上。我们当然可以穷举所有的切分序列,并且通过查询词典验证切分序列的合法性,然后过滤出来合法的切分序列,但这未免也太过低效了。寻找切分序列的问题可以转化成一个图问题来高效解决,这样通过图取表示切分序列的算法称之为N-最短路径法。具体的做法是:
- 对于字序列 t = ( t 1 t 2 t 3 . . . t n ) \bm t=(t_1t_2t_3...t_n) t=(t1t2t3...tn),在序列首尾增加两个特殊字:<s>和<\s>分别表示开始和结束,形成新的字序列 t = ( t 0 t 1 t 2 . . . t m ) \bm t=(t_0t_1t_2...t_m) t=(t0t1t2...tm)
- 对于字序列 t = ( t 0 t 2 t 3 . . . t m ) \bm t=(t_0t_2t_3...t_m) t=(t0t2t3...tm)中的所有节点, 相邻的节点 t i − 1 t_{i-1} ti−1与 t i t_i ti之间建立一个有向边 < t i − 1 , t i > <t_{i-1},t_i> <ti−1,ti>
- 如果子序列 w j = ( t i t i + 1 t i + 2 . . . t j ) w_j=(t_{i}t_{i+1}t_{i+2}...t_j) wj=(titi+1ti+2...tj)是词典 D D D中的一个词,则生成节点 t w t_w tw且建立有向边 < t i − 1 , t w > <t_{i-1}, t_w> <ti−1,tw>和 < t w , t j + 1 > <t_w, t_{j+1}> <tw,tj+1>
经过上述步骤的处理,我们就可以得到一个DAG(无环有向图),遍历这个DAG我们就可以得到所有切分序列。以上文研究所取得的成就
为例,经过处理可以得到DAG如图:
Unigram 模型
好了,现在我们知道如何获取一个切分序列之后,考虑如何计算切分序列的概率
p
(
s
)
p(\bm s)
p(s),最简单的方法,我们假设
s
s
s中的每个词都是独立的,这种假设我们称之为一元模型,也就是unigram模型,那么有:
(1)
p
(
s
)
=
∏
j
p
(
w
j
)
p(\bm s) = \prod_jp(w_j) \tag{1}
p(s)=j∏p(wj)(1)
于是我们寻找最大概率的哪个切分序列,则可以转化为求
l
n
p
(
s
)
=
−
∑
j
l
n
p
(
w
j
)
lnp(\bm s)=-\sum_jlnp(w_j)
lnp(s)=−∑jlnp(wj)最小的切分序列,其中词
p
(
w
j
)
p(w_j)
p(wj)的概率如何得到呢?如果有训练语料的化,可以从训练语料中统计得到,实验时我们可以选用人民日报1998年1月分词语料。但是如果没有训练语料则只能将每个词的概率都设为相等的常数了,因为根据最大熵原则,在没有额外信息的情况下,不乱假设就是最好的假设。
现在回过头来看研究所取得的成就
对应的DAG,如果我们把DAG边的权重设置为后继节点词对应的负概率
−
l
n
p
(
w
j
)
-lnp(w_j)
−lnp(wj),并且特别地令结束和起始节点的概率为常数,那么上述求最佳切分序列的N-最短路径法就可以转化称为求DAG最短路径问题了,最短路径可以通过维特比算法快速求解,这样比遍历切分序列再求最大概率值更加高效。
DAG边赋权后得到的新DAG如图:
这样只要我们找到了最短的路径就是概率最大的切分序列,也就是最合理的分词了。
Bigram 模型
现在我们回过头来看unigram模型的假设:句子中的字相互独立
。这个假设实际上也就没有考虑字所在的上下文了,因为当一个我们去吃
这个序列出现的时候,我们更倾向于猜测下一个字是饭
而不是铁
或者其它,这是因为我们有了句子上下文的知识。而在unigram眼里,这并不影响我们对下一个字的猜测。于是为了利用句子的上下文知识,我们重新将式
(
1
)
(1)
(1)建模为:
(2)
p
(
s
)
=
p
(
w
1
,
w
2
,
w
3
,
.
.
.
,
w
j
)
=
p
(
w
1
)
p
(
w
2
∣
w
1
)
p
(
w
3
∣
w
1
,
w
2
)
.
.
.
p
(
w
j
∣
w
1
,
w
2
,
.
.
.
,
w
j
−
1
)
p(\bm s) = p(w_1, w_2, w_3, ...,w_j) = p(w_1)p(w_2|w_1)p(w_3|w_1,w_2)...p(w_j|w_1,w_2,...,w_{j-1}) \tag{2}
p(s)=p(w1,w2,w3,...,wj)=p(w1)p(w2∣w1)p(w3∣w1,w2)...p(wj∣w1,w2,...,wj−1)(2)
当然,式子
(
2
)
(2)
(2)是十分美好的,因为它可以利用句子的所有上下文来计算切分序列的概率,但这样计算的效率太低了,并且我们直觉上看,一个字与近的字强相关而与距离远的字弱相关,当我们看到我们去吃
这个序列而做出了下一个字是饭
的猜测,其实更多是因为去吃
,而不是因为我们
。于是我们大胆地假设:一个字的出现只与它前
n
n
n个字相关。我们称这样的假设为n阶马尔科夫假设。在分词应用上中我们取一阶马尔科夫假设就表现得足够好了,我们也称这样一阶马尔科夫假设的模型为Bigram模型,相应地式子
(
2
)
(2)
(2)可以写为:
(3)
p
(
s
)
=
∏
j
p
(
w
j
∣
w
j
−
1
)
p(\bm s) = \prod_j p(w_j|w_{j-1}) \tag{3}
p(s)=j∏p(wj∣wj−1)(3)
同样地,式子
(
3
)
(3)
(3)中的
p
(
w
j
∣
w
j
−
1
)
p(w_j|w_{j-1})
p(wj∣wj−1)可以直接从训练语料中统计得到(也可以通过model based的方法得到)。于是我们重新给DAG的边定义权重为
−
l
n
p
(
w
j
∣
w
j
−
1
)
-lnp(w_j|w_{j-1})
−lnp(wj∣wj−1)并且特别地令
p
(
w
j
∣
<
s
>
)
p(w_j|<s>)
p(wj∣<s>)和
p
(
w
j
∣
<
/
s
>
)
p(w_j|</s>)
p(wj∣</s>)为常数,就可以得到Bigram模型下的DAG了,在这个DAG上求最短路径就得到了最优的切分序列。
实现
在实现上可以设计两个类: BigramLanguaModel
和DAG
,其中类BigramLanguaModel
负责从训练语料中统计得到
l
n
p
(
w
j
∣
w
j
−
1
)
lnp(w_j|w_{j-1})
lnp(wj∣wj−1)和词典,而DAG
类负责从给定的BigramLanguaModel
和待分词的字符串建立对应的有向图,并且求出最短路径。这里有我用C++实现的完整代码。在使用人民日报1998年1月分词语料数据集进行训练之后,得到的模型对研究所取得的成就
分词结果为: 研究所/取得/的/成就
最后,请继续期待中文分词的第二弹:《让机器学会断句:基于序列标注的分词算法》