自然语言处理 Paddle NLP - 信息抽取技术及应用

重点:SOP 图、BCEWithLogitsLoss

基于预训练模型完成实体关系抽取

信息抽取旨在从非结构化自然语言文本中提取结构化知识,如实体、关系、事件等。对于给定的自然语言句子,根据预先定义的schema集合,抽取出所有满足schema约束的SPO三元组。

例如,「妻子」关系的schema定义为:

{
S_TYPE: 人物,
P: 妻子,
O_TYPE: {
@value: 人物
}
}

该示例展示了如何使用PaddleNLP快速完成实体关系抽取,参与千言信息抽取-关系抽取比赛打榜。

关系抽取介绍

针对 DuIE2.0 任务中多条、交叠SPO这一抽取目标,比赛对标准的 'BIO' 标注进行了扩展。

对于每个 token,根据其在实体span中的位置(包括B、I、O三种),我们为其打上三类标签,并且根据其所参与构建的predicate种类,将 B 标签进一步区分。给定 schema 集合,对于 N 种不同 predicate,以及头实体/尾实体两种情况,我们设计对应的共 2N 种 B 标签,再合并 I 和 O 标签,故每个 token 一共有 (2N+2) 个标签,如下图所示。

以类别为标注

评价方法

对测试集上参评系统输出的SPO结果和人工标注的SPO结果进行精准匹配,采用F1值作为评价指标。注意,对于复杂O值类型的SPO,必须所有槽位都精确匹配才认为该SPO抽取正确。针对部分文本中存在实体别名的问题,使用百度知识图谱的别名词典来辅助评测。F1值的计算方式如下:

F1 = (2 * P * R) / (P + R),其中

  • P = 测试集所有句子中预测正确的SPO个数 / 测试集所有句子中预测出的SPO个数
  • R = 测试集所有句子中预测正确的SPO个数 / 测试集所有句子中人工标注的SPO个数

Step1:构建模型

该任务可以看作一个序列标注任务,所以基线模型采用的是ERNIE序列标注模型。

PaddleNLP提供了ERNIE预训练模型常用序列标注模型,可以通过指定模型名字完成一键加载。PaddleNLP为了方便用户处理数据,内置了对于各个预训练模型对应的Tokenizer,可以完成文本token化,转token ID,文本长度截断等操作。

文本数据处理直接调用tokenizer即可输出模型所需输入数据。

├── dev_data.json
├── dev.json
├── duie.json
├── duie.json.zip
├── duie_sample
│ └── License.docx
├── id2spo.json
├── predicate2id.json # 有多少类型
├── schema.xlsx
├── test_data.json
├── test.json
├── train_data.json # 训练数据
└── train.json

{
"text":"《邪少兵王》是冰火未央写的网络小说连载于旗峰天下", # 要抽取的一段话
"spo_list":[ # 标签结果(抽多少个三元组)
{
"predicate":"作者", # 关系:作者
"object_type":{
"@value":"人物" # 尾实体,是个人物
},
"subject_type":"图书作品", # 抽首实体是个 图书作品
"object":{
"@value":"冰火未央" # 尾实体人物的值:冰火未央
},
"subject":"邪少兵王" # 图书作品的值 邪少兵王
}
]
}

import os
import sys
import json
from paddlenlp.transformers import ErnieForTokenClassification, ErnieTokenizer # 将 57 种关系标签读进来
label_map_path = os.path.join('/home/aistudio/relation_extraction/data', "predicate2id.json") if not (os.path.exists(label_map_path) and os.path.isfile(label_map_path)):
sys.exit("{} dose not exists or is not a file.".format(label_map_path))
with open(label_map_path, 'r', encoding='utf8') as fp:
label_map = json.load(fp) # 多标签分类的分类数: 57 - 2(I、O) * 2 (2种尾实体(value、inwork),所以在关系里面也要*2 两种关系)+ 2 (最后把 I、O加回来)
# 2N + 2
num_classes = (len(label_map.keys()) - 2) * 2 + 2 # 要做多标签分类问题,所以要把 num_classes 放到 pretrained 里,这边会用到 Sigmoid
model = ErnieForTokenClassification.from_pretrained("ernie-1.0", num_classes=(len(label_map) - 2) * 2 + 2)
tokenizer = ErnieTokenizer.from_pretrained("ernie-1.0") #inputs = tokenizer(text="请输入测试样例", max_seq_len=20)
inputs = tokenizer(text="《邪少兵王》是冰火未央写的网络小说连载于旗峰天下", max_seq_len=20)
inputs
1 => CLS、后面是 token id
token_type_ids 全是0,因为只有一句话
{'input_ids': [1, 56, 1686, 332, 714, 338, 55, 10, 1161, 610, 556, 946, 519, 5, 305, 742, 96, 178, 538, 2],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}

Step2:加载并处理数据

从比赛官网下载数据集,解压存放于data/目录下并重命名为train_data.json, dev_data.json, test_data.json.

我们可以加载自定义数据集。通过继承paddle.io.Dataset,自定义实现__getitem____len__两个方法。

from typing import Optional, List, Union, Dict

import numpy as np
import paddle
from tqdm import tqdm from paddlenlp.transformers import ErnieTokenizer
from paddlenlp.utils.log import logger from data_loader import parse_label, DataCollator, convert_example_to_feature
from extract_chinese_and_punct import ChineseAndPunctuationExtractor class DuIEDataset(paddle.io.Dataset):
def __init__(self, data, label_map, tokenizer, max_length=512, pad_to_max_length=False):
super(DuIEDataset, self).__init__() self.data = data
self.chn_punc_extractor = ChineseAndPunctuationExtractor()
self.tokenizer = tokenizer
self.max_seq_length = max_length
self.pad_to_max_length = pad_to_max_length
self.label_map = label_map def __len__(self):
return len(self.data) def __getitem__(self, item): example = json.loads(self.data[item])
input_feature = convert_example_to_feature(
example, self.tokenizer, self.chn_punc_extractor,
self.label_map, self.max_seq_length, self.pad_to_max_length)
return {
"input_ids": np.array(input_feature.input_ids, dtype="int64"),
"seq_lens": np.array(input_feature.seq_len, dtype="int64"),
"tok_to_orig_start_index":
np.array(input_feature.tok_to_orig_start_index, dtype="int64"),
"tok_to_orig_end_index":
np.array(input_feature.tok_to_orig_end_index, dtype="int64"),
# If model inputs is generated in `collate_fn`, delete the data type casting.
"labels": np.array(input_feature.labels, dtype="float32"),
} @classmethod
def from_file(cls,
file_path,
tokenizer,
max_length=512,
pad_to_max_length=None):
assert os.path.exists(file_path) and os.path.isfile(
file_path), f"{file_path} dose not exists or is not a file."
label_map_path = os.path.join(
os.path.dirname(file_path), "predicate2id.json")
assert os.path.exists(label_map_path) and os.path.isfile(
label_map_path
), f"{label_map_path} dose not exists or is not a file."
with open(label_map_path, 'r', encoding='utf8') as fp:
label_map = json.load(fp) with open(file_path, "r", encoding="utf-8") as fp:
data = fp.readlines()
return cls(data, label_map, tokenizer, max_length, pad_to_max_length)
data_path = 'data'
batch_size = 32
max_seq_length = 128 train_file_path = os.path.join(data_path, 'train_data.json')
train_dataset = DuIEDataset.from_file(
train_file_path, tokenizer, max_seq_length, True) # print(len(train_dataset))
# print(train_dataset[0]) train_batch_sampler = paddle.io.BatchSampler(
train_dataset, batch_size=batch_size, shuffle=True, drop_last=True)
collator = DataCollator()
train_data_loader = paddle.io.DataLoader(
dataset=train_dataset,
batch_sampler=train_batch_sampler,
collate_fn=collator) eval_file_path = os.path.join(data_path, 'dev_data.json') # 防止内存溢出,这边用了 _data 结果的试验数据,dev.json 全量数据 17W+
test_dataset = DuIEDataset.from_file(
eval_file_path, tokenizer, max_seq_length, True)
test_batch_sampler = paddle.io.BatchSampler(
test_dataset, batch_size=batch_size, shuffle=False, drop_last=True)
test_data_loader = paddle.io.DataLoader(
dataset=test_dataset,
batch_sampler=test_batch_sampler,
collate_fn=collator)

Step3:定义损失函数和优化器,开始训练

我们选择均方误差作为损失函数,使用paddle.optimizer.AdamW作为优化器。

在训练过程中,模型保存在当前目录checkpoints文件夹下。同时在训练的同时使用官方评测脚本进行评估,输出P/R/F1指标。

在验证集上F1可以达到69.42。

import paddle.nn as nn

# 多标签分类,BCEWithLogitsLoss
class BCELossForDuIE(nn.Layer):
def __init__(self, ):
super(BCELossForDuIE, self).__init__()
self.criterion = nn.BCEWithLogitsLoss(reduction='none') def forward(self, logits, labels, mask):
loss = self.criterion(logits, labels)
mask = paddle.cast(mask, 'float32') # 有的标签是PAD的,不需要计算,减少 mask 计算量
loss = loss * mask.unsqueeze(-1)
loss = paddle.sum(loss.mean(axis=2), axis=1) / paddle.sum(mask, axis=1)
loss = loss.mean()
return loss
from utils import write_prediction_results, get_precision_recall_f1, decoding

@paddle.no_grad()
def evaluate(model, criterion, data_loader, file_path, mode):
"""
mode eval:
eval on development set and compute P/R/F1, called between training.
mode predict:
eval on development / test set, then write predictions to \
predict_test.json and predict_test.json.zip \
under /home/aistudio/relation_extraction/data dir for later submission or evaluation.
"""
example_all = []
with open(file_path, "r", encoding="utf-8") as fp:
for line in fp:
example_all.append(json.loads(line))
# id2spo.json => {"predicate": ["empty", "empty", "注册资本"..}
id2spo_path = os.path.join(os.path.dirname(file_path), "id2spo.json")
with open(id2spo_path, 'r', encoding='utf8') as fp:
id2spo = json.load(fp) model.eval()
loss_all = 0
eval_steps = 0
formatted_outputs = []
current_idx = 0
for batch in tqdm(data_loader, total=len(data_loader)):
eval_steps += 1
input_ids, seq_len, tok_to_orig_start_index, tok_to_orig_end_index, labels = batch
logits = model(input_ids=input_ids)
mask = (input_ids != 0).logical_and((input_ids != 1)).logical_and((input_ids != 2))
loss = criterion(logits, labels, mask)
loss_all += loss.numpy().item()
probs = F.sigmoid(logits)
logits_batch = probs.numpy()
seq_len_batch = seq_len.numpy()
tok_to_orig_start_index_batch = tok_to_orig_start_index.numpy()
tok_to_orig_end_index_batch = tok_to_orig_end_index.numpy()
formatted_outputs.extend(decoding(example_all[current_idx: current_idx+len(logits)],
id2spo,
logits_batch,
seq_len_batch,
tok_to_orig_start_index_batch,
tok_to_orig_end_index_batch))
current_idx = current_idx+len(logits)
loss_avg = loss_all / eval_steps
print("eval loss: %f" % (loss_avg)) if mode == "predict":
predict_file_path = os.path.join("/home/aistudio/relation_extraction/data", 'predictions.json')
else:
predict_file_path = os.path.join("/home/aistudio/relation_extraction/data", 'predict_eval.json') predict_zipfile_path = write_prediction_results(formatted_outputs,
predict_file_path) if mode == "eval":
precision, recall, f1 = get_precision_recall_f1(file_path,
predict_zipfile_path)
os.system('rm {} {}'.format(predict_file_path, predict_zipfile_path))
return precision, recall, f1
elif mode != "predict":
raise Exception("wrong mode for eval func")
from paddlenlp.transformers import LinearDecayWithWarmup

learning_rate = 2e-5
num_train_epochs = 5
warmup_ratio = 0.06 criterion = 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_rate=lr_scheduler,
parameters=model.parameters(),
apply_decay_param_fun=lambda x: x in [
p.name for n, p in model.named_parameters()
if not any(nd in n for nd in ["bias", "norm"])])
# 模型参数保存路径
!mkdir checkpoints
import time
import paddle.nn.functional as F # Starts training.
global_step = 0
logging_steps = 50
save_steps = 10000
num_train_epochs = 2
output_dir = 'checkpoints'
tic_train = time.time()
model.train()
for epoch in range(num_train_epochs):
print("\n=====start training of %d epochs=====" % epoch)
tic_epoch = time.time()
for step, batch in enumerate(train_data_loader):
input_ids, seq_lens, tok_to_orig_start_index, tok_to_orig_end_index, labels = batch
logits = model(input_ids=input_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 % logging_steps == 0:
print(
"epoch: %d / %d, steps: %d / %d, loss: %f, speed: %.2f step/s"
% (epoch, num_train_epochs, step, steps_by_epoch,
loss_item, logging_steps / (time.time() - tic_train)))
tic_train = time.time() if global_step % save_steps == 0 and global_step != 0:
print("\n=====start evaluating ckpt of %d steps=====" %
global_step)
precision, recall, f1 = evaluate(
model, criterion, test_data_loader, eval_file_path, "eval")
print("precision: %.2f\t recall: %.2f\t f1: %.2f\t" %
(100 * precision, 100 * recall, 100 * f1))
print("saving checkpoing model_%d.pdparams to %s " %
(global_step, output_dir))
paddle.save(model.state_dict(),
os.path.join(output_dir,
"model_%d.pdparams" % global_step))
model.train() global_step += 1
tic_epoch = time.time() - tic_epoch
print("epoch time footprint: %d hour %d min %d sec" %
(tic_epoch // 3600, (tic_epoch % 3600) // 60, tic_epoch % 60)) # Does final evaluation.
print("\n=====start evaluating last ckpt of %d steps=====" %
global_step)
precision, recall, f1 = evaluate(model, criterion, test_data_loader,
eval_file_path, "eval")
print("precision: %.2f\t recall: %.2f\t f1: %.2f\t" %
(100 * precision, 100 * recall, 100 * f1))
paddle.save(model.state_dict(),
os.path.join(output_dir,
"model_%d.pdparams" % global_step))
print("\n=====training complete=====")
=====start training of 0 epochs=====
epoch: 0 / 2, steps: 0 / 5347, loss: 0.741724, speed: 66.93 step/s
epoch: 0 / 2, steps: 50 / 5347, loss: 0.733860, speed: 3.39 step/s
epoch: 0 / 2, steps: 100 / 5347, loss: 0.705046, speed: 3.35 step/s
epoch: 0 / 2, steps: 150 / 5347, loss: 0.633157, speed: 3.30 step/s
epoch: 0 / 2, steps: 200 / 5347, loss: 0.410678, speed: 3.24 step/s
epoch: 0 / 2, steps: 250 / 5347, loss: 0.302669, speed: 3.31 step/s
epoch: 0 / 2, steps: 300 / 5347, loss: 0.254647, speed: 3.29 step/s
epoch: 0 / 2, steps: 350 / 5347, loss: 0.224945, speed: 3.31 step/s
epoch: 0 / 2, steps: 400 / 5347, loss: 0.201895, speed: 3.26 step/s
epoch: 0 / 2, steps: 450 / 5347, loss: 0.179081, speed: 3.20 step/s
epoch: 0 / 2, steps: 500 / 5347, loss: 0.159897, speed: 3.30 step/s
......

Step4:提交预测结果

加载训练保存的模型加载后进行预测。

NOTE: 注意设置用于预测的模型参数路径。

set -eux

export CUDA_VISIBLE_DEVICES=0
export BATCH_SIZE=8
export CKPT=./checkpoints/model_624.pdparams
export DATASET_FILE=./data/test_data.json python run_duie.py \
--do_predict \
--init_checkpoint $CKPT \
--predict_data_file $DATASET_FILE \
--max_seq_length 512 \
--batch_size $BATCH_SIZE
!bash predict.sh

预测结果会被保存在data/predictions.json,data/predictions.json.zip,其格式与原数据集文件一致。

之后可以使用官方评估脚本评估训练模型在dev_data.json上的效果。如:

python re_official_evaluation.py --golden_file=dev_data.json  --predict_file=predicitons.json.zip [--alias_file alias_dict]

输出指标为Precision, Recall 和 F1,Alias file包含了合法的实体别名,最终评测的时候会使用,这里不予提供。

之后在test_data.json上预测,然后预测结果(submission.zip文件)至千言评测页面

Tricks

尝试更多的预训练模型

基线采用的预训练模型为ERNIE,PaddleNLP提供了丰富的预训练模型,如BERT,RoBERTa,Electra,XLNet等

参考预训练模型文档

如可以选择RoBERTa large中文模型优化模型效果,只需更换模型和tokenizer即可无缝衔接。

from paddlenlp.transformers import RobertaForTokenClassification, RobertaTokenizer

model = RobertaForTokenClassification.from_pretrained(
"roberta-wwm-ext-large",
num_classes=(len(label_map) - 2) * 2 + 2)
tokenizer = RobertaTokenizer.from_pretrained("roberta-wwm-ext-large")

原文

https://aistudio.baidu.com/aistudio/projectdetail/1639963?sUid=2631487&shared=1&ts=1686032358184

自然语言处理 Paddle NLP - 基于预训练模型完成实体关系抽取的更多相关文章

  1. 管正雄:基于预训练模型、智能运维的QA生成算法落地

    分享嘉宾:管正雄 阿里云 高级算法工程师 出品平台:DataFunTalk 导读:面对海量的用户问题,有限的支持人员该如何高效服务好用户?智能QA生成模型给业务带来的提效以及如何高效地构建算法服务,为 ...

  2. NLP(二十一)人物关系抽取的一次实战

      去年,笔者写过一篇文章利用关系抽取构建知识图谱的一次尝试,试图用现在的深度学习办法去做开放领域的关系抽取,但是遗憾的是,目前在开放领域的关系抽取,还没有成熟的解决方案和模型.当时的文章仅作为笔者的 ...

  3. 预训练模型——开创NLP新纪元

    预训练模型--开创NLP新纪元 论文地址 BERT相关论文列表 清华整理-预训练语言模型 awesome-bert-nlp BERT Lang Street huggingface models 论文 ...

  4. [信息抽取]基于ERNIE3.0的多对多信息抽取算法:属性关系抽取

    [信息抽取]基于ERNIE3.0的多对多信息抽取算法:属性关系抽取 实体关系,实体属性抽取是信息抽取的关键任务:实体关系抽取是指从一段文本中抽取关系三元组,实体属性抽取是指从一段文本中抽取属性三元组: ...

  5. 【转载】最强NLP预训练模型!谷歌BERT横扫11项NLP任务记录

    本文介绍了一种新的语言表征模型 BERT--来自 Transformer 的双向编码器表征.与最近的语言表征模型不同,BERT 旨在基于所有层的左.右语境来预训练深度双向表征.BERT 是首个在大批句 ...

  6. 最强中文NLP预训练模型艾尼ERNIE官方揭秘【附视频】

    “最近刚好在用ERNIE写毕业论文” “感觉还挺厉害的” “为什么叫ERNIE啊,这名字有什么深意吗?” “我想让艾尼帮我写作业” 看了上面火热的讨论,你一定很好奇“艾尼”.“ERNIE”到底是个啥? ...

  7. Paddle预训练模型应用工具PaddleHub

    Paddle预训练模型应用工具PaddleHub 本文主要介绍如何使用飞桨预训练模型管理工具PaddleHub,快速体验模型以及实现迁移学习.建议使用GPU环境运行相关程序,可以在启动环境时,如下图所 ...

  8. NLP与深度学习(五)BERT预训练模型

    1. BERT简介 Transformer架构的出现,是NLP界的一个重要的里程碑.它激发了很多基于此架构的模型,其中一个非常重要的模型就是BERT. BERT的全称是Bidirectional En ...

  9. 百度NLP预训练模型ERNIE2.0最强实操课程来袭!【附教程】

    2019年3月,百度正式发布NLP模型ERNIE,其在中文任务中全面超越BERT一度引发业界广泛关注和探讨.经过短短几个月时间,百度ERNIE再升级,发布持续学习的语义理解框架ERNIE 2.0,及基 ...

  10. 最强 NLP 预训练模型库 PyTorch-Transformers 正式开源:支持 6 个预训练框架,27 个预训练模型

    先上开源地址: https://github.com/huggingface/pytorch-transformers#quick-tour 官网: https://huggingface.co/py ...

随机推荐

  1. HTTP协议初见

    HTTP协议 四大特性 基于请求端响应 ​ 客户端发送请求,服务端才响应,服务端不会主动给客户端发送响应. 基于TCP/IP作用于应用层之上的协议 ​ 此协议属于应用层 无状态 ​ 服务端不会保存客户 ...

  2. python入门教程之二十二网络编程

    Python 提供了两个级别访问的网络服务.: 低级别的网络服务支持基本的 Socket,它提供了标准的 BSD Sockets API,可以访问底层操作系统Socket接口的全部方法. 高级别的网络 ...

  3. [Linux]CentOS7:卸载、安装Java JDK

    JDK(Java Development Kit)是Java语言的软件开发工具包,包括Java运行环境.Java开发工具.Java基础类库. JRE(Java Runtime Environment) ...

  4. 四月十六号java基础知识

    1.如果没有一个机制来限制对类中成员的访问,则很可能会造成错误的输入如果在类的成员声明前面加上修饰符private,则无法从类的外部访问到该类内部的成员,而只能被该类自身访问和修改,而不能被任何其他类 ...

  5. 介绍一下js垃圾回收机制

    JavaScript中的垃圾回收机制负责自动管理内存,回收不再使用的对象所占用的内存空间.在JavaScript中,开发者不需要显式地分配和释放内存,垃圾回收器会自动完成这些操作.以下是关于JavaS ...

  6. Redis 数据类型 Stream

    Redis 数据类型 Stream Redis 常用命令,思维导图 >>> Redis Stream 是 Redis 5.0 版本新增加的数据结构. Redis Stream 主要用 ...

  7. lua变量、数据类型、if判断条件和数据结构table以及【lua 函数】

    一.lua变量[ 全局变量和局部变量和表中的域] Lua 变量有三种类型:全局变量和局部变量和表中的域. 全局变量:默认情况下,Lua中所有的变量都是全局变量. 局部变量:使用local 显式声明在函 ...

  8. LangChain vs Semantic Kernel

    每当向他人介绍 Semantic Kernel, 会得到的第一个问题就是 Semantic Kernel 类似于LangChain吗,或者是c# 版本的LangChain吗? 为了全面而不想重复的回答 ...

  9. R读入数据

    两种方式: edit()自动生成一个红色的表格,列名会自动的放上去,不够的会显示var5,var6,var7 mydata <- data.frame( age = numeric(0), ge ...

  10. Qt第三方库QtAV--- ubuntu编译与运行

    Qt第三方库QtAV--- ubuntu编译与运行 今天又要接触这个,把一些错误或者不足的地方重新补充下!!!由于前面一段时间,项目中需要借助QtAV接口进行视频播放,特此记录下整个配置过程.整个代码 ...