中文分词简介
在汉语中,词是以字为单位的,但是一篇文章的语义表达却仍然是以词来作为划分的。因此,在处理中文文本时,需要进行分词处理,将句子转化成为词的表示。这个切片过程就是中文分词,通过计算机自动识别出句子的词。
规则分词
通过构建字典,在切分语句时,将语句中的每个字符串与字典中的词逐一比较,找到则切分,找不到则不切分。
正向最大匹配法
假定分词字典中的最长词有i个汉字字符,则用被处理文档的当前字串中的前 i个字作为匹配字段,查找字典,若字典中存在这样的一个i字词,则匹配成功,匹配字段作为一个词切分出来。如果字典中找不到这样的一个i字词,则匹配失败,将匹配字段中的最后一个字去掉,对剩下的字串重新进行匹配处理。直到匹配成功,再从文档中选出下一个i字字串进行同上的匹配,直到文章被扫描完为止。
逆向最大匹配法
逆向最大匹配分词切分与正向最大匹配的方向相反,它从被处理文档的末端开始匹配扫描,每次取最末端的i个字符(i为字典中的最长字数)作为匹配字段,若匹配失败,则去掉匹配字段的前一个字,继续匹配。相应地,它使用的分词字典是逆序字段,其中的每个词条都按照逆序存放。
实际处理时,先将文档进行倒排处理。然后根据逆序字典,对逆序文档用正向最大匹配法处理即可。
双向最大匹配法
综合正向最大匹配和逆向最大匹配,选取词数切分最少(划分出更少的词)的作为结果;若词数相同,则选取单字较少的那个。规则分词相对比较简单,我这里就略写了。
统计分词
语言模型
语言模型(概率论角度):为含有m个词的字符串确定其概率分布
P
(
ω
1
,
ω
2
,
.
.
.
,
ω
m
)
P(\omega_1,\omega_2,...,\omega_m)
P(ω1,ω2,...,ωm),其中
ω
1
−
ω
m
\omega_1-\omega_m
ω1−ωm依次表示文本中的各个词语。
书中此处介绍的语言模型应该是为了求某一句子出现的概率,或者用来预判估计下一个词,为HMM模型做了一定铺垫。
此时按照概率论(条件概率),有下式成立
P
(
ω
1
,
ω
2
,
.
.
.
,
ω
m
)
=
P
(
ω
1
)
P
(
ω
2
∣
ω
2
)
P
(
ω
3
∣
ω
1
ω
2
)
.
.
.
P
(
ω
m
∣
ω
1
ω
2
.
.
.
ω
m
−
1
)
P(\omega_1,\omega_2,...,\omega_m)=P(\omega_1)P(\omega_2|\omega_2)P(\omega_3|\omega_1\omega_2)...P(\omega_m|\omega_1\omega_2...\omega_{m-1})
P(ω1,ω2,...,ωm)=P(ω1)P(ω2∣ω2)P(ω3∣ω1ω2)...P(ωm∣ω1ω2...ωm−1)
显然,当文本过长后,有关概率的计算变得难度大了起来。为了解决这个问题提出n元模型。仅仅考虑前n-1个词对当前词的概率影响,即有下式。(对于句子中前n-1个词语,为原式)
P
(
ω
i
∣
ω
1
,
.
.
.
,
ω
i
−
1
)
≈
P
(
ω
i
∣
ω
i
−
(
n
−
1
)
.
.
.
ω
i
−
1
)
P(\omega_i|\omega_1,...,\omega_{i-1})\approx P(\omega_i|\omega_{i-(n-1)}...\omega_{i-1})
P(ωi∣ω1,...,ωi−1)≈P(ωi∣ωi−(n−1)...ωi−1)
一般使用频率计数比例计算n元条件概率,下式中
c
o
u
n
t
(
ω
)
count(\omega)
count(ω),表示字串
ω
\omega
ω在语料集中出现的次数。
P
(
ω
i
∣
ω
i
−
(
n
−
1
)
,
.
.
.
,
ω
i
−
1
)
=
c
o
u
n
t
(
ω
i
−
(
n
−
1
)
,
.
.
.
,
ω
i
−
1
ω
i
)
c
o
u
n
t
(
ω
i
−
(
n
−
1
)
,
.
.
.
,
ω
i
−
1
)
P(\omega_i|\omega_{i-(n-1)},...,\omega_{i-1}) = \dfrac{count(\omega_{i-(n-1)},...,\omega_{i-1}\omega_i)}{count(\omega_{i-(n-1)},...,\omega_{i-1})}
P(ωi∣ωi−(n−1),...,ωi−1)=count(ωi−(n−1),...,ωi−1)count(ωi−(n−1),...,ωi−1ωi)
n元模型,n=1时,容易发现各个词之间是相互独立的(P(w)=P(w1)P(w2)…P(wn)),当n>=2时,可以保留语序信息。可采用拉普拉斯平滑算法解决分子分母为0情况。
HMM模型
HMM(Hidden Markov model)隐马尔可夫模型可用于分词。
HMM模型首先对句子进行了数学抽象:
λ
=
λ
1
λ
2
.
.
.
λ
n
\lambda=\lambda_1\lambda_2...\lambda_n
λ=λ1λ2...λn。其中
λ
\lambda
λ表示该句子,
λ
i
(
1
≤
i
≤
n
)
\lambda_i(1\leq i\leq n)
λi(1≤i≤n)表示句子中的第i个字,该字可以为B(词首)、M(词中)、E(词尾)、S(单独成词),我们用
o
i
(
o
i
∈
{
B
,
M
,
E
,
S
}
)
o_i(o_i\in \{B,M,E,S\})
oi(oi∈{B,M,E,S})表示第i个字的标注,将分词问题转化为字的标注问题。
那么此时我们的目标函数即为
max
P
(
o
∣
λ
)
=
max
P
(
o
1
,
o
2
,
.
.
.
,
o
n
∣
λ
1
,
λ
2
,
.
.
.
,
λ
n
)
\max P(o|\lambda) =\max P(o_1,o_2,...,o_n|\lambda_1,\lambda_2,...,\lambda_n)
maxP(o∣λ)=maxP(o1,o2,...,on∣λ1,λ2,...,λn)
同时,利用贝叶斯公式对其进行变换可得
P
(
o
∣
λ
)
=
P
(
λ
∣
o
)
P
(
o
)
P
(
λ
)
P(o|\lambda) = \frac{P(\lambda|o)P(o)}{P(\lambda)}
P(o∣λ)=P(λ)P(λ∣o)P(o)
上式中,
P
(
λ
)
P(\lambda)
P(λ)在
λ
\lambda
λ确定时为定值,因此我们仅仅比较分子即可。我们接下来主要针对分子进行处理,首先是
P
(
λ
∣
o
)
P(\lambda |o)
P(λ∣o),我们做出马尔可夫假设,得到
P
(
λ
∣
o
)
=
P
(
λ
1
∣
o
1
)
P
(
λ
2
∣
o
2
)
.
.
.
P
(
λ
n
∣
o
n
)
P(\lambda|o) = P(\lambda_1|o_1)P(\lambda_2|o_2)...P(\lambda_n|o_n)
P(λ∣o)=P(λ1∣o1)P(λ2∣o2)...P(λn∣on)
对于
P
(
o
)
P(o)
P(o),我们做出另一个假设——齐次马尔科夫假设(类似之前的二元模型)
P
(
o
)
=
P
(
o
1
)
P
(
o
2
∣
o
1
)
.
.
.
P
(
o
n
∣
o
1
.
.
.
o
n
−
1
)
≈
P
(
o
1
)
P
(
o
2
∣
o
1
)
.
.
.
P
(
o
n
∣
o
n
−
1
)
P(o) = P(o_1)P(o_2|o_1)...P(o_n|o_1...o_{n-1})\\\approx P(o_1)P(o_2|o_1)...P(o_n|o_{n-1})
P(o)=P(o1)P(o2∣o1)...P(on∣o1...on−1)≈P(o1)P(o2∣o1)...P(on∣on−1)
在这里
P
(
λ
i
∣
o
i
)
(
1
≤
i
≤
n
)
P(\lambda_i|o_i)(1\leq i \leq n)
P(λi∣oi)(1≤i≤n)称为发射概率,
P
(
o
i
∣
o
i
−
1
)
(
1
≤
i
≤
n
)
P(o_i|o_{i-1}) (1\leq i \leq n)
P(oi∣oi−1)(1≤i≤n)称为转移概率。均是通过语料集计算而来,感觉在求
P
(
λ
i
∣
o
i
)
P(\lambda_i|o_i)
P(λi∣oi)时,可以采用拉普拉斯平滑算法。下式中
c
o
u
n
t
count
count函数表示其在语料集出现的次数。如,
c
o
u
n
t
(
λ
i
,
o
i
)
count(\lambda_i,o_i)
count(λi,oi)表示在语料库中字
λ
i
\lambda_i
λi标注为
o
i
o_i
oi的次数,
c
o
u
n
t
(
o
i
,
o
i
−
1
)
count(o_i,o_{i-1})
count(oi,oi−1)表示在语料集中
o
i
−
1
o_{i-1}
oi−1后一个字标注为
o
i
o_{i}
oi的次数等等。
P
(
λ
i
∣
o
i
)
=
c
o
u
n
t
(
λ
i
,
o
i
)
c
o
u
n
t
(
o
i
)
P
(
o
i
∣
o
i
−
1
)
=
c
o
u
n
t
(
o
i
,
o
i
−
1
)
c
o
u
n
t
(
o
i
−
1
)
P(\lambda_i|o_i)=\frac{count(\lambda_i,o_i)}{count(o_i)}\\ P(o_i|o_{i-1})=\frac{count(o_i,o_{i-1})}{count(o_{i-1})}
P(λi∣oi)=count(oi)count(λi,oi)P(oi∣oi−1)=count(oi−1)count(oi,oi−1)
该模型可以通过Viterbi算法进行求解。
input:
P
(
λ
i
∣
o
i
)
(
1
≤
i
≤
n
)
P
(
o
k
1
∣
o
k
2
)
(
o
k
1
∣
o
k
2
∈
{
B
,
M
,
E
,
S
}
)
P
i
(
o
k
)
,
每
个
状
态
作
为
句
子
开
头
概
率
P(\lambda_i|o_i)(1\leq i\leq n) \\P(o_{k_1}|o_{k_2})(o_{k_1}|o_{k_2}\in \{B,M,E,S\})\\ Pi(o_k),每个状态作为句子开头概率
P(λi∣oi)(1≤i≤n)P(ok1∣ok2)(ok1∣ok2∈{B,M,E,S})Pi(ok),每个状态作为句子开头概率
s
t
a
t
e
s
=
{
B
,
M
,
E
,
S
}
states=\{B,M,E,S\}
states={B,M,E,S}
λ
=
λ
1
λ
2
.
.
.
λ
n
(
输
入
句
子
)
\lambda=\lambda_1\lambda_2...\lambda_n (输入句子)
λ=λ1λ2...λn(输入句子)
output:
o
=
o
1
o
2
.
.
.
o
n
o=o_1o_2...o_n
o=o1o2...on
process
设置状态转移概率数组v(v[t][y]表示第t个字是y状态的可能性),以及路径记录数组path
for y in states
\quad
v[1][y] = Pi(y)*P(
λ
1
\lambda_1
λ1|y),path[y]=[y]
end for
for i in [2,n]
\quad
for y in statas
\quad\quad
v[i][y]=max(v[i-1][yp]*P(y|yp)*P(
λ
i
\lambda_i
λi|y)) 对于
∀
\forall
∀ yp
∈
\in
∈ states
\quad\quad
path[y]=path[y]+[ypm] (ypm为上一步取得最大值时的yp)
\quad
end for
end for
ym = arg
y
_y
y{max(v[n][y]) ,对于
∀
\forall
∀ y in states}
算法输出path[ym],结束算法。
注:若输入没有P(
λ
i
\lambda_i
λi|y),可以设置其为1。
该算法的时间复杂度为O(句子长度*状态数量^2),是一种比较快的算法。
其他分词方法
- 条件随机场(CRF,相比HMM,当前状态不仅和之前的状态相关还和之后的状态相关)
- 神经网络分词算法
混合分词
基于字典的分词方式进行分词,再用统计分词的方法进行辅助。多在实际工程中使用。
中文分词工具——jieba
jieba提供了三种分词方式
import jieba
sent = '中文分词是文本处理不可缺少的一步'
- 精确模式
试图将句子最精确地切开,适合文本分析
seg_list = jieba.cut(sent,cut_all=False)
# seg_list = jieba.cut(sent) #默认为精确模式
- 全模式
把文本中所有可以成词的词语都扫描出来
seg_list = jieba.cut(sent,cut_all=True)
- 搜索引擎模式
在精确模式的基础上,再对长词进行切分,提高召回率
seg_list = jieba.cut(sent,cut_all=True)
附录
书上的代码大家估计已经看到了,这里就不再重复了,提供一下jieba中的实现函数。
jieba中viterbi算法的实现
MIN_FLOAT = -3.14e100
MIN_INF = float("-inf")
def viterbi(obs, states, start_p, trans_p, emit_p):
V = [{}] #tabular
mem_path = [{}]
all_states = trans_p.keys()
for y in states.get(obs[0], all_states): #init
V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)
mem_path[0][y] = ''
for t in range(1, len(obs)):
V.append({})
mem_path.append({})
#prev_states = get_top_states(V[t-1])
prev_states = [x for x in mem_path[t-1].keys() if len(trans_p[x]) > 0]
prev_states_expect_next = set((y for x in prev_states for y in trans_p[x].keys()))
obs_states = set(states.get(obs[t], all_states)) & prev_states_expect_next
if not obs_states:
obs_states = prev_states_expect_next if prev_states_expect_next else all_states
for y in obs_states:
prob, state = max([(V[t-1][y0] + trans_p[y0].get(y,MIN_INF) + emit_p[y].get(obs[t],MIN_FLOAT), y0) for y0 in prev_states])
V[t][y] = prob
mem_path[t][y] = state
last = [(V[-1][y], y) for y in mem_path[-1].keys()]
#if len(last)==0:
#print obs
prob, state = max(last)
route = [None] * len(obs)
i = len(obs) - 1
while i >= 0:
route[i] = state
state = mem_path[i][state]
i -= 1
return (prob, route)