赞
踩
在前几章中,我们已经为SNLI数据集上的自然语言推断任务设计了一个基于注意力的结构。现在,我们通过微调BERT来重新审视这项任务,自然语言推断任务时一个序列级别的文本对分类问题,而微调BERT只需要一个额外的基于多层感知机的架构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ArZ3KHum-1665475659087)(images/f1.png)]
在本节中,我们将下载一个预训练好的小版本BERT,然后对其进行微调,以便在SNLI数据集上进行自然语言推断
import json
import multiprocessing
import os
import torch
from torch import nn
from d2l import torch as d2l
我们以前在WikiText-2数据集上预训练BERT(注意,原始的BERT模型是在更大的语料库上预训练的),原始的BERT模型有数以亿计的参数。在下面,我们提供了两个版本的预训练的BERT:"bert.base"与原始的BERT基础模型一样大,需要大量的计算资源才能进行微调。而"bert.small"是一个小版本,便于演示
d2l.DATA_HUB['bert.base'] = (d2l.DATA_URL + 'bert.base.torch.zip',
'225d66f04cae318b841a13d32af3acc165f253ac')
d2l.DATA_HUB['bert.small'] = (d2l.DATA_URL + 'bert.small.torch.zip',
'c72329e68a732bef0452e4b96a1c341c8910f81f')
两个预训练好的BERT模型都包含一个定义词表的“vocab.json”文件和一个预训练参数的“pretrained.params”文件。我们实现了以下load_pretrained_model函数来加载预先训练好的BERT参数
def load_pretrained_model(pretrained_model,num_hiddens,ffn_num_hiddens,num_heads,num_layers,dropout,max_len,devices):
data_dir = d2l.download_extract(pretrained_model)
# 定义空词表以加载预定义词表
vocab = d2l.Vocab()
vocab.idx_to_token = json.load(open(os.path.join(data_dir,
'vocab.json')))
vocab.token_to_idx = {token: idx for idx,token in enumerate(
vocab.idx_to_token)}
bert = d2l.BERTModel(
len(vocab),num_hiddens,norm_shape=[256],
ffn_num_input=256,ffn_num_hiddens=ffn_num_hiddens,
num_heads=4,num_layers=2,dropout=0.2,
max_len=max_len,key_size=256,query_size=256,
value_size=256,hid_in_features=256,
mlm_in_features=256,nsp_in_features=256
)
# 加载预训练BERT参数
bert.load_state_dict(torch.load(os.path.join(data_dir,'pretrained.params')))
return bert,vocab
为了便于在大多数机器上演示,我们将在本节中加载和微调经过预训练的BERT小版本(“bert.small”)。在练习中,我们将展示如何微调大得多的“bert.base”以显著提高测试精度
devices = d2l.try_all_gpus()
bert,vocab = load_pretrained_model(
'bert.small',num_hiddens=256,ffn_num_hiddens=512,num_heads=4,
num_layers=2,dropout=0.1,max_len=512,devices=devices)
对于SNLI数据集的下游任务自然语言推断,我们定义了一个定制的数据集类SNLIBERTDataset。在每个样本中,前提和假设形成一对文本序列,并被打包成一个BERT输入序列。利用预定义的BERT输入序列的最大长度(max_len),持续移除输入文本对中较长文本的最后一个标记,直到满足max_len。为了加速生成用于微调BERT的SNLI数据集,我们使用4个工作进程并行生成训练或测试样本
class SNLIBERTDataset(torch.utils.data.Dataset):
def __init__(self,dataset,max_len,vocab=None):
all_premise_hypothesis_tokens = [
[p_tokens,h_tokens] for p_tokens,h_tokens in zip(
*[d2l.tokenize([s.lower() for s in sentences]) for sentences in dataset[:2]])]
self.labels = dataset[2]
self.vocab = vocab
self.max_len = max_len
(self.all_token_ids,self.all_segments,self.valid_lens) = self._preprocess(all_premise_hypothesis_tokens)
print('read' + str(len(self.all_token_ids)) + ' examples')
def _preprocess(self,all_premise_hypothesis_tokens):
pool = multiprocessing.Pool(4) # 使用4个进程
out = pool.map(self._mp_worker,all_premise_hypothesis_tokens)
all_token_ids = [
token_ids for token_ids,segments,valid_len in out
]
all_segments = [segments for token_ids,segments,valid_len in out]
valid_lens = [valid_len for token_ids,segments,valid_len in out]
return (torch.tensor(all_token_ids,dtype=torch.long),
torch.tensor(all_segments,dtype=torch.long),
torch.tensor(valid_lens))
def _mp_worker(self,premise_hypothesis_tokens):
p_tokens,h_tokens = premise_hypothesis_tokens
self._truncate_pair_of_tokens(p_tokens,h_tokens)
tokens,segments = d2l.get_tokens_and_segments(p_tokens,h_tokens)
token_ids = self.vocab[tokens] + [self.vocab['<pad>']] * (self.max_len - len(tokens))
segments = segments + [0] * (self.max_len - len(segments))
valid_len = len(tokens)
return token_ids,segments,valid_len
def _truncate_pair_of_tokens(self,p_tokens,h_tokens):
# 为BERT输入中的'<CLS>'、'<SEP>'和'<SEP>'词元保留位置
while len(p_tokens) + len(h_tokens) > self.max_len - 3:
if len(p_tokens) > len(h_tokens):
p_tokens.pop()
else:
h_tokens.pop()
def __getitem__(self,idx):
return (self.all_token_ids[idx],self.all_segments[idx],
self.valid_lens[idx],self.labels[idx])
def __len__(self):
return len(self.all_token_ids)
下载完SNLI数据集后,我们通过实例化SNLIBERTDataset类来生成训练和测试样本,这些样本在自然语言推断的训练和测试期间进行小批量读取
import os
import re
def read_snli(data_dir,is_train):
"""将SNLI数据集解析为前提、假设和标签"""
def extract_text(s):
# 删除我们不会使用的信息
s = re.sub('\\(','',s)
s = re.sub('\\(','',s)
# 用一个空格替换两个或多个连续的空格
s = re.sub('\\s{2,}',' ',s)
return s.strip()
#label_set = {'entailment': 0, 'contradiction': 1, 'neutral': 2}
label_set = {0:'entailment',1: 'contradiction',2: 'neutral'}
file_name = os.path.join(data_dir, 'train.txt' if is_train else 'test.txt')
with open(file_name,'r') as f:
rows = [row.split('\t') for row in f.readlines()[1:]]
premises = [extract_text(row[0]) for row in rows]
hypotheses = [extract_text(row[1]) for row in rows]
labels = [label_set[int(row[2].replace('\n',''))] for row in rows]
return premises,hypotheses,labels
# 如果出现显存不足的错误,请减少“batch_size”,在原始的BERT模型中,max_len=512
batch_size,max_len,num_workers = 512,128,d2l.get_dataloader_workers()
data_dir = 'SNLI'
train_set = SNLIBERTDataset(read_snli(data_dir, True), max_len, vocab)
test_set = SNLIBERTDataset(read_snli(data_dir, False), max_len, vocab)
train_iter = torch.utils.data.DataLoader(train_set, batch_size, shuffle=True,num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(test_set, batch_size,num_workers=num_workers)
用于自然语言推断的微雕BERT只需要一个额外的多层感知机,该多层感知机由两个全连接层组成(参见下面BERTClassifier类中的self.hidden和self.output)。这个多层感知机将特殊的“<cls>”词元的BERT表示进行了转换,该词元同时编码前提和假设的信息为自然语言推断的三个输出:蕴涵、矛盾和中性
class BERTClassifier(nn.Module):
def __init__(self,bert):
super(BERTClassifier,self).__init__()
self.encoder = bert.encoder
self.hidden = bert.hidden
self.output = nn.Linear(256,3)
def forward(self,inputs):
tokens_X,segments_X,valid_lens_x = inputs
encoded_X = self.encoder(tokens_X,segments_X,valid_lens_x)
return self.output(self.hidden(encoded_X[:,0,:]))
在下文中,预训练的BERT模型bert被送到用于下游应用的BERTClassifier实例net中,在BERT微调的常见实现中,只有额外的多层感知机(net.output)的输出层的参数将从零开始学习。预训练BERT编码器(net.encoder)和额外的多层感知机的隐藏层(net.hidden)的所有参数都将进行微调
net = BERTClassifier(bert)
回想一下,MaskLM类和NextSentencePred类在其使用的多层感知机中都有一些参数,这些参数是预训练BERT模型bert中参数的一部分,因此是net中参数的一部分。然而,这些参数仅用于计算预训练过程中的遮蔽语言模型损失和下一句预测损失。这两个损失函数与微调下游应用无关,因此当BERT微调时,MaskLM和NextSentencePred中采用的多层感知机的参数不会更新(陈旧的,staled)
为了允许具有陈旧梯度的参数,标注ignore_stale_grad=Ture在step函数d2l.train_batch_ch13中被设置。我们通过该函数使用SNLI训练集(train_iter)和测试集(test_iter)对net模型进行训练和评估。由于计算资源有限,训练和测试精度可以进一步提高:我们把对它的讨论留在练习中
lr,num_epochs = 1e-4,5
trainer = torch.optim.Adam(net.parameters(),lr=lr)
loss = nn.CrossEntropyLoss(reduction='none')
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,devices)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。