在第二章中,我们开始了对Transformer模型体系结构的探讨,其中我们定义了原始Transformer体系结构的构建模块。可以将原始Transformer视为使用LEGO®积木构建的模型。这个构建套件包含编码器、解码器、嵌入层、位置编码方法、多头注意力层、掩码多头注意力层、后层归一化、前馈子层和线性输出层等积木。
这些积木有各种不同的大小和形状。您可以花费数小时使用相同的构建套件构建各种不同的模型!某些构建只需要其中的一些积木。其他构建将添加新的部分,就像我们在使用LEGO®组件构建模型时获取额外的积木一样。
BERT为Transformer构建套件添加了一个新部分:双向多头注意力子层。当我们人类在理解一句话时,不仅仅是看以前的词汇。BERT和我们一样,同时查看句子中的所有词汇。
本章首先将探讨双向编码器表示来自Transformer(BERT)的体系结构。BERT仅以新颖的方式使用Transformer的编码器块,并不使用解码器堆栈。
然后,我们将微调一个预训练的BERT模型。我们将微调的BERT模型是由第三方进行训练并上传到Hugging Face的。Transformers可以进行预训练。然后,例如,可以在几个自然语言处理任务上微调预训练的BERT。我们将通过使用Hugging Face模块来进行下游Transformer使用的这一令人着迷的经验。
本章涵盖以下主题:
- 双向编码器表示来自Transformer(BERT)
- BERT的体系结构
- 两步BERT框架
- 准备预训练环境
- 定义预训练编码器层
- 定义微调
- 下游多任务
- 构建微调的BERT模型
- 加载可接受性判断数据集
- 创建注意力掩码
- BERT模型配置
- 衡量微调模型的性能
我们的第一步将是探讨BERT模型的背景。
BERT的体系结构
BERT引入了Transformer模型的双向注意力。双向注意力需要对原始Transformer模型进行许多其他更改。
我们不会详细介绍第2章中描述的Transformer的构建模块,即《Transformer模型体系结构入门》。您可以随时查阅第2章,以复习Transformer的构建模块的各个方面。在本节中,我们将专注于BERT模型的特定方面。
我们将关注由Devlin等人(2018)设计的演进,其中描述了编码器堆栈。我们首先将介绍编码器堆栈,然后是预训练输入环境的准备。然后,我们将描述BERT的两步框架:预训练和微调。
让我们首先探讨编码器堆栈。
编码器堆栈
我们将从原始Transformer模型中提取的第一个构建模块是编码器层。编码器层,如在第2章《Transformer模型体系结构入门》中描述,如图3.1所示:
BERT模型不使用解码器层。BERT模型具有编码器堆栈,但没有解码器堆栈。被掩码的令牌(用于预测的令牌)位于编码器的注意力层中,我们将在以下部分详细介绍BERT编码器层。
原始Transformer包含N=6个层。原始Transformer的维度数为dmodel=512。原始Transformer的注意力头数为A=8。原始Transformer头部的维度为:
BERT编码器层比原始Transformer模型要大。
可以使用编码器层构建两个BERT模型:
- BERTBASE,其中包含N=12个编码器层。dmodel = 768,也可以表示为H=768,如BERT论文中所述。多头注意力子层包含A=12个注意力头。每个头部的维度zA与原始Transformer模型一样,保持为64:
- 在连接之前,每个多头注意力子层的输出将是这12个头部的输出:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> o u t p u t m u l t i − h e a d a t t e n t i o n = z 0 , z 1 , z 2 , ... , z 11 output_multi-head_attention={z0, z1, z2,..., z11} </math>outputmulti−headattention=z0,z1,z2,...,z11
- BERTLARGE 包含 N=24 个编码器层。dmodel = 1024。多头注意力子层包含 A=16 个头。每个头部的维度 zA 与原始 Transformer 模型一样保持为 64:
- 在连接之前,每个多头注意力子层的输出将是这16个头部的输出:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> o u t p u t m u l t i − h e a d a t t e n t i o n = z 0 , z 1 , z 2 , ... , z 15 output_multi-head_attention={z0, z1, z2,..., z15} </math>outputmulti−headattention=z0,z1,z2,...,z15
模型的大小可以总结如下:
在BERT式预训练中,大小和维度起着关键作用。BERT模型类似于人类。拥有更多工作内存(维度)和更多知识(数据)的BERT模型会产生更好的结果。学习大量数据的大型Transformer模型将更好地进行下游自然语言处理任务的预训练。
让我们进入第一个子层,看看在BERT模型中的输入嵌入和位置编码的基本方面。
准备预训练输入环境
BERT模型没有解码器层堆栈。因此,它没有掩码的多头注意力子层。BERT的设计者表示,掩码的多头注意力层会阻碍注意力过程。
掩码的多头注意力层会掩盖所有超出当前位置的令牌。例如,考虑以下句子:
"The cat sat on it because it was a nice rug." 如果我们刚刚到达单词 "it",编码器的输入可能是:
"The cat sat on it" 这种方法的动机是防止模型看到它应该预测的输出。这种从左到右的方法会产生相对不错的结果。
然而,模型无法通过这种方式学到很多东西。要知道 "it" 指的是什么,我们需要看整个句子,以达到 "rug" 这个词,然后找出 "it" 指的是地毯。
BERT的作者提出了一个想法。为什么不预训练模型以使用不同的方法进行预测?
BERT的作者采用了双向注意力的方法,让一个注意力头能够同时关注从左到右和从右到左的所有单词。换句话说,编码器的自注意力掩码可以完成工作,而不会受到解码器的掩码多头注意力子层的干扰。
该模型通过两种任务进行了训练。第一种方法是掩码语言建模(MLM)。第二种方法是下一句预测(NSP)。
让我们从掩码语言建模开始。
掩码语言建模
掩码语言建模不需要使用一系列可见单词后跟掩码序列进行训练。
BERT引入了对句子进行双向分析,并在句子的一个单词上使用随机掩码。
需要注意的是,BERT对输入应用了WordPiece,一种子词分词的标记方法。它还使用了学习到的位置编码,而不是正弦-余弦方法。
一个潜在的输入序列可能如下:
"The cat sat on it because it was a nice rug." 当模型到达单词 "it" 之后,解码器会掩码注意力序列:
"The cat sat on it ." 但BERT编码器会对一个随机令牌进行掩码以进行预测:
"The cat sat on it [MASK] it was a nice rug." 多头注意力子层现在可以看到整个序列,运行自注意力过程,并预测被掩码的令牌。
输入令牌以一种巧妙的方式被掩码,以迫使模型进行更长时间的训练,但通过三种方法产生更好的结果:
- 在数据集的10%上,以不掩码单个令牌的方式让模型感到意外,例如: "The cat sat on it [because] it was a nice rug."
- 在数据集的10%上,以随机令牌替换令牌的方式让模型感到意外,例如: "The cat sat on it [often] it was a nice rug."
- 在数据集的80%上,通过[MASK]令牌替换令牌,例如: "The cat sat on it [MASK] it was a nice rug."
作者的大胆方法避免了过度拟合,迫使模型进行高效的训练。
BERT还进行了下一句预测的训练。
下一句预测
训练BERT的第二种方法是下一句预测(NSP)。输入包含两个句子。在50%的情况下,第二个句子是文档的实际第二个句子。在另外50%的情况下,第二个句子是随机选择的,与第一个句子没有关系。
添加了两个新的标记:
- [CLS] 是一个二进制分类标记,添加到第一个序列的开头,用于预测第二个序列是否跟随第一个序列。正样本通常是从数据集中取出的一对连续句子。负样本是使用来自不同文档的序列创建的。
- [SEP] 是一个分隔标记,表示序列的结束。
例如,从一本书中提取的输入句子可以是:
"The cat slept on the rug. It likes sleeping all day."
这两个句子将成为一个完整的输入序列:
"[CLS] the cat slept on the rug [SEP] it likes sleep ##ing all day[SEP]"
这种方法需要额外的编码信息来区分序列A和序列B。
如果我们将整个嵌入过程放在一起,我们将得到:
输入嵌入是通过将令牌嵌入、片段(句子、短语、单词)嵌入和位置编码嵌入相加得到的。
BERT模型的输入嵌入和位置编码子层可以总结如下:
- 单词序列被拆分为WordPiece令牌。
- [MASK] 令牌将随机替代原始单词令牌,用于掩码语言建模训练。
- [CLS] 分类令牌插入到序列的开头,用于分类目的。
- [SEP] 令牌用于分隔两个句子(段落、短语)以进行NSP训练。
- 句子嵌入被添加到令牌嵌入中,使句子A具有不同的句子嵌入值,而句子B具有不同的句子嵌入值。
- 位置编码是可学习的。原始Transformer的正弦-余弦位置编码方法不适用。
一些其他重要特点包括:
- BERT在其多头注意力子层中使用双向注意力,打开了广泛的学习和理解令牌之间关系的可能性。
- BERT引入了无监督嵌入的场景,使用未标记的文本来预训练模型。无监督的场景迫使模型在多头注意力学习过程中更加努力思考。这使BERT学习了语言的构建方式,并将这一知识应用于下游任务,而不需要每次都进行预训练。
- BERT还使用了监督学习,覆盖了预训练过程的所有基础。
- BERT改进了Transformer模型的训练环境。现在,让我们看看预训练的动机以及它如何帮助微调过程。
BERT模型的预训练和微调
BERT是一个两步框架。第一步是预训练,第二步是微调,如图3.4所示:
训练一个Transformer模型可能需要数小时,甚至数天。需要相当多的时间来构建模型的体系结构和参数,并选择适合训练Transformer模型的合适数据集。
BERT框架的第一步是预训练,可以分为两个子步骤:
- 定义模型的体系结构:层数、头数、维度以及模型的其他构建块。
- 在MLM和NSP任务上训练模型。
BERT框架的第二步是微调,也可以分为两个子步骤:
- 使用预训练的BERT模型的训练参数来初始化所选的下游模型。
- 为特定的下游任务(如文本蕴涵识别、问答任务、以及包含对抗生成的情境)微调参数。
在本节中,我们涵盖了微调BERT模型所需的信息。在接下来的章节中,我们将更深入地探讨本节中提到的主题:
- 在第4章中,我们将从头开始预训练一个类似BERT的模型,一共经历15个步骤。我们将甚至编译自己的数据,训练一个分词器,然后训练模型。本章的目标是首先深入了解BERT的具体构建块,然后微调一个现有的模型。
- 在第5章中,我们将介绍许多下游任务,探索GLUE、SQuAD v1.1、SQuAD、SWAG以及其他几个NLP评估数据集。我们将运行多个下游Transformer模型,以说明关键任务。本章的目标是微调一个下游模型。
- 在第7章中,我们将探讨OpenAI GPT-2和GPT-3 Transformer模型的架构和使用。BERTBASE的配置与OpenAI GPT接近,以显示它产生了更好的性能。然而,OpenAI的Transformer模型也不断发展!我们将看到它们如何达到超越人类水平的自然语言处理水平。
在本章中,我们将对The Corpus of Linguistic Acceptability(CoLA)进行微调的BERT模型。下游任务基于由Alex Warstadt、Amanpreet Singh和Samuel R. Bowman进行的神经网络可接受性评判。
我们将微调一个BERT模型,用于确定句子的语法可接受性。微调后的模型将获得一定水平的语言能力。
我们已经了解了BERT的体系结构以及其预训练和微调框架。现在,让我们开始微调一个BERT模型。
微调BERT模型
本节将微调一个BERT模型,以预测下游任务的可接受性判断,并使用Matthews相关系数(MCC)来评估预测结果,MCC将在本章的"使用Matthews相关系数进行评估"部分进行解释。
在Google Colab中打开BERT_Fine_Tuning_Sentence_Classification_GPU.ipynb(确保您有一个电子邮件帐户)。该笔记本位于本书的GitHub存储库的Chapter03文件夹中。
笔记本中每个单元格的标题也与本章的各个小节的标题非常接近或相同。
我们首先将检查为什么Transformer模型必须考虑硬件限制。
硬件限制
Transformer模型需要多核硬件支持。在Google Colab的"Runtime"菜单中,选择"Change runtime type",并在"Hardware Accelerator"下拉列表中选择GPU。
Transformer模型是硬件驱动的。在继续本章之前,我建议阅读附录II,关于Transformer模型的硬件限制。
接下来,我们将安装并使用Hugging Face模块。
安装Hugging Face的BERT PyTorch接口
Hugging Face提供了一个预训练的BERT模型。Hugging Face开发了一个名为PreTrainedModel的基础类。通过安装这个类,我们可以从预训练模型配置中加载模型。
Hugging Face提供了TensorFlow和PyTorch的模块。我建议开发人员熟练掌握这两个环境。出色的人工智能研究团队可以使用其中一个或两个环境。
在本章中,我们将按照以下方式安装所需的模块:
ruby
#@title 安装Hugging Face的PyTorch接口
!pip install -q transformers
安装将运行,或者会显示满足要求的消息。
现在,我们可以导入程序所需的模块了。
导入模块
我们将导入所需的预训练模块,例如预训练的BERT分词器和BERT模型的配置。此外,我们还导入了BERTAdam优化器和序列分类模块:
python
#@title 导入模块
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler
from keras.preprocessing.sequence import pad_sequences
from sklearn.model_selection import train_test_split
from transformers import BertTokenizer, BertConfig
from transformers import AdamW, BertForSequenceClassification, get_linear_schedule_with_warmup
我们还从tqdm导入了一个漂亮的进度条模块:
javascript
from tqdm import tqdm, trange
现在,我们可以导入广泛使用的标准Python模块:
kotlin
import pandas as pd
import io
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
如果一切正常,将不会显示任何消息,需要记住Google Colab已经在我们使用的虚拟机上预安装了这些模块。
指定CUDA为torch的设备
我们现在将指定torch使用Compute Unified Device Architecture (CUDA) 来充分利用NVIDIA显卡的并行计算能力,用于我们的多头注意力模型:
ini
#@title 硬件验证和设备属性
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
!nvidia-smi
输出可能会根据Google Colab的配置而有所不同。有关解释和屏幕截图,请参阅附录II:Transformer模型的硬件约束。
现在,我们将加载数据集。
加载数据集
我们现在将加载基于Warstadt等人(2018年)的CoLA数据集。
通用语言理解评估(GLUE)将语言可接受性视为一项首要的自然语言处理任务。在第5章中,我们将探索Transformer必须执行的关键任务,以证明其效率。
笔记本中的以下单元格将自动下载所需的文件:
bash
import os
!curl -L https://raw.githubusercontent.com/Denis2054/Transformers-for-NLP-2nd-Edition/master/Chapter03/in_domain_train.tsv --output "in_domain_train.tsv"
!curl -L https://raw.githubusercontent.com/Denis2054/Transformers-for-NLP-2nd-Edition/master/Chapter03/out_of_domain_dev.tsv --output "out_of_domain_dev.tsv"
您应该会在文件管理器中看到它们。
现在程序将加载数据集:
ini
#@title 加载数据集
#数据集来源:https://nyu-mll.github.io/CoLA/
df = pd.read_csv("in_domain_train.tsv", delimiter='\t', header=None, names=['sentence_source', 'label', 'label_notes', 'sentence'])
df.shape
输出显示了我们导入的数据集的形状:
scss
(8551, 4)
显示了一个包含10行数据的样本,以可视化可接受性判断任务,并查看序列是否合理:
scss
df.sample(10)
输出显示了10行带标签的数据集,每次运行后可能会发生变化。
.tsv文件中的每个样本包含四列,以制表符分隔:
第1列:句子的来源(代码) 第2列:标签(0=不可接受,1=可接受) 第3列:作者标注的标签 第4列:待分类的句子
您可以在本地打开.tsv文件以阅读数据集的一些样本。程序现在将处理数据以供BERT模型使用。
创建句子、标签列表,并添加BERT标记。
程序现在将创建句子,按照本章"准备预训练输入环境"部分的描述:
ini
#@ 创建句子、标签列表并添加BERT标记
sentences = df.sentence.values
# 在每个句子的开头和结尾添加BERT的CLS和SEP标记
sentences = ["[CLS] " + sentence + " [SEP]" for sentence in sentences]
labels = df.label.values
现在已添加[CLS]和[SEP]标记。
程序现在激活分词器。
激活BERT分词器
在本节中,我们将初始化一个预训练的BERT分词器。这将节省从头开始训练分词器所需的时间。
程序选择了一个小写的分词器,激活它,并显示第一个分词后的句子:
ini
#@title 激活BERT分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)
tokenized_texts = [tokenizer.tokenize(sent) for sent in sentences]
print ("分词化第一个句子:")
print (tokenized_texts[0])
输出包含了分类标记和序列分割标记:
分词化第一个句子:
css
['[CLS]', 'our', 'friends', 'wo', 'n', "'", 't', 'buy', 'this', 'analysis', ',', 'let', 'alone', 'the', 'next', 'one', 'we', 'propose', '.', '[SEP]']
程序现在将处理数据。
处理数据
我们需要确定一个固定的最大长度并处理数据以适应模型。数据集中的句子较短。但为了确保,程序将序列的最大长度设置为128,并进行了填充:
ini
#@title 处理数据
# 设置最大序列长度。我们训练集中最长的序列是47,但我们还是留一些余地。
# 在原始论文中,作者使用了长度为512。
MAX_LEN = 128
# 使用BERT分词器将标记转换为它们在BERT词汇表中的索引号
input_ids = [tokenizer.convert_tokens_toids(x) for x in tokenized_texts]
# 填充输入标记
input_ids = pad_sequences(input_ids, maxlen=MAX_LEN, dtype="long", truncating="post", padding="post")
现在,序列已经被处理,程序将创建注意力掩码。
创建注意力掩码
现在,过程中的一部分会变得有点棘手。在前面的单元格中,我们对序列进行了填充。但是,我们希望防止模型对这些填充的标记执行注意力操作!
思路是为每个标记创建一个值为1的掩码,然后为填充的部分创建值为0的掩码:
ini
#@title 创建注意力掩码
attention_masks = []
# 为每个标记创建一个1的掩码,然后为填充的部分创建0的掩码
for seq in input_ids:
seq_mask = [float(i>0) for i in seq]
attention_masks.append(seq_mask)
程序现在将分割数据。
将数据分为训练集和验证集。
程序现在执行将数据分成训练集和验证集的标准过程:
ini
#@title 将数据分割为训练集和验证集
# 使用train_test_split将数据分成训练集和验证集,用于训练
train_inputs, validation_inputs, train_labels, validation_labels = train_test_split(input_ids, labels, random_state=2018, test_size=0.1)
train_masks, validation_masks, _, _ = train_test_split(attention_masks, input_ids, random_state=2018, test_size=0.1)
数据已准备好进行训练,但仍需要适应torch。
将所有数据转换为torch张量
微调模型使用torch张量。程序必须将数据转换为torch张量:
ini
#@title 将所有数据转换为torch张量
# torch张量是我们模型所需的数据类型
train_inputs = torch.tensor(train_inputs)
validation_inputs = torch.tensor(validation_inputs)
train_labels = torch.tensor(train_labels)
validation_labels = torch.tensor(validation_labels)
train_masks = torch.tensor(train_masks)
validation_masks = torch.tensor(validation_masks)
转换完成。现在我们需要创建一个迭代器。
选择批量大小并创建一个迭代器
在这个单元格中,程序选择了一个批量大小并创建了一个迭代器。迭代器是一种聪明的方式,可以避免加载所有数据到内存中的循环。迭代器与torch DataLoader结合使用,可以批量训练大规模数据集,而不会导致机器内存崩溃。
在这个模型中,批量大小为32:
ini
#@title 选择批量大小并创建一个迭代器
# 为训练选择一个批量大小。对于在特定任务上微调BERT,作者建议使用批量大小为16或32
batch_size = 32
# 使用torch DataLoader创建数据的迭代器。这有助于在训练期间节省内存,因为与for循环不同,
# 使用迭代器时不需要将整个数据集加载到内存中
train_data = TensorDataset(train_inputs, train_masks, train_labels)
train_sampler = RandomSampler(train_data)
train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size)
validation_data = TensorDataset(validation_inputs, validation_masks, validation_labels)
validation_sampler = SequentialSampler(validation_data)
validation_dataloader = DataLoader(validation_data, sampler=validation_sampler, batch_size=batch_size)
数据已经处理好了,程序现在可以加载并配置BERT模型。
BERT模型配置
该程序现在初始化了一个BERT uncased配置:
python
#@title BERT模型配置
# 初始化一个BERT bert-base-uncased风格的配置
#@title Transformer安装
try:
import transformers
except:
print("安装transformers库")
!pip -qq install transformers
from transformers import BertModel, BertConfig
configuration = BertConfig()
# 从bert-base-uncased风格的配置初始化模型
model = BertModel(configuration)
# 访问模型配置
configuration = model.config
print(configuration)
输出显示了与Hugging Face主要参数类似的内容(该库经常更新):
json
BertConfig {
"attention_probs_dropout_prob": 0.1,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"model_type": "bert",
"num_attention_heads": 12,
"num_hidden_layers": 12,
"pad_token_id": 0,
"type_vocab_size": 2,
"vocab_size": 30522
}
让我们了解这些主要参数:
attention_probs_dropout_prob
: 0.1 对注意力概率应用了0.1的丢弃比例。hidden_act
: "gelu" 是编码器中的非线性激活函数。它是一个高斯误差线性单元激活函数。输入按其幅度加权,这使它成为非线性的。hidden_dropout_prob
: 0.1 是应用于全连接层的丢弃概率。完全连接层可以在嵌入、编码器和池化器层中找到。输出并不总是对序列内容的很好反映。对隐藏状态序列进行池化会改善输出序列。hidden_size
: 768 是编码层和池化层的维度。initializer_range
: 0.02 是初始化权重矩阵时的标准差值。intermediate_size
: 3072 是编码器的前馈层的维度。layer_norm_eps
: 1e-12 是层归一化层的 epsilon 值。max_position_embeddings
: 512 是模型使用的最大长度。model_type
: "bert" 是模型的名称。num_attention_heads
: 12 是头的数量。num_hidden_layers
: 12 是层数。pad_token_id
: 0 是用于避免训练填充标记的标记 ID。type_vocab_size
: 2 是token_type_ids的大小,用于标识序列。例如,"the dog[SEP] The cat.[SEP]"可以用token IDs [0,0,0, 1,1,1]表示。vocab_size
: 30522 是模型用于表示input_ids的不同标记数。
了解了这些参数,我们可以加载预训练模型。
加载Hugging Face的BERT uncased base模型
程序现在加载了预训练的BERT模型:
ini
#@title 加载Hugging Face的BERT uncased base模型
model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
model = nn.DataParallel(model)
model.to(device)
我们已经定义了模型,定义了并行处理,将模型发送到设备。有关更多解释,请参见附录II,Transformer模型的硬件限制。
如果需要,可以进一步训练这个预训练模型。有趣的是,可以详细探索架构,以可视化每个子层的参数,如下所示:
ini
BertForSequenceClassification(
(bert): BertModel(
(embeddings): BertEmbeddings(
(word_embeddings): Embedding(30522, 768, padding_idx=0)
(position_embeddings): Embedding(512, 768)
(token_type_embeddings): Embedding(2, 768)
(LayerNorm): BertLayerNorm()
(dropout): Dropout(p=0.1, inplace=False)
)
(encoder): BertEncoder(
(layer): ModuleList(
(0): BertLayer(
(attention): BertAttention(
(self): BertSelfAttention(
(query): Linear(in_features=768, out_features=768, bias=True)
(key): Linear(in_features=768, out_features=768, bias=True)
(value): Linear(in_features=768, out_features=768, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(output): BertSelfOutput(
(dense): Linear(in_features=768, out_features=768, bias=True)
(LayerNorm): BertLayerNorm()
(dropout): Dropout(p=0.1, inplace=False)
)
)
(intermediate): BertIntermediate(
(dense): Linear(in_features=768, out_features=3072, bias=True)
)
(output): BertOutput(
(dense): Linear(in_features=3072, out_features=768, bias=True)
(LayerNorm): BertLayerNorm()
(dropout): Dropout(p=0.1, inplace=False)
)
)
(1): BertLayer(
(attention): BertAttention(
(self): BertSelfAttention(
(query): Linear(in_features=768, out_features=768, bias=True)
(key): Linear(in_features=768, out_features=768, bias=True)
(value): Linear(in_features=768, out_features=768, bias=True)
(dropout): Dropout(p=0.1, inplace=False)
)
(output): BertSelfOutput(
(dense): Linear(in_features=768, out_features=768, bias=True)
(LayerNorm): BertLayerNorm()
(dropout): Dropout(p=0.1, inplace=False)
)
)
(intermediate): BertIntermediate(
(dense): Linear(in_features=768, out_features=3072, bias=True)
)
(output): BertOutput(
(dense): Linear(in_features=3072, out_features=768, bias=True)
(LayerNorm): BertLayerNorm()
(dropout): Dropout(p=0.1, inplace=False)
)
)
现在,让我们浏览优化器的主要参数。
优化器分组参数
程序现在将初始化模型参数的优化器。微调模型从初始化预训练模型参数值(而不是它们的名称)开始。
优化器的参数包括权重衰减率以避免过拟合,并且一些参数已被筛选。
目标是为训练循环准备模型的参数:
rust
##@title 优化器分组参数
# 此代码取自:
# https://github.com/huggingface/transformers/blob/5bfcd0485ece086ebcbed2d008813037968a9e58/examples/run_glue.py#L102
# 不要对包含这些标记的参数应用权重衰减。
# (在这里,BERT没有'gamma'或'beta'参数,只有'bias'项)
param_optimizer = list(model.named_parameters())
no_decay = ['bias', 'LayerNorm.weight']
# 将'weight'参数与'bias'参数分开。
# - 对于'weight'参数,这指定'weight_decay_rate'为0.01。
# - 对于'bias'参数,'weight_decay_rate'为0.0。
optimizer_grouped_parameters = [
# 过滤出所有不包含'bias'、'gamma'、'beta'的参数。
{'params': [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)],
'weight_decay_rate': 0.1},
# 过滤出包含这些参数的参数。
{'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)],
'weight_decay_rate': 0.0}
]
# 注意 - 'optimizer_grouped_parameters'仅包括参数值,而不包括参数名称。
参数已经准备好并清理干净。它们已准备好用于训练循环。
训练循环的超参数
训练循环的超参数非常关键,尽管它们似乎毫无害处。例如,Adam将激活权重衰减,并且还会经历一个预热阶段。
学习率(lr)和预热率(warmup)应该在优化阶段早期设置为非常小的值,并在一定数量的迭代之后逐渐增加。这有助于避免大的梯度和过度优化的目标。
一些研究人员认为,在层归一化之前的子层输出级别的梯度不需要预热率。解决这个问题需要进行许多实验运行。
优化器是一个名为BertAdam的BERT版本的Adam优化器:
ini
#@title 训练循环的超参数
optimizer = BertAdam(optimizer_grouped_parameters,
lr=2e-5,
warmup=.1)
程序添加了一个精度测量函数,用于将预测与标签进行比较:
ini
# 创建精度测量函数
# 用于计算我们的预测与标签的精度
def flat_accuracy(preds, labels):
pred_flat = np.argmax(preds, axis=1).flatten()
labels_flat = labels.flatten()
return np.sum(pred_flat == labels_flat) / len(labels_flat)
数据准备好了,参数准备好了,现在是激活训练循环的时候了!
训练循环
训练循环遵循标准的学习过程。将迭代次数设置为4,会绘制损失和准确度的测量结果。训练循环使用数据加载器来加载和训练批次数据。训练过程会进行测量和评估。
代码开始通过初始化train_loss_set
来存储损失和准确度,这些数据将用于绘图。它开始训练多个周期,运行标准的训练循环,如以下节选所示:
ini
#@title 训练循环
t = []
# 用于存储损失和准确度以供绘图
train_loss_set = []
# 训练周期数(作者建议在2和4之间)
epochs = 4
# trange是正常python range的tqdm包装器
for _ in trange(epochs, desc="Epoch"):
..../...
tmp_eval_accuracy = flat_accuracy(logits, label_ids)
eval_accuracy += tmp_eval_accuracy
nb_eval_steps += 1
print("验证准确度: {}".format(eval_accuracy/nb_eval_steps))
输出中使用了trange包装器来显示每个周期的信息,例如 for _ in trange(epochs, desc="Epoch")
:
yaml
***output***
Epoch: 0%| | 0/4 [00:00<?, ?it/s]
Train loss: 0.5381132976395461
Epoch: 25%|██▌ | 1/4 [07:54<23:43, 474.47s/it]
Validation Accuracy: 0.788966049382716
Train loss: 0.315329696132929
Epoch: 50%|█████ | 2/4 [15:49<15:49, 474.55s/it]
Validation Accuracy: 0.836033950617284
Train loss: 0.1474070605354314
Epoch: 75%|███████▌ | 3/4 [23:43<07:54, 474.53s/it]
Validation Accuracy: 0.814429012345679
Train loss: 0.07655430570461196
Epoch: 100%|██████████| 4/4 [31:38<00:00, 474.58s/it]
Validation Accuracy: 0.810570987654321
注意:Transformer模型发展迅速,可能会出现弃用消息甚至错误。Hugging Face也不例外,当出现这种情况时,我们必须相应地更新我们的代码。
模型已经训练完毕,现在可以显示训练评估结果。
训练评估
损失和准确度数值已经存储在训练循环开始时定义的train_loss_set
中。
程序现在绘制这些测量结果:
ruby
#@title 训练评估
plt.figure(figsize=(15,8))
plt.title("训练损失")
plt.xlabel("批次")
plt.ylabel("损失")
plt.plot(train_loss_set)
plt.show()
输出是一个图表,显示了训练过程进行得很顺利,并且效率很高:
模型已经经过微调。现在可以进行预测。
使用保留数据集进行预测和评估
BERT下游模型是使用in_domain_train.tsv
数据集进行训练的。现在,程序将使用out_of_domain_dev.tsv
文件中的保留(测试)数据集进行预测。其目标是预测句子是否具有语法正确性。
以下代码节选显示了在保留数据集部分的代码中重复了用于训练数据的数据准备过程:
ini
#@title 使用保留数据集进行预测和评估
df = pd.read_csv("out_of_domain_dev.tsv", delimiter='\t', header=None, names=['sentence_source', 'label', 'label_notes', 'sentence'])
# 创建句子和标签列表
sentences = df.sentence.values
# 我们需要在每个句子的开头和结尾添加特殊标记,以便BERT能够正常工作
sentences = ["[CLS] " + sentence + " [SEP]" for sentence in sentences]
labels = df.label.values
tokenized_texts = [tokenizer.tokenize(sent) for sent in sentences]
.../...
然后,程序使用数据加载器运行批量预测:
ini
# 预测
for batch in prediction_dataloader:
# 将批次移到GPU
batch = tuple(t.to(device) for t in batch)
# 从数据加载器中提取输入
b_input_ids, b_input_mask, b_labels = batch
# 告诉模型不要计算或存储梯度,以节省内存并加快预测速度
with torch.no_grad():
# 前向传递,计算logit预测
logits = model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask)
预测的logits和标签被移动到CPU:
ini
# 将logits和标签移到CPU
logits = logits['logits'].detach().cpu().numpy()
label_ids = b_labels.to('cpu').numpy()
预测和它们的真实标签被存储:
go
# 存储预测和真实标签
predictions.append(logits)
true_labels.append(label_ids)
现在,程序可以评估预测结果。
使用马修斯相关系数进行评估
马修斯相关系数(Matthews Correlation Coefficient,MCC)最初是设计用于衡量二元分类的质量,并可以进行修改以成为多类别相关系数。在每个预测中,可以进行两类别分类,其中包括四个概率:
- TP = 真正例(True Positive)
- TN = 真负例(True Negative)
- FP = 假正例(False Positive)
- FN = 假负例(False Negative)
1975年,生物化学家Brian W. Matthews设计了MCC,受到他前辈的phi函数的启发。从那时起,它已经发展成了各种不同的格式,如下所示:
MCC产生的值介于-1和+1之间。+1表示最大正值的预测,-1表示反向预测,0表示平均随机预测。
GLUE使用MCC评估语言可接受性。
MCC从sklearn.metrics
导入:
python
#@title 使用马修斯相关系数进行评估
# 导入并使用马修斯相关系数评估每个测试批次
from sklearn.metrics import matthews_corrcoef
然后,创建一组预测:
ini
matthews_set = []
MCC值被计算并存储在matthews_set
中:
ini
for i in range(len(true_labels)):
matthews = matthews_corrcoef(true_labels[i],
np.argmax(predictions[i], axis=1).flatten())
matthews_set.append(matthews)
你可能会看到由于库和模块版本更改而产生的消息。最终得分将基于整个测试集,但让我们查看单个批次的得分,以了解不同批次之间该指标的变化。
单个批次的得分
让我们查看单个批次的得分:
ruby
#@title 单个批次的得分
matthews_set
输出产生了如预期的介于-1和+1之间的MCC值:
csharp
[0.049286405809014416,
-0.2548235957188128,
0.4732058754737091,
0.30508307783296046,
0.3567530340063379,
0.8050112948805689,
0.23329882422520506,
0.47519096331149147,
0.4364357804719848,
0.4700159919404217,
0.7679476477883045,
0.8320502943378436,
0.5807564950208268,
0.5897435897435898,
0.38461538461538464,
0.5716350506349809,
0.0]
几乎所有的MCC值都是正数,这是个好消息。让我们看看整个数据集的评估结果。
整个数据集的马修斯评估
MCC是评估分类模型的实际方式。
程序现在将为整个数据集汇总真实值:
ini
#@title 整个数据集上的马修斯评估
# 展平预测和整个数据集上的真实值以进行总体马修斯评估
flat_predictions = [item for sublist in predictions for item in sublist]
flat_predictions = np.argmax(flat_predictions, axis=1).flatten()
flat_true_labels = [item for sublist in true_labels for item in sublist]
matthews_corrcoef(flat_true_labels, flat_predictions)
MCC生成的相关值在-1和+1之间。0表示平均预测,-1表示反向预测,1表示完美预测。在这种情况下,输出确认了MCC为正,表明该模型与数据集之间存在相关性:
0.45439842471680725
通过对BERT模型的微调的最终积极评估,我们全面了解了BERT训练框架。
总结
BERT为transformers引入了双向注意力。从左到右预测序列并掩盖未来标记以训练模型具有严重限制。如果掩码序列包含我们正在寻找的含义,模型将产生错误。BERT同时关注序列中的所有标记。
我们探讨了BERT的架构,它仅使用transformers的编码器堆栈。BERT被设计为一个两步框架。框架的第一步是对模型进行预训练。第二步是对模型进行微调。我们为Acceptability Judgment下游任务构建了一个微调的BERT模型。微调过程经历了所有阶段。首先,我们加载了数据集并加载了模型的必要的预训练模块。然后训练了模型并测量了其性能。
微调预训练模型所需的机器资源比从头开始训练下游任务要少。微调模型可以执行各种任务。BERT证明了我们只需在两个任务上预训练模型,这本身就是了不起的。但基于BERT预训练模型的训练参数创建多任务微调模型是非同寻常的。
第7章《具有GPT-3引擎的超人类变压器的崛起》显示OpenAI已经在几乎没有或很少微调的情况下达到了零-shot水平。
在这一章中,我们对BERT模型进行了微调。在下一章《第4章 从头开始预训练RoBERTa模型》中,我们将深入研究BERT框架,并从头开始构建一个类似BERT的预训练模型。