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

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

相关文章

Java反转字符串的五种方法总结

《Java反转字符串的五种方法总结》:本文主要介绍五种在Java中反转字符串的方法,包括使用StringBuilder的reverse()方法、字符数组、自定义StringBuilder方法、直接... 目录前言方法一:使用StringBuilder的reverse()方法方法二:使用字符数组方法三:使用自

Golang中拼接字符串的6种方式性能对比

《Golang中拼接字符串的6种方式性能对比》golang的string类型是不可修改的,对于拼接字符串来说,本质上还是创建一个新的对象将数据放进去,主要有6种拼接方式,下面小编就来为大家详细讲讲吧... 目录拼接方式介绍性能对比测试代码测试结果源码分析golang的string类型是不可修改的,对于拼接字

Python依赖库的几种离线安装方法总结

《Python依赖库的几种离线安装方法总结》:本文主要介绍如何在Python中使用pip工具进行依赖库的安装和管理,包括如何导出和导入依赖包列表、如何下载和安装单个或多个库包及其依赖,以及如何指定... 目录前言一、如何copy一个python环境二、如何下载一个包及其依赖并安装三、如何导出requirem

Java嵌套for循环优化方案分享

《Java嵌套for循环优化方案分享》介绍了Java中嵌套for循环的优化方法,包括减少循环次数、合并循环、使用更高效的数据结构、并行处理、预处理和缓存、算法优化、尽量减少对象创建以及本地变量优化,通... 目录Java 嵌套 for 循环优化方案1. 减少循环次数2. 合并循环3. 使用更高效的数据结构4

MySQL表锁、页面锁和行锁的作用及其优缺点对比分析

《MySQL表锁、页面锁和行锁的作用及其优缺点对比分析》MySQL中的表锁、页面锁和行锁各有特点,适用于不同的场景,表锁锁定整个表,适用于批量操作和MyISAM存储引擎,页面锁锁定数据页,适用于旧版本... 目录1. 表锁(Table Lock)2. 页面锁(Page Lock)3. 行锁(Row Lock

Rust格式化输出方式总结

《Rust格式化输出方式总结》Rust提供了强大的格式化输出功能,通过std::fmt模块和相关的宏来实现,主要的输出宏包括println!和format!,它们支持多种格式化占位符,如{}、{:?}... 目录Rust格式化输出方式基本的格式化输出格式化占位符Format 特性总结Rust格式化输出方式

Python使用Pandas对比两列数据取最大值的五种方法

《Python使用Pandas对比两列数据取最大值的五种方法》本文主要介绍使用Pandas对比两列数据取最大值的五种方法,包括使用max方法、apply方法结合lambda函数、函数、clip方法、w... 目录引言一、使用max方法二、使用apply方法结合lambda函数三、使用np.maximum函数

Python中连接不同数据库的方法总结

《Python中连接不同数据库的方法总结》在数据驱动的现代应用开发中,Python凭借其丰富的库和强大的生态系统,成为连接各种数据库的理想编程语言,下面我们就来看看如何使用Python实现连接常用的几... 目录一、连接mysql数据库二、连接PostgreSQL数据库三、连接SQLite数据库四、连接Mo

Git提交代码详细流程及问题总结

《Git提交代码详细流程及问题总结》:本文主要介绍Git的三大分区,分别是工作区、暂存区和版本库,并详细描述了提交、推送、拉取代码和合并分支的流程,文中通过代码介绍的非常详解,需要的朋友可以参考下... 目录1.git 三大分区2.Git提交、推送、拉取代码、合并分支详细流程3.问题总结4.git push

Kubernetes常用命令大全近期总结

《Kubernetes常用命令大全近期总结》Kubernetes是用于大规模部署和管理这些容器的开源软件-在希腊语中,这个词还有“舵手”或“飞行员”的意思,使用Kubernetes(有时被称为“... 目录前言Kubernetes 的工作原理为什么要使用 Kubernetes?Kubernetes常用命令总