自学网站建设哪些网站,中国行业数据分析网,html5 网站模版,西安西工大软件园做网站的公司[ 文章目录 ]1. 信息抽取任务是什么#xff1f;2. 基于PaddleNLP的信息抽取任务2.1 训练任务概览2.2 Predicate列表2.3 SPO列表2.4 代码解析1. 信息抽取任务是什么#xff1f;
在NLP任务中#xff0c;通常当我们拿到一段文本时#xff0c;我们希望机器去理解这段文本描述的…
[ 文章目录 ]1. 信息抽取任务是什么2. 基于PaddleNLP的信息抽取任务2.1 训练任务概览2.2 Predicate列表2.3 SPO列表2.4 代码解析1. 信息抽取任务是什么
在NLP任务中通常当我们拿到一段文本时我们希望机器去理解这段文本描述的是什么内容进而完成一些特定的任务。
例如现在有这么一句话
今日在玩家们的期待中王者荣耀终于上架了李白的新皮肤——凤求凰。
这句话中具体描述了什么事件呢人类一看就知道哦王者出了一款李白的新皮肤。
没错这句话的核心就是「王者」「出了」「李白」「新皮肤」。
至于「…在玩家们的…」这些文字大概率直接被读者自动给过滤掉了这就是人类天然存在的超强信息提取的能力。 扩展人类除了在阅读文字上有着超强的信息抽取能力在听觉上同样如此大部分人都能在多人同时说话的场景下清楚的听取想听的那个人说话的内容。同样的学者们也尝试让机器具备同样的能力Speech Separation的其中一个研究方向便是如此。 为此我们希望机器也能具备人类一样的信息抽取能力能够在一段文字中自动抽出一组一组成对的关键信息。
我们再来回顾一下刚才那句话句子中包含两个主要信息
王者出了一款新皮肤。这款新皮肤是李白的。
这两个主要信息都可以用基本的三元组的结构来表示
王者主语 - 出了谓语 - 新皮肤宾语新皮肤主语 - 属于谓语 - 李白宾语
不难看出不管多么复杂的句子和多么复杂的关系最后都是可以被细分为若干个三元组即 (Entity1,Relation,Entity2)(Entity_1, Relation, Entity_2) (Entity1,Relation,Entity2) **实体Entity**是NLP领域中的常用语用于描述某一个「东西」这个「东西」通常用于描述一种词语类别. 比如「游戏」就可以被定义为一种实体举例来说有如下句子王者荣耀和和平精英都是当下比较流行的游戏。 该句子中就包含了2个「游戏」实体王者荣耀GameEntity1和和平精英GameEntity2都是当下比较流行的游戏。 此外在信息提取InfoExtraction领域中我们需要将关系Relation前后的两个实体稍稍区分一下
关系Relation前面的实体Entity1我们称之为头实体Subject
关系Relation后面的实体Entity2我们称之为尾实体Object
这就是我们常说的SPO表示法。S-头实体O-尾实体P-Predicate即「关系Relation」更专业的叫法。 王者荣耀S−出了Predicate−凤求凰O凤求凰S−属于Predicate−李白O王者荣耀S- 出了Predicate- 凤求凰O\\ 凤求凰S- 属于Predicate- 李白O 王者荣耀S−出了Predicate−凤求凰O凤求凰S−属于Predicate−李白O 至此我们已经明白了什么是信息抽取抽取句子中的SPO结构体以及什么是SPO结构体描述实体关系的最小单位那么接下来就开始进行实战吧。
2. 基于PaddleNLP的信息抽取任务
这篇文章中将选择使用PaddleNLP作为辅助来完成信息抽取任务所用的数据集/示例代码均能在PaddleNLP官网上找到并且很方便的为我们提供了运行环境不用自己手搭环境、下载数据集。接下来的内容只是在官方资料的基础上加上一些自己的理解有兴趣小伙伴可以参考下面的官方资料
官方视频资料https://aistudio.baidu.com/aistudio/education/lessonvideo/2016982官方示例代码https://aistudio.baidu.com/aistudio/projectdetail/3190877?forkThirdPart1
2.1 训练任务概览
本次实战的数据集来自于千言数据集是一个很全面的中文开源数据集合这次我们将选取其中「信息抽取」任务相关的数据集。
我们先从训练数据集中随机挑出一条样本来看看
{text: 2013年哈密地区户籍人口约58万人, spo_list: [{predicate: 人口数量, object_type: {value: Number}, subject_type: 行政区, object: {value: 58万}, subject: 哈密}]
}分析一下这一条数据输入的是一句话「2013年哈密地区户籍人口约58万人」。
我们需要提取出该句子中全部的SPO关系1个spo_list中表示 哈密S−人口数量P−58万O哈密S- 人口数量P- 58万O 哈密S−人口数量P−58万O 其中“subject”、“predicate”、object分别代表SPO三个值
“subject_type” 和 object_type代表头/尾实体的类型这个我们将放在后面讲。
至此我们已经明白了数据集中样本长什么样我们的任务目标就是将一个句子text当中所有的SPO结构体spo_list给提取出来。
2.2 Predicate列表
在明确了提取SPO的任务目标后我们首先要做的事情就是对所有可能的Predicate进行枚举。
Predicate的本质是定义了两个实体之间的「关系」因此我们需要告诉模型模型需要提取哪些「关系」。
例如有如下句子
张裕妃顺天府涿州人父张世登母段氏
这个句子中就存在两种实体关系
张裕妃S- 父亲P1- 张世登O1张裕妃S- 母亲P2- 段氏O2
其中父亲、母亲就是两种不同的实体关系即不同的Predicate。
因此我们首先需要先对数据集进行分析枚举出所有实体之间可能存在的实体关系并记录下来。
这个「实体关系列表」就是项目文件下的 “predicate2id.json” 文件所表达的意思我们将该文件打印出来看看
{O: 0, I: 1, 注册资本: 2, 作者: 3, 所属专辑: 4, 歌手: 5,...上映时间_value: 8, 上映时间_inArea: 9,...
}这个json文件中定义了所有可能的实体关系Predicate其中O和I和BIOBeginIntermediateOther标记法中的O、I完全一致这里不再赘述除了O、I以外剩下的就是数据集中所有可能的实体关系了。
例如
「作者」就代表了一种实体关系 []《道学的病理》是2007年商务印书馆出版的图书作者是韩东育 →\rightarrow→《道学的病理》的作者是韩东育 「歌手」也代表了一种实体关系 [] 写历史作业时林俊杰的《曹操》和周杰伦的《爱在西元前》混搭绝配 →\rightarrow→《曹操》的歌手是林俊杰 注意实体关系Predicate不一定必须出现在原文本中。如「作者」出现在了第一句中但「歌手」没有出现在第二句文本中。
这里可能有同学注意到了8号9号实体关系有些奇怪同样都是「上映时间」为什么后缀不同一个是_value一个是_inArea这个我们放到下一节讲。
2.3 SPO列表
在我们了解完实体关系Predicate列表后我们已经明白数据集当中有哪些实体关系了现在我们需要关注数据集当中有哪些实体了。
还记得实体是什么吗
我们之前提到过实体关系是用来连接首、尾实体的因此这里的实体应当枚举数据集中所有存在的实体类型。
那什么是实体类型呢实体类型的实质就是对所有的实体按类别进行归类后得到的类别标签。
举例来讲 [] 杨幂出演了《绣春刀》里的北斋。 [] 胡歌出演了《琅琊榜》中的梅长苏。 这两句话中「杨幂」和「胡歌」是两个实体但这两个实体都对应同一种实体类型——演员。
实体类型是可以按照自己的需求来自己定义的这取决于要做什么任务。
这很好理解
如果我们要做一个针对影视作品的信息提取模型那我们的实体类型就可能是影视作品、演员、导演…等等。
但如果我们要做一个游戏内容的提取模型那我们的实体类型就有可能是英雄、技能、皮肤…等等。
在SPO列表中对于头实体S和尾实体O的类型进行了分开表示即分为头实体类型subject_type和尾实体类型object_type。
类型列表在项目目录下 “id2spo.json” 文件中存储我们打印该文件看看
{predicate: [empty, empty, 注册资本, 作者, 所属专辑, ...], subject_type: [empty, empty, 企业, 图书作品, 歌曲, ...], object_type: [empty, empty, Number, 人物, 音乐专辑, ...]
}可以看到该json文件下存放了三个list分别代表了SPO的对应type类型。
其中每一个列表中对应的索引是可以构成一个合法三元组的我们将相同索引的各列表中的列表组合打印出来结果如下
---------------------- S | P | O
----------------------
empty | empty | empty
empty | empty | empty
企业 | 注册资本 | Number
图书作品 | 作者 | 人物
歌曲 | 所属专辑 | 音乐专辑
歌曲 | 歌手 | 人物
行政区 | 邮政编码 | Text
影视作品 | 主演 | 人物
影视作品 | 上映时间 | Date_value
影视作品 | 上映时间 | 地点_inArea
娱乐人物 | 饰演 | 人物_value
娱乐人物 | 饰演 | 影视作品_inWork
...其中前2个empty是为了O和I标签留的因为之前定义的predicate列表中的前2个标签分别为O、I这两个标签不会起到连接首尾实体的作用因此需要置为empty。 注意看这两行
...
影视作品 | 上映时间 | Date_value
影视作品 | 上映时间 | 地点_inArea
...还记得我们在1.2.2节中提到的奇怪的问题吗为什么同一个实体关系「上映时间」会有两个不同的后缀——上映时间_value和上映时间_inArea
想必你已经在这个表中找到答案了吧
原因就是同一个「头实体S - 实体关系P- 」可能会对应多个不同的尾实体O。
举例来讲 []《大耳朵图图之美食狂想曲》的动画电影将于2017年7月28日在中国上映 这句话当中《大耳朵图图之美食狂想曲》S- 上映P- 同一个SP可以对应两个不同的O
《大耳朵图图之美食狂想曲》S- 上映P- 2017年7月28日O1《大耳朵图图之美食狂想曲》S- 上映P- 中国O2
第一个SPO代表电影-上映-上映时间
第二个SPO代表电影-上映-上映地点
由此我们可以看出对于同一个S-P句子中是可能存在多个不同的合法O的那我们就需要使用两个或多个不同的S-P来对应这些不同的O。
一种最常见的方法就是对P再进行细分尾实体O不是既有可能是上映时间也有可能是上映地点吗。那我就直接分别设定两个不同的P上映时间标签就好了即
上映时间_value用于连接实体类型为「时间」的尾实体。上映时间_inArea用于连接实体类型为「地点」的尾实体。
这就是我们对应在尾实体列表object_type中看到存在 […, Date_value, 地点_inArea, …] 在predicate2id文件中看到 […, “上映时间_value”, “上映时间_inArea”, …] 的原因。
这里顺带再提一句我们看看在训练数据集中这种关系是如何表示的
{ ... spo_list: [{predicate: 上映时间, object_type: {inArea: 地点, value: Date}, subject_type: 影视作品, object: {inArea: 中国, value: 4月14日}, subject: 垫底辣妹}...
}可以看到训练数据集中“object_type” 和 “object” 字段对应的都是一个字典可以包含多个值而SP对应的都是一个唯一的值。
这就印证了我们刚才的说法确定一组S-P可以对应多个不同的尾实体O。
但是这里和prediacte2id.json中不同训练集中的predicate的值并不包含标签后缀不是「上映时间_value」而直接就是「上映时间」。
这样将原来的predicate后缀变化到 object字段中的key中其实更符合人们的认知毕竟关系不应该存在区别区别的应该是实体类型。
2.4 代码解析
在该任务中我们将实体关系抽取问题建模为Token Classification的问题。
我们知道在命名实体识别Named Entity RecognitionNER问题中通常会用Token Classification的方式来进行任务求解即判断每个字符token属于哪个类别某个实体非实体。
在NER任务中使用字符分类的方式我们非常容易就理解但是我们怎么通过对字符分类的方式来提取出不同实体之间的关系呢
答案就是在字符类别中添加入「关系标签」即该字符是否能和这句话当中的其他字符产生关联关系。
这个想法相当简单暴力也相当符合神经网络「硬train一发」的核心原则。
模型结构图如下所示 可以看到对每个tokenlabel一共有2N 2维其中 N 为我们之前所讲的 predicate 的类别个数。
这还是比较好理解每个词既可能作为一个实体关系Predicate的头实体S也有可能作为尾实体O所以实体关系类别有N个那么一个字符总共可能的 label 数就为 2N。
那么为什么还要 2呢因为某些字符可能只是无效信息O或是词中字符I注这个标注体系中只对B-进行实体类型分类I就不用做分类了因此整体维度还要再 2。
还有一点非常重要在传统的多分类任务中我们是在N个label中选择1个类别作为最后的结果使用的是softmax corss_entropy但在这个任务中我们不使用softmax CE而是使用Binary Cross Entropy LossBCE Loss。
仔细想想这个任务和传统的多分类任务有何不同。对于每个字符来讲其只能属于某一种类别吗
举例来讲 [] 李白的皮肤叫凤求凰技能为青莲剑歌。 这句话中李白S即可以是皮肤凤求凰O的头实体也可以是技能青莲剑歌O的头实体。
即在这2个类别的对应label都应该为1。
这种可能属于多个不同标签的任务叫做多标签分类任务不同于多分类任务其对于每个标签都建立一个sigmoid函数判断其属于这个标签结果为0还是1不同于softmax只选择概率最大的一个标签。
模型搭建
该任务是基于ERNIE 1.0基于Transformer Encoder的模型大体和BERT一样预训练方式不同对中文支持更友好作为pre-trained model实现的paddle提供了很方便的高阶API能够让我们快速导入ERNIE模型。
from paddlenlp.transformers import ErnieForTokenClassification, ErnieTokenizerlabel_map_path os.path.join(data, predicate2id.json)
num_classes (len(label_map.keys()) - 2) * 2 2 # 每个token可能的类别总数model ErnieForTokenClassification.from_pretrained(ernie-1.0, num_classesnum_classes)
tokenizer ErnieTokenizer.from_pretrained(ernie-1.0)
inputs tokenizer(text这是一条测试数据, max_seq_len20)
print(inputs)model 和 tokenizer可以直接导入预训练模型model为神经网络模型tokenizer为文本预处理模型。
我们输入一条测试文本「这是一条测试数据」并打印出被tokenizer encode后的结果
{input_ids: [1, 47, 10, 7, 304, 558, 525, 179, 342, 2], token_type_ids: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}encode后结果是一个json包含 ‘input ids’ 和 ‘token_type_ids’。 input ids
文字对应的索引index文字与index的对应关系在模型预训练的时候就已经规定好了我们可以看看ERNIE模型中的文字-索引对应关系只展示前10个数据
[PAD]
[CLS]
[SEP]
[MASK]的
、
一
人
有
...
...
...可以看到ERNIE 1.0的前4个token id都对应的是功能token从第5个token id起为文字、标点对应的token id。 token_type_ids
这个列表用于表示当前token属于第几个句子。
在某些任务中例如QA问答系统任务我们的输入会是两条问题 文章甚至两条以上的句子token_type_ids用于表征当前字符token属于第几个句子从0开始表示第一个句子。 注意开始符[CLS]和句子分割符[SEP]都会归类为第一条/上一条句子。 损失函数构建 模型训练
搭建完模型后我们现在对损失函数进行设计进而进行模型训练。
在小节开头我们说明了该任务为一个多标签分类任务应使用的BCE作为Loss Function实现如下
import paddle.nn as nnclass BCELossForDuIE(nn.Layer):def __init__(self, ):super(BCELossForDuIE, self).__init__()self.criterion nn.BCEWithLogitsLoss(reductionnone)def forward(self, logits, labels, mask):loss self.criterion(logits, labels)mask paddle.cast(mask, float32)loss loss * mask.unsqueeze(-1)loss paddle.sum(loss.mean(axis2), axis1) / paddle.sum(mask, axis1)loss loss.mean()return loss其中mask0/1列表的作用为去掉一些特殊符号如[SEP][CLS]等功能符号的分类结果Loss因为这些功能符号的token分类结果无关紧要因此不计算它们的token loss用作反向传播。
接下来实例化优化器Optimizer
from paddlenlp.transformers import LinearDecayWithWarmuplearning_rate 2e-5
num_train_epochs 5
warmup_ratio 0.06criterion BCELossForDuIE()# Defines learning rate strategy.
steps_by_epoch len(train_data_loader)
num_training_steps steps_by_epoch * num_train_epochs
lr_scheduler LinearDecayWithWarmup(learning_rate, num_training_steps, warmup_ratio)
optimizer paddle.optimizer.AdamW(learning_ratelr_scheduler,parametersmodel.parameters(),apply_decay_param_funlambda x: x in [p.name for n, p in model.named_parameters()if not any(nd in n for nd in [bias, norm])]) # 只对weights做退火策略通常在进行大模型的Fine-Tune过程中我们需要使用DecayWithWarmup 策略来使得模型具备更好的训练效果。
这是因为pre-trained model通常比较大且在较为复杂的数据集上完成了训练。当我们将模型用在自己较小的数据集上进行训练时如果一开始就设置比较大的学习率可能会导致过拟合或者模型直接训坏。
为此我们需要一开始时用比较柔和的更新策略即设置较小的学习率慢慢地增大学习率等到了一段时间后在将学习率慢慢降低先WarmUp再Decay。
常见的DecayWithWarmup方式有线性Linear和余弦Cosine两种方式该项目中我们使用线性衰减的方式如下图所示 注意在warmup和decay过程中我们都只对weights进行修改而对bias和layer norm的参数都不做升温和退火处理。 最后编写模型训练函数即可
for epoch in range(num_train_epochs):for step, batch in enumerate(train_data_loader):input_ids, seq_lens, tok_to_orig_start_index, tok_to_orig_end_index, labels batchlogits model(input_idsinput_ids)mask (input_ids ! 0).logical_and((input_ids ! 1)).logical_and((input_ids ! 2))loss criterion(logits, labels, mask)loss.backward()optimizer.step()lr_scheduler.step()optimizer.clear_gradients()loss_item loss.numpy().item()if global_step % save_steps 0 and global_step ! 0:precision, recall, f1 evaluate(model, criterion, test_data_loader, eval_file_path, eval)paddle.save(model.state_dict(), os.path.join(output_dir, model_%d.pdparams % global_step))global_step 1(完整代码在这里)
以上便是信息抽取Info Extraction的全部内容。