识别文本中具有特定意义的实体,包括人名、地名、机构名、专有名词等等
在使用的NER数据集中包含七个标签:
文本中以每一个字为单位,每一个字对应上面的任一种标签。
标签前面有分为B和I,"B"表示begin,实体开头的那个字,在实体中间或者结尾部分,,用”I“来标注。
例如:自(B-PER)贸(I-LOC)区(I-LOC),这是一个错误的标注,原因是我们以(B-PER)开头,那么后面的应该是I-PER类型,而不是其他类型。
由此,我们可以发现,仅仅采用语言模型(Bert 或者 LSTM)进行标注的话会产生很多的错误标注,我们需要在语言模型后加上概率图模型(条件随机场)由来约束模型的 输出,从而达到防止输出不合法的标注。
采用训练好的隐马尔可夫模型进行实体标注
from HMM_model import *
model = HMM_NER(char2idx_path="./dicts/char2idx.json",
tag2idx_path="./dicts/tag2idx.json")
model.fit("./corpus/train_data.txt")
model.predict("我在西区300318教室上清华大学的自然语言处理课程")
text = "张吉惟、林国瑞、林玟书、林雅南、江奕云、刘柏宏、阮建安、林子帆"
model.predict(text)
隐马尔可夫模型又称隐马模型又称HMM,是概率图模型之一,我们常见的贝叶斯模型也是概率图模型之一。
HMM属于生成模型,上面描述的BIO实体标签就是一个不可观测的隐藏状态,而HMM模型描述的就是由这些隐藏状态序列(实体标记)生成可观测结果(可读文本)的过程。
例如
隐藏状态序列: B-ORG | I-ORG | I-ORG | I-ORG |
观测结果序列: 清 华 大 学
假设可观测状态序列是由所有汉字组成的集合,用 来表示:
={v1,v2,… ,vM} v表示字典中的单个字,假设已知字数为M
假设所有可能的隐藏状态集合为 ℎ , 一共有 种隐藏状态, 例如现在的命名实体识别数据里面只有7种标签: ℎ = {q1,q2, … ,qN}
假设有观测到的一串自然语言序列文本 , 一共有 个字, 又有这段观测到的文本所对应的实体标记, 也就是隐状态 : = {i1,i2,… ,iT}(隐状态)
O = {o1,o2,… ,oT}(观测)
上述式子中常称 为时刻, 如上式中一共有 个时刻( 个汉字).
HMM模型有两个基本假设: 灰常重要!!!
1. 第 个隐状态(实体标签)只跟前一时刻的 −1 隐状态(实体标签)有关, 与除此之外的其他隐状态无关.
例如上图中: 蓝色的部分指的是 只与 −1 有关, 而与蓝色区域之外的所有内容都无关, 而 (|−1) 指的是隐状态 从 −1 时刻转向 时刻的概率
2. 观测独立的假设, HMM模型中是由隐状态序列(实体标记)生成可观测状态(可读文本)的过程, 观测独立假设是指在任意时刻观测 只依赖于当前时刻的隐状态 , 与其他时刻的隐状态无关.
例如上图中: 粉红色的部分指的是 +1 只与 +1 有关, 跟粉红色区域之外的所有内容都无关。
1. HMM的转移概率(transition probabilities):
我们上面提到了 (|−1) 指的是隐状态 从 −1 时刻转向 时刻的概率, 比如说我们现在实体标签一共有 7 种, 也就是 =7 (注意 是所有可能的实体标签种类的集合), 也就是 ℎ={0,1,…,6} (注意我们实体标签编号从 0 算起), 假设在 −1 时刻任何一种实体标签都可以在 时刻转换为任何一种其他类型的实体标签, 则总共可能的转换的路径一共有 2 种, 所以我们可以做一个 ∗ 的矩阵来表示所有可能的隐状态转移概率.
=(=|-1=)∈ℎ
对A矩阵的每一行求和概率之和为1
2. HMM的发射概率(emission probabilities):
我们上面提到任意时刻观测 t只依赖于当前时刻的隐状态 , 也就是 (|) , 也叫做发射概率, 指的是隐状态生成观测结果的过程. 设字典里有 个字, .={0,1,…,−1} (注意这里下标从0算起, 所以最后的下标是 −1 , 一共有 种观测), 则每种实体标签(隐状态)可以生成 种不同的汉字(也就是观测), 这一过程可以用一个发射概率矩阵来表示, 他的维度是 ∗ .
=(=|=) ∈ℎ ∈={0,1,…,−1}
3. HMM的初始隐状态概率:( )
通常用 来表示, 注意这里可不是圆周率:
=(1=) ∈ℎ={0,1,…,−1}
上式指的是自然语言序列中第一个字 1 的实体标记是 的概率, 也就是初始隐状态概率.
我们现在已经了解了HMM的三大参数 , , , 假设我们已经通过建模学习, 学到了这些参数, 得到了模型的概率, 我们怎么使用这些参数来解决序列标注问题呢?
假设目前在时刻 , 我们有当前时刻的观测到的一个汉字 = (指的第 时刻观测到 汉字), 假设我们还知道在 −1 时刻(前一时刻)对应的实体标记类型 −1=̂−1 (指的 −1 时刻标记为 ̂−1 ). 我们要做的仅仅是列举所有 可能的实体标记 ̂ , 并求可以使下式输出值最大的那个实体类型 (也就是隐状态类型):
将所有 时刻当前可取的实体标签带入下式中, 找出一个可以使下式取值最大的那个实体标签作为当前字的标注:
(当前可取实体标签|上一时刻实体标签)(测到的汉字|当前可取实体标签)
注意: 这里只讲到了怎样求第 时刻的最优标注, 但是在每一时刻进行这样的计算, 并不一定能保证最后能得出全局最优序列路径, 例如在第 时刻最优实体标签是 , 但到了下一步, 由于从 转移到其他某些实体标签的转移概率比较低, 而降低了经过 的路径的整体概率, 所以到了下一时刻最优路径就有可能在第 时刻不经过 了, 所以每一步的局部最优并不一定可以达成全局最优, 所以之后会用到维特比算法来找到全局最优的标注序列.
HMM参数学习(监督学习): 要用HMM解决的是序列标注问题, 所以解决的是监督学习的问题. 也就是说现在有一些文本和与之对应的标注数据, 要训练一个HMM来拟合这些数据, 以便之后用这个模型进行数据标注任务, 最简单的方式是直接用极大似然估计来估计参数:
1. 初始隐状态概率 的参数估计:
上式指的是, 计算在第 1 时刻, 也就是文本中第一个字, 1 出现的次数占总第一个字 1 观测次数的比例, 1上标1指的是第1时刻, 下标 指的是第 种标签(隐状态), 是的是记录次数.
2. 转移概率矩阵 的参数估计:
之前提到过 里面 (矩阵的第i行第j列)指的是在 时刻实体标签为 , 而在 +1 时刻实体标签转换到 的概率, 则转移概率矩阵的参数估计相当与一个二元模型 , 也就是把所有的标注序列中每相邻的两个实体标签分成一组, 统计他们出现的概率:
3. 发射概率矩阵 的参数估计:
我们提到过 中的 (矩阵第j行第k列)指的是在 时刻由实体标签(隐状态) 生成汉字(观测结果) 的概率.
综上,根据上面的方式得到模型的参数 , , 的估计.
import numpy as np
from utils import *
from tqdm import tqdm
class HMM_NER:
def __init__(self, char2idx_path, tag2idx_path):
# 载入一些字典
# char2idx: 字 转换为 token
self.char2idx = load_dict(char2idx_path)
# tag2idx: 标签转换为 token
self.tag2idx = load_dict(tag2idx_path)
# idx2tag: token转换为标签
self.idx2tag = {v: k for k, v in self.tag2idx.items()}
# 初始化隐状态数量(实体标签数)和观测数量(字数)
self.tag_size = len(self.tag2idx)
self.vocab_size = max([v for _, v in self.char2idx.items()]) + 1
# 初始化A, B, pi为全0
self.transition = np.zeros([self.tag_size,
self.tag_size])
self.emission = np.zeros([self.tag_size,
self.vocab_size])
self.pi = np.zeros(self.tag_size)
# 偏置, 用来防止log(0)或乘0的情况
self.epsilon = 1e-8
def fit(self, train_dic_path):
"""
fit用来训练HMM模型
:param train_dic_path: 训练数据目录
"""
print("initialize training...")
train_dic = load_data(train_dic_path)
# 估计转移概率矩阵, 发射概率矩阵和初始概率矩阵的参数
self.estimate_transition_and_initial_probs(train_dic)
self.estimate_emission_probs(train_dic)
# take the logarithm
# 取log防止计算结果下溢
self.pi = np.log(self.pi)
self.transition = np.log(self.transition)
self.emission = np.log(self.emission)
print("DONE!")
def estimate_emission_probs(self, train_dic):
"""
发射矩阵参数的估计
estimate p( Observation | Hidden_state )
:param train_dic:
:return:
"""
print("estimating emission probabilities...")
for dic in tqdm(train_dic):
for char, tag in zip(dic["text"], dic["label"]):
self.emission[self.tag2idx[tag],
self.char2idx[char]] += 1
self.emission[self.emission == 0] = self.epsilon
self.emission /= np.sum(self.emission, axis=1, keepdims=True)
def estimate_transition_and_initial_probs(self, train_dic):
"""
转移矩阵和初始概率的参数估计, 也就是bigram二元模型
estimate p( Y_t+1 | Y_t )
:param train_dic:
:return:
"""
print("estimating transition and initial probabilities...")
for dic in tqdm(train_dic):
for i, tag in enumerate(dic["label"][:-1]):
if i == 0:
self.pi[self.tag2idx[tag]] += 1
curr_tag = self.tag2idx[tag]
next_tag = self.tag2idx[dic["label"][i+1]]
self.transition[curr_tag, next_tag] += 1
self.transition[self.transition == 0] = self.epsilon
self.transition /= np.sum(self.transition, axis=1, keepdims=True)
self.pi[self.pi == 0] = self.epsilon
self.pi /= np.sum(self.pi)
def get_p_Obs_State(self, char):
# 计算p( observation | state)
# 如果当前字属于未知, 则讲p( observation | state)设为均匀分布
char_token = self.char2idx.get(char, 0)
if char_token == 0:
return np.log(np.ones(self.tag_size)/self.tag_size)
return np.ravel(self.emission[:, char_token])
def predict(self, text):
# 预测并打印出预测结果
# 维特比算法解码
if len(text) == 0:
raise NotImplementedError("输入文本为空!")
best_tag_id = self.viterbi_decode(text)
self.print_func(text, best_tag_id)
def print_func(self, text, best_tags_id):
# 用来打印预测结果
for char, tag_id in zip(text, best_tags_id):
print(char+"_"+self.idx2tag[tag_id]+"|", end="")
def viterbi_decode(self, text):
"""
维特比解码, 详见视频教程或文字版教程
:param text: 一段文本string
:return: 最可能的隐状态路径
"""
# 得到序列长度
seq_len = len(text)
# 初始化T1和T2表格
T1_table = np.zeros([seq_len, self.tag_size])
T2_table = np.zeros([seq_len, self.tag_size])
# 得到第1时刻的发射概率
start_p_Obs_State = self.get_p_Obs_State(text[0])
# 计算第一步初始概率, 填入表中
T1_table[0, :] = self.pi + start_p_Obs_State
T2_table[0, :] = np.nan
for i in range(1, seq_len):
# 维特比算法在每一时刻计算落到每一个隐状态的最大概率和路径
# 并把他们暂存起来
# 这里用到了矩阵化计算方法, 详见视频教程
p_Obs_State = self.get_p_Obs_State(text[i])
p_Obs_State = np.expand_dims(p_Obs_State, axis=0)
prev_score = np.expand_dims(T1_table[i-1, :], axis=-1)
# 广播算法, 发射概率和转移概率广播 + 转移概率
curr_score = prev_score + self.transition + p_Obs_State
# 存入T1 T2中
T1_table[i, :] = np.max(curr_score, axis=0)
T2_table[i, :] = np.argmax(curr_score, axis=0)
# 回溯
best_tag_id = int(np.argmax(T1_table[-1, :]))
best_tags = [best_tag_id, ]
for i in range(seq_len-1, 0, -1):
best_tag_id = int(T2_table[i, best_tag_id])
best_tags.append(best_tag_id)
return list(reversed(best_tags))
if __name__ == '__main__':
model = HMM_NER(char2idx_path="./dicts/char2idx.json",
tag2idx_path="./dicts/tag2idx.json")
model.fit("./corpus/train_data.txt")
model.predict("我在中国吃美国的面包")