基于特定语料库生成HMM转移概率分布和发射概率分布用于词性标注 Python

上篇文章我们以Brown语料库中的一个特例讲解了HMM和Viterbi算法。
那么如何使用特定语料库通过HMM算法进行词性标注呢?我们可以从HMM的五元组入手。

大致步骤:

  1. 得到语料库中词性标注种类和个数==>得到隐序列;
  2. 对输入的句子进行分词==>得到观测序列;
  3. 对每个词性标注,计算该词性出现时的前一个词性的次数/该词性出现的总次数==>得到转移概率矩阵;
  4. 对每个观测状态,计算该状态不同词性出现的次数/该观测状态出现的总次数==>得到发射概率矩阵;
  5. 计算各词性初始概率: n c ( p o s − t a g ) n s u m \frac{n_{c(pos-tag)}}{n_{sum}} nsumnc(postag),某一个词性出现的次数/语料库总词性标记数;

在实际操作中某些小细节是需要引起我们关注的。

1 如何做数据平滑?

我们要考虑并处理发射矩阵和转移矩阵中条件概率为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

2 未登录词的词性?

这里使用的分词词典是基于该语料库的,所以分割结果都能找到对应的词性标注,所以不存在未登录词未知词性的情况。

但是,如果我们使用别的分词词典或者分词工具呢?那就会出现未登录词的情况,这时候每种词性的发射概率都相同,该未登录词的词性就跟前后的词性有关了,不过对未登录词测试结果并不是太好。

最大概率分词:
今天/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

3 语料库的影响?

先看一下这个例子:

我/r 喜欢/v 学习/v 。/w

我们再看看词典,学习有/v,/vn这两种词性,但是在这个例子中应该是vn才对。这是什么情况呢?通过打印发射概率矩阵发现,学习的vn和v这两个发射概率竟然差不多?问题可能出在这里。

我们可以试着将语料库中学习/v全部替换成学习/vn。这时候,我们的标注结果变成了:

我/r 喜欢/v 学习/vn 。/w

分析发现,出现这样的标注差异应该是语料库本身造成的,有趣的是我们使用LTP词性标注测试该例子时也是第一种结果。

4 转移概率矩阵和发射概率矩阵的存储?

数据结构使用numpy.float64,保证有足够小的精度尽量防止溢出,能够小到1e-308。

转移概率矩阵大小为44*44,发射概率矩阵大小为44*分词词串数目,两者都是在程序运行时生成。

若在大规模词性标注情况下建议保存44*4444*60366(语料库总词数)大小的转移概率矩阵和发射概率矩阵到本地文件中,需要时直接一次读取该文件存入内存而不需动态生成,提高效率。

5 其他问题

语料库中有[香港/ns 特区/n 基本法/n 推广/vn 督导/vn 委员会/n]nt这样的组合标记,为了处理方便,这里特别处理这样的词性标注,在实际应用中需要注意处理这种情况。

在计算转移概率时是以句子为单位进行处理的,因此,对于一段文字建议按标点分成几个句子处理,一来能提高词性标注的准确性,二来防止因句子长度过长出现下溢现象

6 实现代码

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

你可能感兴趣的:(自然语言处理)