在前一篇博客中,讨论了隐马尔可夫模型的原理,三个问题,以及每个问题的解决方案和对应的算法推导。在本文中,就利用HMM中的预测问题来实现汉语分词。
对于一段文字,对里边的词(可以是单个字,也可以是多个字)进行标注,标注的类型分为4中,分别为:Begin(开头)、End(结尾)、Middle(中间)、Single(单独)。因此我们需要有一个已经分好词的训练数据,训练数据据。点击获取数据集
![](https://i-blog.csdnimg.cn/blog_migrate/74bbaa05d34b0b06413653d0433ff1e2.jpeg)
计算 λ = ( A , B , π ) \lambda=(A,B,\pi) λ=(A,B,π)
根据训练集计算处HMM需要的 λ = ( A , B , π ) \lambda=(A,B,\pi) λ=(A,B,π),计算过程如下:
def get_ABPi(train_data, sep=" "):
"""
train_data: 已经分好词的数据集
sep: 分词使用的分隔符
"""
tokens = train_data.split(sep)
# 初始状态转移值,0,1,2,3分别代表Begin,Middle,End,Single
pi = np.zeros(4)
# 状态转移矩阵
A = np.zeros((4, 4))
# 发射矩阵,取值65535主要是为了适应所有字符,也可以根据自己的数据集字符数量选定
B = np.zeros((4, 65536))
last_token = tokens[0]
for token in tokens:
token = token.strip()
len_token = len(token)
# 如果前一个词的长度为1,则说明前一个状态是Single,如果长度不是1,则说明前一个状态是End
last_token_state = 3 if len(last_token) == 1 else 2
if len_token == 0:
continue
if len_token == 1:
# 当前词长度为1时,改变pi,A,B中Single的数量
pi[3] += 1
A[last_token_state][3] += 1
B[3][ord(token)] += 1
elif len_token == 2:
# 当前词长度为2时,改变pi,A,B中Begin,End的数量
pi[0] += 1
pi[2] += 1
A[0][2] += 1
A[last_token_state][0] += 1
B[0][ord(token[0])] += 1
B[2][ord(token[1])] += 1
else:
# 当前词为其他长度时,改变pi,A,B中Begin,Middle,End的数量
pi[0] += 1
pi[2] += 1
pi[1] += len_token - 2
A[0][1] += 1
A[1][1] += len_token - 3
A[1][2] += 1
A[last_token_state][0] += 1
B[0][ord(token[0])] += 1
B[2][ord(token[len_token-1])] += 1
for i in range(1, len_token-1):
B[1][ord(token[i])] += 1
last_token = token
# 这里将为0的数据重置为极小值,防止出现除以0的错误
pi[pi == 0] = 1e-32
A[A == 0] = 1e-32
B[B == 0] = 1e-32
# 这里用对数log主要是为了提高精度(后边的操作都是乘法,用对数就可以变成加法或减法)
pi = np.log(pi) - np.log(np.sum(pi))
A = np.log(A) - np.log(np.sum(A, 1).reshape((-1, 1)))
B = np.log(B) - np.log(np.sum(B, 1).reshape((-1, 1)))
return A, B, pi
维特比算法分词
根据从训练集中获取到的
λ
=
(
A
,
B
,
π
)
\lambda=(A,B,\pi)
λ=(A,B,π),来预测分词,这里使用维特比算法进行计算。(如需看详细推导过程,请查看我的维特比算法推导)
t
t
t时刻与
t
+
1
t+1
t+1时刻
δ
\delta
δ之间的关系如下:
δ
t
+
1
(
i
)
=
b
i
(
o
t
+
1
)
max
1
≤
j
≤
N
δ
t
(
j
)
a
j
i
\delta_{t+1}(i)= b_i(o_{t+1})\max_{1\le j \le N} \delta_t(j) a_{ji}
δt+1(i)=bi(ot+1)1≤j≤Nmaxδt(j)aji
特别的,
t
=
1
t=1
t=1时刻:
δ
1
(
i
)
=
b
i
(
o
1
)
π
(
i
)
\delta_1(i)=b_i(o_1)\pi(i)
δ1(i)=bi(o1)π(i)
def viterbi(pi, A, B, O):
"""
pi: 初始状态转移概率
A: 状态转移矩阵
B: 发射矩阵
"""
O = O.strip()
len_O = len(O)
len_pi = len(pi)
if len_O == 0:
return None
# 保存当前时刻状态的delta达到最大概率时,前一个时刻的状态
states = np.full((len_O, len_pi), fill_value=0.00)
# 保存当前时刻状态的delta达到最大概率时,delta的值
deltas = np.full((len_O, len_pi), fill_value=0.00)
# 计算t=1时的delta值
for j in range(len_pi):
deltas[0][j] = pi[j] + B[j][ord(O[0])]
# 计算t>1时的delta值
for k in range(1, len_O):
# 计算每一个可能状态
for i in range(len_pi):
deltas[k][i] = -1e20
# 计算当前时刻delta的最大值
for j in range(len_pi):
current = deltas[k-1][j] + A[j][i]
if current > deltas[k][i]:
# 记录当前最大的delta值
deltas[k][i] = current
# 记录当前delta达到最大时,前一刻的状态
states[k][i] = j
deltas[k][i] += B[i][ord(O[k])]
# 求出最后一个时刻的最大delta值所在的状态
max1 = deltas[len_O-1][0]
best_state = np.zeros(len_O)
for i in range(len_pi):
if deltas[len_O-1][i] > max1:
max1 = deltas[len_O-1][i]
best_state[len_O-1] = i
# 根据最优状态通过state往回寻找,确定最佳路径
for k in range(len_O-2, -1, -1):
best_state[k] = states[k+1][int(best_state[k+1])]
return best_state
通过维特比算法就可以得出当前文本的分词结果,根据 best_state将结果呈现出来,部分结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/d7b1411b1c2346d48ecf0930bab9543c.png)