本文为博客 BERT Fine-Tuning Tutorial with PyTorch 的翻译
在本教程中,我将向你展示如何使用 BERT 与 huggingface PyTorch 库来快速高效地微调模型,以获得接近句子分类的最先进性能。更广泛地讲,我将描述转移学习在NLP中的实际应用,以最小的努力在一系列NLP任务上创建高性能模型。
介绍
历史
2018年是NLP的突破性一年。转移学习,特别是像Allen AI的ELMO、OpenAI的Open-GPT和谷歌的BERT这样的模型,让研究人员用最小的特定任务微调粉碎了多个基准,并为NLP社区的其他成员提供了预训练的模型,这些模型可以轻松地(用更少的数据和更少的计算时间)进行微调和实施,以产生最先进的结果。遗憾的是,对于许多刚开始接触NLP的人,甚至对于一些有经验的实践者来说,这些强大模型的理论和实际应用仍然没有得到很好的理解。
什么是 BERT?
BERT(Bidirectional Encoder Representations from Transformers)于2018年底发布,我们将在本教程中使用该模型,为读者更好地理解和实践指导在NLP中使用转移学习模型。BERT是一种预训练语言表征的方法,它被用来创建模型,然后NLP实践者可以免费下载并使用这些模型。你可以使用这些模型从你的文本数据中提取高质量的语言特征,也可以用你自己的数据在特定的任务(分类、实体识别、问题回答等)上对这些模型进行微调,以产生最先进的预测。
这篇文章将解释如何修改和微调BERT,以创建一个强大的NLP模型,快速给你提供最先进的结果。
微调的优势
在本教程中,我们将使用BERT来训练一个文本分类器。具体来说,我们将把预先训练好的 BERT 模型,在最后添加一层未经训练的神经元,并为我们的分类任务训练新模型。为什么要这样做,而不是训练一个很适合你需要的特定深度学习模型(CNN、BiLSTM等)?
更快的发展
- 首先,预先训练的BERT模型权重已经编码了很多关于我们语言的信息。因此,训练我们的微调模型所需要的时间要少得多--就好像我们已经广泛地训练了我们网络的底层,只需要在使用它们的输出作为分类任务的特征时轻轻地调整它们。事实上,作者建议在特定的NLP任务上对BERT进行微调只需要2-4个纪元的训练(相比之下,从头开始训练原始的BERT模型或LSTM需要数百个GPU小时!)。
更少的数据
- 此外,也许同样重要的是,由于预先训练的权重,这种方法允许我们在一个比从头开始建立的模型所需的更小的数据集上微调我们的任务。从零开始建立的NLP模型的一个主要缺点是,我们通常需要一个大得令人望而却步的数据集来训练我们的网络以达到合理的精度,这意味着必须将大量的时间和精力投入到数据集的创建中。通过对BERT的微调,我们现在能够摆脱在更小的训练数据量上训练一个模型达到良好的性能。
更好的结果
- 最后,这种简单的微调程序(通常是在BERT的基础上增加一个全连接的层,并进行几个纪元的训练)被证明可以通过最小的任务特定调整来实现最先进的结果,适用于各种各样的任务:分类、语言推理、语义相似性、问题回答等。与其实施在特定任务上表现出良好效果的定制和有时模糊的架构,不如简单地对BERT进行微调,这被证明是一个更好的(或至少相等的)替代方案。
NLP的转变
这种向转移学习的转变与几年前计算机视觉领域发生的相同转变并行。为计算机视觉任务创建一个好的深度学习网络可能需要数百万个参数,而且训练成本非常高。研究人员发现,深度网络可以学习分层的特征表示(在最低层有简单的特征,如边缘,在较高层有逐渐复杂的特征)。与其每次从头开始训练一个新的网络,不如将训练好的网络的低层泛化图像特征复制并转移到另一个有不同任务的网络中使用。很快,下载一个预先训练好的深度网络,并迅速对其进行重新训练以适应新的任务,或者在上面添加额外的层,这比从头开始训练网络的昂贵过程要好得多。对于许多人来说,2018年引入的深度预训练语言模型(ELMO、BERT、ULMFIT、Open-GPT等)标志着NLP中向转移学习的转变,就像计算机视觉看到的那样。
让我们开始吧!
1. 设置
1.1. 检查 GPU
为了让 torch 使用 GPU,我们需要识别并指定 GPU 作为设备。稍后,在我们的训练循环中,我们将把数据加载到设备上。
import torch |
1.2. 安装 HuggingFace 库
接下来,让我们安装 HuggingFace 的transformers包,它将为我们提供一个与BERT一起工作的pytorch接口。(这个库包含了其他预训练语言模型的接口,如OpenAI的GPT和GPT-2)。我们选择了pytorch接口,因为它在高级API(它很容易使用,但不能深入了解事情的工作原理)和tensorflow代码(它包含了很多细节,但经常让我们偏离了关于tensorflow的课程,而这里的目的是BERT!)之间取得了很好的平衡。
目前,Hugging Face库似乎是最被广泛接受的、最强大的与BERT合作的pytorch接口。除了支持各种不同的预先训练好的变换模型外,该库还包含了这些模型的预构建修改,适合你的特定任务。例如,在本教程中,我们将使用BertForSequenceClassification
。
该库还包括用于标记分类、问题回答、下句预测等的特定任务类。使用这些预建的类可以简化为您的目的修改BERT的过程。
本笔记本中的代码其实是 HuggingFace 的run_glue.py示例脚本的简化版。
run_glue.py
是一个很有用的工具,它允许你选择你想运行的GLUE基准任务,以及你想使用的预训练模型(你可以看到可能的模型列表这里)。它还支持使用CPU、单个GPU或多个GPU。如果你想进一步提高速度,它甚至支持使用16位精度。
不幸的是,所有这些可配置性都是以可读性为代价的。在这篇Notebook中,我们已经大大简化了代码,并添加了大量的注释,以使其清楚地了解发生了什么。
2. 加载 CoLA 数据集
我们将使用The Corpus of Linguistic Acceptability (CoLA)数据集进行单句分类。它是一组被标记为语法正确或不正确的句子。它于2018年5月首次发布,是 "GLUE Benchmark "中包含的测试之一,BERT等模型都在此基础上进行比赛。
2.1. 下载和解压
该数据集托管在GitHub上的这个repo中:https://nyu-mll.github.io/CoLA/,下载链接
2.2. 解析
我们可以从文件名中看到,"tokenized" 和 "raw"版本的数据都是可用的。
我们不能使用预标记版本,因为为了应用预训练的BERT,我们必须使用模型提供的标记器。这是因为:(1)模型有一个特定的、固定的词汇,(2)BERT tokenizer有一种特殊的方式来处理词汇外的词汇。
我们将使用pandas来解析"域内"训练集,并查看其一些属性和数据点。
import pandas as pd |
我们实际关心的两个属性是"句子"和它的"标签",这个标签被称为"可接受性判断"(0=不可接受,1=可接受)。
下面是五个被标注为语法上不可接受的句子。请注意,这个任务比情感分析之类的工作要难得多!
print(df.loc[df.label == 0].sample(5)[['sentence', 'label']]) |
让我们将训练集的句子和标签提取为 numpy ndarrays。
# Get the lists of sentences and their labels. |
3. Tokenization & Input 格式化
在本节中,我们将把我们的数据集转换为BERT可以训练的格式。
3.1. BERT Tokenizer
为了将我们的文本输入到 BERT,必须将其分割成 tokens,然后这些 tokens 必须被映射到 tokenizer 词汇表中的索引。
Tokenization 必须由 BERT 中包含的 Tokenizer 来执行--下面的单元格将为我们下载。我们将在这里使用 "uncases "版本。
from transformers import BertTokenizer |
让我们把tokenizer应用到一个句子上,看看输出。
# Print the original sentence. |
当我们实际转换所有的句子时,我们将使用tokenize.encode
函数来处理这两个步骤,而不是分别调用tokenize
和convert_tokens_to_ids
。
不过,在我们这样做之前,我们需要先谈谈BERT的一些格式化要求。
3.2. 需要的格式化
上面的代码遗漏了一些必要的格式化步骤,我们将在这里看看。
- 补充说明:我觉得BERT的输入格式似乎 "过于规范"了......。我们被要求提供一些信息,这些信息看起来是多余的,或者说它们可以很容易地从数据中推断出来,而不需要我们明确地提供。但事实就是如此,我想一旦我对BERT的内部结构有了更深入的了解,它就会变得更有意义。
我们需要做的是 1. 在每个句子的开头和结尾添加特殊的标记。 2. 将所有句子的长度固定为一个固定的长度。 3. 用"注意力遮盖"明确区分真正的 token 和填充 token。
特殊 Tokens
[SEP]
在每个句子的末尾,我们需要附加特殊的"[SEP]"令牌。
这个标记是双句子任务的产物,即给BERT两个独立的句子,并要求它确定一些事情(例如,句子A中的问题的答案能否在句子B中找到?
我还不确定为什么当我们只有单句输入的时候,还需要 token,但它确实需要!
[CLS]
对于分类任务,我们必须在每个句子的开头加上特殊的"[CLS]"标记。
这个标记具有特殊的意义。BERT 由12个 Transformer 层组成。每个 Transformer 都会接收一个标记嵌入的列表,并在输出中产生相同数量的嵌入(当然是改变了特征值!)。

在最后一个(第12个) Transformer 的输出端,分类器只使用第一个嵌入(对应[CLS]标记)。
"The first token of every sequence is always a special classification token (
[CLS]
). The final hidden state corresponding to this token is used as the aggregate sequence representation for classification tasks." (摘自BERT论文)
你可能会想到在最终的嵌入上尝试一些池化策略,但这并不是必须的。因为 BERT 被训练成只使用这个[CLS]标记进行分类,我们知道模型已经被激励将分类步骤所需的一切编码到那个单一的 768 值嵌入向量中。它已经为我们完成了池化工作!
句子长度 & 注意力遮盖
我们数据集中的句子显然有不同的长度,那么BERT是如何处理的呢?
BERT有两个约束条件。 1. 所有的句子必须被填充或截断成一个固定的长度。 2. 最大的句子长度是512个tokens。
填充是通过一个特殊的"[PAD]"令牌来完成的,它在BERT词汇表中的索引0。下面的插图演示了填充到8个令牌的 "MAX_LEN"。
"注意力遮盖"只是一个1和0的数组,表示哪些标记是padding,哪些不是(看起来有点多余,不是吗!)。这个掩码告诉BERT中的"自我关注"机制不要将这些pad标记纳入它对句子的解释中。
不过,最大长度确实会影响训练和评估速度。
例如,用特斯拉K80。
MAX_LEN = 128 --> 训练一个 epoch 需要 5:28
MAX_LEN = 64 --> 训练一个 epoch 需要 2:57
。
3.3. Tokenize 数据集
transformers库提供了一个有用的 "encode" 函数,它将为我们处理大部分的解析和数据准备步骤。
在我们准备好对文本进行编码之前,我们需要决定一个最大句子长度来进行填充/截断。
下面的单元格将对数据集进行一次标记化处理,以测量最大句子长度。
max_len = 0 |
为了防止有一些较长的测试句子,我将最大长度设置为64。
现在我们准备好执行真正的 tokenization 了。
tokenizer.encode_plus
函数为我们结合了多个步骤。
- 将句子分割成token。
- 添加特殊的
[CLS]
和[SEP]
标记。 - 将这些标记映射到它们的ID上。
- 把所有的句子都垫上或截断成相同的长度。
- 创建注意力遮盖,明确区分真实 token 和
[PAD]
token。
前四项功能在tokenizer.encode
中,但我使用tokenizer.encode_plus
来获得第五项(注意力遮盖)。文档在这里.
# Tokenize all of the sentences and map the tokens to thier word IDs. |
3.4. 训练 & 验证切分
把我们的训练集分成 90% 用于训练,10% 用于验证。
from torch.utils.data import TensorDataset, random_split |
我们还将使用 torch DataLoader 类为我们的数据集创建一个迭代器。这有助于在训练过程中节省内存,因为与for循环不同,有了迭代器,整个数据集不需要加载到内存中。
from torch.utils.data import DataLoader, RandomSampler, SequentialSampler |
4. 训练我们的分类模型
现在我们的输入数据已经被正确格式化了,是时候微调一下BERT模型了。
4.1. BertForSequenceClassification
对于这个任务,我们首先要修改预先训练好的 BERT 模型,给出分类的输出,然后我们要在我们的数据集上继续训练模型,直到整个模型,端到端都很适合我们的任务。
值得庆幸的是,huggingface pytorch的实现中包含了一套针对各种NLP任务设计的接口。虽然这些接口都是建立在训练好的 BERT 模型之上,但每个接口都有不同的顶层和输出类型,以适应其特定的 NLP 任务。
以下是目前提供的类列表,供微调。
- BertModel
- BertForPreTraining
- BertForMaskedLM
- BertForNextSentencePrediction(下句预测)
- BertForSequenceClassification - 我们将使用的那个。
- BertForTokenClassification
- BertForQuestionAnswering
这些文档可以在这里下找到。
我们将使用BertForSequenceClassification。这是普通的BERT模型,上面增加了一个用于分类的单线性层,我们将使用它作为句子分类器。当我们输入数据时,整个预先训练好的BERT模型和额外的未经训练的分类层会根据我们的特定任务进行训练。
OK,让我们加载 BERT 吧! 有几个不同的预训练 BERT 模型可供选择。"bert-base-uncased "指的是只有小写字母("uncased")的版本,是两者中较小的版本("base "vs "large")。
from_pretrained
的文档可以在这里找到,附加参数定义在这里。
from transformers import BertForSequenceClassification, AdamW, BertConfig |
为了好奇,我们可以在这里按名称浏览所有模型的参数。
在下面的单元格中,我打印出了权重的名称和尺寸,分别为。
- 嵌入层。
- 十二个变压器中的第一个。
- 输出层。
# Get all of the model's parameters as a list of tuples. |
4.2. 优化器 & 学习率调度器
现在我们已经加载了我们的模型,我们需要从存储的模型中抓取训练超参数。
为了微调的目的,作者建议从以下数值中选择(来自BERT论文的附录A.3)。
- batch大小: 16,32。
- 学习率(Adam): 5e-5、3e-5、2e-5。
- epoch数: 2、3、4。
我们选择的是: * batch大小:32(在创建DataLoaders时设置)。 * 学习率:2e-5 * Epochs: 4 (我们将看到这可能是太多了...)
epsilon 参数eps = 1e-8
是 "一个非常小的数字,以防止在实现中出现任何除以零的情况" (来自这里)。
你可以在run_glue.py
这里中找到AdamW优化器的创建。
# Note: AdamW is a class from the huggingface library (as opposed to pytorch) |
4.3. 训练循环
下面是我们的训练循环。有很多事情要做,但从根本上讲,我们的循环中的每一个过程都有一个训练阶段和一个验证阶段。
*感谢Stas Bekman贡献了使用验证损失来检测过度拟合的见解和代码!
训练: - 解开我们的数据输入和标签 - 将数据加载到GPU上进行加速 - 清空上一次计算的梯度。 - 在pytorch中,默认情况下梯度会累积(对RNNs等有用),除非你明确地清除它们。 - 正向传递(通过网络输入数据)。 - 后传(反向传播) - 用optimizer.step()告诉网络更新参数。 - 跟踪监测进展的变量
验证: - 解开我们的数据输入和标签 - 将数据加载到GPU上进行加速 - 正向传递(通过网络输入数据) - 计算我们的验证数据的损失,并跟踪监测进度的变量。
Pytorch 向我们隐藏了所有的详细计算,但我们已经对代码进行了注释,以指出上述步骤中的每一行都在进行。
PyTorch也有一些初学者教程,你可能也会觉得很有帮助。
定义一个用于计算精度的辅助函数。
import numpy as np |
用于格式化 "hh:mm:ss" 的经过时间的辅助函数。
import time |
我们准备开始训练了!
import random |
我们来看看训练过程的总结。
import pandas as pd |
请注意,虽然训练损失随着时间的推移在下降,但验证损失却在增加!这说明我们的模型训练时间过长,对训练数据的拟合过度。
作为参考,我们使用的是7695个训练样本和856个验证样本)。
验证损失是一个比准确率更精确的衡量标准,因为对于准确率,我们并不关心准确的输出值,而只是关心它落在阈值的哪一边。
如果我们预测的答案是正确的,但置信度较低,那么验证损失会抓住这一点,而准确性则不会。
# Commented out IPython magic to ensure Python compatibility. |
5. 测试集的性能
现在,我们将加载保持数据集,并准备输入,就像我们对训练集所做的那样。然后我们将使用Matthew's correlation coefficient来评估预测,因为这是广大NLP社区用来评估CoLA性能的度量。通过这个指标,+1是最好的分数,-1是最差的分数。通过这种方式,我们可以看到我们在这个特定任务上与最先进模型的表现。
5.1. 数据准备
我们需要应用所有与训练数据相同的步骤来准备我们的测试数据集。
import pandas as pd |
5.2. 测试集上进行评估
准备好了测试集,我们就可以应用我们的微调模型对测试集产生预测。
# Prediction on test set |
使用"Matthews correlation coefficient"来衡量CoLA基准的准确性。(MCC)。
我们在这里用MCC是因为班级不平衡。
print('Positive samples: %d of %d (%.2f%%)' % (df.label.sum(), len(df.label), (df.label.sum() / len(df.label) * 100.0))) |
最后的分数将基于整个测试集,但我们来看看各个 batch 的分数,以了解各 batch 之间指标的差异性。
每个批次都有 32 个句子,除了最后一个 batch 只有 (516 % 32)=4 个测试句子。
# Create a barplot showing the MCC score for each batch of test samples. |
现在我们将综合所有批次的结果,计算出我们最终的MCC分数。
# Combine the results across all batches. |
酷! 在大约半小时内,在不做任何超参数调整(调整学习率、epochs、批次大小、ADAM属性等)的情况下,我们能够得到一个不错的分数。
*注意:为了最大限度地提高分数,我们应该删除 "验证集"(我们用它来帮助确定要训练多少个epochs),并对整个训练集进行训练。
库中记录了这个基准的预期精度这里为49.23
。
你也可以看看官方的排行榜这里。
请注意,(由于数据集规模较小?)运行之间的准确率可能会有很大差异。
结论
本篇文章演示了利用预先训练好的 BERT 模型,无论你对哪个具体的 NLP 任务感兴趣,你都可以使用 pytorch 接口以最小的努力和训练时间快速有效地创建一个高质量的模型。