本文主要是介绍NLP-信息抽取-NER-2015-BiLSTM+CRF(一):命名实体识别【预测每个词的标签】【评价指标:精确率=识别出正确的实体数/识别出的实体数、召回率=识别出正确的实体数/样本真实实体数】,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、命名实体识别介绍
命名实体识别(Named Entity Recognition,NER)就是从一段自然语言文本中找出相关实体,并标注出其位置以及类型。是信息提取, 问答系统, 句法分析, 机器翻译等应用领域的重要基础工具, 在自然语言处理技术走向实用化的过程中占有重要地位. 包含行业, 领域专有名词, 如人名, 地名, 公司名, 机构名, 日期, 时间, 疾病名, 症状名, 手术名称, 软件名称等。具体可参看如下示例图:
第三方NER工具包无法识别专业领域的NE,需根据已有专业名词数据集来训练自用NER模型
1、命名实体识别的作用
- 识别专有名词, 为文本结构化提供支持.
- 主体识别, 辅助句法分析.
- 实体关系抽取, 有利于知识推理.
2、命名实体识别常用方法:基于规则(正则表达式)、基于模型(BiLSTM+CRF)
基于规则: 针对有特殊上下文的实体, 或实体本身有很多特征的文本, 使用规则的方法简单且有效. 比如抽取文本中物品价格, 如果文本中所有商品价格都是“数字+元”的形式, 则可以通过正则表达式”\d*.?\d+元”进行抽取. 但如果待抽取文本中价格的表达方式多种多样, 例如“一千八百万”, “伍佰贰拾圆”, “2000万元”, 遇到这些情况就要修改规则来满足所有可能的情况. 随着语料数量的增加, 面对的情况也越来越复杂, 规则之间也可能发生冲突, 整个系统也可能变得不可维护. 因此基于规则的方式比较适合半结构化或比较规范的文本中的进行抽取任务, 结合业务需求能够达到一定的效果.
- 优点: 简单, 快速.
- 缺点: 适用性差, 维护成本高后期甚至不能维护.
基于模型: 从模型的角度来看, 命名实体识别问题实际上是序列标注问题。序列标注问题指的是模型的输入是一个序列, 包括文字, 时间等, 输出也是一个序列. 针对输入序列的每一个单元, 输出一个特定的标签. 以中文分词任务进行举例, 例如输入序列是一串文字: “我是中国人”, 输出序列是一串标签: “OOBII”, 其中“BIO"组成了一种中文分词最基础的标签体系: B表示这个字是词的开始, I表示词的中间到结尾, O表示其他类型词(也可以用更多的字母来表示标签体系,“BIO”是最基础的一种标签体系). 因此我们可以根据输出序列"OOBII"进行解码, 得到分词结果"我\是\中国人”.
- 序列标注问题涵盖了自然语言处理中的很多任务, 包括语音识别, 中文分词, 机器翻译, 命名实体识别等, 而常见的序列标注模型包括HMM, CRF, RNN, LSTM, GRU等模型.
- 其中在命名实体识别技术上, 目前主流的命名实体识别技术是通过 “BiLSTM+CRF” 模型进行序列标注, 也是项目中要用到的模型.
3、医学文本特征
- 简短精炼
- 形容词相对较少
- 泛化性相对较小
- 医学名词错字率比较高
- 同义词、简称比较多
4、常见问题
4.1 介绍下命名实体识别任务,如何标注?
命名实体识别是为了找出文本中具有特定意义的实体字符串边界,并归类到预定义类别。包括两个部分,实体边界划分与实体类别预测。通常有3种标注体系:IO、BIO、BIOES。大部分情况下,标签体系越复杂准确度也越高,但相应的训练时间也会增加。因此需要根据实际情况选择合适的标签体系,一般采用BIO模式。
4.2 命名实体识别的评价标准?
命名实体识别输入为文本序列,输出为类别序列。可看做基于token标签的多分类问题或者考虑实体边界与实体类型结合的基于实体的多分类问题。基于token标签的多分类不能真实反应识别的效果。一般基于实体进行多分类的评价,通过precision、recall、f1进行衡量。
4.3 介绍下HMM,如何应用到NER任务中?
HMM即隐马尔可夫模型,是一种统计模型,用来描述一个含有隐含未知参数的马尔可夫过程。
包含两个假设:齐次马尔科夫假设与观测独立假设。如果将NER数据的实体标签看做一个不可观测的隐状态, 而HMM模型描述的就是由这些隐状态序列(实体标记)生成可观测状态(可读文本)的过程。通过HMM的学习问题,采用极大似然估计对标注数据进行学习,估计模型的参数。预测时已知模型参数和观测序列,采用维特比解码求最有可能对应的状态序列。
4.4 介绍下CRF,如何应用到NER任务中?
这里说的CRF指的是用于序列标注问题的线性链条件随机场,是由输入序列来预测输出序列的判别式模型。CRF是条件概率分布模型P(Y|X),表示的是给定一组输入随机变量X的条件下另一组输出随机变量Y的马尔可夫随机场。CRF是序列标注问题的对数线性模型,通过定义状态特征与转移特征的特征函数,通过标注数据学习特征函数对应的权重。预测时采用维特比解码求最有可能对应的状态序列。
4.5 HMM与CRF的区别与联系?
CRF更加强大- CRF可以为任何HMM能够建模的事物建模,甚至更多。CRF可以定义更加广泛的特征集。 而HMM在本质上必然是局部的,而CRF就可以使用更加全局的特征。CRF可以有任意权重值,HMM的概率值必须满足特定的约束。
二、BiLSTM介绍
所谓的BiLSTM,就是(Bidirectional LSTM)双向LSTM. 单向的LSTM模型只能捕捉到从前向后传递的信息, 而双向的网络可以同时捕捉正向信息和反向信息, 使得对文本信息的利用更全面, 效果也更好.
在BiLSTM网络最终的输出层后面增加了一个线性层, 用来将BiLSTM产生的隐藏层输出结果投射到具有某种表达标签特征意义的区间, 具体如下图所示:
BiLSTM作用:记忆上下文。
如下图所示,如果用前馈神经网络(FNN)替代LSTM,则只能识别出槽位,不能识别出该槽位是目的地还是出发地。
BiLSTM模型实现:
- 第一步: 实现类的初始化和网络结构的搭建.
- 第二步: 实现文本向量化的函数.
- 第三步: 实现网络的前向计算.
1、第一步: 实现类的初始化和网络结构的搭建
本段代码构建类BiLSTM, 完成初始化和网络结构的搭建。
总共3层:
- 词嵌入层,
- 双向LSTM层,
- 全连接线性层
# 本段代码构建类BiLSTM, 完成初始化和网络结构的搭建
# 总共3层: 词嵌入层, 双向LSTM层, 全连接线性层
import torch
import torch.nn as nnclass BiLSTM(nn.Module):"""description: BiLSTM 模型定义"""def __init__(self, vocab_size, tag_to_id, input_feature_size, hidden_size,batch_size, sentence_length, num_layers=1, batch_first=True):"""description: 模型初始化:param vocab_size: 所有句子包含字符大小:param tag_to_id: 标签与 id 对照:param input_feature_size: 字嵌入维度( 即LSTM输入层维度 input_size ):param hidden_size: 隐藏层向量维度:param batch_size: 批训练大小:param sentence_length 句子长度:param num_layers: 堆叠 LSTM 层数:param batch_first: 是否将batch_size放置到矩阵的第一维度"""# 类继承初始化函数super(BiLSTM, self).__init__()# 设置标签与id对照self.tag_to_id = tag_to_id# 设置标签大小, 对应BiLSTM最终输出分数矩阵宽度self.tag_size = len(tag_to_id)# 设定LSTM输入特征大小, 对应词嵌入的维度大小self.embedding_size = input_feature_size# 设置隐藏层维度, 若为双向时想要得到同样大小的向量, 需要除以2self.hidden_size = hidden_size // 2# 设置批次大小, 对应每个批次的样本条数, 可以理解为输入张量的第一个维度self.batch_size = batch_size# 设定句子长度self.sentence_length = sentence_length# 设定是否将batch_size放置到矩阵的第一维度, 取值True, 或Falseself.batch_first = batch_first# 设置网络的LSTM层数self.num_layers = num_layers# 构建词嵌入层: 字向量, 维度为总单词数量与词嵌入维度# 参数: 总体字库的单词数量, 每个字被嵌入的维度self.embedding = nn.Embedding(vocab_size, self.embedding_size)# 构建双向LSTM层: BiLSTM (参数: input_size 字向量维度(即输入层大小),# hidden_size 隐藏层维度,# num_layers 层数,# bidirectional 是否为双向,# batch_first 是否批次大小在第一位)self.bilstm = nn.LSTM(input_size=input_feature_size,hidden_size=self.hidden_size,num_layers=num_layers,bidirectional=True,batch_first=batch_first)# 构建全连接线性层: 将BiLSTM的输出层进行线性变换self.linear = nn.Linear(hidden_size, self.tag_size)
代码实现位置: /data/doctor_offline/ner_model/bilstm.py
输入参数:
# 参数1:码表与id对照
char_to_id = {"双": 0, "肺": 1, "见": 2, "多": 3, "发": 4, "斑": 5, "片": 6,"状": 7, "稍": 8, "高": 9, "密": 10, "度": 11, "影": 12, "。": 13}# 参数2:标签码表对照
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}# 参数3:字向量维度
EMBEDDING_DIM = 200# 参数4:隐层维度
HIDDEN_DIM = 100# 参数5:批次大小
BATCH_SIZE = 8# 参数6:句子长度
SENTENCE_LENGTH = 20# 参数7:堆叠 LSTM 层数
NUM_LAYERS = 1
调用:
# 初始化模型
model = BiLSTM(vocab_size=len(char_to_id),tag_to_id=tag_to_id,input_feature_size=EMBEDDING_DIM,hidden_size=HIDDEN_DIM,batch_size=BATCH_SIZE,sentence_length=SENTENCE_LENGTH,num_layers=NUM_LAYERS)
print(model)
输出效果:
BiLSTM((embedding): Embedding(14, 200)(bilstm): LSTM(200, 50, batch_first=True, bidirectional=True)(linear): Linear(in_features=100, out_features=5, bias=True)
)
2、第二步:实现文本向量化的函数(将中文文本中的每个字映射为序列化的序号)
将句子中的每一个字符映射到码表中,比如:
char_to_id = {“双”: 0, “肺”: 1, “见”: 2, “多”: 3, “发”: 4, “斑”: 5, “片”: 6, “状”: 7, “稍”: 8, “高”: 9, “密”: 10, “度”: 11, “影”: 12, “。”: 13…}
# 本函数实现将中文文本映射为数字化的张量
def sentence_map(sentence_list, char_to_id, max_length):"""description: 将句子中的每一个字符映射到码表中:param sentence: 待映射句子, 类型为字符串或列表:param char_to_id: 码表, 类型为字典, 格式为{"字1": 1, "字2": 2}:return: 每一个字对应的编码, 类型为tensor"""# 字符串按照逆序进行排序, 不是必须操作sentence_list.sort(key=lambda c:len(c), reverse=True)# 定义句子映射列表sentence_map_list = []for sentence in sentence_list:# 生成句子中每个字对应的 id 列表sentence_id_list = [char_to_id[c] for c in sentence]# 计算所要填充 0 的长度padding_list = [0] * (max_length-len(sentence))# 组合sentence_id_list.extend(padding_list)# 将填充后的列表加入句子映射总表中sentence_map_list.append(sentence_id_list)# 返回句子映射集合, 转为标量return torch.tensor(sentence_map_list, dtype=torch.long)
代码实现位置: /data/doctor_offline/ner_model/bilstm.py
输入参数:
# 参数1:句子集合
sentence_list = ["确诊弥漫大b细胞淋巴瘤1年","反复咳嗽、咳痰40年,再发伴气促5天。","生长发育迟缓9年。","右侧小细胞肺癌第三次化疗入院","反复气促、心悸10年,加重伴胸痛3天。","反复胸闷、心悸、气促2多月,加重3天","咳嗽、胸闷1月余, 加重1周","右上肢无力3年, 加重伴肌肉萎缩半年"]# 参数2:码表与id对照
char_to_id = {"<PAD>":0} # 初始化的码表# 参数3:句子长度
SENTENCE_LENGTH = 20
调用:
if __name__ == '__main__':for sentence in sentence_list:# 获取句子中的每一个字for _char in sentence:# 判断是否在码表 id 对照字典中存在if _char not in char_to_id:# 加入字符id对照字典char_to_id[_char] = len(char_to_id)# 将句子转为 id 并用 tensor 包装sentences_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)print("sentences_sequence:\n", sentences_sequence)
输出效果:
sentences_sequence:
tensor([[14, 15, 16, 17, 18, 16, 19, 20, 21, 13, 22, 23, 24, 25, 26, 27, 28, 29, 30, 0],[14, 15, 26, 27, 18, 49, 50, 12, 21, 13, 22, 51, 52, 25, 53, 54, 55, 29, 30, 0],[14, 15, 53, 56, 18, 49, 50, 18, 26, 27, 57, 58, 59, 22, 51, 52, 55, 29, 0, 0],[37, 63, 64, 65, 66, 55, 13, 22, 61, 51, 52, 25, 67, 68, 69, 70, 71, 13, 0, 0],[37, 38, 39, 7, 8, 40, 41, 42, 43, 44, 45, 46, 47, 48, 0, 0, 0, 0, 0, 0],[16, 17, 18, 53, 56, 12, 59, 60, 22, 61, 51, 52, 12, 62, 0, 0, 0, 0, 0, 0],[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 0, 0, 0, 0, 0, 0, 0],[31, 32, 24, 33, 34, 35, 36, 13, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
3、第三步: 实现网络的前向计算
将句子利用BiLSTM进行特征计算,分别经过Embedding->BiLSTM->Linear,获得发射矩阵(emission scores)。
BiLSTM层的输出维度是tag_size, 也就是每个单词 w i w_i wi 映射到 各个tag的发射概率值。
# 本函数实现类BiLSTM中的前向计算函数forward()
def forward(self, sentences_sequence):"""description: 将句子利用BiLSTM进行特征计算,分别经过Embedding->BiLSTM->Linear,获得发射矩阵(emission scores):param sentences_sequence: 句子序列对应的编码,若设定 batch_first 为 True,则批量输入的 sequence 的 shape 为(batch_size, sequence_length):return: 返回当前句子特征,转化为 tag_size 的维度的特征"""# 初始化隐藏状态值h0 = torch.randn(self.num_layers * 2, self.batch_size, self.hidden_size)# 初始化单元状态值c0 = torch.randn(self.num_layers * 2, self.batch_size, self.hidden_size)# 生成字向量, shape 为(batch, sequence_length, input_feature_size)# 注:embedding cuda 优化仅支持 SGD 、 SparseAdaminput_features = self.embedding(sentences_sequence)# 将字向量与初始值(隐藏状态 h0 , 单元状态 c0 )传入 LSTM 结构中# 输出包含如下内容:# 1, 计算的输出特征,shape 为(batch, sentence_length, hidden_size)# 顺序为设定 batch_first 为 True 情况, 若未设定则 batch 在第二位# 2, 最后得到的隐藏状态 hn , shape 为(num_layers * num_directions, batch, hidden_size)# 3, 最后得到的单元状态 cn , shape 为(num_layers * num_directions, batch, hidden_size)output, (hn, cn) = self.bilstm(input_features, (h0, c0))# 将输出特征进行线性变换,转为 shape 为 (batch, sequence_length, tag_size) 大小的特征sequence_features = self.linear(output)# 输出线性变换为 tag 映射长度的特征return sequence_features
代码实现位置: /data/doctor_offline/ner_model/bilstm.py
输入参数:
# 参数1:标签码表对照
tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}# 参数2:字向量维度
EMBEDDING_DIM = 200# 参数3:隐层维度
HIDDEN_DIM = 100# 参数4:批次大小
BATCH_SIZE = 8# 参数5:句子长度
SENTENCE_LENGTH = 20# 参数6:堆叠 LSTM 层数
NUM_LAYERS = 1char_to_id = {"<PAD>":0}
SENTENCE_LENGTH = 20
调用:
if __name__ == '__main__':for sentence in sentence_list:for _char in sentence:if _char not in char_to_id:char_to_id[_char] = len(char_to_id)sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)model = BiLSTM(vocab_size=len(char_to_id), tag_to_id=tag_to_id, input_feature_size=EMBEDDING_DIM, \hidden_size=HIDDEN_DIM, batch_size=BATCH_SIZE, sentence_length=SENTENCE_LENGTH, num_layers=NUM_LAYERS)sentence_features = model(sentence_sequence)print("sequence_features:\n", sentence_features)
输出效果:
sequence_features:
tensor([[[ 4.0880e-02, -5.8926e-02, -9.3971e-02, 8.4794e-03, -2.9872e-01],[ 2.9434e-02, -2.5901e-01, -2.0811e-01, 1.3794e-02, -1.8743e-01],[-2.7899e-02, -3.4636e-01, 1.3382e-02, 2.2684e-02, -1.2067e-01],[-1.9069e-01, -2.6668e-01, -5.7182e-02, 2.1566e-01, 1.1443e-01],...[-1.6844e-01, -4.0699e-02, 2.6328e-02, 1.3513e-01, -2.4445e-01],[-7.3070e-02, 1.2032e-01, 2.2346e-01, 1.8993e-01, 8.3171e-02],[-1.6808e-01, 2.1454e-02, 3.2424e-01, 8.0905e-03, -1.5961e-01],[-1.9504e-01, -4.9296e-02, 1.7219e-01, 8.9345e-02, -1.4214e-01]],...[[-3.4836e-03, 2.6217e-01, 1.9355e-01, 1.8084e-01, -1.6086e-01],[-9.1231e-02, -8.4838e-04, 1.0575e-01, 2.2864e-01, 1.6104e-02],[-8.7726e-02, -7.6956e-02, -7.0301e-02, 1.7199e-01, -6.5375e-02],[-5.9306e-02, -5.4701e-02, -9.3267e-02, 3.2478e-01, -4.0474e-02],[-1.1326e-01, 4.8365e-02, -1.7994e-01, 8.1722e-02, 1.8604e-01],...[-5.8271e-02, -6.5781e-02, 9.9232e-02, 4.8524e-02, -8.2799e-02],[-6.8400e-02, -9.1515e-02, 1.1352e-01, 1.0674e-02, -8.2739e-02],[-9.1461e-02, -1.2304e-01, 1.2540e-01, -4.2065e-02, -8.3091e-02],[-1.5834e-01, -8.7316e-02, 7.0567e-02, -8.8845e-02, -7.0867e-02]],[[-1.4069e-01, 4.9171e-02, 1.4314e-01, -1.5284e-02, -1.4395e-01],[ 6.5296e-02, 9.3255e-03, -2.8411e-02, 1.5143e-01, 7.8252e-02],[ 4.1765e-03, -1.4635e-01, -4.9798e-02, 2.7597e-01, -1.0256e-01],...[-3.9810e-02, -7.6746e-03, 1.2418e-01, 4.9897e-02, -8.4538e-02],[-3.4474e-02, -1.0586e-02, 1.3861e-01, 4.0395e-02, -8.3676e-02],[-3.4092e-02, -2.3208e-02, 1.6097e-01, 2.3498e-02, -8.3332e-02],[-4.6900e-02, -5.0335e-02, 1.8982e-01, 3.6287e-03, -7.8078e-02],[-6.4105e-02, -4.2628e-02, 1.8999e-01, -2.9888e-02, -1.1875e-01]]],grad_fn=<AddBackward0>)
输出结果说明: 该输出结果为输入批次中句子的特征, 利用线性变换分别对应到每个汉字在每个tag上的得分. 例如上述标量第一个值:[ 4.0880e-02, -5.8926e-02, -9.3971e-02, 8.4794e-03, -2.9872e-01]
表示的意思为第一个句子第一个字分别被标记为[“O”, “B-dis”, “I-dis”, “B-sym”, “I-sym”]的分数, 由此可以判断, 在这个例子中, 第一个字被标注为"O"的分数最高.
4、BILSTM模型完整代码
BiLSTM层的输出维度是tag_size, 也就是每个单词 w i w_i wi 映射到 各个tag的发射概率值
# 本段代码构建类BiLSTM, 完成初始化和网络结构的搭建
# 总共3层: 词嵌入层, 双向LSTM层, 全连接线性层
import torch
import torch.nn as nn# BiLSTM 模型定义
class BiLSTM(nn.Module):def __init__(self, vocab_size, tag_to_id, input_feature_size, hidden_size, batch_size, sentence_length, num_layers=1, batch_first=True):"""description: 模型初始化:param vocab_size: 所有句子包含字符大小【词汇表总数量】:param tag_to_id: 标签与 id 对照【 {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}】:param input_feature_size: 字嵌入维度( 即LSTM输入层维度 input_size ):param hidden_size: 隐藏层向量维度:param batch_size: 批训练大小:param sentence_length 句子长度:param num_layers: 堆叠 LSTM 层数:param batch_first: 是否将batch_size放置到矩阵的第一维度"""super(BiLSTM, self).__init__() # 类继承初始化函数self.tag_to_id = tag_to_id # 设置标签与id对照self.tag_size = len(tag_to_id) # 设置标签大小, 对应BiLSTM最终输出分数矩阵宽度self.embedding_size = input_feature_size # 设定LSTM输入特征大小, 对应词嵌入的维度大小self.hidden_size = hidden_size // 2 # 设置隐藏层维度, 若为双向时想要得到同样大小的向量, 需要除以2self.batch_size = batch_size # 设置批次大小, 对应每个批次的样本条数, 可以理解为输入张量的第一个维度self.sentence_length = sentence_length # 设定句子长度self.batch_first = batch_first # 设定是否将batch_size放置到矩阵的第一维度, 取值True, 或Falseself.num_layers = num_layers # 设置网络的LSTM层数self.embedding = nn.Embedding(vocab_size, self.embedding_size) # 构建词嵌入层; vocab_size: 词汇表总单词数量; embedding_size: 每个字的词嵌入维度# 构建BiLSTM层【input_size:词向量维度(即输入层大小); hidden_size: 隐藏层维度; num_layers: 层数; bidirectional: 是否为双向; batch_first: 是否批次大小在第一位)self.bilstm = nn.LSTM(input_size=input_feature_size, hidden_size=self.hidden_size, num_layers=num_layers, bidirectional=True, batch_first=batch_first) # 此处的hidden_size:【self.hidden_size = hidden_size // 2】self.linear = nn.Linear(hidden_size, self.tag_size) # 构建全连接线性层: 将BiLSTM的输出层进行线性变换【最终维度是tag的类型数量】 # 此处的hidden_size就是传入的参数hidden_size# 本函数实现类BiLSTM中的前向计算函数forward()【将句子利用BiLSTM进行特征计算,分别经过Embedding->BiLSTM->Linear,获得发射矩阵(emission scores),返回当前句子特征,转化为 tag_size 的维度的特征】def forward(self, sentences_sequence): # entences_sequence: 句子序列对应的编码, 若设定 batch_first 为 True,则批量输入的 sequence 的 shape 为(batch_size, sequence_length)hidden0 = torch.randn(self.num_layers * 2, self.batch_size, self.hidden_size) # 初始化隐藏状态值cell0 = torch.randn(self.num_layers * 2, self.batch_size, self.hidden_size) # 初始化cell状态值input_features = self.embedding(sentences_sequence) # 生成字向量, shape 为(batch, sequence_length, input_feature_size)【注:embedding cuda 优化仅支持 SGD 、 SparseAdam】# bilstm层输出如下内容:# 1, output:输出【shape 为(batch_size, sentence_length, hidden_size)】【顺序为设定 batch_first 为 True 情况, 若未设定则 batch_size 在第二位】# 2, hn:最后时间步的隐藏状态 【shape 为(num_layers * num_directions, batch_size, hidden_size)】# 3, cn:最后时间步的Cell状态【shape 为(num_layers * num_directions, batch_size, hidden_size)】output, (hn, cn) = self.bilstm(input_features, (hidden0, cell0)) # 将字向量与初始值(隐藏状态初始值 hidden0 , cell状态初始值 cell0 )传入 LSTM 结构中sequence_features = self.linear(output) # 将输出特征进行线性变换,转为 shape 为 (batch, sequence_length, tag_size) 大小的特征return sequence_features # 输出线性变换为 tag 映射长度的特征【最终维度是tag的类型数量】# 工具函数:本函数实现将中文文本映射为数字化的张量【将句子中的每一个字符映射到码表中, 返回每一个字对应的编码, 类型为tensor】
def sentence_map(sentence_list, char_to_id, max_length): # sentence: 待映射句子, 类型为字符串或列表; char_to_id: 完整版码表, 类型为字典, 格式为{"字1": 1, "字2": 2};sentence_list.sort(key=lambda c:len(c), reverse=True) # 字符串按照逆序进行排序, 不是必须操作sentence_map_list = [] # 定义句子映射列表for sentence in sentence_list:sentence_id_list = [char_to_id[c] for c in sentence] # 生成句子中每个字对应的 id 列表【序列化的句子】padding_list = [0] * (max_length-len(sentence)) # 计算所要填充 0 的长度sentence_id_list.extend(padding_list) # 组合sentence_map_list.append(sentence_id_list) # 将填充后的列表加入句子映射总表中【序列化的句子列表】return torch.tensor(sentence_map_list, dtype=torch.long) # 返回句子映射集合, 转为张量【返回:序列化的句子列表】# 工具函数:创建字符-序号映射字典
def char2id(char_to_id, sentence_list):for sentence in sentence_list:for _char in sentence:if _char not in char_to_id:char_to_id[_char] = len(char_to_id)return char_to_idif __name__ == '__main__':# 参数1:句子集合sentence_list = ["确诊弥漫大b细胞淋巴瘤1年", "反复咳嗽、咳痰40年,再发伴气促5天。", "生长发育迟缓9年。", "右侧小细胞肺癌第三次化疗入院"]# 参数2:汉字与id对照码表char_to_id = {"<PAD>": 0} # 初始化的码表# 参数3:句子长度SENTENCE_LENGTH = 20# 参数4:标签码表对照tag_to_id = {"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}# 参数5:字向量维度EMBEDDING_DIM = 200# 参数6:隐层维度HIDDEN_DIM = 100# 参数7:批次大小BATCH_SIZE = 4# 参数8:堆叠 LSTM 层数NUM_LAYERS = 1# 1、构建汉字-序号对应码表char_to_id = char2id(char_to_id, sentence_list) # 创建char_to_id汉字与id对照码表# 2、根据char_to_id码表将字符串句子文本转为序列化表示sentence_sequence = sentence_map(sentence_list, char_to_id, SENTENCE_LENGTH)print("sentence_sequence.shpae = {0}----sentence_sequence = \n{1}\n".format(sentence_sequence.shape, sentence_sequence))# 3、实例化模型model = BiLSTM(vocab_size=len(char_to_id), tag_to_id=tag_to_id, input_feature_size=EMBEDDING_DIM, hidden_size=HIDDEN_DIM, batch_size=BATCH_SIZE, sentence_length=SENTENCE_LENGTH, num_layers=NUM_LAYERS)print("model: ", model)# 4、通过模型得到序列化句子中的每个汉字的tag表达概率【每个汉字在每个tag上的得分】sentence_features = model(sentence_sequence)print("sentence_features.shpae = {0}----sentence_features = \n{1}\n".format(sentence_features.shape, sentence_features))
输出结果:
ssh://root@47.93.247.255:22/root/anaconda3/bin/python -u /data/doctor_offline/ner_model/bilstm.py
sentence_sequence.shpae = torch.Size([4, 20])----sentence_sequence =
tensor([[14, 15, 16, 17, 18, 16, 19, 20, 21, 13, 22, 23, 24, 25, 26, 27, 28, 29, 30, 0],[37, 38, 39, 7, 8, 40, 41, 42, 43, 44, 45, 46, 47, 48, 0, 0, 0, 0, 0, 0],[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 0, 0, 0, 0, 0, 0, 0],[31, 32, 24, 33, 34, 35, 36, 13, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])sentence_features.shpae = torch.Size([4, 20, 5])----sentence_features =
tensor([[[-0.1389, 0.0338, 0.1532, -0.1136, -0.0621],[ 0.0874, -0.2526, -0.1340, -0.1838, -0.0949],[-0.0882, -0.1751, -0.0677, -0.1099, 0.0795],[-0.0999, -0.2465, -0.2183, 0.1657, -0.0813],[-0.0022, -0.0362, -0.0282, -0.1996, 0.1867],[-0.0015, -0.0256, -0.0995, -0.1907, 0.1462],[ 0.1290, 0.1615, -0.1282, -0.0963, 0.0899],[ 0.1446, -0.0498, -0.1076, 0.0376, -0.1198],[ 0.0719, -0.2300, -0.0392, 0.0354, 0.0492],[-0.0085, -0.0866, 0.0783, 0.1802, 0.0266],[-0.0266, -0.1050, -0.1074, -0.0714, 0.1258],[-0.2334, -0.1496, 0.0507, -0.0399, 0.1245],[-0.0549, -0.1062, 0.0189, -0.1190, 0.0947],[ 0.2127, -0.0431, -0.0334, -0.0583, 0.0572],[ 0.2617, -0.1306, -0.2478, 0.0185, -0.0254],[ 0.1582, 0.0231, 0.1112, -0.1882, 0.1195],[ 0.3084, 0.0007, 0.2140, -0.2551, 0.2404],[ 0.0784, 0.0823, -0.0452, -0.1004, -0.1108],[ 0.2645, 0.0199, -0.2783, -0.1242, -0.0405],[ 0.1166, -0.0279, 0.1355, -0.2478, -0.0557]],[[-0.0221, -0.0436, -0.1802, 0.0448, 0.0517],[ 0.0469, -0.0873, -0.1866, -0.1121, -0.0372],[ 0.1139, 0.0719, -0.0171, -0.1958, 0.0423],[ 0.1153, -0.0461, -0.0654, -0.1380, -0.1022],[ 0.0093, -0.0765, -0.1502, -0.1107, 0.0768],[ 0.1161, 0.0030, -0.2444, -0.2255, 0.0137],[ 0.0762, 0.0662, 0.0076, -0.0135, 0.1286],[-0.0092, 0.0267, 0.1190, -0.0239, 0.2241],[-0.0475, 0.1052, -0.0475, 0.0289, 0.1960],[-0.0270, -0.0362, 0.0288, 0.0927, 0.1177],[-0.0535, 0.0816, 0.0935, 0.2074, -0.0816],[-0.0061, 0.2228, 0.0605, -0.0023, 0.0238],[ 0.1011, -0.0206, -0.0435, -0.3221, -0.0308],[ 0.2295, 0.2631, 0.1293, -0.4822, 0.0822],[ 0.1453, 0.1953, 0.2544, -0.3759, 0.0442],[ 0.1539, 0.2058, 0.2637, -0.3136, -0.0085],[ 0.1598, 0.2140, 0.2518, -0.2772, -0.0306],[ 0.1591, 0.2153, 0.2263, -0.2427, -0.0502],[ 0.1438, 0.1987, 0.1815, -0.1906, -0.0859],[ 0.0954, 0.1449, 0.1361, -0.1113, -0.1260]],[[ 0.1831, -0.1770, -0.0104, 0.1610, -0.1085],[ 0.2623, 0.0652, -0.1827, -0.0236, -0.1678],[ 0.1192, 0.0590, -0.1336, 0.0076, 0.1512],[-0.0304, 0.1055, -0.1486, -0.0601, 0.0876],[-0.0663, 0.0646, -0.0286, -0.0374, 0.2744],[ 0.0619, 0.0144, -0.0481, -0.1420, 0.2053],[ 0.1240, 0.0207, -0.0548, -0.2478, -0.0184],[ 0.0130, 0.0061, -0.1453, -0.1595, 0.1096],[-0.0148, 0.2643, -0.0448, -0.1963, 0.0510],[-0.0912, -0.1276, -0.0617, -0.0942, -0.1681],[ 0.0496, 0.0565, -0.2059, -0.3369, 0.1429],[ 0.1273, 0.0750, -0.0227, -0.2329, 0.0736],[ 0.1588, -0.0276, 0.0487, 0.0212, 0.1070],[ 0.1745, 0.0976, 0.2603, -0.2484, -0.0521],[ 0.1703, 0.1468, 0.2871, -0.2596, -0.0569],[ 0.1752, 0.1775, 0.3019, -0.2583, -0.0519],[ 0.1785, 0.2040, 0.3153, -0.2497, -0.0447],[ 0.1776, 0.2328, 0.3282, -0.2260, -0.0351],[ 0.1653, 0.2622, 0.3342, -0.1599, -0.0290],[ 0.1273, 0.2633, 0.2943, -0.0550, 0.0185]],[[ 0.0722, 0.0339, -0.2793, -0.0150, 0.0826],[ 0.0678, -0.0308, 0.0347, -0.1229, -0.0095],[-0.0262, -0.0251, -0.0107, -0.1373, 0.0980],[ 0.0927, -0.1573, -0.1421, -0.0923, 0.1980],[ 0.0977, 0.0286, 0.0303, 0.0571, 0.2332],[ 0.1933, 0.0145, -0.1637, 0.1374, 0.3501],[ 0.1239, -0.0021, 0.0452, -0.0581, 0.0789],[ 0.1195, 0.0247, 0.0203, 0.1014, 0.1502],[ 0.4034, 0.0358, -0.2396, -0.1338, 0.0848],[ 0.2128, 0.1202, 0.2143, -0.2778, 0.0352],[ 0.1909, 0.1404, 0.2351, -0.2659, -0.0088],[ 0.1854, 0.1564, 0.2455, -0.2601, -0.0272],[ 0.1850, 0.1688, 0.2514, -0.2553, -0.0368],[ 0.1862, 0.1804, 0.2548, -0.2498, -0.0436],[ 0.1881, 0.1928, 0.2560, -0.2419, -0.0499],[ 0.1900, 0.2077, 0.2538, -0.2291, -0.0569],[ 0.1908, 0.2261, 0.2452, -0.2065, -0.0630],[ 0.1866, 0.2479, 0.2237, -0.1647, -0.0619],[ 0.1683, 0.2699, 0.1807, -0.0827, -0.0448],[ 0.1224, 0.2449, 0.0946, 0.0894, -0.0302]]],grad_fn=<AddBackward0>)Process finished with exit code 0
三、CRF介绍
CRF(全称Conditional Random Fields), 条件随机场. 是给定输入序列的条件下, 求解输出序列的条件概率分布模型.
下面举两个应用场景的例子:
-
场景一: 假设有一堆日常生活的给小朋友排拍的视频片段, 可能的状态有睡觉、吃饭、喝水、洗澡、刷牙、玩耍等, 大部分情况, 我们是能够识别出视频片段的状态. 但如果你只是看到一小段拿杯子的视频, 在没有前后相连的视频作为前后文参照的情况下, 我们很难知道拿杯子是要刷牙还是喝水. 这时, 可以用到CRF模型.
-
场景二: 假设有分好词的句子, 我们要判断每个词的词性, 那么对于一些词来说, 如果我们不知道相邻词的词性的情况下, 是很难准确判断每个词的词性的. 这时, 我们也可以用到CRF.
基本定义: 我们将随机变量的集合称为随机过程. 由一个空间变量索引的随机过程, 我们将其称为随机场. 上面的例子中, 做词性标注时, 可以将{名词、动词、形容词、副词}这些词性定义为随机变量, 然后从中选择相应的词性, 而这组随机变量在某种程度上遵循某种概率分布, 将这些词性按照对应的概率赋值给相应的词, 就完成了句子的词性标注.
1、马尔科夫假设(HMM) v.s. 条件随机场(CRF)
马尔科夫假设,:当前位置的取值只和与它相邻的位置的值有关, 和它不相邻的位置的值无关。应用到我们上面的词性标注例子中, 可以理解为当前词的词性是根据前一个词和后一个词的词性来决定的, 等效于从词性前后文的概率来给出当前词的词性判断结果.
条件随机场(CRF):现实中可以做如下假设,假设一个动词或者副词后面不会连接同样的动词或者副词, 这样的概率很高. 那么, 可以假定这种给定隐藏状态(也就是词性序列)的情况下, 来计算观测状态的计算过程. 本质上CRF模型考虑到了观测状态这个先验条件, 这也是条件随机场中的条件一词的含义。而隐马尔可夫模型(HMM)不考虑先验条件。
2、转移概率矩阵
首先假设我们需要标注的实体类型有以下几类:
{"O": 0, "B-dis": 1, "I-dis": 2, "B-sym": 3, "I-sym": 4}
其中dis表示疾病(disease), sym表示症状(symptom), B表示命名实体开头, I表示命名实体中间到结尾, O表示其他类型.
因此我们很容易知道每个字的可能标注类型有以上五种可能性, 那么在一个句子中, 由上一个字到下一个字的概率乘积就有5 × 5种可能性, 具体见下图所示【其中的概率数值是通过模型在所给语料的基础上训练得到每个词的tag,然后统计出的结果】:
最终训练出来结果大致会如上图所示, 其中下标索引为(i, j)的方格代表如果当前字符是第i行表示的标签, 那么下一个字符表示第j列表示的标签所对应的概率值. 以第二行为例, 假设当前第i个字的标签为B-dis, 那么第i+1个字最大可能出现的概率应该是I-dis.
转移概率矩阵是行数、列数都为tag-size的方阵。
3、发射概率矩阵
发射概率, 是指已知当前标签的情况下, 对应所出现各个不同字符的概率. 通俗理解就是当前标签比较可能出现的文字有哪些, 及其对应出现的概率.
下面是几段医疗文本数据的标注结果:
可以得到以上句子的转移矩阵概率如下(比如:其中 28 表示标记为 “O” 的汉字转移到所有标签汉字的总转移次数):
对应的发射矩阵可以理解为如下图所示结果(其中:29表示标记为O的所有汉字、符号的总数量):
四、BiLSTM+CRF模型代码解析【优化方案:Bert代替BiLSTM】
BiLSTM+CRF模型结构:
- 模型的标签定义与整体架构
- 模型内部的分层展开
- CRF层的作用
1、模型的标签定义与整体架构
假设我们的数据集中有两类实体-人名, 地名, 与之对应的在训练集中有5类标签如下所示:
B-Person, I-Person, B-Organization, I-Organization, O# B-Person: 人名的开始
# I-Person: 人名的中间部分
# B-Organization: 地名的开始
# I-Organization: 地名的中间部分
# O: 其他非人名, 非地名的标签
假设一个句子有5个单词构成。序列 ( w 0 , w 1 , w 2 , w 3 , w 4 ) (w_0, w_1, w_2, w_3, w_4) (w0,w1,w2,w3,w4) 中的每一个单元 w i w_i wi 都代表着由一个字。
其中字/词嵌入向量是随机初始化的, 字/词嵌入是通过数据训练得到的, 所有的字/词嵌入在训练过程中都会调整到最优解。
这些字嵌入或词嵌入作为BiLSTM+CRF模型的输入, 而输出的是句子中每个字 w i w_i wi 的标签.
2、模型内部的分层展开
整个模型明显有两层, 第一层是BiLSTM层(输出一个 w i w_i wi 到 t a g j tag_j tagj 的输出发射矩阵), 第二层是CRF层(转移矩阵), 将层的内部展开如下图所示:
BiLSTM层的输出为每一个标签的预测分值(发射矩阵), 例如对于单词 w 0 w_0 w0, BiLSTM层输出是
1.5 (B-Person), 0.9 (I-Person), 0.1 (B-Organization), 0.08 (I-Organization), 0.05 (O)
这些分值将作为CRF层的输入.
3、CRF层的作用
如果没有CRF层, 也可以训练一个BiLSTM命名实体识别模型, 如下图所示:
由于BiLSTM的输出为单元的每一个标签分值, 我们可以挑选分值最高的一个作为该单元的标签.例如, 对于单词 w 0 w_0 w0, "B-Person"的分值-1.5是所有标签得分中最高的, 因此可以挑选"B-Person"作为单词 w 0 w_0 w0 的预测标签. 同理, 可以得到 w 1 w_1 w1 - “I-Person”, w 2 w_2 w2 - “O”, w 3 w_3 w3 - “B-Organization”, w 4 w_4 w4 - “O”
虽然在没有CRF层的条件下我们也可以只使用BiLSTM模型得到序列中每个单元的预测标签, 但是不能保证标签的预测每次都是正确的。如果出现下图的BiLSTM层输出结果, 则明显预测是错误的.
CRF层能从训练数据中获得约束性的规则:CRF层可以为最后预测的标签添加一些约束来保证预测的标签是合法的。
在训练数据训练的过程中, 这些约束可以通过CRF层自动学习到。比如以下规则:
- 句子中的第一个词总是以标签"B-"或者"O"开始, 而不是"I-"开始.
- 标签"B-label1 I-label2 I-label3 …", 其中的label1, label2, label3应该属于同一类实体。比如, "B-Person I-Person"是合法的序列, 但是"B-Person I-Organization"是非法的序列.
- 标签序列"O I-label"是非法序列, 任意实体标签的首个标签应该是"B-“, 而不是"I-”。比如, "O B-label"才是合法的序列
有了上述这些约束, 标签序列的预测中非法序列出现的概率将会大大降低。
要怎样得到这个CRF转移矩阵呢?实际上,CRF转移矩阵是BiLSTM-CRF模型的一个参数。在训练模型之前,你可以随机初始化转移矩阵的分数。这些分数将随着训练的迭代过程被更新,换句话说,CRF层可以自己学到这些约束条件。
在CRF层也可以人为添加规则来实现人工干预,比如模型上线后会有BadCase出现,通过分析BadCase,将新的规则添加的CRF层;
4、损失函数的定义
CRF损失函数由两部分组成:
- 真实路径的分数 ;
- 所有路径的总分数;
真实路径的分数应该是所有路径中分数最高的。
详细参考:NLP:BiLSTM+CRF 的损失函数【BiLSTM+CRF模型适用于:中文分词、词性标注、命名实体识别】
BiLSTM层的输出维度是tag_size, 也就是每个单词 w i w_i wi 映射到 各个tag的发射概率值。
- 假设BiLSTM的输出是发射矩阵 P P P, 其中 P ( i , j ) P(i,j) P(i,j) 代表单词 w i w_i wi 映射到 t a g j tag_j tagj 的非归一化概率。
- 对于CRF层, 假设存在一个转移矩阵 A A A,其中 A ( i , j ) A(i,j) A(i,j)代表 t a g j tag_j tagj 转移到 t a g i tag_i tagi 的概率.
对于输入序列 X X X 对应的输出tag序列 y y y, 定义分数如下(本质上就是发射概率和转移概率的累加和):
S ( X , y ) = ∑ i = 1 n P i , y i + ∑ i = 0 n A y i , y i + 1 = 发射概率+转移概率 \color{red}{S(X,y)=\sum_{i=1}^nP_{i,y_i}+\sum_{i=0}^nA_{y_i,y_{i+1}}=\text{发射概率+转移概率}} S(X,y)=i=1∑nPi,yi+i=0∑nAyi,yi+1=发射概率+转移概率
利用softmax函数, 为每一个正确的tag序列 y y y 定义一个概率值, 在真实的训练中, 只需要最大化似然概率 p ( y ∣ X ) p(y|X) p(y∣X)即可, 具体使用对数似然如下:
p ( y ∣ X ) = 贝 叶 斯 概 率 公 式 S ( X , y ) S ( X ) = S ( X , y ) ∑ y ~ ∈ Y x S ( X , y ~ ) = 进 行 s o f t m a x 处 理 e S ( X , y ) ∑ y ~ ∈ Y x e S ( X , y ~ ) p(y|X)\xlongequal{贝叶斯概率公式}\cfrac{S(X,y)}{S(X)}=\cfrac{S(X,y)}{\sum_{\tilde{y}∈Y_x}S(X,\tilde{y})}\xlongequal{进行softmax处理}\cfrac{e^S(X,y)}{\sum_{\tilde{y}∈Y_x}e^{S(X,\tilde{y})}} p(y∣X)贝叶斯概率公式
这篇关于NLP-信息抽取-NER-2015-BiLSTM+CRF(一):命名实体识别【预测每个词的标签】【评价指标:精确率=识别出正确的实体数/识别出的实体数、召回率=识别出正确的实体数/样本真实实体数】的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!