前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >四大嵌套实体识别方法对比与总结

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

作者头像
zenRRan
发布2022-10-08 14:44:47
3.3K0
发布2022-10-08 14:44:47
举报
文章被收录于专栏:深度学习自然语言处理

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


写在前面

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

1. 嵌套实体识别

1.1 什么是嵌套实体识别

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

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

1.2 嵌套实体识别方法

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

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

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

在本文中,我们将对比目前接触到的部分实体矩阵的构建方法在 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),在其文中显示引入位置信息能给结果带来极大提升,符合预期先验。

代码语言:javascript
复制
class GlobalPointer(Module):
    """全局指针模块
    将序列的每个(start, end)作为整体来进行判断
    """
    def __init__(self, heads, head_size,hidden_size,RoPE=True):
        super(GlobalPointer, self).__init__()
        self.heads = heads
        self.head_size = head_size
        self.RoPE = RoPE
        self.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_pos
            kw2 = 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中不同的是,加性仍然能实现快速并行,需要在计算设计上加入一些技巧。

代码语言:javascript
复制
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_size
        self.c_size = c_size
        self.abPosition = abPosition
        self.rePosition = rePosition
        self.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_keys
        hij = 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在加性的基础上加入了更多信息交互元素,比如 ,(作差与点乘),但同时也提高了内存的占用量。

代码语言:javascript
复制
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_size
        self.c_size = c_size
        self.abPosition = abPosition
        self.rePosition = rePosition
        self.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_keys
        hij = 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是加性与乘性的结合在笔者复现的关系抽取任务中,双仿射确实带来的一定提升,但这种建模思路在实体识别中是否有效还有待验证

代码语言:javascript
复制
class Biaffine(Module):

    def __init__(self, in_size, out_size, Position = False):
        super(Biaffine, self).__init__()
        self.out_size = out_size
        self.weight1 = Parameter(torch.Tensor(in_size, out_size, in_size))
        self.weight2 = Parameter(torch.Tensor(2 * in_size + 1, out_size))
        self.Position = Position
        self.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->boxy
        logits_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_2
        logits = 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的性能优势。

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

总结

  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 | 重新审视区域视觉特征在基于知识的视觉问答中的作用

代码语言:javascript
复制

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

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

记得备注~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-09-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深度学习自然语言处理 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 1. 嵌套实体识别
    • 1.1 什么是嵌套实体识别
      • 1.2 嵌套实体识别方法
      • 2. 实体矩阵构建框架
        • 2.1 变量与符号约定
          • 2.2 相关配置
            • 2.3 对比方法
            • 3. 代码实现
              • 3.1 GlobalPointer
                • 3.2 TPLinker
                  • 3.3 Tencent Muti-head
                    • 3.4 Deep Biaffine
                      • 参考资料
                  • 4. 实验结果
                  • 总结
                  相关产品与服务
                  语音识别
                  腾讯云语音识别(Automatic Speech Recognition,ASR)是将语音转化成文字的PaaS产品,为企业提供精准而极具性价比的识别服务。被微信、王者荣耀、腾讯视频等大量业务使用,适用于录音质检、会议实时转写、语音输入法等多个场景。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档