四大嵌套实体识别方法对比与总结

2023-11-02 05:30

本文主要是介绍四大嵌套实体识别方法对比与总结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

每天给你送来NLP技术干货!


写在前面

嵌套命名实体识别是命名实体识别中的一个颇具挑战的子问题。我们在《实体识别LEAR论文阅读笔记》与《实体识别BERT-MRC论文阅读笔记》中已经介绍过针对这个问题的两种方法。今天让我们通过本文来看看在嵌套实体识别上哪一个方法更胜一筹。

1. 嵌套实体识别

1.1 什么是嵌套实体识别

嵌套实体识别是命名实体识别中一个子问题。那么什么才是嵌套实体呢?我们看下面这个例子:

  • “北京天安门”是地点实体;

  • “北京天安门”中“北京”也是地点实体;两者存在嵌套关系。

1.2 嵌套实体识别方法

CRF等传统序列标注方法无法应用于嵌套实体识别。现阶段,业界比较流行的是构建实体矩阵,即用一个矩阵 来代表语料中的所有实体及其类型。

其中任一元素 表示类为 ,起点为 ,结尾为 的实体。比如在下图所示实体矩阵中,就有两个Location类的实体:北京、北京天安门。

5df22ee32f0091551579cdb849819c43.png

通过这样的标注方式我们可以对任何嵌套实体进行标注,从而解决训练和解码的问题。

在本文中,我们将对比目前接触到的部分实体矩阵的构建方法在 CMeEE 数据集(医学NER,有一定比例的嵌套实体)上的表现。

2. 实体矩阵构建框架

2.1 变量与符号约定

为了方便后续对比说明,这里我们先定义几个统一的变量与符号。

首先,上文中 表示类为 ,起点为 ,结尾为 的实体。

在本实验中,我们均使用 bert-base-chinese 作为 编码器。假设 表示最后一层隐藏层中第 个 token 的 embedding,那么 和 分别表示经过编码器之后实体 start 和 end token 的embedding。

我们有公式 ,其中 就表示我们所需要对比的实体矩阵构建头(姑且这么称呼)。

2.2 相关配置

在对比实验中,除了不同实体矩阵构建头对应的batch_size,learning_rate不同,所使用的编码器、损失函数、评估方式以及训练轮次均保持一致。

2.3 对比方法

本文选取了四种实体矩阵构建方法进行比较,分别是:

  • GlobalPointer

  • TPLinker(Muti-head selection);

  • Tencent Muti-head

  • Deep Biaffine(双仿射)。

3. 代码实现

3.1 GlobalPointer

GlobalPointer 出自苏剑林的博客GlobalPointer:用统一的方式处理嵌套和非嵌套NER[1]

Global Pointer 的核心计算公式为:

其中 ,。

GlobalPointer 的核心思想类似 attention的打分机制,将多个实体类型的识别视为 Muti-head机制,将每一个head视为一种实体类型识别任务,最后利用attention的score(QK)作为最后的打分。

为考虑Start和end之间距离的关键信息,作者在此基础上引入了旋转式位置编码(RoPE),在其文中显示引入位置信息能给结果带来极大提升,符合预期先验。

class GlobalPointer(Module):"""全局指针模块将序列的每个(start, end)作为整体来进行判断"""def __init__(self, heads, head_size,hidden_size,RoPE=True):super(GlobalPointer, self).__init__()self.heads = headsself.head_size = head_sizeself.RoPE = RoPEself.dense = nn.Linear(hidden_size,self.head_size * self.heads * 2)def forward(self, inputs, mask=None):inputs = self.dense(inputs)inputs = torch.split(inputs, self.head_size * 2 , dim=-1)inputs = torch.stack(inputs, dim=-2)qw, kw = inputs[..., :self.head_size], inputs[..., self.head_size:]# RoPE编码if self.RoPE:pos = SinusoidalPositionEmbedding(self.head_size, 'zero')(inputs)cos_pos = pos[..., None, 1::2].repeat(1,1,1,2)sin_pos = pos[..., None, ::2].repeat(1,1,1,2)qw2 = torch.stack([-qw[..., 1::2], qw[..., ::2]], 4)qw2 = torch.reshape(qw2, qw.shape)qw = qw * cos_pos + qw2 * sin_poskw2 = torch.stack([-kw[..., 1::2], kw[..., ::2]], 4)kw2 = torch.reshape(kw2, kw.shape)kw = kw * cos_pos + kw2 * sin_pos# 计算内积logits = torch.einsum('bmhd , bnhd -> bhmn', qw, kw)# 排除padding,排除下三角logits = add_mask_tril(logits,mask)return logits / self.head_size ** 0.5

3.2 TPLinker

TPLinker 来自论文《TPLinker: Single-stage Joint Extraction of Entities and Relations Through Token Pair Linking》[2]

TPLinker 原本是为解决实体关系抽取设计的方法,原型为《Joint entity recognition and relation extraction as a multi-head selection problem》[3]论文中的 Muti-head selection机制。此处选取其中用于识别实体部分的机制,作为对比方法。

TPLinker中相应的计算公式如下:

其中

与GlobalPointer不同的是,GlobalPointer 是乘性的,而 Muti-head是加性的。这两种机制,谁的效果更好,我们无法仅通过理论进行分析,因此需要做相应的对比实验,从结果进行倒推。但是在实际实现的过程中,笔者发现加性比乘性占用更多的内存,但是与GlobalPointer中不同的是,加性仍然能实现快速并行,需要在计算设计上加入一些技巧。

class MutiHeadSelection(Module):def __init__(self,hidden_size,c_size,abPosition = False,rePosition=False, maxlen=None,max_relative=None):super(MutiHeadSelection, self).__init__()self.hidden_size = hidden_sizeself.c_size = c_sizeself.abPosition = abPositionself.rePosition = rePositionself.Wh = nn.Linear(hidden_size * 2,self.hidden_size)self.Wo = nn.Linear(self.hidden_size,self.c_size)if self.rePosition:self.relative_positions_encoding = relative_position_encoding(max_length=maxlen,depth= 2 * hidden_size,max_relative_position=max_relative)def forward(self, inputs, mask=None):input_length = inputs.shape[1]batch_size = inputs.shape[0]if self.abPosition:# 由于为加性拼接,我们无法使用RoPE,因此这里直接使用绝对位置编码inputs = SinusoidalPositionEmbedding(self.hidden_size, 'add')(inputs)x1 = torch.unsqueeze(inputs, 1)x2 = torch.unsqueeze(inputs, 2)x1 = x1.repeat(1, input_length, 1, 1)x2 = x2.repeat(1, 1, input_length, 1)concat_x = torch.cat([x2, x1], dim=-1)# 与TPLinker原论文中不同的是,通过重复+拼接的方法构建的矩阵能满足并行计算的要求。if self.rePosition:# 如果使用相对位置编码,我们则直接在矩阵上实现相加relations_keys = self.relative_positions_encoding[:input_length, :input_length, :].to(inputs.device)concat_x += relations_keyshij = torch.tanh(self.Wh(concat_x))logits = self.Wo(hij)logits = logits.permute(0,3,1,2)logits = add_mask_tril(logits, mask)return logits

3.3 Tencent Muti-head

《EMPIRICAL ANALYSIS OF UNLABELED ENTITY PROBLEM IN NAMED ENTITY RECOGNITION》[4] 提出了一种基于片段标注解决实体数据标注缺失的训练方法,并在部分数据集上达到了SOTA。关注其实体矩阵构建模块,相当于Muti-head的升级版,因此我把它叫做Tencent Muti-head。

Tencent Muti-head的计算公式如下:

其中

与TPLinker相比,Tencent Muti-head在加性的基础上加入了更多信息交互元素,比如 ,(作差与点乘),但同时也提高了内存的占用量。

class TxMutihead(Module):def __init__(self,hidden_size,c_size,abPosition = False,rePosition=False, maxlen=None,max_relative=None):super(TxMutihead, self).__init__()self.hidden_size = hidden_sizeself.c_size = c_sizeself.abPosition = abPositionself.rePosition = rePositionself.Wh = nn.Linear(hidden_size * 4, self.hidden_size)self.Wo = nn.Linear(self.hidden_size,self.c_size)if self.rePosition:self.relative_positions_encoding = relative_position_encoding(max_length=maxlen,depth= 4 * hidden_size,max_relative_position=max_relative)def forward(self, inputs, mask=None):input_length = inputs.shape[1]batch_size = inputs.shape[0]if self.abPosition:# 由于为加性拼接,我们无法使用RoPE,因此这里直接使用绝对位置编码inputs = SinusoidalPositionEmbedding(self.hidden_size, 'add')(inputs)x1 = torch.unsqueeze(inputs, 1)x2 = torch.unsqueeze(inputs, 2)x1 = x1.repeat(1, input_length, 1, 1)x2 = x2.repeat(1, 1, input_length, 1)concat_x = torch.cat([x2, x1,x2-x1,x2.mul(x1)], dim=-1)if self.rePosition:relations_keys = self.relative_positions_encoding[:input_length, :input_length, :].to(inputs.device)concat_x += relations_keyshij = torch.tanh(self.Wh(concat_x))logits = self.Wo(hij)logits = logits.permute(0,3,1,2)logits = add_mask_tril(logits, mask)return logits

3.4 Deep Biaffine

此处使用的双仿射结构出自《Named Entity Recognition as Dependency Parsing》[5]。原文用于识别实体依存关系,因此也可以直接用于实体命名识别。

Deep Biaffine的计算公式如下:

简单来说双仿射分别 为头 为尾的实体类别后验概率建模 + 对 或 为尾的实体类别的后验概率分别建模 + 对实体类别 的先验概率建模。

不难看出Deep Biaffine是加性与乘性的结合在笔者复现的关系抽取任务中,双仿射确实带来的一定提升,但这种建模思路在实体识别中是否有效还有待验证

class Biaffine(Module):def __init__(self, in_size, out_size, Position = False):super(Biaffine, self).__init__()self.out_size = out_sizeself.weight1 = Parameter(torch.Tensor(in_size, out_size, in_size))self.weight2 = Parameter(torch.Tensor(2 * in_size + 1, out_size))self.Position = Positionself.reset_parameters()def reset_parameters(self):torch.nn.init.kaiming_uniform_(self.weight1,a=math.sqrt(5))torch.nn.init.kaiming_uniform_(self.weight2,a=math.sqrt(5))def forward(self, inputs, mask = None):input_length = inputs.shape[1]hidden_size = inputs.shape[-1]if self.Position:#引入绝对位置编码,在矩阵乘法时可以转化为相对位置信息inputs = SinusoidalPositionEmbedding(hidden_size, 'add')(inputs)x1 = torch.unsqueeze(inputs, 1)x2 = torch.unsqueeze(inputs, 2)x1 = x1.repeat(1, input_length, 1, 1)x2 = x2.repeat(1, 1, input_length, 1)concat_x = torch.cat([x2, x1], dim=-1)concat_x = torch.cat([concat_x, torch.ones_like(concat_x[..., :1])],dim=-1)# bxi,oij,byj->boxylogits_1 = torch.einsum('bxi,ioj,byj -> bxyo', inputs, self.weight1, inputs)logits_2 = torch.einsum('bijy,yo -> bijo', concat_x, self.weight2)logits = logits_1 + logits_2logits = logits.permute(0,3,1,2)logits = add_mask_tril(logits, mask)return logits

4. 实验结果

实验所用的GPU为: P40 24G (x1)。为了把各方法的内存占用情况考虑在内,本次对比实验全都在一张P40 24G的GPU上进行,并把Batch_size开到最大

  • 仅GlobalPointer可达到16;

  • Tencent Muti-head batch_size只能达到4。

Tencent Muti-head因为需要构建超大矩阵 ,所以占用内存较大,batch_size最大只能到4。从中,我们可以看出GlobalPointer的性能优势。

需要注意的是,我们这里只比较了各方法在训练过程中在验证集上的最好表现

ebf2fe40b1a8582fa6acb61f8aac5dcc.png

总结

  1. GlobalPointer作为乘性方法,在空间内存占用上明显优于其他方法,并且训练速度较快,能达到一个具有竞争力的效果

  2. TPLinker 和 Tencent Muti-head作为加性方法,在优化过程中均表现出 相对位置编码 > 绝对位置编码 > 不加入位置编码 的特征。这意味着在通过构建实体矩阵进行实体命名识别时位置信息具有绝对重要的优势,且直接引入相对位置信息较优。

  3. 在绝对位置编码和不加入位置编码的测试中Tencent Muti-head的效果明显优于TPLinker而两者均差于GlobalPointer,但在引入相对位置信息后Tencent Muti-head略微超越了GlobalPointer,而TPLinker提点显著,作为Tencent Muti-head的原型在最高得分上甚至可能有更好的表现。

  4. Biaffine双仿射表现不佳,意味着这种建模思路不适合用于实体命名识别。

  5. 计算资源有限的情况下GlobalPointer是最优的baseline选择,如果拥有足够的计算资源且对训练、推理时间的要求较为宽松,尝试使用TPLinker/Tencent Muti-head + 相对位置编码或许能取得更好的效果。

参考资料

[1]

GlobalPointer:用统一的方式处理嵌套和非嵌套NER: https://kexue.fm/archives/8373

[2]

《TPLinker: Single-stage Joint Extraction of Entities and Relations Through Token Pair Linking》: https://www.aclweb.org/anthology/2020.coling-main.138/

[3]

《Joint entity recognition and relation extraction as a multi-head selection problem》: https://www.sciencedirect.com/science/article/pii/S095741741830455X

[4]

《EMPIRICAL ANALYSIS OF UNLABELED ENTITY PROBLEM IN NAMED ENTITY RECOGNITION》: https://www.researchgate.net/publication/346933553_Empirical_Analysis_of_Unlabeled_Entity_Problem_in_Named_Entity_Recognition

[5]

《Named Entity Recognition as Dependency Parsing》: https://arxiv.org/pdf/2005.07150.pdf

文章来源:https://blog.csdn.net/weixin_45839693/article/details/116425297?spm=1001.2014.3001.5506
作者:是算法不是法术
编辑:@公众号 AI算法小喵


📝论文解读投稿,让你的文章被更多不同背景、不同方向的人看到,不被石沉大海,或许还能增加不少引用的呦~ 投稿加下面微信备注“投稿”即可。

最近文章

为什么回归问题不能用Dropout?

Bert/Transformer 被忽视的细节

中文小样本NER模型方法总结和实战

一文详解Transformers的性能优化的8种方法

DiffCSE: 将Equivariant Contrastive Learning应用于句子特征学习

苏州大学NLP团队文本生成&预训练方向招收研究生/博士生(含直博生)

NIPS'22 | 重新审视区域视觉特征在基于知识的视觉问答中的作用


投稿或交流学习,备注:昵称-学校(公司)-方向,进入DL&NLP交流群。

方向有很多:机器学习、深度学习,python,情感分析、意见挖掘、句法分析、机器翻译、人机对话、知识图谱、语音识别等。

1b89e845172de1ad24dd69874197e41d.png

记得备注~

这篇关于四大嵌套实体识别方法对比与总结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/328454

相关文章

锐捷和腾达哪个好? 两个品牌路由器对比分析

《锐捷和腾达哪个好?两个品牌路由器对比分析》在选择路由器时,Tenda和锐捷都是备受关注的品牌,各自有独特的产品特点和市场定位,选择哪个品牌的路由器更合适,实际上取决于你的具体需求和使用场景,我们从... 在选购路由器时,锐捷和腾达都是市场上备受关注的品牌,但它们的定位和特点却有所不同。锐捷更偏向企业级和专

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

什么是 Ubuntu LTS?Ubuntu LTS和普通版本区别对比

《什么是UbuntuLTS?UbuntuLTS和普通版本区别对比》UbuntuLTS是Ubuntu操作系统的一个特殊版本,旨在提供更长时间的支持和稳定性,与常规的Ubuntu版本相比,LTS版... 如果你正打算安装 Ubuntu 系统,可能会被「LTS 版本」和「普通版本」给搞得一头雾水吧?尤其是对于刚入

TP-LINK/水星和hasivo交换机怎么选? 三款网管交换机系统功能对比

《TP-LINK/水星和hasivo交换机怎么选?三款网管交换机系统功能对比》今天选了三款都是”8+1″的2.5G网管交换机,分别是TP-LINK水星和hasivo交换机,该怎么选呢?这些交换机功... TP-LINK、水星和hasivo这三台交换机都是”8+1″的2.5G网管交换机,我手里的China编程has

Java向kettle8.0传递参数的方式总结

《Java向kettle8.0传递参数的方式总结》介绍了如何在Kettle中传递参数到转换和作业中,包括设置全局properties、使用TransMeta和JobMeta的parameterValu... 目录1.传递参数到转换中2.传递参数到作业中总结1.传递参数到转换中1.1. 通过设置Trans的

SpringBoot嵌套事务详解及失效解决方案

《SpringBoot嵌套事务详解及失效解决方案》在复杂的业务场景中,嵌套事务可以帮助我们更加精细地控制数据的一致性,然而,在SpringBoot中,如果嵌套事务的配置不当,可能会导致事务不生效的问题... 目录什么是嵌套事务?嵌套事务失效的原因核心问题:嵌套事务的解决方案方案一:将嵌套事务方法提取到独立类

C# Task Cancellation使用总结

《C#TaskCancellation使用总结》本文主要介绍了在使用CancellationTokenSource取消任务时的行为,以及如何使用Task的ContinueWith方法来处理任务的延... 目录C# Task Cancellation总结1、调用cancellationTokenSource.

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

hdu1254(嵌套bfs,两次bfs)

/*第一次做这种题感觉很有压力,思路还是有点混乱,总是wa,改了好多次才ac的思路:把箱子的移动当做第一层bfs,队列节点要用到当前箱子坐标(x,y),走的次数step,当前人的weizhi(man_x,man_y),要判断人能否将箱子推到某点时要嵌套第二层bfs(人的移动);代码如下:

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;