文章目录
本文主要是对苏神的多篇分词博客的学习和总结 【中文分词系列】 1. 基于AC自动机的快速分词
分词
中文分词主要有两种思路:查词典和字标注。机械的最大匹配法、最少词数法,以及基于有向无环图的最大概率组合,还有基于语言模型的最大概率组合。最大概率法尤其是结合了语言模型的最大概率法,能够很好地而解决歧义问题 (歧义问题这里有较详细的描述漫话中文自动分词和语义识别(上):中文分词算法)
另外一个问题则是未登录词,人们也提出了基于字标注的思路(通过sbme标注字),后面详细总结一下。
1. 基于AC自动机的分词
AC自动机实际上是字典树+KMP来完成的快速匹配词表,这个我之前写过一篇,可以简单看下字典树trie与分词
苏神写了三种基于AC自动机的分词法
- 最大匹配法:左到右逐渐匹配词库中的词语,匹配到最长的词语为止
- 最大概率组合:分词的结果概率作乘法后的概率最大
- 最少词匹配:以分词的个数最少为目标获得的分词结果
后两种方法都会用到动态规划的方式来获取每个位置的最优分词路径。
2. 基于切分的新词发现
新词发现这块是比较有趣的,这里分为两个思路,第一个思路是互联网时代的社会语言学:基于SNS的文本数据挖掘,总结而言就是通过片段的频数、片段的凝固度和片段的左右信息熵各取一个阈值来判断一个片段是否可以成词。
- 定义“电影院”的凝合程度就是 p(电影院) 与 p(电) · p(影院) 比值和 p(电影院) 与 p(电影) · p(院) 的比值中的较小值,“的电影”的凝合程度则是 p(的电影) 分别除以 p(的) · p(电影) 和 p(的电) · p(影) 所得的商的较小值
- “信息熵”是一个非常神奇的概念,可以具体看下文章,考虑一个片段的左邻字和右邻字的信息熵,信息熵越大,表示成词的概率越大
以上思路苏神有代码实现过新词发现的信息熵方法与实现
第二种思路,也巧妙的令人兴奋,我们知道第一种思路判断的是片段的凝固度大于一定程度可以成词,反过来思考,当片段的额凝固度低于一定程度时,则不成词,我们就可以在语料中断开。
简化为以下思路,如果a,b是语料相邻两字,统计(a,b)成对出现的次数
n
(
a
,
b
)
n(a,b)
n(a,b),继而估计它的频率
P
(
a
,
b
)
P(a,b)
P(a,b),然后我们分别统计a,b出现的次数
n
a
na
na,
n
b
nb
nb,然后估计它们的频率
P
(
a
)
P(a)
P(a),
P
(
b
)
P(b)
P(b)。若
P
(
a
,
b
)
P
(
a
)
P
(
b
)
<
α
\frac{P(a,b)}{P(a)P(b)}<\alpha
P(a)P(b)P(a,b)<α
则把这两个字断开,初步分词后,再统计词频,根据词频筛选。
这个思路的优势在于
- 只需要计算两个字的凝固度,省去了很多片段如对于一个三字片段,21和12的凝固度
- 分词的长度没有限制,只要一个词的相邻字凝固度比较高,就不会被分开
3. 字标注法与HMM(隐马尔科夫模型)
通过字标注法来进行分词的模型有隐马尔科夫模型(HMM)、最大熵模型(ME)、条件随机场模型(CRF),它们在精度上都是递增的。
关于字标注法,复制了一段苏神的理解:“字标注法有效有两个主要的原因,第一个原因是它将分词问题变成了一个序列标注问题,而且这个标注是对齐的,也就是输入的字跟输出的标签是一一对应的,这在序列标注中是一个比较成熟的问题;第二个原因是这个标注法实际上已经是一个总结语义规律的过程,以4tag标注为为例,我们知道,“李”字是常用的姓氏,一半作为多字词(人名)的首字,即标记为b;而“想”由于“理想”之类的词语,也有比较高的比例标记为e,这样一来,要是“李想”两字放在一起时,即便原来词表没有“李想”一词,我们也能正确输出be,也就是识别出“李想”为一个词,也正是因为这个原因,即便是常被视为最不精确的HMM模型也能起到不错的效果”
HMM
按苏神的思路推导一下,假设输入n个字,输出n个标签(sbme),用
x
=
x
1
x
2
.
.
.
x
n
x=x_1x_2...x_n
x=x1x2...xn表示输入,
o
=
o
1
o
2
.
.
.
o
n
o=o_1o_2...o_n
o=o1o2...on表示输出,用概率来表示最优的输出,我们希望
m
a
x
P
(
o
∣
x
)
=
m
a
x
P
(
o
1
o
2
.
.
.
o
n
∣
x
1
x
2
.
.
.
x
n
)
maxP(o|x)=maxP(o_1o_2...o_n|x_1x_2...x_n)
maxP(o∣x)=maxP(o1o2...on∣x1x2...xn)
简言之,o有很多可能性,最优的o应该是最大概率的o
要
P
(
o
∣
x
)
P(o|x)
P(o∣x)涉及2n个变量,直接计算是很困难的,简化一下,假设每个字的输出仅仅与当前字有关,则有
P
(
o
1
o
2
.
.
.
o
n
∣
x
1
x
2
.
.
.
x
n
)
=
P
(
o
1
∣
x
1
)
P
(
o
2
∣
x
2
)
.
.
.
P
(
o
n
∣
x
n
)
P(o_1o_2...o_n|x_1x_2...x_n)=P(o_1|x_1)P(o_2|x_2)...P(o_n|x_n)
P(o1o2...on∣x1x2...xn)=P(o1∣x1)P(o2∣x2)...P(on∣xn),这样要使
P
(
o
∣
x
)
P(o|x)
P(o∣x)最大,只需每个
P
(
o
k
∣
x
k
)
P(o_k|x_k)
P(ok∣xk)最大,以上假设称为马尔科夫假设。
以上假设没有考虑上下文,会出现不合理的情况,例如b后面只能接m或e,上述简化会出现bbb的情况,我们需要把类似于转移概率放进上述公式,提出了一种隐马尔科夫模型。
由贝叶斯公式
P
(
o
∣
x
)
=
P
(
o
,
x
)
P
(
x
)
=
P
(
x
∣
o
)
P
(
o
)
P
(
x
)
P(o|x)=\frac{P(o,x)}{P(x)}=\frac{P(x|o)P(o)}{P(x)}
P(o∣x)=P(x)P(o,x)=P(x)P(x∣o)P(o)
由于x是给定的输入,
P
(
x
)
P(x)
P(x)是常数可以忽略,最大化
P
(
o
∣
x
)
P(o|x)
P(o∣x)就等价于最大化
P
(
x
∣
o
)
P
(
o
)
P(x|o)P(o)
P(x∣o)P(o)
我们可以对
P
(
x
∣
o
)
P(x|o)
P(x∣o)作马尔可夫假设,得到
P
(
x
∣
o
)
=
P
(
x
1
∣
o
1
)
P
(
x
2
∣
o
2
)
.
.
.
P
(
x
n
∣
o
n
)
P(x|o)=P(x_1|o_1)P(x_2|o_2)...P(x_n|o_n)
P(x∣o)=P(x1∣o1)P(x2∣o2)...P(xn∣on)
同理,对于
P
(
o
)
P(o)
P(o)有
P
(
o
)
=
P
(
o
1
)
P
(
o
2
∣
o
1
)
P
(
o
3
∣
o
1
,
o
2
)
.
.
.
P
(
o
n
∣
o
1
,
o
2
,
.
.
.
,
o
n
−
1
)
P(o)=P(o_1)P(o_2|o_1)P(o_3|o_1,o_2)...P(o_n|o_1,o_2,...,o_{n-1})
P(o)=P(o1)P(o2∣o1)P(o3∣o1,o2)...P(on∣o1,o2,...,on−1)
又可以作另一个马尔可夫假设:每个输出仅仅与上一个输出有关,
P
(
o
)
=
P
(
o
1
)
P
(
o
2
∣
o
1
)
P
(
o
3
∣
o
2
)
.
.
.
P
(
o
n
∣
o
n
−
1
)
≈
P
(
o
2
∣
o
1
)
P
(
o
3
∣
o
2
)
.
.
.
P
(
o
n
∣
o
n
−
1
)
P(o)=P(o_1)P(o_2|o_1)P(o_3|o_2)...P(o_n|o_{n-1})\approx P(o_2|o_1)P(o_3|o_2)...P(o_n|o_{n-1})
P(o)=P(o1)P(o2∣o1)P(o3∣o2)...P(on∣on−1)≈P(o2∣o1)P(o3∣o2)...P(on∣on−1)
这里后面省去了
P
(
o
1
)
P(o_1)
P(o1),因为这个要估的话大概是s和b都为0.5,对结果影响不大。
最终,
P
(
x
∣
o
)
P
(
o
)
≈
P
(
x
1
∣
o
1
)
P
(
o
2
∣
o
1
)
P
(
x
2
∣
o
2
)
P
(
o
3
∣
o
2
)
.
.
.
P
(
x
n
−
1
∣
o
n
−
1
)
P
(
o
n
∣
o
n
−
1
)
P
(
x
n
∣
o
n
)
P(x|o)P(o) \approx P(x_1|o_1)P(o_2|o_1)P(x_2|o_2)P(o_3|o_2)...P(x_{n-1}|o_{n-1})P(o_n|o_{n-1})P(x_n|o_n)
P(x∣o)P(o)≈P(x1∣o1)P(o2∣o1)P(x2∣o2)P(o3∣o2)...P(xn−1∣on−1)P(on∣on−1)P(xn∣on)
我们称
P
(
x
k
∣
o
k
)
P(x_k|o_k)
P(xk∣ok)为发射概率,
P
(
o
k
∣
o
k
−
1
)
P(o_k|o_{k-1})
P(ok∣ok−1)为转移概率,可以通过设置
P
(
o
k
∣
o
k
−
1
)
=
0
P(o_k|o_{k-1})=0
P(ok∣ok−1)=0来排除bb、bs等不合理的组合。
当然,也可以把马尔可夫假设加强——比如假设每个状态跟前面两个状态有关,那样肯定会得到更精确的模型,但是模型的参数就更难估计了。
代码
苏神的代码分为三部分
- 根据文本或词典估计 P ( x k ∣ o k ) P(x_k|o_k) P(xk∣ok)
- 估计转移概率
- 通过viterbi算法动态规划求最大概率路径,一句话总结,算出每一个位置分别为sbme的最优路径
from collections import Counter
def read_dict(path):
hmm_d = {c:Counter() for c in 'sbme'}
with open(path,'r') as f:
for line in f:
line = line.strip().split('\t')
if len(line[0])==1:
# 根据词频获取字的频数
hmm_d['s'][line[0]] += int(line[1])
else:
hmm_d['b'][line[0]] += int(line[1])
hmm_d['e'][line[0][-1]] += int(line[1])
for m in line[0][1:-1]:
hmm_d['m'][m] += int(line[1])
return hmm_d
path = 'dict.txt'
hmm_d = read_dict(path)
# 计算sbme的总数
log_total = {i:log(sum(hmm_d[i].values())) for i in 'sbme'}
trans = {'ss':0.3, 'sb':0.7, 'bm':0.3, 'be':0.7, 'mm':0.3, 'me':0.7, 'es':0.3, 'eb':0.7}
trans = {i: log(j) for i,j in trans.items()}
# viterbi算法计算最大概率路径
def viterbi(nodes):
"""nodes = [{'s':,'b':},{},{}]"""
paths = nodes[0]
for i in range(1,len(nodes)):
paths_ = paths
paths = {}
# j可能为sbme,找到当前位置每个标注对应的最大概率
for j in nodes[i].keys():
tmp = {}
# k表示当前的标注路径 v表示value
for k,v in paths.items():
if k[-1]+j in trans:
tmp[k+j] = v+trans[k[-1]+j]+nodes[i][j]
k = tmp.keys().index(max(tmp.values()))
paths[tmp.keys()[k]] = tmp.values()[k]
return paths.keys()[paths.values().index(max(paths.values))]
def hmm_cut(s):
"""分词,计算分词结果"""
nodes = [{i: log(j[c]+1)-log_total(i)} for i,j in hmm_d for c in s]
res = viterbi(nodes)
words = [s[0]]
for i in range(1,len(res)):
if res[i] in ['s','b']:
words.append(s[i])
else:
words[-1] += s[i]
return words
4. 基于双向LSTM的seq2seq字标注
一句话总结下,利用双向LSTM直接预测smbe标签,并结合viterbi算法(加入转移概率)输出最终预测结果。
值得实践下!
【中文分词系列】 4. 基于双向LSTM的seq2seq字标注
5. 基于语言模型的无监督分词
本篇博客介绍了一种应用语言模型来完成无监督分词的思路。
总结前面的分词思路,字标注的方式输出了每个位置不同标注的概率值,通过viterbi算法结合转移概率输出最大概率路径,也就得到了分词结果。
这里采用了四字符标注,为了保证可以出现大于4个字的情况e后面可以继续跟e,但是转移概率设的很小
- b:单字词或者多字词的首字
- c:多字词的第二字
- d:多字词的第三字
- e:多字词的其余部分
转移概率还是根据直觉设计,发射概率则是通过语言模型获取 - p ( b ) = p ( s k ) p(b)=p(s_k) p(b)=p(sk)
- p ( c ) = p ( s k ∣ s k − 1 ) p(c)=p(s_k|s_{k-1}) p(c)=p(sk∣sk−1)
- p ( d ) = p ( s k ∣ s k − 2 s k − 1 ) p(d)=p(s_k|s_{k-2}s_{k-1}) p(d)=p(sk∣sk−2sk−1)
- p ( e ) = p ( s k ∣ s k − 3 s k − 2 s k − 1 ) p(e)=p(s_k|s_{k-3}s_{k-2}s_{k-1}) p(e)=p(sk∣sk−3sk−2sk−1)
获取的代码如下,因为model.score
输出的概率值是log后的,所以每一个位置的概率通过减前一个位置获得
def cp(s):
return (model.score(' '.join(s), bos=False, eos=False) - model.score(' '.join(s[:-1]), bos=False, eos=False)) or -100.0
【中文分词系列】 5. 基于语言模型的无监督分词
熟悉了分词套路的同学觉得更多的新意在于语言模型,贴两篇关于kenlm的文章
原理推导 图解N-gram语言模型的原理–以kenlm为例
kenlm实践 python | 高效使用统计语言模型kenlm:新词发现、分词、智能纠错等
6. 基于全卷积网络的中文分词
基于全卷积网络的中文分词与前面的分词类似,通过模型训练一个字标注“smbe”的标签概率值,通过viterbi算法输出最终结果。
这里苏神提供了一种硬编码字典的方式,将字典的取值融入到深度学习训练结果。具体操作如下
添加一个add_dict.txt文件,每一行是一个词,包括词语和倍数,这个倍数就是要将相应的标签概率扩大的倍数,比如词表中指定词语“科学空间,10”,而对“科学空间挺好”进行分词时,先用模型得到这六个字的标签概率,然后查找发现“科学空间”这个词在这个句子里边,所以将第一个字为s的概率乘以10,将第二、三个字为m的概率乘以10,将第4个字为e的概率乘以10(不用归一化,因为只看相对值就行了),同样地,如果某些地方切漏了(该切的没有切),也可以加入到词表中,然后设置小于1的倍数就行了。
这种思路值得借鉴下。
【中文分词系列】 6. 基于全卷积网络的中文分词
7. 深度学习分词?只需一个词典
只有一个词典,怎么训练分词呢?苏神给出的答案是:随机组合。把词典中的词随机组合,就有了有标注的数据,采用之前的lstm方式训练即可
- 首先得准备一个带词频的词表,词频是必须的,如果没有词频,则效果会大打折扣,因为需要以正比于词频的概率,随机挑选词表中的词语,组合成“句子”。
这边文章主要的特点就是利用词典和词频,以词频作为权重,随机抽取一些词组成句子训练,在有词典的情况下,相当于无监督学习,思路奇特,简单且确实效果可以,并且词典的优化有助于效果的提升。
【中文分词系列】 7. 深度学习分词?只需一个词典!
8. 更好的新词发现算法
本文的目标是根据相关性的划分无监督生成一个词典。
本文的思路主要分为3步
- 第一步,统计:选取某个固定的n,统计2grams、3grams、…、ngrams,计算它们的内部凝固度(凝固度计算可以参考前面的文章),只保留高于某个阈值的片段,构成一个集合G。多字的凝固度计算有很大的好处,比如前面新词发现算法会受制于“和国”比较低,导致分不出“共和国”,并且不会把“共和”分为一个词。
- 第二步,切分:用上述grams对语料进行切分(粗糙的分词),并统计频数。注意这里切分的粒度很粗。比如“各项目”,主要“各项”和“项目”都在G中,就算“各项目”不在G中,也不会切分。统计分词的结果,筛选出高频部分。
- 第三步,回溯:经过第二步,“各项目”会被切出来(因为第二步保证宁放过,不切错)。回溯就是检查,如果它是一个小于等于n字的词,那么检测它在不在G中,不在就出局;如果它是一个大于n字的词,那个检测它每个n字片段是不是在G中,只要有一个片段不在,就出局。还是以“各项目”为例,回溯就是看看,“各项目”在不在3gram中,不在的话,就得出局。上面的“各项目”由于不在凝固度高的ngrams中,所以会被移除。
本文的思路看起来还是有些迷惑,需要多理解,我目前理解的是通过凝固度选词+词频选词+凝固度筛选的方式生成词典,从苏神的描述看结果很不错的,有时间尝试一下。