1.大意失荆州
笔者上个星期上了一门创新研修课需要利用HMM来进行词性标注,我一开始的思路大致如下:
- 首先利用语料库训练出每个隐状态的初始概率、状态之间的转移概率和隐状态生成观测值的发射概率,
- 然后对于给定的观测序列利用viterbi算法求出最大概率的隐状态序列,我的算法在原来的训练集上面进行词性标注效果很好。
于是我乐呵呵的拿到讲台上去讲,但是老师二话不说,让我找个句子随便来测试一下,我自信满满,上百度随便找了一段话,用我之前写的分词算法分好词,然后来进行词性标注,也就是这,出大问题了!
我拿着一段从来没出现在训练语料库的句子进行词性标注,出现了这样的结果:
也就是说,所有的词都被标注为同一个词性。
一开始我觉得肯定是句子的问题,然后我找了很多的测试句子,发现结果都一样!
我一开始找的viterbi算法如下所示:
def viterbi(obs, states, start_p, trans_p, emit_p):
"""
:param obs: 可见序列
:param states: 隐状态
:param start_p: 开始概率
:param trans_p: 转换概率
:param emit_p: 发射概率
:return: 序列+概率
"""
path = {}
V = [{}] # 记录第几次的概率
for state in states:
V[0][state] = start_p[state] * emit_p[state].get(obs[0], 0)
path[state] = [state]
for n in range(1, len(obs)):
V.append({})
newpath = {}
for k in states:
pp,pat=max([(V[n - 1][j] * trans_p[j].get(k,0) * emit_p[k].get(obs[n], 0) ,j )for j in states])
V[n][k] = pp
newpath[k] = path[pat] + [k]
# path[k] = path[pat] + [k]#不能提起变,,后面迭代好会用到!
path=newpath
(prob, state) = max([(V[len(obs) - 1][y], y) for y in states])
return prob, path[state]
oh,我猛然想起来,我没有对状态转移概率为0的情况进行讨论,也没有对发射概率为0的情况进行讨论(这个是最主要的),也就是说对于新出现的词,通过原先我们训练出的语料库得到的发射概率中,不存在任何一种隐状态能够生成这个观测值。
我在github上找了很多很多的不同版本的viterbi算法,发现他们都没有对这种情况进行讨论。
2.后悔可及
但是肯定是要对上述的情况进行讨论,我的想法是对于状态转移概率为0、发射概率为0的观测值需要赋值一个非常小非常小的值,这样才能保证正确的分词结果。我的代码如下:
def viterbi(obs, states, start_p, trans_p, emit_p):
"""
:param obs: 可见序列
:param states: 隐状态
:param start_p: 开始概率
:param trans_p: 转换概率
:param emit_p: 发射概率
:return: 序列+概率
"""
path = {}
v = [{}] # 记录第几次的概率
for state in states:
if obs[0] not in emit_p[state]:
v[0][state] = start_p[state] * 1e-20
else :
v[0][state] = start_p[state] * emit_p[state].get(obs[0], 0)
path[state] = [state]
# 以时间循环,从第一天开始循环
for t in range(1, len(obs)):
# print obs[t]
v.append({})
newpath = {}
for y1 in states : # 从path中取路径,作为上一个开始的路径
max_prob = -1
for y0 in v[t-1]:
if y1 not in trans_p[y0] :#判断转移概率是否为0
print("转移"+y0 + y1)
if obs[t] not in emit_p[y1]:#判断发射率是否为0
nprob = v[t - 1][y0] * 1e-20 * 1e-20
else:
nprob = v[t - 1][y0] * 1e-20 * emit_p[y1][obs[t]]
else:
if obs[t] not in emit_p[y1]:
nprob = v[t - 1][y0] * trans_p[y0][y1] * 1e-20
else:
nprob = v[t - 1][y0] * trans_p[y0][y1] * emit_p[y1][obs[t]]
if nprob > max_prob:
# 暂时记录到y1状态节点最后的概率
max_prob = nprob
# 暂时记录到y1状态节点的上一个y0节点
max_state = y0
# 保存到当前的y1状态的最好的概率值
v[t][y1] = max_prob
newpath[y1] = path[max_state] + [y1]
path = newpath
(prob, state) = max([(v[len(obs) - 1][y], y) for y in states])
return path[state]
很幸运,我的分析是正确的,我找了很多的测试集进行测试,都能够很好的进行词性标注。上面我那个标注有误的句子用改正后的viterbi算法标注如下:
这下可以放心的去上课了
3.三省吾身
这下子应该对于HMM进行词性标注有点掌握了。附上训练部分的代码。(viterbi算法代码上面给了)
# -*- coding:utf-8 -*-
import sys
import collections
start_c={}#开始概率,就是一个字典,state:chance=Word/lines
transport_c={}#转移概率,是字典:字典,state:{state:num,state:num....} num=num(state1)/num(statess)
emit_c={}#发射概率,也是一个字典,state:{word:num,word,num} num=num(word)/num(words)
Count_dic = {} # 一个属性下的所有单词,为了求解emit
state_list = ['Ag', 'a', 'ad', 'an', 'Bg', 'b', 'c', 'Dg',
'd', 'e', 'f', 'h', 'i', 'j', 'k', 'l',
'Mg', 'm', 'Ng', 'n', 'nr', 'ns', 'nt', 'nx',
'nz', 'o', 'p', 'q', 'Rg', 'r', 's','na',
'Tg', 't','u', 'Vg', 'v', 'vd', 'vn','vvn',
'w', 'Yg', 'y', 'z']
lineCount=-1#句子总数,为了求出开始概率
for state0 in state_list:
transport_c[state0]={}
for state1 in state_list:
transport_c[state0][state1]=0.0
emit_c[state0]={}
start_c[state0]=0.0
vocabs=[]
classify=[] #存放每一行的每个状态的列表
class_count=collections.defaultdict(list) #存放每一行的每个状态对应的词的列表
for state in state_list:
class_count[state]=0.0
with open('人民日报1998下半年/199808.txt','r',encoding='gb2312') as filess:
for line in filess: #一次处理全部的行??列表可能会影响效率??
line=line.strip()
if not line:continue
lineCount += 1#应该在有内容的行处加 1
words=line.split(" ")#分解为多个单词
for word in words:
#osition= word.index('/') #如果是[中国人民/n]
for i in range(len(word)):
if word[i] =='/':
position = i
if '[' in word and ']' in word:
vocabs.append(word[1:position])
vocabs.append(word[position+1:-1])
break
if '[' in word:
vocabs.append(word[1:position])
classify.append(word[position+1:])
break
if ']' in word:
vocabs.append(word[:position])
classify.append(word[position+1:-1])
break
vocabs.append(word[:position])
classify.append(word[position+1:])
if len(vocabs)!=len(classify):
print('词汇数量与类别数量不一致')
break #不一致退出程序
# start_c = {} # 开始概率,就是一个字典,state:chance=Word/lines
# transport_c = {} # 转移概率,是字典:字典,state:{state:num,state:num....} num=num(state1)/num(statess)
# emit_c = {} # 发射概率,也是一个字典,state:{word:num,word,num} num=num(word)/num(words)
else:
for n in range(0,len(vocabs)):
class_count[classify[n]] += 1.0
if vocabs[n] in emit_c[classify[n]]:
emit_c[classify[n]][vocabs[n]] += 1.0
else:
emit_c[classify[n]][vocabs[n]] = 1.0
if n==0:
start_c[classify[n]] += 1.0
else:
transport_c[classify[n-1]][classify[n]]+=1.0
vocabs = []
classify = []
for state in state_list:
start_c[state]=start_c[state]*1.0/lineCount
for li in emit_c[state]:
emit_c[state][li]=emit_c[state][li]/class_count[state]
for li in transport_c[state]:
transport_c[state][li]=transport_c[state][li]/class_count[state]
file0=open('start.txt','w',encoding='utf8')
file0.write(str(start_c))
file1=open('tran.txt','w',encoding='utf8')
file1.write(str(transport_c))
file2=open('emit.txt','w',encoding='utf8')
file2.write(str(emit_c))
file0.close()
file1.close()
file2.close()