序列标注 | (3) NER入门+BiLSTM-CRF模型原理+Pytorch代码详解(资料汇总)-程序员宅基地

技术标签: 序列标注  

原文地址

最近在系统地接触学习NER(命名实体识别/实体抽取),但是发现这方面的小帖子还比较零散。所以我把学习的记录放出来给大家作参考,其中汇聚了很多其他博主的知识,在本文中也放出了他们的原链。希望能够以这篇文章为载体,帮助其他跟我一样的学习者梳理、串起NER的各个小知识点,最后上手NER的主流模型(Bilstm+CRF)。

全文结构

一、NER资料

二、主流模型Bilstm-CRF实现详解(Pytorch篇)

三、实现代码的拓展(在第二点的基础上进行拓展)

一、NER资料

参考:NLP之CRF应用篇(序列标注任务) 包括:CRF++的详细解析、Bi-LSTM+CRF中CRF层的详细解析、Bi-LSTM后加CRF的原因、Bert+Bi-LSTM+CRF、CRF和Bi-LSTM+CRF优化目标的区别

CRF++完成的是学习和解码的过程:训练即为学习的过程,预测即为解码的过程。

参考: BiLSTM+CRF中CRF详解 (这份资料对后面代码的理解是有帮助的)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
序列标注问题就是对序列中每个元素打标签(基于标签集合进行多分类,这里的元素单位是字,上一篇博客是词,本质原理是一样的,具体描述稍有区别)。

参考: BiLSTM-CRF中CRF层解析-2

在上一篇的参考中提到,会在每一句话的开始加上“START”,在句尾加上“END”,这点我们可能会有疑惑。

这篇参考给予了解答:

这是为了使转移得分矩阵的鲁棒性更好,才额外加两个标签:START和END,START表示一句话的开始,注意这不是指该句话的第一个单词,START后才是第一个单词,同样的,END代表着这句话的结束。

下表就是一个转移得分矩阵的示例,该示例包含了START和END标签。
在这里插入图片描述
每一个格里的值表示的意思是:这个格的行值转成列值的概率大小。打个比方:上图中红框(B-Person,I-person)的值为0.9,表示的意思就是B-person转移至I-person的概率为0.9(上一个元素(字或词)标注为B-Person,下一个元素标注为I-Person的概率),这是合乎BIO标注的规定的(B是实体的开始,I是实体的内部或结束,O非实体)。类推一下,蓝框的意思代表的就是B-Organization转移至I-Organization的概率为0.8。

参考: BiLSTM-CRF中CRF层解析-3 (看完前面的参考来看这份,简直不要太良心了,易懂很多)

但是前面很多概念有提到,就不赘述了,只是加深一下印象,顺带推一下这个博主对CRF的一系列解析。

在这里插入图片描述
其中 P i , y i P_{i,y_i} Pi,yi为第 i 个位置(序列中第i个元素(字或词)) softmax 输出为(标签) y i y_i yi 的概率, A y i , y i + 1 A_{y_i,y_{i+1}} Ayi,yi+1 为从 (前一个元素标签) y i y_i yi 到(当前元素标签) y i + 1 y_{i+1} yi+1 的转移概率,当tag(B-person,B-location…)个数为n(标签集的大小)的时候,转移概率矩阵为(n+2)*(n+2),因为额外增加了一个开始位置(开始标签START)和结束位置(结束标签END)。这个得分函数S就很好地弥补了传统BiLSTM的不足,因为我们当一个预测序列得分很高时,并不是各个位置都是softmax输出最大概率值对应的label,还要考虑前面转移概率相加最大,即还要符合输出规则(B后面不能再跟B,开头不能是I等约束),比如假设BiLSTM输出的最有可能序列为BBIBIOOO,那么因为我们的转移概率矩阵中B->B的概率很小甚至为负,那么根据s得分,这种序列不会得到最高的分数,即就不是我们想要的序列。(相比于只使用BiLSTM可以去掉一些无效的预测序列,添加一些约束)。

整个过程中需要训练的参数为:

  • BiLSTM中的参数
  • 转移概率/得分矩阵A

BiLSTM+CRF的预测:

在这里插入图片描述

参考: BiLSTM+CRF的一些理解

model中由于CRF中有转移特征,即它会考虑输出label之间的顺序性(依赖或关联性),所以考虑用CRF去做BiLSTM的输出层。(只用BiLSTM的话,输出label之间是独立的)。

二、NER主流模型——BiLSTM-CRF代码详解部分(pytorch篇)

参考1: ADVANCED: MAKING DYNAMIC DECISIONS AND THE BI-LSTM CRF (PyTorch关于BILSTM+CRF的tutorial)

从参考1中 找到 pytorch 关于 Bilstm-CRF 模型的tutorial,然后运行它,我这里讲一下几个主体部分的作用:

def argmax(vec):
    # return the argmax as a python int
    _, idx = torch.max(vec, 1) #idx 最大值所在的索引
    return idx.item()


def prepare_sequence(seq, to_ix): #把序列中的元素(字或词)转换为索引
    idxs = [to_ix[w] for w in seq]
    return torch.tensor(idxs, dtype=torch.long) #list转换为tensor


# Compute log sum exp in a numerically stable way for the forward algorithm
def log_sum_exp(vec):  #找到一维tensor最大值 计算全体与该值的离差平方和后 再计算log
    max_score = vec[0, argmax(vec)]
    max_score_broadcast = max_score.view(1, -1).expand(1, vec.size()[1])
    return max_score + \
        torch.log(torch.sum(torch.exp(vec - max_score_broadcast))) #再求exp之前 减去最大值,做一个平移 使最大值为0,防止指数爆炸,计算溢出

在这里插入图片描述
训练数据集的格式:list内为tuple(一个tuple表示一条数据),然后分字/词以及bio(序列中每个字/词对应的标签,B实体开始,I实体中间或结束部分,O非实体)字段。

在这里插入图片描述
将输入句子中的字/词转换为对应的索引,把标签转换为对应的索引(上图中只定义了一种实体类型)。

在这里插入图片描述
建立BiLSTM_CRF model,及优化器

在这里插入图片描述
这是一个toy例子,很简单。

在这里插入图片描述
训练300epoch,画红框的是核心。将text字段(输入序列/句子)及bio label(序列中每个字/词对应的标签序列)转换为映射的数字索引,输入模型即可训练。

现在的很多NLP的网红模型,无非是将文字到数字索引的映射建立的更合理。是可拓展的。

另外,这里的模型训练使用的损失函数是: model.neg_log_likelihood() 。这是代码中建立好的 BiLSTM_CRF 类的一部分,弄明白需继续看 model(参考:pytorch版的bilstm+crf实现sequence label,有模型注解)

torch.nn.Parameter():首先可以把这个函数理解为类型转换函数,将一个不可训练的类型Tensor转换成可以训练的类型parameter并将这个parameter绑定到这个module里面(net.parameter()中就有这个绑定的parameter,所以在参数优化的时候可以进行优化的),所以经过类型转换这个self.v变成了模型的一部分,成为了模型中根据训练可以改动的参数了。使用这个函数的目的也是想让某些变量在学习的过程中不断的修改其值以达到最优化。(参考)【一句话解释:就是(转换为模型的参数)希望它能够梯度下降(计算提督),更新优化】

在这里插入图片描述
(建立转移矩阵A,并加了两个我们不会变动的约束条件:1是我们不会从其他tag转向start。2是不会从stop开始转向其他。所以这些位置设为-1e4)

注意:转移矩阵是随机初始化的,而且声明为模型参数,放入了网络中,是会随训练更新的)(如果转移矩阵A的概念不懂可以理解了转移矩阵再回来看)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
lstm层:经过了embedding,lstm,linear层, 输出为发射矩阵——emission matrix(seq_len*len(labels)) (该例中batch_size=1)

在这里插入图片描述
forward_score应该是所有路径的总得分 减去 真实标签对应路径的得分 即为损失。

在这里插入图片描述
计算所有可能路径的总得分。

在这里插入图片描述

参考2: pytorch实现BiLSTM+CRF用于NER(命名实体识别)(提到了viterbi编码,很有启发!记录如下)【统筹CRF算法code,以及forward_score - gold_score 作为loss的根本原因】

CRF是判别模型, 判别公式如下 y 是标记序列,x 是单词序列,即已知单词序列,求最有可能的标记序列:

在这里插入图片描述
Score(x, y) 即单词序列 x 产生标记序列 y 的得分,得分越高,说明其产生的概率越大。

在pytorch的tutorial中,其用于实体识别定义的 Score(x,y) 包含两个特征函数,一个是转移特征函数,一个是状态特征函数.
在这里插入图片描述
代码中用到了前向算法和维特比算法(viterbi)

log_sum_exp函数就是计算:
在这里插入图片描述
,**前向算法(_forward_alg)**需要用到这个函数

前向算法,求出α(alpha),即Z(x),也就是(所有可能标注路径的得分和):
在这里插入图片描述
,如果不懂可以看一下李航的书关于CRF的前向算法.

但是不同于李航书的是,代码中α都取了对数,一个是为了运算方便,一个为了后面的最大似然估计。

这个代码里面没有进行优化,作者也指出来了,**其实对feats的迭代完全没有必要用两次循环,其实用矩阵相乘就够了,**作者是为了方便我们理解,所以细化了步骤。

维特比算法(viterbi)中规中矩,可以参考李航书上条件随机场的预测算法

neg_log_likelihood函数的作用(计算损失):
在这里插入图片描述
我们知道forward_score是log Z(x),即(所有可能标注路径的得分和):
在这里插入图片描述
gold_score是(真实标注路径的得分):
在这里插入图片描述
我们的目标是极大化:
在这里插入图片描述
两边取对数即:
在这里插入图片描述
所以我们需要极大化 gold_score - forward_score,也就是极小化 forward_score - gold_score。

这就是为什么 forward_score - gold_score 可以作为loss的根本原因。

参考3: BiLSTM-CRF for Sequence Labeling

这篇跟参考2讲的是一个意思。得分score表示为:

在这里插入图片描述
也很清晰地提到了CRF的作用以及score中P和A矩阵分别代表的含义:P为Bi-LSTM的输出矩阵(seq_len*len(labels));A为tag之间的转移矩阵(len(labels)*len(labels)). len(labels)=tag size.
在这里插入图片描述
在许多参考文章中都有提到score的成分包含了两部分,一个是Bilstm的输出结果,另一个就是CRF的转移矩阵,而转移矩阵的作用就是去给标注结果一些约束。例如标注B的后面不能接B这种约束。这种约束是根据转移矩阵A提供的。而转移矩阵A是随机初始化,然后根据你提供的训练集,训练学习、梯度下降得到的。根据画红线的去看上方score的定义,就明白定义了每一种标注情况为一条路径,使用score去计算该路径的得分的意思了。再啰嗦一下: A y i , y i + 1 A_{y_i,y_{i+1}} Ayi,yi+1是表达这个tag y i y_i yi(标注 y i y_i yi)转移至下一个tag y i + 1 y_{i+1} yi+1(标注 y i + 1 y_{i+1} yi+1)的分数(概率)。而 P i , y i P_{i,y_i} Pi,yi就是Bilstm的输出矩阵,可以看到每个字/词对应到不同tag(标注)的分数。【不懂也没关系,有很多文章都提到了。反复看就会有感觉了】

CRF的概率函数表示为

在这里插入图片描述
S(X,y)的计算很简单,而
在这里插入图片描述)
(下面记作logsumexp)的计算稍微复杂一些,因为需要计算每一条可能路径的分数。这里用一种简便的方法,对于到词的路径,可以先把到词的logsumexp计算出来,因为:
在这里插入图片描述
因此先计算每一步的路径分数直接计算全局分数相同,但这样可以大大减少计算的时间

参考4: BiLSTM-CRF中CRF层解析-4 (用程序的思想去理解怎么计算所有路径的得分和,巨良心)

这篇文章提到了动态规划的编程思想,虽然跟pytorch的tutorial有些许偏差。但已经很到位了。卡在**_foward_alg函数**的同学多看几遍这篇文章,先理解一下动态规划的思路吧。会有帮助的。

参考5: BiLSTM-CRF中CRF层解析-5

上一篇在讲loss的一部分:所有路径的得分和。现在讲怎么去解码预测。大概的思路就是根据最高的得分去反哺这条路径,使用较多的就是Viterbi解码了。这篇文章就很详细很详细地提到了怎么去解码这个路径,具体就直接进到博主的解析上看吧!(我的序列标注系列博客的第二篇也完整转载了该系列博客)

参考6: pytorch lstm crf 代码理解(走心的解读,统筹代码块的作用,其心得部分十分到位)

这里就罗列一下作者的心得体会:

  • 反向传播不需要一定使用forward(),而且不需要定义loss=nn.MSError()等,直接score1 - score2 (neg_log_likelihood函数),就可以反向传播了。
  • 使用self.transitions = nn.Parameter(torch.randn(self.tagset_size, self.tagset_size)) 将想要更新的矩阵,放入到module的参数中,然后两个矩阵无论怎么操作,只要满足 y = f(x, w),就能够反向传播
  • 从代码看出每个循环里只是取了转移矩阵A的一行,或者就是一个值,进行操作,转移矩阵就能够更新。至于为什么能够更新,作者也不知道,这涉及到pytorch的机制。
  • 发射矩阵(emit score)是 BiLSTM算出来的。转移矩阵是单独定义的,要学习的(模型参数)。初始矩阵是 [-1000,-1000,-1000,0,-1000],固定的。因为当加了开始符号后,第一个位置是开始符号的概率是100%。
  • 显式的加入了start标记,隐式的使用了end标记(总是最后多一步转移到end)的分数

参考7: Pytorch高级实战教程:基于BiLSTM-CRF实现命名实体识别和中文分词

对这份pytorch NER tutorial,只需要将中文分词的数据集预处理成作者提到的格式,即可很快的迁移这个代码到中文分词中。但这种方式并不适合处理很多的数据(数据格式迁移问题),但是对于 demo 来说非常友好,把英文改成中文,标签改成分词问题中的 “BEMS” (B是词开头,E词结束,M词中间部分,S单字词,输入序列以字为单位)就可以跑起来了。

参考资料:

三、模型代码扩展部分(pytorch部分)

前面我们介绍了很久pytorch实现NER任务的主流model——Bilstm+CRF,为了便于新手入门,所以还是稍微简陋了一些。刚好看到有份资源是移植这个tutorial去实践的,还是很有必要学习的

资料: ChineseNER (中文NER、有tf和torch版,市面上Bilstm+CRF的torch code基本都是出自官方tutorial)(py2.7)

因为是py2的代码,所以是需要改成py3的。

**训练代码:**train_py3.py
在这里插入图片描述
但这个“Bosondata.pkl”(预处理完的数据文件)是需要我们先到路径“ChineseNER\data\boson”下运行"data_util.py"才生成的。

当然,原代码也是存在python版本的问题(原代码是py2的)例如:
报错:AttributeError: ‘str’ object has no attribute ‘decode’
解决方法:把 .decode("*") 那部分删除即可

溯源: https://www.cnblogs.com/xiaodai0/p/10564471.html

报错:ImportError: No module named ‘compiler.ast’
解决方法:重新写一个函数来替代 from compiler.ast import flatten 的flatten函数

import collections
def flatten(x):
    result = []
    for el in x:
        if isinstance(x, collections.Iterable) and not isinstance(el, str):
            result.extend(flatten(el))
        else:
            result.append(el)
    return result

溯源: https://blog.csdn.net/w5688414/article/details/78489277
在这里插入图片描述
当成功运行"data_util.py"生成“Bosondata.pkl”后,把"train_py3.py"里面第38行的"word2id"修改为"id2word"(应该是作者打错了),然后在代码路径下创造文件夹“model”(用于存储模型文件),就可以开始训练了。

最后附上修改后的github源码https://github.com/Hyfred/Pytroch_NER_tutorial

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/sdu_hao/article/details/105339546

智能推荐

学java为什么要报java培训班?-程序员宅基地

文章浏览阅读580次。  学java为什么要报java培训班?对于没有基础的小白来说,选择报java培训班是最合适不过的,自学是没有任何规划的,学到的技术都是模棱两可,工作入职后是存在很大风险的,具体的来看看下面的详细介绍吧。    学java为什么要报java培训班?  1、学习的路上不走弯路  在培训机构学习java的话那么至少你在学习的时候都会知道你相对应的要学习的知识有哪些,也不用说自己去那些网站随便看,然后看的太多太杂之后就不知道自己要学习的是什么了。通过培训机构学习的话那么你就可以系统的学习._报java培训班

VS2008整合DirectX9.0开发环境_dxsdk vs2008-程序员宅基地

文章浏览阅读5.7k次。微软的的东西还是很庞大,很复杂,很不好用....但是看到directX自带的那些sample的时候,还是果断呆掉了,我想说,我也要做出这个!接着是花了将近一天的时间完成来搞定微软的这一套图形开发的环境。准备工作:win7旗舰版 vs2008 express(比较轻量) directX 9.0SDK 网上能下到得最新版的是2010年的那个版本,微软官网下载不了。接下来可以动_dxsdk vs2008

基于 springboot websocket 的分布式群聊实现_java开发基于springboot+websocket+redis分布式即时通讯群聊系统.-程序员宅基地

文章浏览阅读516次。基于 springboot websocket 的群聊实现仓库地址https://github.com/yemingfeng/jchat-server功能列表分布式同一帐号多设备登录群聊多设备简单鉴权心跳检查依赖mavenjdk11redisredis 配置redis 默认使用 localhost:6379。如果需要修改 host:port,可以修改 application.ymlredis 仅仅用于存储用户 username / password源码分析Aut_java开发基于springboot+websocket+redis分布式即时通讯群聊系统.

Linux(CentOS7)系统安装部署(RPM方式)Oracle19c_/etc/redhat-release 被 oracle-database-preinstall-1-程序员宅基地

文章浏览阅读1.7k次,点赞3次,收藏12次。1.准备安装目录mkdir /opt/oracle2.安装oracle-database-preinstall-19c-1.0-1.el7.x86_64.rpm安装oracle-database-preinstall-19c-1.0-1.el7.x86_64.rpm它会为我们省掉自己创建用户和用户组等麻烦;安装oracle-database-preinstall-19c-1.0-1.el7.x86_64.rpm时会用到很多依赖包,我将可能用到的所有rpm依赖包下载链接放到下面,自行下载;rpm _/etc/redhat-release 被 oracle-database-preinstall-19c-1.0-1.el7.x86_64 需要

CentOS6开关机日志查询_centos 重启日志-程序员宅基地

文章浏览阅读1.5k次。CentOS6开关机日志查询1."switch root"之前的HAL信息[root@iZ23ss0jj3qZ ~]#tail -n 10 /var/log/dmesgudev: starting version 147piix4_smbus 0000:00:01.3: SMBus base address uninitialized - upgrade BIOS or use force_addr=0xaddrInitialising Xen virtual ethernet driver..._centos 重启日志

PCB(印刷电路板)制造过程和工艺详解_印刷电路板技术分解表-程序员宅基地

文章浏览阅读2.8k次。PCB(印刷电路板)制造过程和工艺详解来源:hc360慧聪网丝印特印行业频道 频道:包装印刷 发布时间:2007-10-02 【hc360慧聪网丝印特印行业频道】pcb(印刷电路板)的原料是玻璃纤维,这种材料我们在日常生活中出处可见,比如防火布、防火毡的核心就是玻璃纤维,玻璃纤维很容易和树脂相结合,我们把结构紧密、强度高的玻纤布浸入树脂中,硬化就得到了隔热绝缘、不易弯曲的p_印刷电路板技术分解表

随便推点

emacs新手配置基本c/c++编程环境_emacs c++配置-程序员宅基地

文章浏览阅读7k次,点赞4次,收藏10次。emacs新手配置基本c/c++编程环境系统环境及版本ubuntu 16 及 emacs 24.5emacs安装文件结构简单说明安装emacs时采用的是apt-get install的方式,文件都是保存在默认的目录结构下有必要做简单的文件结构说明,是晚上很多教程都只给了文件的配置,新手甚至不知道将那些配置代码保存在什么位置使用sudo find / | grep emacs 可以查找所有文_emacs c++配置

java中线程队列BlockingQueue的用法_java线程中的 blockingqueue<string>-程序员宅基地

文章浏览阅读153次。在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景。[@more@]认识BlockingQueue阻塞队列,顾名思义,首先它是一个队列,而一个队列_java线程中的 blockingqueue

Ubuntu Dns设置-程序员宅基地

文章浏览阅读1.3k次。Ubuntu Dns设置以及设置失败处理Ubuntu Dns Server要通过修改 /etc/resonconf/resolv.conf.d/base 完成。但有时依然设置失败。

matlab simulink模型打开或者运行时就自动退出_simulink模型运行崩溃退出matlab-程序员宅基地

文章浏览阅读9.5k次。在不同的版本matlab simulink中创建的模型在运行的时候会出现无法运行或者matlab软件自动退出的现象,这个时候需要高版本的的simulink通过另存降低simulink的版本export model to &gt; previous version..._simulink模型运行崩溃退出matlab

jquery获取和设置radio,select,checkbox的值_attr("cheched")-程序员宅基地

文章浏览阅读822次。获取radio的值$('input:radio[name=name]:checked').val(); $('input[name=name][checked]').val(); 设置radio的值$("input:radio[id=id]").attr("cheched",true); 获取select被选中项的text和value var ite_attr("cheched")

软考高项备考时间仅剩不到一周?如何冲刺?_高项冲刺-程序员宅基地

文章浏览阅读1.1k次。针对基本没有接触过这个的人群,甚至是本专业的人。高项的考试总共分三科,上午选择,下午一案例分析,下午二论文。说白了,这项考试还是应试为主,我以下要说的都是以通过考试为目的,打算真正提升自己能力的,还得另寻他法。下面开始讲解,我的复习方法。第一阶段:教程快速浏览做笔记买一本教程,把目录手写抄录一遍。(之所以手写,是因为考试时有论文,我们平时都很少动笔了,既然考试时要手写,我们必须提前适应下)。包..._高项冲刺

推荐文章

热门文章

相关标签