中文分词,就是给一个汉语句子作为输入,以“BEMS”组成的序列串作为输出,然后再进行切词,进而得到输入句子的划分。其中,B代表该字是词语中的起始字,M代表是词语中的中间字,E代表是词语中的结束字,S则代表是单字成词。
HMM简介
给定一个句子:
小明昨天去游泳馆学游泳了
得到BEMS组成的序列为
BEBESBMESBES
因为句尾只可能是E或者S,所以得到切词方式为
BE/BE/S/BME/S/BE/S
小明/昨天/去/游泳馆/学/游泳/了
五元组:
状态集合Q={B,E,M,S}
观测集合V={词典中的字}
观测序列O={小,明,昨,天,去,游,泳,馆,学,游,泳,了}
初始概率分布π:句子的第一个字属于{B,E,M,S}这四种状态的概率(假设句子第一个字属于B的概率为0.6,第一个字属于S的概率为0.4)π=(0.6, 0,0, 0.4)T
状态转移概率分布A:如果前一个字位置是B,那么后一个字位置为BEMS的概率各是多少(4x4矩阵)
观测概率矩阵B:在状态B的条件下,观察值为某个字的概率
HMM进行中文分词,属于解码问题,即已知模型(π,A,B)和观测序列O,求对给定观测序列最大的状态序列I(给定观测序列,求最有可能的状态序列)。
使用Viterbi算法
观测序列值是Viterbi的输入,而状态序列值是Viterbi的输出,输入和输出之间Viterbi算法还需要借助三个模型参数,分别是初始状态概率(π), 状态转移概率(A), 观测概率(B)。
def viterbi(pi, A, B, O):
O = O.strip()
O_len = len(O)
pi_len = len(pi)
if O_len == 0:
return
# 保存所有状态的最大值是由哪一个状态产生的也就是计算δ[t](i)时,是由哪一个δ[t-1](q)产生的,q就是哪个状态
states = np.full(shape=(O_len, pi_len), fill_value=0.0)
# 保存计算过所有的计算的δ
deltas = np.full(shape=(O_len, pi_len), fill_value=0.0)
# 初始化计算最优P(I,O1) = max{P(O1|I)*p(I)}
for j in range(0, pi_len):
deltas[0][j] = pi[j] + B[j][ord(O[0])] # 变加法是因为取了对数
# dp计算P(I|O1,O2,O3,...Ot,I1,I2...It-1)
for t in range(1, O_len):
for i in range(0, pi_len): # 计算每一个δ[t](i=q1...q[pi_len]) = max{δt[j]*A[ji]*B[qi|Ot]},j是遍历所有状态
deltas[t][i] = deltas[t - 1][0] + A[0][i]
# 寻找最大的δ[t](i)
for j in range(1, pi_len):
current = deltas[t - 1][j] + A[j][i]
if current > deltas[t][i]:
deltas[t][i] = current
# 保存当前δ[t](i)取得最大值是是从上一个哪个状态来的
states[t][i] = j
deltas[t][i] += B[i][ord(O[t])]
# 回溯找到最优概率路径
max1 = deltas[O_len - 1][0]
best_state = np.zeros(O_len)
# 先找出最后一个观测的最可能状态是什么
for i in range(1, pi_len):
if deltas[O_len - 1][i] > max1:
max1 = deltas[O_len - 1][i]
best_state[O_len - 1] = i
# 由最后一个观测得到的最好状态往前回溯找出状态序列
for i in range(O_len - 2, -1, -1):
best_state[i] = states[i + 1][int(best_state[i + 1])]
return best_state