查看原文
其他

飞桨代码实例详解用BERT进行实体抽取【珠峰书 知识图谱 命名实体识别 NER】

走向未来 走向未来 2023-08-08

在知识图谱中,实体抽取是一个基本任务,在珠峰书《知识图谱:认知智能理论与实战》第3章(P78~136)详细介绍了各种实体抽取的方法,本文介绍基于 BERT的实体抽取方法。此前的文章可关注如下公众号进入历史记录阅读:



BERT


BERT 是英文“Bidirectional Encoder Representations from Transformers”的缩写,是一个大规模预训练模型,它通过对数十亿个词所组成的语料进行预训练而形成强大的基础语义,同时通过精心设计的掩码语言模型(Masked Language Model,MLM)来模拟人类对语言的认知,形成了效果卓绝的模型。在2020 年的一篇论文《A Primer in BERTology: What We Know About How BERT Works》中提到:


In a little over a year, BERT has become a ubiquitous baseline in NLP experiments and inspired numerous studies analyzing the model and proposing various improvements. The stream of papers seems to be accelerating rather than slowing down, and we hope that this survey helps the community to focus on the biggest unresolvedquestions.


自从 BERT 出来以后,也引导了至今炙手可热的“大模型”浪潮。其本质就是“预训练”+“微调”的模式,而 这一切都是从BERT开始的而。


关于 BERT模型的详细解析可阅读珠峰书《知识图谱:认知智能理论与实战》 P123~130的“BERT 模型详解”部分内容。在书中对 BERT 模型的每个细节都给予了详细的解析,包括多头注意力机制、掩码语言模型、位置嵌入、片段嵌入和词元嵌入,以及大规模语料的预训练。同时,书中 使用了transformers程序包来示例。不过本文为了与前面几篇介绍实体抽取的方法保持一致,则采用了 paddle 来实现,使用了 paddlenlp 程序包来示例。


顺带值得一提的是,对于普罗大众来说,人工智能的标志性事件当属 AlphaGo,号称人类最难的智力游戏败于机器,可是街头巷尾的谈资。但在自然语言处理领域,BERT,在当时的自然语言处理领域可谓掀起轩然大波,总结起来有:

  • 在机器阅读理解顶级水平测试SQuAD1.1中表现出惊人成绩,首次两个衡量指标上全面超越人类,并且还在11种不同NLP测试中创出最佳成绩

  • 谷歌团队成员Thang Luong表示,BERT模型开启了NLP的新时代

  • 证明了通过大规模语料集预训练的技术,能够大幅度提升各类文本阅读理解的效果,也因此,“大模型”自此兴起

  • Masked LM通过学习masked的词,不仅让模型学会了上下文信息,还学会了语法syntax、语义semantics、语用pragmatics等,并能够很好的学会部分领域知识。

  • 预训练模型越大,效果越好;对应的,成本也会越高。相比于单任务模型来说,无监督的预训练模型成本要大1000倍以上

  • 学术界传统上认为很难处理的一些文字阅读理解任务上,计算机有望能够全面超越人类



语料准备


本文采用“MSRA实体抽取数据集”,并使用 BIO 标记方法来标记,数据集在 github 上有很多,也可以从【https://github.com/wgwang/kg-book/tree/main/datasets/NER-MSRA】下载。MSRA数据集中,实体类型有三种:


  • LOC:地点类型

  • ORG:机构类型

  • PER:人物类型


一个例子为:



1 O、 O中 B-ORG国 I-ORG作 I-ORG协 I-ORG和 O现 B-LOC代 I-LOC文 I-LOC学 I-LOC馆 I-LOC负 O责 O人 O在 O巴 B-PER金 I-PER家 O中 O介 O绍 O文 B-LOC学 I-LOC馆 I-LOC新 O馆 O设 O计 O模 O型 O。O


从上述例子可以看出,

  • 中国作协是组织机构(ORG)类型:

oB-ORG标签,因为它是一个机构实体的开始

oI-ORG标签,因为它是一个机构实体的延续

  •  “现代文学馆”和“文学馆”是地点(LOC) 类型:

oB-LOC标签,因为它是地点实体的开始

oI-LOC 标签:因为它是地点实体的延续

  • “巴金”是任务(PER) 类型:

oB-PER标签,因为它是人物实体的开始

oI-PER 标签:因为它是人物实体的延续

  • 其他词被分配O标签,因为它们不是任何实体



读入数据



#载入数据
def read_data(filename): '''读入训练语料,每个句子使用 list 存储 格式为适合crf++的格式: 每行格式为 token\t标签 空行表示句子结束 @param filename: 语料文件名 ''' data = []
sent = [] lbl = [] with open(filename) as f: for line in f: line = line.strip() if not line: data.append((sent, lbl)) sent = [] lbl = [] continue c, t = line.split("\t") sent.append(c) lbl.append(t) if sent: data.append((sent, lbl)) return data
train_data = read_data('./msra/train.txt')print('train: ', len(train_data))test_data = read_data('./msra/test.txt')print('test: ', len(test_data))




Paddle 的 Dataset


将数据集转化为 Paddle 的 Dataset 格式,方便后续给模型使用

paddle.io.Dataset 是Paddle数据集的抽象类,需要实现如下两个方法:

  • __getitem__: 根据给定索引获取数据集中指定样本,在 paddle.io.DataLoader 中需要使用此函数通过下标获取样本。

  • __len__: 返回数据集样本个数, paddle.io.BatchSampler 中需要样本个数生成下标序列。

参考paddle 官方文档说明:

https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/io/Dataset_cn.html



class TheDataset(paddle.io.Dataset): def __init__(self, data): self.data = data
def __getitem__(self, idx): input_ids, token_type_ids, attention_mask, labels = self.data[idx] return input_ids, token_type_ids, attention_mask, labels
def __len__(self): return len(self.data)



转化为 Paddle 的 Dataset


这里使用了 BERT 模型,输入的文本要使用BertTokenizer来将其转化为 id

在本例中使用了谷歌原始发布的 BERT 中文基础版模型bert-base-chinese,其基本信息如下:

  • 12-layer, 768-hidden, 12-heads, 108M parameters.

  • Trained on cased Chinese Simplified and Traditional text.


BERT_NAME = 'bert-base-chinese'max_seq_len = 256


将文本转化为用于训练和测试的 dataset


# 初始化tokenizertokenizer = BertTokenizer.from_pretrained(BERT_NAME)print(tokenizer.convert_tokens_to_ids('中'))for i in ['[CLS]', '[SEP]', '[PAD]']: print(i, tokenizer.convert_tokens_to_ids(i))# 转化为datasetlabel2id = { 'O': 0, 'B-LOC': 1, 'I-LOC': 2, 'B-ORG': 3, 'I-ORG': 4, 'B-PER': 5, 'I-PER': 6}
pad_id = tokenizer.convert_tokens_to_ids('[PAD]')cls_id = tokenizer.convert_tokens_to_ids('[CLS]')sep_id = tokenizer.convert_tokens_to_ids('[SEP]')unk_id = tokenizer.convert_tokens_to_ids('[UNK]')unword_label_id = label2id['O']
def trans2TheDataset(data, max_seq_len): data2 = []
for k, v in data: k = [tokenizer.convert_tokens_to_ids(i) for i in k] v = [label2id[i] for i in v]        input_ids = [cls_id] + k labels = [unword_label_id] + v
if len(input_ids) > max_seq_len-1: input_ids = input_ids[:max_seq_len-1] labels = labels[:max_seq_len-1]        input_ids.append(sep_id) labels.append(unword_label_id)
        klen = len(input_ids)           if klen < max_seq_len: pad_len = max_seq_len-klen
input_ids = input_ids + [pad_id] * pad_len labels = labels + [unword_label_id] * pad_len
token_type_ids = [0] * max_seq_len attention_mask = [1] * klen + [0] * pad_len      data2.append((np.asarray(input_ids, dtype='int64'), np.asarray(token_type_ids, dtype='int64'), np.asarray(attention_mask, dtype='int64'), np.asarray(labels, dtype='int64'))) return TheDataset(data2)



创建数据集


由于 msra 数据集仅提供了 train 和 test,没有 dev 数据集。这里将 train 进行二八划分为 dev 和 train 两个数据集。

另外,在输入中需要对过长的输入句子进行截断,这里设置max_seq_len为255,在实际应用中可根据情况取值。


# train_data 拆分为 train 和 devtrain_data_count = len(train_data)dev_data_count = int(train_data_count * 0.2)train_data_count -= dev_data_countprint(train_data_count, dev_data_count)dev_data = train_data[:dev_data_count]train_data = train_data[dev_data_count:]print(len(train_data), len(dev_data))
train_dataset = trans2TheDataset(train_data, max_seq_len)dev_dataset = trans2TheDataset(dev_data, max_seq_len)test_dataset = trans2TheDataset(test_data, max_seq_len)
print(len(train_dataset), len(dev_dataset), len(test_dataset))


构建支持获取微批数据的 Dataloader


  • DataLoader返回一个迭代器,该迭代器根据 batch_sampler 给定的顺序迭代一次给定的 dataset

  • DataLoader支持单进程和多进程的数据加载方式,当 num_workers 大于0时,将使用多进程方式异步加载数据。

参考paddle 官方文档说明:

https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/io/DataLoader_cn.html



batch_size = 8# 加载数据train_loader = paddle.io.DataLoader(train_dataset, shuffle=True, batch_size=batch_size, drop_last=True)dev_loader = paddle.io.DataLoader(dev_dataset, shuffle=False, batch_size=batch_size, drop_last=False)test_loader = paddle.io.DataLoader(test_dataset, shuffle=False, batch_size=batch_size, drop_last=False)


构建BERT实体抽取模型


在《知识图谱:认知智能理论与实战》一书中,对实体的定义为:


实体(Entity):是指一种独立的、拥有清晰特征的、能够区别于其他事物的事物。在信息抽取、自然语言处理和知识图谱等领域,用来描述这些事物的信息即实体。实体可以是抽象的或者具体的。

                    ——王文广 《知识图谱:认知智能理论与实战》 P81

这是对1996年MUC-6会议对命名实体的扩展。MUC组委会在当时提出的“命名实体”任务要求从文本中识别出所有的人物名称(人名)、组织机构名称(机构名)和地理位置名称(地名),以及时间、货币和百分数的表述。如果仅仅识别人名、地名、机构名等实体的话,常见的分词库(如 jieba、HanLP、LAC 等)都支持的,可以直接使用这些库来识别,效果通常还不错。

而如果要在产业应用中进行实体抽取,仅仅能够处理这几个命名实体则远远不够。比如书名的识别、建筑物名称的识别、汽车品牌的识别、汽车零部件的识别等等。


在实践中,实体不一定是对物理事物的表述,也可以是对虚拟事物的表述。比如“经济指标”类型的实体“CPI”、人物或者组织机构发表的“观点”类型的实体、某个领域权威人物发表的“言论”类型的实体,在制造业质量和可靠性工程中的“失效事件”类型的实体,以及在各类机械与电子电器设备制造领域中的“性能”类型的实体等。

                    ——王文广 《知识图谱:认知智能理论与实战》 P81

实体抽取(命名实体识别)就是从一段文本中抽取出符合要求的实体,常见的实体抽取方法非常多,在《知识图谱:认知智能理论与实战》的第三章介绍了主流的几种实体抽取方法。下面的模型来自于该书(珠峰书)3.5.3节《预训练模型用于实体抽取》,详细内容参考珠峰书《知识图谱:认知智能理论与实战》一书P122~133。

值得注意的是,本文使用飞桨框架,而书中的代码示例则使用了 pytorch。

关于 BERT 的模型详解,可参考珠峰书的3.5.3节《预训练模型用于实体抽取》P122~133。书中非常详细地解析了 BERT 的结构,包括掩码语言模型、多头注意力机制、位置嵌入、片段嵌入和词元嵌入等。

顺带值得一提的是,对于普罗大众来说,人工智能的标志性事件当属 AlphaGo,号称人类最难的智力游戏败于机器,可是街头巷尾的谈资。但在自然语言处理领域,BERT,在当时的自然语言处理领域可谓掀起轩然大波,总结起来有:

在机器阅读理解顶级水平测试SQuAD1.1中表现出惊人成绩,首次两个衡量指标上全面超越人类,并且还在11种不同NLP测试中创出最佳成绩

  • 谷歌团队成员Thang Luong表示,BERT模型开启了NLP的新时代

  • 证明了通过大规模语料集预训练的技术,能够大幅度提升各类文本阅读理解的效果,也因此,“大模型”自此兴起

  • Masked LM通过学习masked的词,不仅让模型学会了上下文信息,还学会了语法syntax、语义semantics、语用pragmatics等,并能够很好的学会部分领域知识。

  • 预训练模型越大,效果越好;对应的,成本也会越高。相比于单任务模型来说,无监督的预训练模型成本要大1000倍以上

  • 学术界传统上认为很难处理的一些文字阅读理解任务上,计算机有望能够全面超越人类


关于BertForTokenClassification可从参考:

https://paddlenlp.readthedocs.io/zh/latest/source/paddlenlp.transformers.bert.modeling.html

# BERT 实体抽取模型class EEModel(nn.Layer): """用于实体抽取(命名实体识别)的BERT模型"""

def __init__(self, num_labels): """ @param num_labels: 标签数量 """ super(EEModel, self).__init__() self.num_labels = num_labels # 嵌入层 self.bert = BertForTokenClassification.from_pretrained(BERT_NAME, num_classes=num_labels) def forward(self, input_ids, attention_mask,token_type_ids, labels): output = self.bert(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids, labels=labels, output_hidden_states=False, output_attentions=False, return_dict=False) return output


实例化模型


num_labels = max([v for k, v in label2id.items()]) + 1print(num_labels)
model = EEModel(num_labels)


模型训练准备


由于BERT 已经使用了大规模语料训练并得到了通用的语义表示,通常使用的话,仅需要重新微调最上面一层即可。为此,需要冻结其他不需要训练的层的参数。

decay_layer = [n for n, p in model.named_parameters() if 'layers.11' in n ]optimizer_grouped_parameters = [ {'params': [p for n, p in model.named_parameters() if any(nd in n for nd in decay_layer)], 'weight_decay': 0.01}, {'params': [p for n, p in model.named_parameters() if not any(nd in n for nd in decay_layer)], 'weight_decay': 0.0}]


使用 AdamW 优化器


lr = 0.0001optimizer = paddle.optimizer.AdamW(learning_rate=lr, parameters=optimizer_grouped_parameters)


模型训练


这里使用前述语料训练30个 epoch,训练的方法是最简单的方法

在训练模型中,需要使用 gpu 来,否则会非常耗时。GPU 建议使用1080ti 及其更高版本皆可。


for ep in range(30): total_acc_train = 0 total_loss_train = 0 train_count = 0
model.train() for input_ids, token_type_ids, attention_mask, labels in tqdm(train_loader): optimizer.clear_grad() loss, logits = model(input_ids, attention_mask,token_type_ids, labels) for i in range(logits.shape[0]): logits_clean = logits[i][labels[i] != unword_label_id] preds = logits_clean.argmax(axis=1) label_clean = labels[i][labels[i] != unword_label_id] acc = (preds == label_clean).cast('float').mean() total_acc_train += acc.item() total_loss_train += loss.item() train_count += 1 loss.backward() optimizer.step() print(f'Ep {ep} Train Acc: {total_acc_train/train_count:.3f} Loss: {total_loss_train/train_count:.3f}') model.eval() total_acc_val = 0 total_loss_val = 0 dev_count = 0
with paddle.no_grad(): for input_ids, token_type_ids, attention_mask, labels in tqdm(dev_loader):            loss, logits = model(input_ids, attention_mask,token_type_ids, labels) for i in range(logits.shape[0]): logits_clean = logits[i][labels[i] != unword_label_id] preds = logits_clean.argmax(axis=1) label_clean = labels[i][labels[i] != unword_label_id] acc = (preds == label_clean).cast('float').mean() total_acc_val += acc.item() total_loss_val += loss.item() dev_count += 1 print(f'Ep {ep} Eval Acc: {total_acc_val/dev_count:.3f} Loss: {total_loss_val/dev_count:.3f}')


使用测试集评估效果


在文章中(https://mp.weixin.qq.com/s/STS8N1PBML_2BvkO5NfiXg ),详细介绍了模型效果的评估,有两种方法:

  • 基于词元的效果评估

  • 基于实体的效果评估


这里采用基于实体的效果评估,详情见:实体抽取:如何评估算法的效果?


使用模型预测测试集的标签


model.eval() total_acc = 0total_loss = 0test_count = 0
all_inputs = []all_logits = []all_labels = []all_masks = []
with paddle.no_grad():    for input_ids, token_type_ids, attention_mask, labels in tqdm(test_loader): all_inputs.append(input_ids) all_labels.append(labels) all_masks.append(attention_mask) loss, logits = model(input_ids, attention_mask,token_type_ids, labels) logits_cpu = logits.detach().numpy() all_logits.append(logits_cpu)
for i in range(logits.shape[0]): logits_clean = logits[i][labels[i] != unword_label_id] preds = logits_clean.argmax(axis=1) label_clean = labels[i][labels[i] != unword_label_id] acc = (preds == label_clean).cast('float').mean() total_acc += acc.item() total_loss += loss.item() test_count += 1
print(f'Test Acc: {total_acc/test_count:.3f} Loss: {total_loss/test_count:.3f}')


提取实体


从结果中提取实体的方法,相见文章:

https://mp.weixin.qq.com/s/STS8N1PBML_2BvkO5NfiXg

def entities_of_sentence(sent, labels, seq_len=None, sentid=-1): '''适用于BIO标记方法'''
if type(sent) == str: sent = sent.split() if type(labels) == str: labels = labels.split() if seq_len is None: seq_len = len(sent) entities = [] tokens_of_entity = [] type_of_entity = None idx = 0 while idx < seq_len: label = labels[idx] word = sent[idx] idx += 1 if label == 'O': continue if label.startswith('B'): # print(tokens_of_entity, type_of_entity) if tokens_of_entity: entities.append((sentid, idx, ''.join(tokens_of_entity), type_of_entity)) tokens_of_entity = [word] # B-type, 比如B-ORG表示ORG类型 type_of_entity = label[2:] continue if label.startswith('I'): # I-type, 比如I-ORG表示ORG类型 if label[2:] != type_of_entity: # B-type 和 I-type不同,说明抽取结果有误 # 删除该抽取结果 tokens_of_entity = [] type_of_entity = None else: tokens_of_entity.append(word) if tokens_of_entity: entities.append((sentid, idx, ''.join(tokens_of_entity), type_of_entity)) return entities
id2label = {v:k for k, v in label2id.items()}
all_ent_pred = []all_ent_label = []sent_count = 0for input_ids, masks, logits, labels in zip(all_inputs, all_masks, all_logits, all_labels): seq_lens = masks.sum(1).numpy() preds = logits.argmax(axis=2) labels = labels.numpy() for sent, seq_len, pred, label in zip(input_ids,seq_lens, preds, labels): sent_count += 1 sent = tokenizer.convert_ids_to_tokens(sent)[:seq_len] pred = [id2label[i] for i in pred[:seq_len]] label = [id2label[i] for i in label[:seq_len]] all_ent_pred.extend(entities_of_sentence(sent, pred, sentid=sent_count)) all_ent_label.extend(entities_of_sentence(sent, label, sentid=sent_count))


计算准确率、精确率、召回率和 F1分数


ee_pred = all_ent_predee_gt = all_ent_label
y = len(ee_pred)intersect = len(set(ee_gt).intersection(set(ee_pred)))y_hat = len(ee_gt)union = len(set(ee_gt).union(set(ee_pred)))
print(y, y_hat, intersect, union)
p = intersect/y*100r = intersect/y_hat*100f1 = 2*p*r/(p+r)
print('精确率: p=', p, sep='')print('召回率: r=', r, sep='')print('F1分数: F1=', f1, sep='')print('准确率: acc=', intersect/union*100, sep='')


计算每个类别的 F1


ee_pred = {}ee_gt = {}for sentid, idx, ent, tent in all_ent_pred: if tent not in ee_pred: ee_pred[tent] = [] ee_pred[tent].append((sentid, idx, ent))for sentid, idx, ent, tent in all_ent_label: if tent not in ee_gt: ee_gt[tent] = [] ee_gt[tent].append((sentid, idx, ent))
for cate in ee_gt.keys(): y = set(ee_gt[cate]) y_hat = set(ee_pred[cate]) y_i = y.intersection(y_hat) p, r, f1 = 0, 0, 0 if y_i: p = len(y_i) / len(y) r = len(y_i) / len(y_hat) f1 = 2 * (p * r) / (p + r) print(cate, 'F1分数为:', f1*100)



结论


本文配合珠峰书《知识图谱》全面介绍了如何使用 BERT 进行实体抽取,并给出了飞桨框架的代码实例。通过本文,读者可以很方便地“依瓢画葫芦”实现一个基于 BERT 模型的实体抽取程序。


参考文献


  • Ashish Vaswani etc., Attention Is All You Need, arxiv:1706.03762, 2017

  • Jacob Devlin etc., BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding, arxiv:1810.04805, 2018

  • Anna Rogers etc., A Primer in BERTology: What We Know About How BERT Works, arxiv:2002.12327 2020

  • 王文广, 知识图谱:认知智能理论与实战, 电子工业出版社, 2022

  • Paddle官方文档:https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/index_cn.html

  • PaddleNLP 官方文档:https://paddlenlp.readthedocs.io/zh/latest/index.html

  • 本文代码及数据可从https://github.com/wgwang/kg-book上获取


历史回顾

  1. 实体抽取:基于词典匹配的方法【珠峰书《知识图谱:认知智能理论与实战》配套】

  2. 实体抽取:使用CRF++(1)

  3. 代码实例详解用BiLSTM-CRF模型进行实体抽取【珠峰书 知识图谱 深度学习 NER】

  4. 实体抽取:如何评估算法的效果?

  5. 知识图谱:企业获得持续竞争优势的关键技术


 

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存