1. 微调BERT

2. 句子分类

3. 命名实体识别

4. 问题回答

5. 总结

  • 即使下游任务各有不同,使用BERT微调时只需要增加输出层
  • 但根据任务的不同,输入的表示,和使用的BERT特征也会不一样

6. 自然语言推理数据集

斯坦福自然语言推断语料库(Stanford Natural Language Inference,SNLI)]是由500000多个带标签的英语句子对组成的集合 。我们在路径../data/snli_1.0中下载并存储提取的SNLI数据集。

import os
import re
import torch
from torch import nn
from d2l import torch as d2ld2l.DATA_HUB['SNLI'] = ('https://nlp.stanford.edu/projects/snli/snli_1.0.zip','9fcde07509c7e87ec61c640c1b2753d9041758e4')data_dir = d2l.download_extract('SNLI')

6.1 读取数据集

原始的SNLI数据集包含的信息比我们在实验中真正需要的信息丰富得多。因此,我们定义函数read_snli以仅提取数据集的一部分,然后返回前提、假设及其标签的列表。

def read_snli(data_dir, is_train):
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}file_name = os.path.join(data_dir, 'snli_1.0_train.txt'if is_train else 'snli_1.0_test.txt')with open(file_name, 'r') as f:# rows是一个list,其中包含多个list,这每个list就是由多个字符串组成,如:# rows[i]:['contradiction',# '( ( ( A person ) ( on ( a horse ) ) ) ( ( jumps ( over ( a ( broken ( down airplane ) ) ) ) ) . ) )',# '( ( A person ) ( ( is ( ( training ( his horse ) ) ( for ( a competition ) ) ) ) . ) )',]# 第一个字符串会指示属于哪个标签rows = [row.split('\t') for row in f.readlines()[1:]]# 第2个字符串表示premises(前提)premises = [extract_text(row[1]) for row in rows if row[0] in label_set]# 第3个字符串表示hypotheses(假设)hypotheses = [extract_text(row[2]) for row in rows if row[0] \in label_set]# 第一个字符串表示label(标签),再通过label_set得到具体的数字labels = [label_set[row[0]] for row in rows if row[0] in label_set]return premises, hypotheses, labels

现在让我们打印前3对前提和假设,以及它们的标签(“0”“1”和“2”分别对应于“蕴涵”“矛盾”和“中性”)。

train_data = read_snli(data_dir, is_train=True)
# train_data[0]表示premises,[:3]表示premises前三个
for x0, x1, y in zip(train_data[0][:3], train_data[1][:3], train_data[2][:3]):print('前提:', x0)print('假设:', x1)print('标签:', y)

运行结果:

训练集约有550000对,测试集约有10000对。下面显示了训练集和测试集中的三个标签“蕴涵”“矛盾”和“中性”是平衡的。

test_data = read_snli(data_dir, is_train=False)
for data in [train_data, test_data]:# data[2]是label这一数组,row就取到每一个label,一个label也能代表一行# 计算训练集和测试集中所有的label的0,1,2的数量print([[row for row in data[2]].count(i) for i in range(3)])

运行结果:

6.2 定义用于加载数据集的类

下面我们来定义一个用于加载SNLI数据集的类。类构造函数中的变量num_steps指定文本序列的长度,使得每个小批量序列将具有相同的形状。换句话说,在较长序列中的前num_steps个标记之后的标记被截断,而特殊标记“< pad>”将被附加到较短的序列后,直到它们的长度变为num_steps。通过实现__getitem__功能,我们可以任意访问带有索引idx的前提、假设和标签。

class SNLIDataset(torch.utils.data.Dataset):"""用于加载SNLI数据集的自定义数据集"""def __init__(self, dataset, num_steps, vocab=None):self.num_steps = num_steps# dataset[0]是premises数组(由很多字符串组成),每个字符串就是一个句子# 每个句子词元化后就是一个列表,premises数组就变成了一个二维数组(包含了很多词元list)# 词元化后的列表是如:['person','on','a','horse','jumps','over','a','broken','down','airplane']# 所以,all_premise_tokens就是一个二维listall_premise_tokens = d2l.tokenize(dataset[0])# dataset[1]是hypothesis数组,进行词元化# 同理,all_hypothesis_tokens是一个包含了很多个list的大list,# 每个小list是每个句子的词元列表all_hypothesis_tokens = d2l.tokenize(dataset[1])if vocab is None:# 如果没有vocab,就对假设和前提进行构建词表self.vocab = d2l.Vocab(all_premise_tokens + \all_hypothesis_tokens, min_freq=5, reserved_tokens=['<pad>'])else:# 有的话就用给的词表self.vocab = vocab# 对premises中的所有句子和hypotheses中的进行pad处理self.premises = self._pad(all_premise_tokens)self.hypotheses = self._pad(all_hypothesis_tokens)# labels就是dataset[2]self.labels = torch.tensor(dataset[2])# len(self.premises) 其实就是全文有多少个这样的'假设、前提、标签'组成的句子对print('read ' + str(len(self.premises)) + ' examples')# 对句子长度不够的进行pad,长度够的进行截断def _pad(self, lines):return torch.tensor([d2l.truncate_pad(self.vocab[line], self.num_steps, self.vocab['<pad>'])for line in lines])def __getitem__(self, idx):# 通过id,得到对应的premises、hypotheses、labelsreturn (self.premises[idx], self.hypotheses[idx]), self.labels[idx]def __len__(self):return len(self.premises)

6.3 整合代码

现在,我们可以调用read_snli函数和SNLIDataset类来下载SNLI数据集,并返回训练集和测试集的DataLoader实例,以及训练集的词表。值得注意的是,我们必须使用从训练集构造的词表作为测试集的词表。因此,在训练集中训练的模型将不知道来自测试集的任何新词元。

在pytorch中,提供了一种十分方便的数据读取机制,即,使用torch.utils.data.Dataset与torch.utils.data.DataLoader组合得到数据迭代器。在每次训练时,利用这个迭代器输出每一个batch数据,并能在输出时对数据进行相应的预处理或数据增强等操作。

Pytorch构建数据集——torch.utils.data.Dataset()和torch.utils.data.DataLoader()

def load_data_snli(batch_size, num_steps=50):"""下载SNLI数据集并返回数据迭代器和词表"""# 获取工作线程数量num_workers = d2l.get_dataloader_workers()data_dir = d2l.download_extract('SNLI')# 读取所有的训练数据和测试数据train_data = read_snli(data_dir, True)test_data = read_snli(data_dir, False)# 得到自定义的训练数据集和测试数据集,这是为了之后能得到数据迭代器train_set = SNLIDataset(train_data, num_steps)test_set = SNLIDataset(test_data, num_steps, train_set.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,shuffle=False,num_workers=num_workers)return train_iter, test_iter, train_set.vocab

在这里,我们将批量大小设置为128时,将序列长度设置为50,并调用load_data_snli函数来获取数据迭代器和词表。然后我们打印词表大小。

train_iter, test_iter, vocab = load_data_snli(128, 50)
len(vocab)

运行结果:

现在我们打印第一个小批量的形状。与情感分析相反,我们有分别代表前提和假设的两个输入X[0]和X[1]。

for X, Y in train_iter:# 打印一下X[0]的前两个批量,也就是训练数据集的前两个premisesprint(X[0][:2])print(X[0].shape)print(X[1].shape)print(Y.shape)break

运行结果:

7. BERT微调代码实现

在本章的前面几节中,我们已经为SNLI数据集上的自然语言推断任务设计了一个基于注意力的结构。现在,我们通过微调BERT来重新审视这项任务。正如在 finetuning-bert中讨论的那样,自然语言推断是一个序列级别的文本对分类问题,而微调BERT只需要一个额外的基于多层感知机的架构,如 图所示。

本节将下载一个预训练好的小版本的BERT,然后对其进行微调,以便在SNLI数据集上进行自然语言推断。

import json
import multiprocessing
import os
import torch
from torch import nn
from d2l import torch as d2l

7.1 加载预训练的BERT

我们已经在 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参数。

torch.load_state_dict()函数的用法总结

神经网络load_state_dict()进阶使用

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()# type(vocab.idx_to_token) = <class 'list'># 'vocab.json'是预训练好的bert模型包含的vocab.idx_to_token = json.load(open(os.path.join(data_dir,'vocab.json')))# 定义词表的token映射到idx的词典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参数# 'pretrained.params'是预训练好的bert模型包含的# 在Pytorch中构建好一个模型后,一般需要进行预训练权重中加载,# torch.load_state_dict()函数就是用于将预训练的参数权重加载到新的模型之中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)

7.2 微调BERT的数据集

对于SNLI数据集的下游任务自然语言推断,我们定义了一个定制的数据集类SNLIBERTDataset。在每个样本中,前提和假设形成一对文本序列,并被打包成一个BERT输入序列。

片段索引用于区分BERT输入序列中的前提和假设。利用预定义的BERT输入序列的最大长度(max_len),持续移除输入文本对中较长文本的最后一个标记,直到满足max_len。为了加速生成用于微调BERT的SNLI数据集,我们使用4个工作进程并行生成训练或测试样本。

Python进程池pool使用方法以及map函数用法

class SNLIBERTDataset(torch.utils.data.Dataset):def __init__(self, dataset, max_len, vocab=None):# dataset[:2]取到 premise 数组和 hypothesis数组# for sentences in dataset[:2]:取到这两个数组中的每一句话# s.lower() for s in sentences:把每一句话的每一个单词全部用小写表示# 再把这些单词进行词元化,也就是进行过滤出现次数少的单词等等处理,# 最后premise 数组和hypothesis数组中的每一个句子都是用一个字符串列表来表示# 再把这两个数组中的句子(两个序列)一一对应,打包放入 all_premise_hypothesis_tokensall_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]])]# print(all_premise_hypothesis_tokens[0])的形状如下:# [ ['a', 'person', 'on', 'a', 'horse', 'jumps', 'over', 'a', 'broken', 'down', 'airplane', '.'], # ['a', 'person', 'is', 'training', 'his', 'horse', 'for', 'a', 'competition', '.'] ]# 可以看出all_premise_hypothesis_tokens[i]是一个大的list,包含两个词元列表,从而可以得知:# all_premise_hypothesis_tokens是一个更大的list,包含多个 由两个词元list的大list# 第一个词元列表是presmise,第二个词元列表是hypothesis# 标签self.labels = torch.tensor(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个进程# pool.map()函数,有两个参数可以传,第一个参数传的是函数,第二个参数传的是数据列表。# 用4个进程处理all_premise_hypothesis_tokens,# 返回的out是由多个元组组成的list,每个元组的元素是:(token_ids, segments, valid_len)out = pool.map(self._mp_worker, all_premise_hypothesis_tokens)# print(type(out))  <class 'list'># print(out[0]):( [长为maxz_len] , [长为maxz_len], 一个数字),分别举例如下:# [3, 12, 844, 17, 12, 1710, 12168, 66, 12,1,1,1,1,1,1],# [1, 1, 1, 1, 1, 1, 1, 1, 1,0,0,0,0,0,0],# 9# 获取到每个句子的token_idsall_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):# 因为用了多进程来处理,all_premise_hypothesis_tokens,# 所以每个进程处理一个premise_hypothesis_tokens,# premise_hypothesis_tokens的形状如下:# [ ['a', 'person', 'on', 'a', 'horse', 'jumps', 'over', 'a', 'broken', 'down', 'airplane', '.'], # ['a', 'person', 'is', 'training', 'his', 'horse', 'for', 'a', 'competition', '.'] ]# 所以p_tokens是premises中所有的词元数组,h_tokens是hypothesis中所有的词元数组p_tokens, h_tokens = premise_hypothesis_tokens# 减小所有的 每两个词元列表 的长度大于 max_len-3的列表长度,使得两个句子拼接后长度为 max_len-3# 当然有些 两个词元列表相加长度很小的就不做处理,留到之后的代码语句进行pad# 得到新的p_tokens和h_tokensself._truncate_pair_of_tokens(p_tokens, h_tokens)# tokens[i]:两个词元列表用特殊词元拼接后的字符串列表# segments[i]:与tokens[i]对应的长度一样的向量(由连续的0和1组成)tokens, segments = d2l.get_tokens_and_segments(p_tokens, h_tokens)# 找到每个token对应的id,对于长度小的序列,如果长度不够max_len,用pad去补token_ids = self.vocab[tokens] + [self.vocab['<pad>']] \* (self.max_len - len(tokens))# 同样,也对segments进行padsegments = segments + [0] * (self.max_len - len(segments))# 有效长度valid_len = len(tokens)# 返回的是一个元组,(句子中每个词元对应的id,segments,以及有效长度)return token_ids, segments, valid_lendef _truncate_pair_of_tokens(self, p_tokens, h_tokens):# 为BERT输入中的'<CLS>'、'<SEP>'和'<SEP>'词元保留位置while len(p_tokens) + len(h_tokens) > self.max_len - 3:# 如果两个序列长度加上'<CLS>'、'<SEP>'和'<SEP>' 这3个长度大于max_len,# 则对两个序列不断移除最后的字符串,如可以移除'.','competition'等# ps:先从长度更大的移除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类来生成训练和测试样本。这些样本将在自然语言推断的训练和测试期间进行小批量读取。

# 如果出现显存不足错误,请减少“batch_size”。在原始的BERT模型中,max_len=512
batch_size, max_len, num_workers = 512, 128, d2l.get_dataloader_workers()
data_dir = d2l.download_extract('SNLI')
train_set = SNLIBERTDataset(d2l.read_snli(data_dir, True), max_len, vocab)
test_set = SNLIBERTDataset(d2l.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)

运行结果:

7.3 微调BERT

用于自然语言推断的微调BERT只需要一个额外的多层感知机,该多层感知机由两个全连接层组成(请参见下面BERTClassifier类中的self.hiddenself.output)。这个多层感知机将特殊的“< cls>”词元的BERT表示进行了转换,该词元同时编码前提和假设的信息(为自然语言推断的三个输出):蕴涵、矛盾和中性。

class BERTClassifier(nn.Module):def __init__(self, bert):super(BERTClassifier, self).__init__()self.encoder = bert.encoderself.hidden = bert.hiddenself.output = nn.Linear(256, 3)def forward(self, inputs):tokens_X, segments_X, valid_lens_x = inputsencoded_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)

回想一下,在 sec_bert中,MaskLM类NextSentencePred类在其使用的多层感知机中都有一些参数。这些参数是预训练BERT模型bert中参数的一部分,因此是net中的参数的一部分。然而,这些参数仅用于计算预训练过程中的遮蔽语言模型损失和下一句预测损失。这两个损失函数与微调下游应用无关,因此当BERT微调时,MaskLMNextSentencePred中采用的多层感知机的参数不会更新(陈旧的,staled)。

为了允许具有陈旧梯度的参数,标志ignore_stale_grad=Truestep函数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)

运行结果:

97. BERT微调、自然语言推理数据集以及代码实现相关推荐

  1. 自然语言推理与自然语言解释

    原文: e-SNLI: Natural Language Inference with Natural Language Explanations 摘要 In order for machine le ...

  2. 自然语言处理数据集集锦(持续更新ing...)

    诸神缄默不语-个人CSDN博文目录 最近更新时间:2023.6.27 最早更新时间:2023.4.25 文本摘要主题的数据集见我之前写的另一篇博文:文本摘要数据集的整理.总结及介绍(持续更新ing-) ...

  3. 自然语言推理:微调BERT

    自然语言推理:微调BERT Natural Language Inference: Fine-Tuning BERT SNLI数据集上的自然语言推理任务设计了一个基于注意力的体系结构.现在通过微调BE ...

  4. 自然语言推理和数据集

    自然语言推理和数据集 Natural Language Inference and the Dataset 情绪分析的问题.此任务旨在将单个文本序列分类为预定义的类别,例如一组情感极性.然而,当需要判 ...

  5. cs224u 自然语言推理:任务和数据集-1

    cs224u 自然语言推理:任务和数据集-1 nli_01_task_and_data.ipynb __author__ = "Christopher Potts" __versi ...

  6. 基于TensorRT的BERT实时自然语言理解(下)

    基于TensorRT的BERT实时自然语言理解(下) BERT Inference with TensorRT 请参阅Python脚本bert_inference.py还有详细的Jupyter not ...

  7. BERT微调finetune笔记

    参考: 什么是BERT? - 知乎 (zhihu.com) 词向量之BERT - 知乎 (zhihu.com) BERT 详解 - 知乎 (zhihu.com) 详解Transformer (Atte ...

  8. 基于TensorRT的BERT实时自然语言理解(上)

    基于TensorRT的BERT实时自然语言理解(上) 大规模语言模型(LSLMs)如BERT.GPT-2和XL-Net为许多自然语言理解(NLU)任务带来了最先进的精准飞跃.自2018年10月发布以来 ...

  9. WSDM Cup 2019自然语言推理任务获奖解题思路

    WSDM(Web Search and Data Mining,读音为Wisdom)是业界公认的高质量学术会议,注重前沿技术在工业界的落地应用,与SIGIR一起被称为信息检索领域的Top2. 刚刚在墨 ...

最新文章

  1. C# SQL封装(一)
  2. 小工匠聊架构-布隆过滤器在亿级流量的电商系统中的应用
  3. 2011.10.16
  4. 空间谱专题16:信号个数估计
  5. Android JNI编程(五)——C语言的静态内存分配、动态内存分配、动态创建数组...
  6. linux借助expect完成自动登录
  7. CentOS7安装单机版RabbitMQ集群
  8. (转)淘淘商城系列——分布式文件系统FastDFS
  9. Elasticsearch-2.3.x填坑之路
  10. pandas按条件筛选数据
  11. 国外服务器https协议,AriaNg使用Https/WebSocket(安全)协议连接Aria2的方法
  12. 遗传算法的理解及例子解释
  13. blender 2.8 python bpy 编写脚本操作物体
  14. Unity模型制作导出规范
  15. android SENSOR_ACCELEROMETER 三轴加速度传感器
  16. Java开发一个登陆窗体,包括用户名,密码以及提交按钮和重置按钮,当用户输入amy,密码123456,弹出登陆成功提示对话框
  17. hybrid7 宝马active_宝马Active Hybrid 7:最快的混合动力轿车
  18. java中对象类型转换_Java 对象类型的转换
  19. 电子邮件签名档 HTML 手写时的折腾(附 原创工具)
  20. 酷壳用的还是 Wordpress

热门文章

  1. BreederDAO 元宇宙游戏资产工厂
  2. 正则表达式_Pattern_Marcher_System类_Date\SimpleDateFormat
  3. 烤箱和空气炸锅有什么区别
  4. 第四单元 用python学习微积分(二十七)积分-部分分式-分部积分
  5. java代码实现证书生成客户端证书 实现ssl双向认证
  6. k8s kubernetes-dashboard 免密+客户端证书登录
  7. 沃舍尔算法_坐在马桶上看算法:只有五行的Floyd最短路算法
  8. 算法分析:芯片测试问题 非递归 JAVA 有n片芯片,已知好芯片比坏芯片至少多1片......
  9. 【Python入门自学笔记专辑】——Python集合
  10. Java保留两位有效数字