中⽂命名实体识别:基于PyTorch的多模型中⽂命名实体识别
命名实体识别作为序列标注类的典型任务,其使⽤场景特别⼴泛。本项⽬基于PyTorch搭建HMM、CRF、BiLSTM、BiLSTM+CRF及BERT模型,实现中⽂命名识别任务,全部代码上可找。
数据集
数据集来源于论⽂中从新浪财经收集的简历数据,数据的格式如下,它的每⼀⾏由⼀个字及其对应的标注组成,标注集采⽤BIOES(B表⽰实体开头,E表⽰实体结尾,I表⽰在实体内部,LOC、PER等表⽰具体的实体,O表⽰⾮实体),句⼦与句⼦之间⽤⼀个空⾏隔开。
美 B-LOC
国 E-LOC
的 O
华 B-PER
莱 I-PER
⼠ E-PER
我 O
跟 O
他 O
谈 O
笑 O
风 O
⽣ O
模型搭建
共实现了5个模型,HMM、CRF、BiLSTM、BiLSTM+CRF以及BERT。传统的基于统计学习⽅法HMM和CRF,基于深度学习⽅法BiLSTM以及nlp中的战⽃机BERT。
统计学习⽅法
隐马尔科夫模型(HMM)
HMM是关于时序的概率模型,描述由⼀个隐藏的马尔科夫链随机⽣成不可观测的状态随机序列,再由各个状态⽣成⼀个观测⽽产⽣观测随机序列的过程(李航 统计学习⽅法)。HMM基于2个假设—齐次马尔科夫假设与观测独⽴性假设,这是HMM相较于其它⼏个模型性能差的主要原因。HMM由三要素—初始状态向量、观测概率矩阵及状态转移概率矩阵所确定。模型涉及HMM的2⼤问题,参数即三要素的学习算法和解码预测算法。
class HMM(object):
def __init__(lf,N,M):
"""
HMM模型
:param N: 状态数,这⾥对应存在的标注的种类
:param M: 观测数这⾥对应有多少个不同的字
"""
lf.N = N
lf.M = M
# 初始状态概率 Pi[i]表⽰初始时刻状态为i的概率
lf.Pi = s(N)
# 状态转移概率矩阵 A[i][j]表⽰状态从i转移到j的概率
lf.A = s(N,N)
# 观测概率矩阵 B[i][j]表⽰i状态下⽣成j观测的概率
lf.B = s(N,M)
def train(lf,word_lists,tag_lists,word2id,tag2id):
"""HMM的训练,即根据训练语料对模型参数进⾏估计,
因为我们有观测序列以及其对应的状态序列,所以我们
可以使⽤极⼤似然估计的⽅法来估计隐马尔可夫模型的参数
"""
asrt len(word_lists) == len(tag_lists)
# 估计状态转移概率矩阵A
# HMM的⼀个假设:齐次马尔科夫假设即任意时刻的隐藏状态只依赖以前⼀个隐藏状态
for tag_list in tag_lists:
q_len = len(tag_list)
for i in range(q_len-1):
current_tagid = tag2id[tag_list[i]]
next_tagid = tag2id[tag_list[i+1]]
lf.A[current_tagid][next_tagid] += 1
# smoth
lf.A[lf.A == 0.] = 1e-10
# 计算概率
lf.A = lf.A / torch.sum(lf.A,dim=1,keepdim=True)
attacker# 估计观测概率矩阵
# 观测独⽴假设,即当前的观测值只依赖以当前的隐藏状态
for tag_list,word_list in zip(tag_lists,word_lists):
asrt len(tag_list)==len(word_list)
figureout
for tag,word in zip(tag_list,word_list):
tag_id = tag2id[tag]
word_id = word2id[word]金融数学专业
lf.B[tag_id][word_id] += 1
lf.B[lf.B==0.] = 1e-10
lf.B = lf.B / torch.sum(lf.B,dim=1,keepdim=True)
# 估计初始概率,即每个句⼦开头标注状态
for tag_list in tag_lists:
init_tagId = tag2id[tag_list[0]]
lf.Pi[init_tagId] += 1
lf.Pi[lf.Pi==0] = 1e-10
lf.Pi = lf.Pi/lf.Pi.sum()
HMM的三要素确定好后,对于给定的句⼦求解每个字对应的命名实体标注,使⽤维特⽐(viterbi)算法。维特⽐算法实际是⽤动态规划解隐马尔可夫模型预测问题,即⽤动态规划求概率最⼤路径(最优路径),这⾥⼀条路径对应着⼀个状态序列。递推公式为:
具体代码如下:
def decoding(lf,word_list,word2id,tag2id):
# 概率取对数
A = torch.log(lf.A)
B = torch.log(lf.B)
Pi = torch.log(lf.Pi)
q_len = len(word_list)
viterbi = s(lf.N, q_len)
# backpointer[i, j]存储的是标注序列的第j个标注为i时,第j-1个标注的id,⽤于解码
backpointer = s(lf.N, q_len).long()
# 初始第⼀步
start_wordid = (word_list[0],None)
Bt = B.t() # 转置,[M,N]
if start_wordid is None:
# 如果word不在字典⾥,则假设其状态转移概率分布为均匀分布
bt1 = (s(lf.N)/lf.N).long()孙燕姿 仓木麻衣
el:
bt1 = Bt[start_wordid]
viterbi[:,0] = lf.Pi + bt1you were my everything
backpointer[:,0] = -1
# 递推公式
# viterbi[tag_id,step] = max(viterbi[:,step-1] * lf.A[:,tag_id]) * Bt[word]
# word是step时刻对应的观测值
for step in range(1,q_len):
wordid = (word_list[step],None)
# bt为时刻t字为wordid时,状态转移的概率
if wordid is None:
bt = (s(lf.N)/lf.N).long()
el:
# 从观测矩阵B中取值
bt = Bt[wordid]
for tag_id in range(len(tag2id)):
# 求前⼀个step中即维特⽐矩阵中的前⼀列每个元素和对应的状态转移矩阵的概率乘积的最⼤值
max_prob,max_id = torch.max(viterbi[:,step-1] + A[:,tag_id],dim=0)
viterbi[tag_id,step] = max_prob + bt[tag_id]
backpointer[tag_id, step] = max_id
# 终⽌, t=q_len 即 viterbi[:, q_len]中的最⼤概率,就是最优路径的概率
best_path_prob, best_path_pointer = torch.max(viterbi[:, q_len - 1], dim=0)
# 回溯,求最优路径
best_path_pointer = best_path_pointer.item()
best_path = [best_path_pointer]
for back_step in range(q_len-1, 0, -1):
best_path_pointer = backpointer[best_path_pointer, back_step]
best_path_pointer = best_path_pointer.item()
best_path.append(best_path_pointer)
# 将tag_id组成的序列转化为tag
个人简历翻译
asrt len(best_path) == len(word_list)
id2tag = dict((id_, tag) for tag, id_ in tag2id.items())
tag_list = [id2tag[id_] for id_ in reverd(best_path)]
return tag_list
模型在test集上结果为:
precision recall f1-score
0.91290.90970.9107
条件随机场(CRF)
HMM是典型的⽣成模式,对联合概率建模且受限于2个假设,其性能天花板相对较低。CRF(我们⽤的是特殊CRF,即线性链条件随机场)是典型的判别模式。CRF通过引⼊⾃定义的特征函数,不仅可以表达观测之间的依赖,还可表⽰当前观测与前后多个状态之间的复杂依赖,可以有效克服HMM模型⾯临的问题。
⾸先定义⼀个特征函数集,该函数集内的每个特征函数都以标注序列作为输⼊,提取的特征作为输出。假设该函数集为:
其中 表⽰观测序列, 表
⽰状态序列。然后,CRF使⽤对数线性模型来计算给定观测序列下状态序列的条件概率:
是条件随机场模型的参数,可以把它看成是每个
陈慧娴飘雪日文版特征函数的权重。CRF模型的训练其实就是对参数的估计。解码的时候与HMM类似,也可以采⽤维特⽐算法。CRF的实现实在太过复杂、繁琐,笔者借⽤了第三⽅库实现。
class CRFModel(object):
def __init__(lf,algorithm='lbfgs',c1=0.1,c2=0.1,max_iterations=100,all_possible_transitions=Fal):
c1=c1,
c2=c2,
max_iterations=max_iterations,
all_possible_transitions=all_possible_transitions)
def train(lf,ntences,tag_lists):
# list of lists of dicts
features = [nt2features(nt) for nt in ntences]
def test(lf,ntences):
features = [nt2features(s) for s in ntences]
pred_tag_lists = lf.model.predict(features)
return pred_tag_lists
def word2features(nt,i):
"""抽取单个字的特征"""
word = nt[i]
prev_word = "<s>" if i == 0 el nt[i-1]
next_word = "</s>" if i == (len(nt)-1) el nt[i+1]
# 使⽤的特征:
# 前⼀个词,当前词,后⼀个词,
# 前⼀个词+当前词, 当前词+后⼀个词
feature = {
'w':word,
'w-1':prev_word,
'w+1':next_word,
'w-1:w':prev_word+word,
'w:w+1':word+next_word,
'bias':1
}
return feature
def nt2features(nt):
"""抽取序列特征"""
return [word2features(nt,i) for i in range(len(nt))]
x =(x ,...,x )1m s =(s ,....,s )1m w w
CRF模型结果:
precision recall f1-score
0.95430.95430.9542
深度学习⽅法
rolly
Bi-LSTM
循环神经⽹络RNN的改进型LSTM,特别适⽤于时序⽂本数据的处理。LSTM依靠神经⽹络超强的⾮线
性拟合能⼒以及隐状态对信息的传递,在训练时将样本通过⾼维空间中的复杂⾮线性变换,学习到从样本到标注的函数。双向LSTM对前后向的序列依赖的捕获能⼒更强:
Bi-LSTM⽐起CRF模型最⼤的好处就是简单直接,不需要做繁杂的特征⼯程,建⽴模型直接训练即可,同时准确率也相当⾼。模型相当简单,⼀个embedding层+双向LSTM+全连接层:
class BiLSTM(nn.Module):
def __init__(lf,vocab_size,emb_size,hidden_size,out_size,dropout=0.1):
super(BiLSTM,lf).__init__()
lf.bilstm = nn.LSTM(emb_size,hidden_size,batch_first=True,bidirectional=True)
lf.fc = nn.Linear(2*hidden_size,out_size)
lf.dropout = nn.Dropout(dropout)
def forward(lf,x,lengths):
# [b,l,emb_size ]
emb = lf.bedding(x))
# 这⾥要求输⼊按长度递减排好序,否则enforce_sorted设置为fal,低版本⽅法有不同之处
emb = pack_padded_quence(emb,lengths,batch_first=True)
emb,_ = lf.bilstm(emb)
emb,_ = pad_packed_quence(emb,batch_first=True,padding_value=0.,total_length=x.shape[1])
scores = lf.fc(emb)
jino
return scores
Bi-LSTM的模型结果:
precision recall f1-score
0.95530.95520.9551
class BiLSTM_CRF(nn.Module):
that
def __init__(lf,vocab_size,emb_size,hidden_size,out_size):
"""