栗友们,大家好!
上次说写一篇关于SpERT模型的代码部分介绍,但是发现属实是太简单了。决定还是写一下NER的方法论吧
之前在面试时候经常被问到关于NER的几个问题:你了解NER么?NER怎么做?NER有哪些难点?
说到NER,我相信各位栗友都能够说上一点,然而要把它讲清楚还是需要一定的知识储备,今天就来做一个小小的总结
NER是NLP领域内一个常见的任务——命名实体识别(Named Entity Recognition)英文缩写,通俗来讲就是把一段文本中的存在的属于预先定义好的类别中的短语识别出来。
讲起来有点拗口,举个栗子:
六只栗子都喜欢周杰伦、张学友的音乐和电影。
预先定义的实体类别有:【人物、艺术、地点、国籍】
这个时候实体识别结果为:
人物:六只栗子、周杰伦、张学友
艺术:周杰伦的音乐、周杰伦的电影、张学友的音乐、张学友的电影
熟悉NER的栗友可能会发现这短短的一句话里面包含了 嵌套实体(实体张学友的电影里面嵌套了实体张学友)和非连续实体(周杰伦的音乐这一实体是不连续的),嵌套实体和非连续实体是命名实体识别的难点,其实我觉得嵌套实体还好,非连续实体才是让人头疼。
命名实体识别的任务讲完了,接下来就讲一下面试的重点——方法论
01
—
一元标注
BIO方法的本质就是使用序列标注的思想来做命名实体识别,而序列标注的方案一般都是使用[B、I、O]或者[B、I、O、E、S]来表示每个标签的类别。
-
BIO:B 即 begin ,表示实体开始的字符,I 即 inside,表示为实体的一部分,O 即 outside,表示不是实体字符。 -
BIOES:E 即 end,表示实体字符的结束,S 即 single, 表示单个字为实体。
当然表示方式多种多样,也有将标签扩充成更多类别的标注方案,但是本质上都是序列标注的思想,来预测每个token的类别,这里就不展开了。BIO方法应该是在做NER任务的入门方法,因为它十分简单,直接预测每个token的类别来达到识别实体的目的,一般的做法是通过模型去编码token的上下文的语义信息来达到对这个token进行预测,早期的做法是用bi-lstm去编码文本,然后接softmax进行分类,随着预训练模型的快速发展,现在的BIO方法的baseline一般都是 BERT + CRF + softmax,CRF(条件随机场)主要是对那些预测不合理的token类别进行一个纠正,比如说BIOES标注方案中的某个token被预测为B标签,其后面的token就不能被预测为S标签了。
02
—
指针标注
上面的标注方案被称为一元标注,简单实用,在简单的数据集上可以取得较高的识别效果,但是由于在NER任务中会存在大量的嵌套实体和非连续实体,这就导致一段文本中的某个token可能有多个身份标签,比如说:
张学友的音乐 中的“友”它既是人物实体“张学友”中的最后一个token,也是艺术类别实体“张学友的音乐”中的第三个token,这就导致在标注时候出现问题,上面这种标注方案并不能涵盖两个实体。
怎么办呢?
这个时候就出现了多头标注,一个token可能存在多个标注方案,那就叠加多层标注,每一层代表一种实体类型的标注方案,如下图所示:
这样一看确实是解决办法,上面有两层,分别表示 呼吸中枢和呼吸中枢受累两个实体,不过总有一种头痛医头,脚痛医脚的感觉,问题是解决了,但是一点都不优雅,可以看到它只需要在最后预测每个token的边界标签是“0”还是“1”就可以了,但是每一层都有大量“0”,整个标签的空间十分稀疏,训练的收敛速度会非常慢,尤其是识别那些长实体的效果很差。
03
—
多头标注
对于上面的标签矩阵稀疏问题,又有聪明的学者想到了一个方案,如下图:
这里构建了一个标签矩阵,只使用一个二维矩阵就可以来容纳所有的标签,如上图中的第一行第四列的数字为1,表示文本中第一个到第四个字组成的片段——呼吸中枢属于实体类别1——部位,其他的数字也是一样的表示方式。
说实话,我第一眼看到这种方案的时候,感觉确实比层叠式的标注方案优雅多了,模型的重点就是如何构建强有力的分类矩阵,实现的方案也比较简单,将bert的输出扩展一个维度,构建表,最后连接一层全连接输出。相关代码如下:
class myModel(nn.Module):
def _forward_unimplemented(self, *input: Any) -> None:
pass
def __init__(self, pre_train_dir: str, dropout_rate: float):
super().__init__()
self.roberta_encoder = BertModel.from_pretrained(pre_train_dir)
self.roberta_encoder.resize_token_embeddings(len(tokenizer))
self.lstm=torch.nn.LSTM(input_size=768,hidden_size=768,
num_layers=1,batch_first=True,
dropout=0.5,bidirectional=True)
self.logits_layer=torch.nn.Linear(in_features=4*768, out_features=num_label)
def forward(self, input_ids, input_mask, input_seg, is_training=False):
bert_output = self.roberta_encoder(input_ids=input_ids,
attention_mask=input_mask,
token_type_ids=input_seg)
encoder_rep = bert_output[0]
encoder_rep,_ = self.lstm(encoder_rep)
batch_size, seq_len, hid_size = encoder_rep.size()
start_extend = encoder_rep.unsqueeze(2).expand(-1, -1, seq_len, -1)
# end_extend = encoder_rep.unsqueeze(1).expand(-1, seq_len, -1, -1)
end_extend = torch.transpose(start_extend,dim0=1,dim1=2)
span_matrix = torch.cat([start_extend, end_extend], 3)
span_logits = self.logits_layer(span_matrix)
span_prob = torch.nn.functional.softmax(span_logits, dim=-1)
if is_training:
return span_logits
else:
return span_prob
最后,总结一下~
把命名实体识别任务当成序列标注任务来做,确实感觉有点简单直接,不过我感觉在处理那些嵌套实体的时候还是显得有点繁琐,不过在那些比赛的NER任务上或者在实际工作中,BIO的表现确实是很不错,虽然我不太喜欢它,但是它的效果确实好。
但是NER任务并不只是序列标注这一种方案,那些“狡猾”的面试官肯定还会在你说完之后来一句“还有其他的方案么?”
与BIO党打的难舍难分的NER方案正是在下——Span党,多年来孰优孰劣也一直没有一个确定的说法,在各种数据集的SOTA上也是“你方唱罢我登场”的状态,甚至还涌现了把NER任务当作MRC(机器阅读理解)任务的做法。
下一篇将介绍下基于Span的NER做法~
关注六只栗子,面试不迷路!
作者 Zarc
编辑 一口栗子
原文始发于微信公众号(六只栗子):BIO or Span?谁才是NER任务的天花板——BIO篇
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/88414.html