当前位置:   article > 正文

越学越有趣:『手把手带你学NLP』系列项目04 ——实体关系抽取的那些事儿

nlp提取实体间的关系

点击左上方蓝字关注我们

课程简介

“手把手带你学NLP”是基于飞桨PaddleNLP的系列实战项目。本系列由百度多位资深工程师精心打造,提供了从词向量、预训练语言模型,到信息抽取、情感分析、文本问答、结构化数据问答、文本翻译、机器同传、对话系统等实践项目的全流程讲解,旨在帮助开发者更全面清晰地掌握百度飞桨框架在NLP领域的用法,并能够举一反三、灵活使用飞桨框架和PaddleNLP进行NLP深度学习实践。

从6月7日起,百度飞桨 & 自然语言处理部携手推出了12节NLP精品课,课程中会介绍到这里的实践项目。

课程报名请戳:

https://aistudio.baidu.com/aistudio/course/introduce/24177

欢迎来课程QQ群(群号:758287592)交流吧~~

1. 背景介绍

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

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

  1. {
  2.   S_TYPE: 人物,
  3.   P: 妻子,
  4.   O_TYPE: {
  5.     @value: 人物
  6.   }
  7. }

针对 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个数

本示例展示了以ERNIE(Enhanced Representation through Knowledge Integration)为代表的预训练模型如何Finetune完成关系抽取任务。

记得给PaddleNLP点个小小的Star

开源不易,希望大家多多支持~

GitHub地址:

https://github.com/PaddlePaddle/PaddleNLP

AI Studio平台后续会默认安装PaddleNLP最新版,在此之前可使用如下命令更新安装。

!pip install --upgrade paddlenlp -i https://pypi.org/simple

2. 快速实践

2.1 构建模型

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

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

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

  1. import os
  2. import json
  3. from paddlenlp.transformers import ErnieForTokenClassification, ErnieTokenizer
  4. label_map_path = os.path.join('data'"predicate2id.json")
  5. if not (os.path.exists(label_map_path) and os.path.isfile(label_map_path)):
  6. sys.exit("{} dose not exists or is not a file.".format(label_map_path))
  7. with open(label_map_path, 'r', encoding='utf8') as fp:
  8.     label_map = json.load(fp)  
  9. num_classes = (len(label_map.keys()) - 2) * 2 + 2
  10. model=ErnieForTokenClassification.from_pretrained("ernie-1.0",num_classes=(len(label_map) - 2) * 2 + 2)
  11. tokenizer = ErnieTokenizer.from_pretrained("ernie-1.0")
  12. inputs = tokenizer(text="请输入测试样例", max_seq_len=20)

2.2加载并处理数据

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

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

  1. from typing import Optional, List, Union, Dict
  2. import numpy as npimport paddlefrom tqdm import tqdmfrom paddlenlp.utils.log import logger
  3. from data_loader import parse_label, DataCollator, convert_example_to_featurefrom extract_chinese_and_punct import ChineseAndPunctuationExtractor
  4. class DuIEDataset(paddle.io.Dataset):
  5.     """
  6.     Dataset of DuIE.
  7.     """
  8.     def __init__(
  9.             self,
  10.             input_ids: List[Union[List[int], np.ndarray]],
  11.             seq_lens: List[Union[List[int], np.ndarray]],
  12.             tok_to_orig_start_index: List[Union[List[int], np.ndarray]],
  13.             tok_to_orig_end_index: List[Union[List[int], np.ndarray]],
  14.             labels: List[Union[List[int], np.ndarray, List[str], List[Dict]]]):
  15.         super(DuIEDataset, self).__init__()
  16.         self.input_ids = input_ids
  17.         self.seq_lens = seq_lens
  18.         self.tok_to_orig_start_index = tok_to_orig_start_index
  19.         self.tok_to_orig_end_index = tok_to_orig_end_index
  20.         self.labels = labels
  21.     def __len__(self):
  22.         if isinstance(self.input_ids, np.ndarray):
  23.             return self.input_ids.shape[0]
  24.         else:
  25.             return len(self.input_ids)
  26.     def __getitem__(self, item):
  27.         return {
  28.             "input_ids": np.array(self.input_ids[item]),
  29.             "seq_lens": np.array(self.seq_lens[item]),
  30.             "tok_to_orig_start_index":
  31.             np.array(self.tok_to_orig_start_index[item]),
  32.             "tok_to_orig_end_index": np.array(self.tok_to_orig_end_index[item]),
  33.             # If model inputs is generated in `collate_fn`delete the data type casting.
  34.             "labels": np.array(
  35.                 self.labels[item], dtype=np.float32),
  36.         }
  37.     @classmethod
  38.     def from_file(cls,
  39.                   file_path: Union[str, os.PathLike],
  40.                   tokenizer: ErnieTokenizer,
  41.                   max_length: Optional[int]=512,
  42.                   pad_to_max_length: Optional[bool]=None):
  43.         assert os.path.exists(file_path) and os.path.isfile(
  44.             file_path), f"{file_path} dose not exists or is not a file."
  45.         label_map_path = os.path.join(
  46.             os.path.dirname(file_path), "predicate2id.json")
  47.         assert os.path.exists(label_map_path) and os.path.isfile(
  48.             label_map_path
  49.         ), f"{label_map_path} dose not exists or is not a file."
  50.         with open(label_map_path, 'r', encoding='utf8') as fp:
  51.             label_map = json.load(fp)
  52.         chineseandpunctuationextractor = ChineseAndPunctuationExtractor()
  53.         input_ids, seq_lens, tok_to_orig_start_index, tok_to_orig_end_index, labels = (
  54.             [] for _ in range(5))
  55.         dataset_scale = sum(1 for line in open(file_path, 'r'))
  56.         logger.info("Preprocessing data, loaded from %s" % file_path)
  57.         with open(file_path, "r", encoding="utf-8") as fp:
  58.             lines = fp.readlines()
  59.             for line in tqdm(lines):
  60.                 example = json.loads(line)
  61.                 input_feature = convert_example_to_feature(
  62.                     example, tokenizer, chineseandpunctuationextractor,
  63.                     label_map, max_length, pad_to_max_length)
  64.                 input_ids.append(input_feature.input_ids)
  65.                 seq_lens.append(input_feature.seq_len)
  66.                 tok_to_orig_start_index.append(
  67.                     input_feature.tok_to_orig_start_index)
  68.                 tok_to_orig_end_index.append(
  69.                     input_feature.tok_to_orig_end_index)
  70.                 labels.append(input_feature.labels)
  71.         return cls(input_ids, seq_lens, tok_to_orig_start_index,
  72.                    tok_to_orig_end_index, labels)
  1. data_path = 'data'
  2. batch_size = 32
  3. max_seq_length = 128
  4. train_file_path = os.path.join(data_path, 'train_data.json')
  5. train_dataset = DuIEDataset.from_file(
  6.     train_file_path, tokenizer, max_seq_length, True)
  7. train_batch_sampler = paddle.io.BatchSampler(
  8.     train_dataset, batch_size=batch_size, shuffle=True, drop_last=True)
  9. collator = DataCollator()
  10. train_data_loader = paddle.io.DataLoader(
  11.     dataset=train_dataset,
  12.     batch_sampler=train_batch_sampler,
  13.     collate_fn=collator)
  14. eval_file_path = os.path.join(data_path, 'dev_data.json')
  15. test_dataset = DuIEDataset.from_file(
  16.     eval_file_path, tokenizer, max_seq_length, True)
  17. test_batch_sampler = paddle.io.BatchSampler(
  18.     test_dataset, batch_size=batch_size, shuffle=False, drop_last=True)
  19. test_data_loader = paddle.io.DataLoader(
  20.     dataset=test_dataset,
  21.     batch_sampler=test_batch_sampler,
  22.     collate_fn=collator)

2.3定义损失函数和优化器

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

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

  1. import paddle.nn as nn
  2. class BCELossForDuIE(nn.Layer):
  3.     def __init__(self, ):
  4.         super(BCELossForDuIE, self).__init__()
  5.         self.criterion = nn.BCEWithLogitsLoss(reduction='none')
  6.     def forward(self, logits, labels, mask):
  7.         loss = self.criterion(logits, labels)
  8.         mask = paddle.cast(mask, 'float32')
  9.         loss = loss * mask.unsqueeze(-1)
  10.         loss = paddle.sum(loss.mean(axis=2), axis=1) / paddle.sum(mask, axis=1)
  11.         loss = loss.mean()
  12.         return loss
  1. from utils import write_prediction_results, get_precision_recall_f1, decoding
  2. @paddle.no_grad()def evaluate(model, criterion, data_loader, file_path, mode):
  3.     """
  4.     mode eval:
  5.     eval on development set and compute P/R/F1, called between training.
  6.     mode predict:
  7.     eval on development / test set, then write predictions to \
  8.         predict_test.json and predict_test.json.zip \
  9.         under /home/aistudio/relation_extraction/data dir for later submission or evaluation.
  10.     """
  11.     example_all = []
  12.     with open(file_path, "r", encoding="utf-8") as fp:
  13.         for line in fp:
  14.             example_all.append(json.loads(line))
  15.     id2spo_path = os.path.join(os.path.dirname(file_path), "id2spo.json")
  16.     with open(id2spo_path, 'r', encoding='utf8') as fp:
  17.         id2spo = json.load(fp)
  18.     model.eval()
  19.     loss_all = 0
  20.     eval_steps = 0
  21.     formatted_outputs = []
  22.     current_idx = 0
  23.     for batch in tqdm(data_loader, total=len(data_loader)):
  24.         eval_steps += 1
  25.         input_ids, seq_len, tok_to_orig_start_index, tok_to_orig_end_index, labels = batch
  26.         logits = model(input_ids=input_ids)
  27.         mask = (input_ids != 0).logical_and((input_ids != 1)).logical_and((input_ids != 2))
  28.         loss = criterion(logits, labels, mask)
  29.         loss_all += loss.numpy().item()
  30.         probs = F.sigmoid(logits)
  31.         logits_batch = probs.numpy()
  32.         seq_len_batch = seq_len.numpy()
  33.         tok_to_orig_start_index_batch = tok_to_orig_start_index.numpy()
  34.         tok_to_orig_end_index_batch = tok_to_orig_end_index.numpy()
  35.         formatted_outputs.extend(decoding(example_all[current_idx: current_idx+len(logits)],
  36.                                           id2spo,
  37.                                           logits_batch,
  38.                                           seq_len_batch,
  39.                                           tok_to_orig_start_index_batch,
  40.                                           tok_to_orig_end_index_batch))
  41.         current_idx = current_idx+len(logits)
  42.     loss_avg = loss_all / eval_steps
  43.     print("eval loss: %f" % (loss_avg))
  44.     if mode == "predict":
  45.         predict_file_path = os.path.join("/home/aistudio/relation_extraction/data"'predictions.json')
  46.     else:
  47.         predict_file_path = os.path.join("/home/aistudio/relation_extraction/data"'predict_eval.json')
  48.     predict_zipfile_path = write_prediction_results(formatted_outputs,
  49.                                                     predict_file_path)
  50.     if mode == "eval":
  51.         precision, recall, f1 = get_precision_recall_f1(file_path,
  52.                                                         predict_zipfile_path)
  53.         os.system('rm {} {}'.format(predict_file_path, predict_zipfile_path))
  54.         return precision, recall, f1
  55.     elif mode != "predict":
  56.         raise Exception("wrong mode for eval func")
  1. from paddlenlp.transformers import LinearDecayWithWarmup
  2. learning_rate = 2e-5
  3. num_train_epochs = 5
  4. warmup_ratio = 0.06
  5. criterion = BCELossForDuIE()# Defines learning rate strategy.
  6. steps_by_epoch = len(train_data_loader)
  7. num_training_steps = steps_by_epoch * num_train_epochs
  8. lr_scheduler = LinearDecayWithWarmup(learning_rate, num_training_steps, warmup_ratio)
  9. optimizer = paddle.optimizer.AdamW(
  10.     learning_rate=lr_scheduler,
  11.     parameters=model.parameters(),
  12.     apply_decay_param_fun=lambda x: x in [
  13.         p.name for n, p in model.named_parameters()
  14.         if not any(nd in n for nd in ["bias""norm"])])

2.4 模型训练与评估

模型训练的过程通常有以下步骤:

  1. 从dataloader中取出一个batch data。

  2. 将batch data喂给model,做前向计算。

  3. 将前向计算结果传给损失函数,计算loss。将前向计算结果传给评价方法,计算评价指标。

  4. loss反向回传,更新梯度。重复以上步骤。

每训练一个epoch时,程序将会评估一次,评估当前模型训练的效果。

  1. import time
  2. import paddle.nn.functional as F
  3. # Starts training.
  4. global_step = 0
  5. logging_steps = 50
  6. save_steps = 10000
  7. num_train_epochs = 2
  8. output_dir = 'checkpoints'
  9. tic_train = time.time()
  10. model.train()
  11. for epoch in range(num_train_epochs):
  12.     print("\n=====start training of %d epochs=====" % epoch)
  13.     tic_epoch = time.time()
  14.     for step, batch in enumerate(train_data_loader):
  15.         input_ids, seq_lens, tok_to_orig_start_index, tok_to_orig_end_index, labels = batch
  16.         logits = model(input_ids=input_ids)
  17.         mask = (input_ids != 0).logical_and((input_ids != 1)).logical_and(
  18.             (input_ids != 2))
  19.         loss = criterion(logits, labels, mask)
  20.         loss.backward()
  21.         optimizer.step()
  22.         lr_scheduler.step()
  23.         optimizer.clear_gradients()
  24.         loss_item = loss.numpy().item()
  25.         if global_step % logging_steps == 0:
  26.             print(
  27.                 "epoch: %d / %d, steps: %d / %d, loss: %f, speed: %.2f step/s"
  28.                 % (epoch, num_train_epochs, step, steps_by_epoch,
  29.                     loss_item, logging_steps / (time.time() - tic_train)))
  30.             tic_train = time.time()
  31.         if global_step % save_steps == 0 and global_step != 0:
  32.             print("\n=====start evaluating ckpt of %d steps=====" %
  33.                     global_step)
  34.             precision, recall, f1 = evaluate(
  35.                 model, criterion, test_data_loader, eval_file_path, "eval")
  36.             print("precision: %.2f\t recall: %.2f\t f1: %.2f\t" %
  37.                     (100 * precision, 100 * recall, 100 * f1))
  38.             print("saving checkpoing model_%d.pdparams to %s " %
  39.                     (global_step, output_dir))
  40.             paddle.save(model.state_dict(),
  41.                         os.path.join(output_dir, 
  42.                                         "model_%d.pdparams" % global_step))
  43.             model.train()
  44.         global_step += 1
  45.     tic_epoch = time.time() - tic_epoch
  46.     print("epoch time footprint: %d hour %d min %d sec" %
  47.             (tic_epoch // 3600, (tic_epoch % 3600) // 60, tic_epoch % 60))
  48. # Does final evaluation.
  49. print("\n=====start evaluating last ckpt of %d steps=====" %
  50.         global_step)
  51. precision, recall, f1 = evaluate(model, criterion, test_data_loader,
  52.                                     eval_file_path, "eval")
  53. print("precision: %.2f\t recall: %.2f\t f1: %.2f\t" %
  54.         (100 * precision, 100 * recall, 100 * f1))
  55. paddle.save(model.state_dict(),
  56.             os.path.join(output_dir,
  57.                             "model_%d.pdparams" % global_step))
  58. print("\n=====training complete=====")
  59. =====start training of 0 epochs=====
  60. epoch: 0 / 2, steps: 0 / 312, loss: 0.724156, speed: 110.16 step/s
  61. epoch: 0 / 2, steps: 50 / 312, loss: 0.487328, speed: 4.28 step/s
  62. ·········
  63. eval loss: 0.027972
  64. precision: 0.00     recall: 0.00    f1: 0.00   
  65. =====training complete=====

2.5 模型预测

训练保存好的模型,即可用于预测。

!bash predict.sh

模型预测详情请参见项目『基于预训练模型完成实体关系抽取』:

https://aistudio.baidu.com/aistudio/projectdetail/1639963

2.6 尝试更多的预训练模型

基线采用的预训练模型为ERNIE,PaddleNLP提供了丰富的预训练模型,如BERT,RoBERTa,Electra,XLNet等,请参考预训练模型文档。 如可以选择RoBERTa large中文预训练模型优化模型效果,只需更换模型和tokenizer即可无缝衔接。

  1. from paddlenlp.transformers import RobertaForTokenClassification, RobertaTokenizer
  2. model = RobertaForTokenClassification.from_pretrained(
  3.     "roberta-wwm-ext-large",
  4.     num_classes=(len(label_map) - 2) * 2 + 2)
  5. tokenizer = RobertaTokenizer.from_pretrained("roberta-wwm-ext-large")

加入交流群,一起学习吧

如果你在学习过程中遇到任何问题或疑问,欢迎加入PaddleNLP的QQ技术交流群!

动手试一试

是不是觉得很有趣呀。小编强烈建议初学者参考上面的代码亲手敲一遍,因为只有这样,才能加深你对代码的理解呦。

本次项目对应的代码:

https://aistudio.baidu.com/aistudio/projectdetail/1639963

除此之外, PaddleNLP提供了多种预训练模型,可一键调用,来更换一下预训练模型试试吧:

https://paddlenlp.readthedocs.io/zh/latest/model_zoo/transformers.html

更多PaddleNLP信息,欢迎访问GitHub点star收藏后体验:

https://github.com/PaddlePaddle/PaddleNLP

回顾往期

越学越有趣:『手把手带你学NLP』系列项目01 ——词向量应用的那些事儿

越学越有趣:『手把手带你学NLP』系列项目02 ——语义相似度计算的那些事儿

越学越有趣:『手把手带你学NLP』系列项目03 ——快递单信息抽取的那些事儿

飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,集深度学习核心训练和推理框架、基础模型库、端到端开发套件和丰富的工具组件于一体,是中国首个自主研发、功能完备、开源开放的产业级深度学习平台。飞桨企业版针对企业级需求增强了相应特性,包含零门槛AI开发平台EasyDL和全功能AI开发平台BML。EasyDL主要面向中小企业,提供零门槛、预置丰富网络和模型、便捷高效的开发平台;BML是为大型企业提供的功能全面、可灵活定制和被深度集成的开发平台。

END

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号