上篇文章我们以Brown语料库中的一个特例讲解了HMM和Viterbi算法。
那么如何使用特定语料库通过HMM算法进行词性标注呢?我们可以从HMM的五元组入手。
大致步骤:
在实际操作中某些小细节是需要引起我们关注的。
我们要考虑并处理发射矩阵和转移矩阵中条件概率为0而出现整条路径概率为0情况,因此我们需要做数据平滑。下面以他是创业大赛国赛金奖?
为例展开分析:
数据平滑的方法有很多种,一开始我使用的是加一法,在测试的时候我发现无论输入什么句子,词性标注都是Rg。
他/Rg 是/Rg 创业/Rg 大赛/Rg 国/Rg 赛/Rg 金奖/Rg ?/Rg
平滑方法改进后:
他/r 是/v 创业/vn 大赛/vn 国/n 赛/vn 金奖/n ?/w
为什么会出现这样的情况呢?
我们回到语料库可以发现总词性种类有44种,而Rg只出现了10次,如果我们使用加一法平滑数据,对数据的概率影响还是比较大的,因此加一法在该语料库中的表现上效果不是特别理想。我们可以使用Laplace平滑方法,并使参数p足够小,对平滑方法进行改进,从而更好地弥补语料库数据量的不足。其他数据平滑方法还有Kneser-Ney、Katz等,在宗成庆的《统计自然语言处理》中有所提及。
# 加一法平滑方法
for row in range(transaction_matrix.shape[0]):
transaction_matrix[row] += 1
transaction_matrix[row] /= np.sum(transaction_matrix[row])
for row in range(emission_matrix.shape[0]):
emission_matrix[row] += 1
emission_matrix[row] /= tags_num[tags_list[row]] + emission_matrix.shape[1]
# Laplace平滑方法 l=1,p=1e-16
# 这里也可以看做使用一个极小的数字如1e-16代替0,分母+1为避免分母为0的情况出现
for row in range(transaction_matrix.shape[0]):
n = np.sum(transaction_matrix[row])
transaction_matrix[row] += 1e-16
transaction_matrix[row] /= n + 1
for row in range(emission_matrix.shape[0]):
emission_matrix[row] += 1e-16
emission_matrix[row] /= tags_num[tags_list[row]] + 1
这里使用的分词词典是基于该语料库的,所以分割结果都能找到对应的词性标注,所以不存在未登录词未知词性的情况。
但是,如果我们使用别的分词词典或者分词工具呢?那就会出现未登录词的情况,这时候每种词性的发射概率都相同,该未登录词的词性就跟前后的词性有关了,不过对未登录词测试结果并不是太好。
最大概率分词:
今天/t 和/p 明天/t 我/r 不/d 用/v 再/d 做/v 自然/a 语言/n 处理/vn 作业/n 了/y !/w
其他分词:(含未登录词-自然语言处理)
今天/t 和/p 明天/t 我/r 不用/d 再/d 做/v 自然语言处理/vd 作业/v 了/y 。/w
先看一下这个例子:
我/r 喜欢/v 学习/v 。/w
我们再看看词典,学习有/v,/vn这两种词性,但是在这个例子中应该是vn才对。这是什么情况呢?通过打印发射概率矩阵发现,学习的vn和v这两个发射概率竟然差不多?问题可能出在这里。
我们可以试着将语料库中学习/v
全部替换成学习/vn
。这时候,我们的标注结果变成了:
我/r 喜欢/v 学习/vn 。/w
分析发现,出现这样的标注差异应该是语料库本身造成的,有趣的是我们使用LTP词性标注测试该例子时也是第一种结果。
数据结构使用numpy.float64,保证有足够小的精度尽量防止溢出,能够小到1e-308。
转移概率矩阵大小为44*44
,发射概率矩阵大小为44*分词词串数目
,两者都是在程序运行时生成。
若在大规模词性标注情况下建议保存44*44
和44*60366(语料库总词数)
大小的转移概率矩阵和发射概率矩阵到本地文件中,需要时直接一次读取该文件存入内存而不需动态生成,提高效率。
语料库中有[香港/ns 特区/n 基本法/n 推广/vn 督导/vn 委员会/n]nt
这样的组合标记,为了处理方便,这里特别处理这样的词性标注,在实际应用中需要注意处理这种情况。
在计算转移概率时是以句子为单位进行处理的,因此,对于一段文字建议按标点分成几个句子处理,一来能提高词性标注的准确性,二来防止因句子长度过长出现下溢现象。
import numpy as np
import hmm_viterbi
import max_probability_seg
def cal_hmm_matrix(observation):
# 得到所有标签
word_pos_file = open('ChineseDic.txt').readlines()
tags_num = {}
for line in word_pos_file:
word_tags = line.strip().split(',')[1:]
for tag in word_tags:
if tag not in tags_num.keys():
tags_num[tag] = 0
tags_list = list(tags_num.keys())
# 转移矩阵、发射矩阵
transaction_matrix = np.zeros((len(tags_list), len(tags_list)), dtype=float)
emission_matrix = np.zeros((len(tags_list), len(observation)), dtype=float)
# 计算转移矩阵和发射矩阵
word_file = open('199801.txt').readlines()
for line in word_file:
if line.strip() != '':
word_pos_list = line.strip().split(' ')
for i in range(1, len(word_pos_list)):
tag = word_pos_list[i].split('/')[1]
pre_tag = word_pos_list[i - 1].split('/')[1]
try:
transaction_matrix[tags_list.index(pre_tag)][tags_list.index(tag)] += 1
tags_num[tag] += 1
except ValueError:
if ']' in tag:
tag = tag.split(']')[0]
else:
pre_tag = tag.split(']')[0]
transaction_matrix[tags_list.index(pre_tag)][tags_list.index(tag)] += 1
tags_num[tag] += 1
for o in observation:
# 注意这里用in去找(' 我/',' **我/'的区别),用空格和‘/’才能把词拎出来
if ' ' + o in line:
pos_tag = line.strip().split(o)[1].split(' ')[0].strip('/')
if ']' in pos_tag:
pos_tag = pos_tag.split(']')[0]
emission_matrix[tags_list.index(pos_tag)][observation.index(o)] += 1
for row in range(transaction_matrix.shape[0]):
n = np.sum(transaction_matrix[row])
transaction_matrix[row] += 1e-16
transaction_matrix[row] /= n + 1
for row in range(emission_matrix.shape[0]):
emission_matrix[row] += 1e-16
emission_matrix[row] /= tags_num[tags_list[row]] + 1
times_sum = sum(tags_num.values())
for item in tags_num.keys():
tags_num[item] = tags_num[item] / times_sum
# 返回隐状态,初始概率,转移概率,发射矩阵概率
return tags_list, list(tags_num.values()), transaction_matrix, emission_matrix
if __name__ == '__main__':
input_str = "今天和明天我弹琴。"
obs = max_probability_seg.seg(input_str).strip().split(' ')
hid, init_p, trans_p, emit_p = cal_hmm_matrix(obs)
result = hmm_viterbi.viterbi(len(obs), len(hid), init_p, trans_p, emit_p)
tag_line = ''
for k in range(len(result)):
tag_line += obs[k] + hid[int(result[k])] + ' '
print(tag_line)
这里用到了上两篇文章讲到的概率最大化分词和Viterbi算法。
用到的语料库链接: https://pan.baidu.com/s/1ifm_pARa8tJaf6_wVHQ5kg 提取码: 9sfb