在自然语言处理和知识图谱中,实体抽取、NER是一个基本任务,也是产业化应用NLP 和知识图谱的关键技术之一。BERT是一个大规模预训练模型,它通过精心设计的掩码语言模型(Masked Language Model,MLM)来模拟人类对语言的认知,并对数十亿个词所组成的语料进行预训练而形成强大的基础语义,形成了效果卓绝的模型。通过 BERT来进行实体抽取、NER的方法是当前在NLP和知识图谱的产业化应用中最常用的方法,是效果与成本权衡下的最佳选择。本文详细讲解使用BERT来进行实体抽取,看完本文就会用当前工业界最佳的模型了。
什么是实体抽取?
举例来说,有一段文本:
达观数据与同济大学联合共建的“知识图谱与语义计算联合实验室”正式揭牌成立
识别出其中的蓝色部分,并标记为“机构”类型的实体,就是实体抽取。实体抽取的过程通常可以分为是两个阶段:
识别出所有表示实体的词汇
将这些词汇分类到不同实体类型中
图2 实体抽取案例
图3 语言理解困难重重
历史上,用来进行实体抽取的方法包括基于规则的方法、机器学习方法、深度学习方法和弱监督学习方法等几大类,每类方法中都有很多种不同的算法,具体的算法内容可以参考《知识图谱:认知智能理论与实战》第三章《实体抽取》(P78~136)。
BERT介绍
BERT 是英文“Bidirectional Encoder Representations from Transformers”的缩写,是Google开发的一种基于Transformer的深度学习技术,用于人工智能领域的文本预训练。BERT 由Jacob Devlin和他在 Google 的同事于 2018 年创建,并在论文《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》中发布。在2019 年,谷歌宣布已开始在其搜索引擎中使用 BERT,到 2020 年底,它几乎在搜索查询中都使用了 BERT。在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 unresolved questions.”自从 BERT 出来以后,也引导了至今炙手可热的“大模型”浪潮。其本质就是“预训练”+“微调”:
对于普罗大众来说,人工智能的标志性事件当属AlphaGo,号称人类最难的智力游戏败于机器,可是街头巷尾的谈资。
在自然语言处理领域,BERT在当时的自然语言处理领域可谓掀起轩然大波,总结起来有:
在机器阅读理解顶级水平测试SQuAD1.1中表现出惊人成绩,首次两个衡量指标上全面超越人类,并且还在11种不同NLP测试中创出最佳成绩。
关于 SQuAD数据集和评测参考https://rajpurkar.github.io/SQuAD-explorer/,当前是2.0版本。
谷歌团队成员Thang Luong表示,BERT模型开启了NLP的新时代
证明了通过大规模语料集预训练的技术,能够大幅度提升各类文本阅读理解的效果,也因此,“大模型”自此兴起
Masked LM(见下图)通过学习masked的词,不仅让模型学会了上下文信息,还学会了语法syntax、语义semantics、语用pragmatics等,并能够很好地学会部分领域知识
预训练模型越大,效果越好;对应的,成本也会越高。相比于单任务模型来说,无监督的预训练模型成本要大1000倍以上
学术界传统上认为,在一些很难处理的文字阅读理解任务上,计算机有望能够全面超越人类
掩码语言模型的提出则来自于Google 的更早些的一篇论文《Attention Is All You Need》(下载地址:https://arxiv.org/pdf/1706.03762.pdf) 所提到的 Transformers 模型(见下图)。但 BERT 与 Transformers 略有区别,使用的是其编码器部分,这点从BERT 的论文标题也可以看出来。事实上,当前炙手可热的大模型中(如 GPT-3等),几乎都依赖于 Transformers 的模型结构,特别是其中的自注意力机制。《知识图谱:认知智能理论与实战》一书 P123~130的“BERT 模型详解”一节则对结合代码实现该BERT 的模型结构进行详细的解析,通俗易懂,非常适合阅读来深入理解BERT 模型。上述两篇论文则提供了更多的实验对比素材,深入阅读有助于了解BERT 为何会在当时引起轰动,以至于在NLP领域引领风骚数年。
此外值得一提的是,Google最初 发布的 BERT 模型有两种配置:
BERT BASE:12 个编码器,带有 12 个双向自注意力头;
BERT LARGE:24 个编码器,带有 16 个双向自注意力头。
这两种配置结构类似,Large版本要比Base版本“更大”,效果自然更好,同时使用时资源要求也更高。本文以Base版本示例,以使得在一张显卡上即可完成。换成 Large 版本不用改变任何代码,但因为网络更大,可能需要更高级的显卡或多卡来支持模型的训练。
语料准备
本文采用“MSRA实体抽取数据集”,并使用BIO标记方法来标记,数据集在GitHub 上有很多,也可以GitHub官网下载。
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
使用 BERT 来进行实体抽取的话,需要将语料进行处理,转化成列表的形式(train_data和test_data,对应于原始的train.txt 和test.txt),列表的每一个元素是一个键值对元组,键为文本列表,值为标签列表。如下图所示:
用BERT进行实体抽取
import json
from tqdm import tqdm
from transformers import BertForTokenClassification
from transformers import BertTokenizerFast
import torch
from torch import nn
import numpy as np
BERT_NAME = 'bert-base-chinese'
转化为torch能够使用的数据集
label2id = {
'O': 0,
'B-LOC': 1,
'I-LOC': 2,
'B-ORG': 3,
'I-ORG': 4,
'B-PER': 5,
'I-PER': 6
}
tokenizer = BertTokenizerFast.from_pretrained(BERT_NAME)
max_seq_len = 256
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 = len(label2id)
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)
#train_data拆分为train和dev,其中dev占20%,train占80%
train_data_count = len(train_data)
dev_data_count = int(train_data_count * 0.2)
train_data_count -= dev_data_count
print(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)
batch_size = 16
num_workers = 1
train_dataloader = torch.utils.data.DataLoader(train_dataset, num_workers=num_workers, batch_size=batch_size, shuffle=True)
dev_dataloader = torch.utils.data.DataLoader(dev_dataset, num_workers=num_workers, batch_size=batch_size, shuffle=False)
test_dataloader = torch.utils.data.DataLoader(test_dataset, num_workers=num_workers, batch_size=batch_size, shuffle=False)
构建模型
在本文中,我们将使用来自HuggingFace的预训练 BERT 基础模型。既然我们要在token级别对文本进行分类,那么我们需要使用BertForTokenClassificationclass。
BertForTokenClassificationclass是一个包装 BERT 模型并在 BERT 模型之上添加线性层的模型,将充当token级分类器。基于BertForTokenClassificationclass来创建基于 BERT 的实体抽取模型非常简单,见下面代码:
class BertModel(nn.Module):
def __init__(self, num_labels):
super(BertModel, self).__init__()
self.bert = BertForTokenClassification.from_pretrained(BERT_NAME, num_labels=num_labels)
def forward(self, input_ids, attention_mask,token_type_ids, labels):
output = self.bert(input_ids=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
model = BertModel(len(label2id)+1)
训练
我们的BERT模型的训练循环是标准的PyTorch训练循环。在训练模型中,需要使用 GPU,否则会非常耗时。GPU建议使用nvidia的显卡提供,1080ti及其更高版本皆可。
model = model.cuda()
由于BERT已经使用了大规模语料训练并得到了通用的语义表示,通常使用的话,仅需要重新微调最上面一层即可。为此,需要冻结其他不需要训练的层的参数。下面代码实现了这个逻辑:
decay_layer = [n for n, p in model.named_parameters() if 'layer.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优化器,其他优化器也可以根据需要进行选择,具体的参考PyTorch的官方文档。
lr = 0.00005
optimizer = torch.optim.AdamW(optimizer_grouped_parameters, lr=lr)
for ep in range(10):
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_dataloader):
labels = labels.to(device)
attention_mask = attention_mask.to(device)
input_ids = input_ids.to(device)
token_type_ids = token_type_ids.to(device)
optimizer.zero_grad()
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(dim=1)
label_clean = labels[i][labels[i] != unword_label_id]
acc = (preds == label_clean).float().mean()
total_acc_train += acc.item()
total_loss_train += loss.item()
train_count += 1
loss.backward()
optimizer.step()
{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
for input_ids, token_type_ids, attention_mask, labels in tqdm(dev_dataloader):
labels = labels.to(device)
attention_mask = attention_mask.to(device)
input_ids = input_ids.to(device)
token_type_ids = token_type_ids.to(device)
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(dim=1)
label_clean = labels[i][labels[i] != unword_label_id]
acc = (preds == label_clean).float().mean()
total_acc_val += acc.item()
total_loss_val += loss.item()
dev_count += 1
{total_acc_val/dev_count:.3f} Loss: {total_loss_val/dev_count:.3f}') :
通常来说,BERT+微调的模式中,仅需若干个epoch即可,比如上面示例代码中的10。在实际应用中可根据效果进行判断,酌情增减 epoch 的数量。
在测试数据上评估模型
现在我们已经训练了用于抽取实体的BERT模型。在前面数据集准备中还有测试集,这里可以使用测试集来看效果:
model.eval()
total_acc = 0
total_loss = 0
test_count = 0
all_inputs = []
all_logits = []
all_labels = []
all_masks = []
with torch.no_grad():
for input_ids, token_type_ids, attention_mask, labels in tqdm(test_dataloader):
all_inputs.append(input_ids)
all_labels.append(labels)
all_masks.append(attention_mask)
labels = labels.to(device)
attention_mask = attention_mask.to(device)
input_ids = input_ids.to(device)
token_type_ids = token_type_ids.to(device)
logits = model(input_ids, attention_mask,token_type_ids, labels)
#all_inputs.append(input_ids.to('cpu'))
logits_cpu = logits.to('cpu').detach().numpy()
all_logits.append(logits_cpu)
#all_labels.append(labels.to('cpu'))
for i in range(logits.shape[0]):
logits_clean = logits[i][labels[i] != unword_label_id]
preds = logits_clean.argmax(dim=1)
label_clean = labels[i][labels[i] != unword_label_id]
acc = (preds == label_clean).float().mean()
total_acc += acc.item()
total_loss += loss.item()
test_count += 1
Acc: {total_acc/test_count:.3f} Loss: {total_loss/test_count:.3f}')
结论
本文介绍了全面介绍了如何使用BERT进行实体抽取,并给出了代码实例。通过本文,读者可以很方便地“依瓢画葫芦”实现一个基于BERT模型的实体抽取程序,从而学会工业界中最好用的实体抽取模型,并将其应用到学习或工作当中。
具体来说,本文内容包括:
介绍了什么是实体抽取
介绍了什么是 BERT
用代码实例介绍了如何使用 BERT 进行实体抽取的完整过程,这包括四个内容:
准备数据
构建模型
训练模型
测试模型效果
作者简介