大型语言模型(LLMs)如GPT(生成式预训练transformer)系列吸引了公众的兴趣。与此同时,像BERT这样的小型Transformer模型的实用性也不应被低估。虽然可能使用GPT这样的模型做语言理解任务效果很好,但使用BERT这样的小型模型也能提供相同或更好的性能,而且它们通常更快、更便宜,因为它们是专门为重点语言理解任务而训练的,但同时使用的计算量和能源要少得多。
Graphcore(拟未)最近正在推进一种名为打包的方法,用于微调和推理,在应用层面优化自然语言处理(NLP)模型。这篇文章将详解用于微调NLP任务的打包概念,并展示如何将其与易于使用的实用程序一起用于IPU上的Hugging Face。
Graphcore的客户Pienso在他们的生产文本分析和洞见平台中使用了这项技术,你可以在这篇文章中了解更多详情。
为什么我们要加速特定领域的任务?
打包对于创建可在线的解决方案特别有利,这意味着它们:
- 扩展良好
- 很好地处理大量传入的数据负载
- 消耗较少的计算量而不降低性能
- 减少微调/推理任务的总体时间
该技术注重吞吐量,并且尽可能地减少计算浪费以提高效率。它能很好地适应不断扩大的规模,以可忽略不计的开销有效地大幅增加批尺寸,使其对生产和研究任务都很有用。
例如,在使用打包对GoEmotions数据集进行多标签分类中,微调速度提高了6倍,推理工作负载速度提高了9倍,使模型处理的速度接近于实时。
请注意,打包并非特定于 BERT。它在理论上适用于任何以token-by-token为基础处理数据,且没有或只有极少跨标记交互的模型。它也能够应用于基因组学和蛋白质折叠模型,以及其他transformer模型。然而值得注意的是,它的适用性取决于所使用的数据集的结构,这点我们将在下文中详细阐述。
这种用于微调和推理任务的实施,其灵感来自于为预训练创建Packed BERT所做的工作,并以此为基础。
什么是打包?
简而言之,作为LLM输入的概念,打包意味着将多个标记化序列连接成一个输入,我们称之为“包”。
许多数据集包含的长度分布严重倾斜,更加倾向于较短的长度,然而transformer模型接收固定大小的输入。该问题一般通过简单地用未使用的值来填充未被序列使用过的输入部分来处理。
将序列打包在一起可以消除填充,利用未被使用的空间来减少计算浪费,同时保持模型可以表示为具有恒定大小输入的静态图的优势。这也意味着每批次处理更多的序列,多个序列(在一个包内)在标记级别上并行处理。这有效地增加了批尺寸,带来巨大的吞吐量优势,并且开销很小。
数据集长度分布
打包带来的时间优化有这样一个基础:在数据集的序列长度分布中,许多序列长度倾向于较短的一侧。它的工作原理是,将多个序列拟合到一个样本中,基本上将每个样本视为批次中的批次。
对于训练,以这种方式增加有效批尺寸意味着需要进一步调整超参数。对于推理,由于不需要考虑超参数,理论上可以实现更高的吞吐量。
本文中的代码演练展示了9倍的推理提速和6倍的训练提速。
从现在来看,我们根据下载次数选取了Hugging Face Hub上一些流行的微调文本分类数据集,并对它们的数据集特征进行了简要分析。这包含了10个流行的数据集以及大部分子集(去掉了一些相对过于庞大的数据集)。一共39个数据集。
这些数据集被标记为256的任意短序列长度,仅从数据集中每个样本的序列长度列表中提取,手动生成。通过使用这些序列长度可以创建出一个从0到256的长度分布直方图,bin的大小为 5:
.png?width=1815&height=1068&name=39ds_hist%20(1).png)
观察这个直方图可以发现,大多数数据集都强烈偏向于较短的长度序列,只有少部分长度较长,而它们中的大部分都是那些专注长内容的数据集。
为什么打包适用于BERT这样的语言模型?
BERT是一种编码繁重的自然语言模型,常用于语言分析和预测任务。文本样本在被标记化(映射到与模型词汇表相对应的特定于单词的整数值)后被传递给BERT。对于BERT,每个标记序列都是在token-by-token的基础上并行处理,并在样本嵌入中编码了有关标记的位置、句法和语义信息。这些信息是通过Transformer多头自注意力机制[8]从每个序列上学习得来的。
这意味着在理论上可以就单个标记所处的序列分析该标记的信息,不受输入中传递的其他序列的干扰,非常有用。在单个输入序列包含不止一个句子的数据集中,Transformers已经表现了出这种行为。
Transformers的一个关键元素是注意力掩码,它支持自注意力将其面向上下文的特定标记学习集中在序列的某个特定部分。
这使我们能够将一个输入与多个序列打包,以实现预期的吞吐量效益。在实践中,为了能够用打包的输入来执行分类和预测任务,需要对模型做一些调整,特别是在输出阶段。从下图可以直观地了解与没有打包的BERT相比,我们如何能够通过打包多个序列来减少填充导致的计算浪费,并加快模型的速度:
%20(1).png?width=875&height=1097&name=bert-packed.drawio(1)%20(1).png)
打包基本上有3个阶段,特别是对于微调:
- 尽可能快地将尽可能多的序列打包在一起的算法,尽可能接近最优。
- 调整模型的输入以将单个输入解释为多个序列而不是单个序列,并使用注意力掩码进行填充。
- 调整模型的输出从而将打包的输入分解回不同的序列进行最终的交叉标记计算,例如损失。
打包算法
预训练的原始打包实施有三种算法,它们是:
- 非负最小二乘直方图打包(NNLSHP)
- 最短包优先直方图打包(SPFHP)
- 最长包优先直方图打包(LPFHP)
这些算法使用序列长度直方图来尝试以最优方式创建具有不同长度的序列包,以尽可能减少包的总数(模型的输入)。
在之前的大型数据集预训练实施中,最优长度配置是通过使用NNLSHP得到的。该算法在原始文章中解释得很清楚:
“棘手的部分是战略矩阵。每列的最大总和为三,并对将哪些序列打包在一起才能完全匹配所需的总长度——在我们的例子中是512——进行编码。这些行对每个可能的组合进行编码,使其长度等于总长度。策略向量x是我们要找的,它描述了我们选择这两万种组合中的任何一种的频率。有趣的是,最后只有大约600个组合被选中。为了得到精确的解,x中的策略计数必须是正整数,但我们意识到只有非负x的近似舍入解就足够了。对于近似解,使用简单的开箱即用的求解器可以在30秒内得到结果。”
非负最小二乘直方图打包
这种方法的缺点是:
- 当每包序列值大于3时,复杂度会显著增加。
- 30秒内每包最多3个序列对于预训练是合理的,但对于可在线任务以及较小的数据集微调和推理可能就太长了,尤其是当整个训练运行可以在几秒钟内完成时。
在小的微调数据集中,偏态分布支持每个包打包更多的序列,这在NNLSHP中是不可行的,所以让我们来看看一个更简单、更自适应的算法:SPFHP。
最短包第一直方图打包
SPFHP 可以很好地扩展以适应每包序列数量的增加。它对长度从最长到最短的排序直方图进行操作,并简单地查看每个序列,检查其是否适合任何包。如果它适合一个包,它将被放置于那个包里,不考虑任何未来的序列。如果它适合多个包,它将被放置在序列长度最短的包里。
它更适合中小型数据集,并且它的复杂性不会因为包的数量的增加而增加。它在几乎恒定的时间内解决任何给定的数据集,在0.02秒内完成多达1600万个样本的处理。SPFHP的复杂性随着序列长度而增加,而不是数据集大小或每包序列数,因此对于不同的数据集大小,它保持相对恒定。
LPFHP是SPFHP的从最短到最长的变体,拆分计数以获得更理想的拟合。在某些情况下,它以最长包优先的方式处理任务可能会很有用,提供稍微更优一些的拟合。
为Packed BERT Hugging Face notebook创建的打包实用程序支持使用SPFHP或LPFHP的其中之一,以减轻使用NNLSHP的潜在预处理瓶颈。
所有打包算法的原始算法代码都可以在博客代码中找到。
用Hugging Face实现BERT微调的打包
尝试使用打包来加速BERT的多标签序列分类:在本节中,我们将了解如何使用Hugging Face在GoEmotions数据集上轻松地使用BERT打包。
你可以通过在Paperspace Gradient notebook中运行这个演练,获得更多的可视化和进一步的解释,本文中使用的所有代码都可以在多标签文本分类notebook中获取。
Github上的深入讲解释了高级函数的内部功能,这些功能使得打包在Hugging Face中非常容易使用。如果你希望将打包应用于你自己的模型/任务,那么该演练中提供了对这些内容和你可能需要的内在细节的完整解释。
GoEmotions数据集是尝试对打包进行微调的理想选择,因为它的序列长度非常强烈地倾向于较短的序列,并且将提供大量的吞吐量增加,如序列长度分布所示:
.png?width=1259&height=947&name=histge99%20(1).png)
关于数据集:GoEmotions是一个多标签情绪分析数据集,包含大约58000条仔细挑选的评论,标记为28类情绪,包括一个“中性”类别。我们将使用独热编码对所有标签进行训练,以表示多标签输出。
在本演练过程中你可以使用Hugging Face的数据集库轻松下载此数据集。它支持从Hugging Face Hub下载已经处理过、可以使用的数据集,因此你不需要自己下载和设置数据集。
设置你的环境
首先,通过安装本演练所需的所有pip包来设置你的环境。如果你在Paperspace中尝试此演练,请确保你处于IPU (Optimum Graphcore)环境中的Hugging Face Transformers,你可以在其中从packed-bert文件夹访问打包实用程序。用于打包的notebook已经装有可访问的实用程序,但如果你在单独的个人环境中尝试此操作,请确保使用以下方法克隆存储库:
git clone git@github.com:huggingface/optimum-graphcore.git
并导航到notebooks/packed-bert,以便能够使用模型、实用程序和流水线的功能。
pip install git+https://github.com/huggingface/optimum-graphcore.git
pip install scikit-learn;
pip install datasets
pip install evaluate
pip install tokenizers
pip install matplotlib
pip install scipy
pip install huggingface_hub;
我们将首先导入我们需要的包以及用于序列分类的、特定于包的模型和推理流水线。
import transformers
import optimum.graphcore
import torch
import numpy as np
import evaluate
from datasets import load_dataset
from transformers import AutoConfig
from huggingface_hub import notebook_login
from transformers import AutoTokenizer
from optimum.graphcore import IPUConfig, IPUTrainer, IPUTrainingArguments
from models.modeling_bert_packed import PipelinedPackedBertForSequenceClassification
from pipeline.packed_bert import PackedBertTextClassificationPipeline
from utils.packing.dataset_creator import PackedDatasetCreator
接下来,我们为演练定义一些通用参数:
task = "sst2"
model_checkpoint = "bert-base-uncased"
ipu_config_name = "Graphcore/bert-base-uncased"
micro_batch_size = 2
gradient_accumulation_steps = 32
device_iterations = 32
max_seq_length = 256
pod_type = os.getenv("GRAPHCORE_POD_TYPE", "pod4")
executable_cache_dir = os.getenv("POPLAR_EXECUTABLE_CACHE_DIR", "./exe_cache/") + "/packed_bert_slseqcls/"
任务是go_emotions,检查点是默认的bert-base-uncased,我们将使用它作为基础来微调go_emotions的模型。该模型将在Graphcore IPU上运行,为了利用IPU上可用的并行性,我们需要定义一些特定于IPU的配置,从微调的角度来看,这些有效地为模型创建了更大的批尺寸。
max_seq_length是所有输入序列固定的最大长度,低于这个长度的所有序列都会被填充到这个长度。鉴于GoEmotions数据集中的序列较小,我们可以将模型的最大输入尺寸减小到max_seq_length = 256。
IPU并行性:我们同时使用数据并行性和流水线并行性(更多相关信息,请参阅教程)。因此,全局批尺寸,即用于权重更新的实际样本数,由四个因素决定:
全局批尺寸=微批次尺寸*梯度累积步数*设备迭代*复制因子
我们定义了梯度累积步数、设备迭代和微批次尺寸。复制因子(在多组IPU上复制模型)默认设置为1,本演练默认将4个IPU用于单个副本。ipu_config是Hugging Face Hub上Graphcore空间中所有检查点中可用的特殊配置。它包含基本检查点的这些IPU特定参数的默认值,并且必须传递给特定于IPU的模型,以对其进行实例化。
获取和准备数据集
要检索数据集,我们可以简单地使用Datasets库(数据集库)中的load_dataset,它将从Hugging Face Hub检索数据集,并将其加载到你的脚本中。
我们还想初始化用于验证数据集的度量,因为这是一个带有单热编码输出的多标签任务,我们将使用ROC-AUC度量,这是一种用于多类、多标签分类问题的常用性能指标。我们可以从Hugging Face的Evaluate库中加载它。
为了预处理模型并将句子的字符串转换为与BERT解释的词汇相对应的整数标记,我们还需要初始化模型分词器。这会将单个词/子词转换为标记。
使用Transformers库中的AutoTokenizer可以轻松完成此操作。默认的预训练BERT检查点包含一个预配置的分词器,因此我们可以使用from_pretrained直接为模型加载带有预训练嵌入和词汇的分词器。
dataset = load_dataset(task)
metric = evaluate.load("roc_auc", "multilabel")
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, use_fast=True)
Datasets库中的可用map函数可以用来标记数据集。
首先,创建一个简单的函数,将分词器应用于一个文本样本,为此我们:
- 定义最大序列长度
- 将截断设置为True:这将截断任何大于最大序列长度的序列。
- 请注意,对于打包,我们在此阶段并未将填充设置为真。我们将在打包数据集时对其进行填充,但打包要求其输入最初是未填充的。
其次,我们需要单热格式对标签进行编码。由于数据集允许多个标签对一个输入为真,为了保持恒定的标签维度,我们需要将数据集中的标签列编码为二进制数组,而不是表示标签类别的动态长度整数列表。由于有28个类,因此示例如下所示:
Original labels: [3, 27]
One hot encoded labels: [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
在这里,3和27是给定输入的有效标签。在单热编码格式中,将索引3和索引27设置为1来定义这一点,从而允许有效标签具有任何大小。
在第一个函数中封装了对一个样本进行标记的调用,在第二个函数中封装了将一组标签转换为单热编码格式的调用。然后我们使用map函数迭代地对每个样本进行预处理,使用内部批处理来加快速度,并用成批的参数进行设置。
#Tokenising function for one sample
def preprocess_function(examples):
return tokenizer(
examples['sentence'], truncation=True, max_length=max_seq_length)
#Label ID to one-hot-encoded conversion for one sample
def id_to_N_hot(example):
indexes = example['labels']
label = np.zeros((num_labels,), dtype=np.float16)
for idx in indexes:
label[idx] = 1
example['labels'] = label
return example
# Call to map the data
encoded_dataset = dataset.map(id_to_N_hot)
encoded_dataset = encoded_dataset.map(preprocess_function, batched=True)
打包数据集
现在我们有了一个预处理和标记化的数据集,可以打包数据集。总结一下我们需要为打包做些什么,有四个步骤来确保我们可以为模型使用打包数据集:
- 创建数据集序列长度的直方图。
- 使用一种最先进的打包算法,为数据集生成一组“策略”,这些算法绘制出需要被打包在一起的序列的顺序和索引。
- 使用此策略创建实际数据集,将数据集中每一列的标记化特征连接在一起,包括标签。
- 最后,将这些新的列传递到自定义PyTorch数据集中,准备传递给数据加载器。
什么是策略?“序列顺序的映射”使用序列长度的直方图将最适合给定最大长度的长度值组合在一起。数据集的策略集包含多个长度列表,涵盖数据集中的所有序列,例如:
策略集中的第一个策略可能是[120,40,40,60],表示一个长度为120的序列、两个长度为40的序列和一个长度为60的序列应该按给定的顺序放置,以形成一个包。这些长度可能有多个序列,因此在形成包时,我们可以将一个序列标记为“已使用”,以确保序列检索不会重复。
上述步骤已通过Graphcore Hugging Face存储库中提供的易于使用的utils.packing进行了简化。我们可以通过将所有必要的打包配置传递给PackedDatasetCreator 类,在通常的标记化和预处理之后简单地生成打包数据集,并使用.create()生成随时可用的数据集。
至此,上述所有三个步骤已经完成,并创建了 PyTorch 数据集。
首先,我们定义一些基本的包装参数:
max_seq_per_pack = 6
num_labels = 2
problem_type = 'single_label_classification'
max_seq_per_pack是可以连接成一个输入的最大序列数,称为“包”。这会影响分类阶段数据的整体批尺寸,因此,微调的值太大可能会导致需要更广泛的超参数调整才能获得最佳结果。
我们已决定将当前示例的默认值保持为6,理论上意味着加速比非打包数据集的速度快了可达6倍。对于推理,这可以设置得更高,以获得更大的批量推理工作负载的吞吐量。
PackedDatasetCreator 提供了一些选项来根据数据集修改流程:
- 可调整的最大序列长度。
- 每包可调整的序列数 max_seq_per_pack。
- 要使用的打包算法(LPFHP 或SPFHP 之一)。
- 自定义标签键允许类访问数据集中的标签,因为这些标签未标记化。
- 目前,创建者类支持单标签和多标签分类以及问答。任务类型使用problem_type来定义,它是single_label_classification、multi_label_classification或question answering 中的一个。
- 对于不同的任务,可以设置训练为True,验证为True, 或者推理为True。
PackedDatasetCreator类还有一些其他专门用于推理的功能,例如 pad_to_global_batch_size,当我们不想在创建数据迭代器时丢失任何样本时,该功能可用于对大样本执行批量推理,它将“垂直”填充应用到数据集,添加填充行以使数据集达到可被全局批量大小整除的值,并允许使用最大可能的批尺寸而不会丢失任何数据。
一旦创建了打包器(生成了直方图和打包策略——第一步和第二步),我们可以简单地调用.create(),它将返回初始的、标记化的未打包数据集的完全打包版本,这将执行打包数据集的第 3 步和第 4 步。
train_data_packer = PackedDatasetCreator(
tokenized_dataset = encoded_dataset['train'],
max_sequence_length = max_seq_length,
max_sequences_per_pack = max_seq_per_pack,
training = True,
num_labels = num_labels,
problem_type = problem_type,
algorithm = 'SPFHP',
custom_label_key = 'label'
)
val_data_packer = PackedDatasetCreator(
tokenized_dataset = encoded_dataset['validation'],
max_sequence_length = max_seq_length,
max_sequences_per_pack = max_seq_per_pack,
validation = True,
num_labels = num_labels,
problem_type = problem_type,
algorithm = 'SPFHP',
custom_label_key = 'label'
)
packed_train_dataset = train_data_packer.create()
packed_val_dataset = val_data_packer.create()
然后我们可以观察数据集创建的输出,这将向我们展示打包在这里实际完成了什么工作:
Packing efficiency (fraction of real tokens): 42.5296
Speed-up theoretical limit: 13.3547
Achieved speed-up over un-packed dataset: 5.67971
Runtime: Packed 43410 sequences in 0.001 seconds
Average packing factor: 5.6797069213659555
Packing efficiency (fraction of real tokens): 43.7226
Speed-up theoretical limit: 13.3873
Achieved speed-up over un-packed dataset: 5.85329
Runtime: Packed 5426 sequences in 0.001 seconds
Average packing factor: 5.853290183387271
Packed dataset creation time: 1.9252s
Packed dataset creation time: 0.1407s
输出显示,与未打包的数据集相比,理论上实现了5.68倍的提速。该算法速度很快,在0.001秒内完成了所有43410个训练序列的策略。创建数据集的整个过程需要几秒钟,训练加速可以有效减轻开销,可以忽略不计。
观察打包后的数据集
让我们看看打包后的数据集里的列是什么样子的,以便更深入地了PackedDatasetCreator对原始数据集的处理。
在该函数中,它需要分词器生成的默认列名,具体而言:
- input_ids
- attention_mask(注意掩码)
- labels 标签(用于训练、验证)
- token_type_ids (可选)
它还会生成一些额外的列:
- position_ids是一列,表示单个标记在包内各自序列中的位置。
- example_ids(用于推理)是与样本在输入数据中的位置相对应的整数索引,用于将推理数据输出重新排序,使之与输入的顺序相同。由于一次性数据改组是打包的必要副作用,所以重新排序是必需的,以便将数据集的大小减少到最优水平。
下图是一个简化的可视化呈现,展示了使用算法生成的策略创建数据集时实际发生的情况 (同时显示每个输入的position_id):
%20(1).png?width=1643&height=1817&name=packing-noteouts(5)%20(1).png)
打包数据集创建器的内部:图中中间的框表示如何使用前面提到的策略选择要连接的序列。数据集用于形成堆叠列表(“排序序列”),其中列表的第一行是序列的排序长度,第二行是数据集中序列的相应索引,从中获得要打包的序列的相关索引(这些也相当于存储的example_ids)。
这用于检索一个长度与策略相对应的序列,然后取消该序列在数据集中的特定索引,因此不能再次选择相同的序列。
打包后输入的变化:打包数据集涉及到将输入连接在一起,但是有些输入的形成可能会改变,以便一次处理多个序列,如上图所示:
- 生成一个自定义的attention_mask(注意力掩码):它包含包的每个序列的唯一索引和剩余填充标记的0。这告诉模型的自注意机制仅从与标记相关的序列中检索单个标记的上下文。在上面的例子中,像[1,1,1,1,0,0,0,0,0,0]这样的东西一旦与其他序列一起打包,就会变成[1,1,1,1,2,2, 3,0,1,2,3],以表示对包中每个序列的具体注意。
- 必须将每个序列的CLS标记移动到包的末尾以进行分类任务,然后BERT Pooler可以轻松地从序列的末尾提取一组固定的全局表示,而不是从输入里的间歇和动态变化的位置检索它们。对于基于标记的任务或预测任务,这不是必需的,因为只有在任务要求Pooler时才需要这样做。
- 包的position_ids包含每个序列的连接position_ids,即与序列相对应的单个标记位置。
- 标签和token_type_ids也被打包(连接)以对应于input_ids包。请注意,虽然其他列用0填充,但标签用-100值填充,这是为了利用PyTorch中的某些损失函数自动忽略值为-100的索引。它还减少了可能使用0作为标签的任务之间的索引混淆(例如在单热编码的情况下)。
对于此任务,BERT的多标签分类使用binary cross-entropy loss with logits,并且不会忽略索引。因此,提供的模型类被设置为使用-100值,手动忽略这些索引。
有关PackedDatasetCreator的实现以及如何将其应用于不同的微调任务的更多深入信息,请查看打包微调的深入详解。
准备模型进行微调
首先,我们来定义一个函数,它可以在验证期间为我们计算度量。该函数由 Hugging Face Optimum的Trainer类自动调用,传递模型预测和相应的标签。在这里,我们必须应用一些简单的后处理来确保忽略打包时创建的未使用的填充样本。需要注意几个关键事项:
- 如前所述,所有设置为-100的标签在使用掩码计算精度时将被忽略。
- 同样,对于回归logit,我们必须确保忽略这些填充的索引。我们可以使用逐元素比较来创建有效和无效标签的布尔掩码。然后,我们可以使用此掩码对logit和标签进行动态切片,只为了使用我们对有效索引的度量来执行准确性计算。
- 我们应用softmax函数从我们的logit中检索概率分布,这进一步隔离了多标签分类的高分类别,并且该分布是准确性度量所期望的。
预测和标签被传递给我们使用Evaluate库初始化的指标,它将返回数据集中验证样本的准确率百分比。
from scipy.special import softmax
def compute_metrics(eval_pred):
predictions, labels = eval_pred
labels = labels.reshape(-1, labels.shape[-1])
predictions = predictions.reshape(-1, predictions.shape[-1])
# Remove the padding labels
mask = (labels != -100)[:,0]
labels = labels[mask,:]
predictions = predictions[mask,:]
pred_scores = softmax(predictions.astype("float32"), axis=1)
auc = metric.compute(
prediction_scores=pred_scores, references=labels, multi_class="ovr"
)["roc_auc"]
return {"roc_auc": auc}
针对打包的模型修改
接下来,我们可以继续实例化我们的模型。使用打包实用程序,我们可以使用 PipelinedPackedBertForSequenceClassification,它是Transformers BertForSequenceClassification的修改版本,继承了IPU流水线并行性,并在模型的输出和输入上进行了一些小的修改,使其与打包的数据集兼容。BertForSequenceClassification的内部向前和向后传递没有改变。
回想一下微调打包的第二个和第三个基本阶段:修改模型输入以接收打包后的序列,并修改模型输出以解包输出计算的序列。我们总结对模型所做的关键更改如下:
1. 注意力掩码(输入阶段)
回想一下我们使用PackedDatasetCreator生成的递增整数注意力掩码。BERT不会按照我们的需要解释整数表示,而是通过重塑和转置递增整数注意力掩码中的值,并使用整数表示的顺序来创建二进制扩展注意力掩码。
这表示输入序列中每个标记的相关注意掩力码,为每个标记定义相关上下文。这是在Packed BERT模型头内部完成的,就在将输入传递到Transformers BERT前向传递之前。
我们不直接传递这样的掩码的原因是因为这会使数据集增大几个数量级,并且使数据加载器难以从输入中推断出批次维度。
%20(1).png?width=1172&height=812&name=articleattn.drawio(1)%20(1).png)
2. BERT Pooler(输出阶段):
仅对于分类任务而言,需要对BERT Pooler进行轻微修改,不是从第一个标记位置提取输出嵌入,而是从最后N个标记位置提取输出嵌入,其中N=max_seq_per_pack。
3. 解包(输出阶段):
对于非分类任务而言,如果隐藏状态用于直接获取特定于标记的logit,则必须使用输入中可用的位置信息“解包”这些logit。可以为尽可能多的序列堆叠logit,并乘以对应于序列位置的稀疏掩码,这将允许损失将输入作为单个序列的更大批尺寸来对待。
4.掩蔽(输出阶段):
如果使用你无法使用ignore_index的损失函数(例如带logit的二进制损失)或类似的函数,目的是确保未使用的间歇性logit/标签值不会污染损失,可以从标签索引中找到未使用的索引设置为-100,以创建一个稀疏掩码。这些可用于将这些索引的logit,掩蔽为0,或者简单地返回所有损失值,类似地屏蔽损失,然后返回平均值。
这些更改的示例和进一步解释可以在深入演练中阅读,当前可用任务的源代码位于Graphcore Hugging Face (Optimum)存储库中,位于 notebooks/packed_bert/models/modeling_packed_bert.py之下
为了准备模型进行训练,我们需要使用我们所做的一些模型修改从预训练的检查点初始化BERT配置。对于打包,我们必须指定max_sequences_per_pack、num_labels和problem_type,因为这些对于模型中的更改至关重要。然后,我们可以简单地在模型上调用from_pretrained来继承预训练检查点的所有默认配置以及我们添加的一些配置。为了加快速度,并尽量减少对性能的损害,我们可以以半精度 (FP16) 训练模型。
config = AutoConfig.from_pretrained(model_checkpoint)
config.max_sequences_per_pack = max_seq_per_pack
config.num_labels = num_labels
config.problem_type = problem_type
model = PipelinedPackedBertForSequenceClassification.from_pretrained(
model_checkpoint, config=config).train().half()
我们从预训练检查点继承了大部分配置,添加了一些特定于此用例的新配置选项:
BertConfig {
"_name_or_path": "bert-base-uncased",
"architectures": [
"BertForMaskedLM"
],
"attention_probs_dropout_prob": 0.1,
"classifier_dropout": null,
"gradient_checkpointing": false,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"id2label": {
"0": "LABEL_0",
"1": "LABEL_1",
"2": "LABEL_2",
...
"27": "LABEL_27"
},
"initializer_range": 0.02,
"intermediate_size": 3072,
"label2id": {
"LABEL_0": 0,
"LABEL_1": 1,
"LABEL_10": 10,
...
"LABEL_9": 9
},
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"max_sequences_per_pack": 6,
"model_type": "bert",
"num_attention_heads": 12,
"num_hidden_layers": 12,
"pad_token_id": 0,
"position_embedding_type": "absolute",
"problem_type": "multi_label_classification",
"transformers_version": "4.25.1",
"type_vocab_size": 2,
"use_cache": true,
"vocab_size": 30522
}
在演练开始时,我们设置了一些默认的IPU配置,以及一个可执行的缓存目录(这很有用,因为它可以让你在第一次编译模型后跳过编译)。这些现在可以传递给Optimum Graphcore IPUConfig,以简化将它们传递给模型的过程。
ipu_config = IPUConfig.from_pretrained(
ipu_config_name,
executable_cache_dir = executable_cache_dir,
gradient_accumulation_steps=gradient_accumulation_steps,
device_iterations = device_iterations,
replication_factor=1,
inference_device_iterations = 64,
inference_replication_factor = 1
)
为了训练模型,我们使用IPUTrainer类创建了一个训练器,该类处理IPU上的模型编译、训练和评估。该类的工作方式与Hugging Face Trainer类相似,但传入了额外的IPUConfig。
我们还可以使用IPUTrainingArguments类指定一些额外的训练参数,这些参数将传递给训练器。这对于轻松调整训练超参数非常有用,包括要训练的epoch数、每个设备的批尺寸、学习率、预热率和一些数据加载器选项(如drop_last)等参数。
现在让我们使用定义过的IPUTrainingArguments实例化训练。
from transformers import default_data_collator
args = IPUTrainingArguments(
"./"+f"{model_checkpoint}-{task}",
per_device_train_batch_size=micro_batch_size,
per_device_eval_batch_size=4,
num_train_epochs=5,
learning_rate=2e-4,
adam_epsilon=1e-6,
loss_scaling=16.0,
warmup_ratio=0.1,
weight_decay=0,
lr_scheduler_type = "cosine",
metric_for_best_model=metric_name,
dataloader_drop_last=True,
logging_steps=1,
pod_type=pod_type,
gradient_accumulation_steps=gradient_accumulation_steps
)
trainer = IPUTrainer(
model,
ipu_config,
args,
train_dataset=packed_train_dataset,
eval_dataset=packed_val_dataset,
data_collator=default_data_collator,
compute_metrics=compute_metrics
)
设置到此完成,剩下的就是训练模型了。
关于超参数调整的注意事项
当打包和增加序列数量时,你可能会发现模型收敛速度与不打包时的收敛速度不一样,这是因为增加最大序列数量对学习的影响与显著增加批尺寸相似。一次传递计算更多的样本,因此必须根据有效批尺寸的增加来调整超参数。
对于此处和Hugging Face notebook中提供的示例,我们没有进行广泛的超参数调整,但是通过按照与有效批尺寸增加大致相同的速率、仅对初始学习率进行递增来实现收敛。
训练模型
使用Hugging Face Optimum库,你可以在一行中完成此操作,不需要复杂的训练循环或反向传播的实现。我们可以通过在实例化的训练器上调用.train()方便地启动训练过程。这将开始迭代数据,并使用定义过的超参数并针对使用给定批配置定义的epoch进行训练:
trainer.train()
然后我们可以在本地保存:
trainer.save_model("./"+f"{model_checkpoint}-{task}")
或者推送给Hugging Face hub:
trainer.push_to_hub()
观察训练输出
默认的IPUTrainer不会考虑输入的样本数量,因为它预期每个输入都有一个样本,但是我们可以通过查看IPUTrainer训练的输出来计算实际的吞吐量:
Packed - BERT
***** Running training *****
Num examples = 7643
Num Epochs = 5
Instantaneous batch size per device = 2
Total train batch size (w. parallel, distributed & accumulation) = 2496
Gradient Accumulation steps = 39
Total optimization steps = 15
{'loss': 0.7289, 'learning_rate': 0.0001, 'epoch': 0.33}
{'loss': 0.192, 'learning_rate': 0.0002, 'epoch': 0.67}
{'loss': 0.143, 'learning_rate': 0.0001970941817426052, 'epoch': 1.0}
{'loss': 0.169, 'learning_rate': 0.000188545602565321, 'epoch': 1.33}
{'loss': 0.1223, 'learning_rate': 0.00017485107481711012, 'epoch': 1.67}
{'loss': 0.1726, 'learning_rate': 0.00015680647467311557, 'epoch': 2.0}
{'loss': 0.1644, 'learning_rate': 0.00013546048870425356, 'epoch': 2.33}
{'loss': 0.1231, 'learning_rate': 0.0001120536680255323, 'epoch': 2.67}
{'loss': 0.1311, 'learning_rate': 8.79463319744677e-05, 'epoch': 3.0}
{'loss': 0.1501, 'learning_rate': 6.453951129574644e-05, 'epoch': 3.33}
{'loss': 0.2156, 'learning_rate': 4.3193525326884435e-05, 'epoch': 3.67}
{'loss': 0.1241, 'learning_rate': 2.514892518288988e-05, 'epoch': 4.0}
{'loss': 0.142, 'learning_rate': 1.1454397434679021e-05, 'epoch': 4.33}
{'loss': 0.1344, 'learning_rate': 2.905818257394799e-06, 'epoch': 4.67}
{'loss': 0.1233, 'learning_rate': 0.0, 'epoch': 5.0}
100%|█████████████████████████████████████████████████████████| 15/15 [00:54<00:00, 3.62s/it]
Training completed. Do not forget to share your model on huggingface.co/models =)
{
'train_runtime': 54.571,
'train_samples_per_second': 686.079,
'train_steps_per_second': 0.275,
'train_loss': 0.1890590712428093,
'epoch': 5.0
}
虽然train_samples_per_second大约是每秒686个样本,但我们可以看到示例的总数是7643个。这是因为由于打包,GoEmotions训练集中的实际样本数量是43410。因此,实际的训练吞吐量可以计算为686*5.68,或大约每秒3896个样本。
为了定量地展示其优势,你可以以相同的批尺寸,面向相同的数据集,在相同的条件下运行训练,但不进行打包。这样你可以得到以下的训练输出:
Unpacked - BERT
***** Running training *****
Num examples = 43410
Num Epochs = 5
Instantaneous batch size per device = 2
Total train batch size (w. parallel, distributed & accumulation) = 2496
Gradient Accumulation steps = 39
Total optimization steps = 85
100%
85/85 [06:12<00:00, 4.38s/it]
{'loss': 0.7563, 'learning_rate': 2.2222222222222223e-05, 'epoch': 0.06}
{'loss': 0.4773, 'learning_rate': 4.4444444444444447e-05, 'epoch': 0.12}
{'loss': 0.1841, 'learning_rate': 6.666666666666667e-05, 'epoch': 0.18}
{'loss': 0.1415, 'learning_rate': 8.888888888888889e-05, 'epoch': 0.24}
{'loss': 0.119, 'learning_rate': 0.00011111111111111112, 'epoch': 0.29}
{'loss': 0.2073, 'learning_rate': 0.00013333333333333334, 'epoch': 0.35}
{'loss': 0.1138, 'learning_rate': 0.00015555555555555556, 'epoch': 0.41}
{'loss': 0.178, 'learning_rate': 0.00017777777777777779, 'epoch': 0.47}
{'loss': 0.2422, 'learning_rate': 0.0002, 'epoch': 0.53}
{'loss': 0.0984, 'learning_rate': 0.0001999145758387301, 'epoch': 0.59}
{'loss': 0.142, 'learning_rate': 0.000199658449300667, 'epoch': 0.65}
{'loss': 0.145, 'learning_rate': 0.0001992320579737045, 'epoch': 0.71}
{'loss': 0.0774, 'learning_rate': 0.00019863613034027224, 'epoch': 0.76}
{'loss': 0.1322, 'learning_rate': 0.00019787168453273544, 'epoch': 0.82}
{'loss': 0.2188, 'learning_rate': 0.00019694002659393305, 'epoch': 0.88}
{'loss': 0.1536, 'learning_rate': 0.0001958427482458253, 'epoch': 0.94}
{'loss': 0.1007, 'learning_rate': 0.00019458172417006347, 'epoch': 1.0}
{'loss': 0.0267, 'learning_rate': 0.0001931591088051279, 'epoch': 1.06}
{'loss': 0.0623, 'learning_rate': 0.00019157733266550575, 'epoch': 1.12}
{'loss': 0.1069, 'learning_rate': 0.0001898390981891979, 'epoch': 1.18}
{'loss': 0.0558, 'learning_rate': 0.0001879473751206489, 'epoch': 1.24}
{'loss': 0.0684, 'learning_rate': 0.00018590539543698854, 'epoch': 1.29}
{'loss': 0.0535, 'learning_rate': 0.00018371664782625287, 'epoch': 1.35}
{'loss': 0.2245, 'learning_rate': 0.0001813848717270195, 'epoch': 1.41}
{'loss': 0.0355, 'learning_rate': 0.00017891405093963938, 'epoch': 1.47}
{'loss': 0.0364, 'learning_rate': 0.00017630840681998066, 'epoch': 1.53}
{'loss': 0.1504, 'learning_rate': 0.00017357239106731317, 'epoch': 1.59}
{'loss': 0.0346, 'learning_rate': 0.00017071067811865476, 'epoch': 1.65}
{'loss': 0.0334, 'learning_rate': 0.00016772815716257412, 'epoch': 1.71}
{'loss': 0.0862, 'learning_rate': 0.00016462992378609407, 'epoch': 1.76}
{'loss': 0.0844, 'learning_rate': 0.0001614212712689668, 'epoch': 1.82}
{'loss': 0.1204, 'learning_rate': 0.00015810768154019385, 'epoch': 1.88}
{'loss': 0.1338, 'learning_rate': 0.00015469481581224272, 'epoch': 1.94}
{'loss': 0.1765, 'learning_rate': 0.00015118850490896012, 'epoch': 2.0}
{'loss': 0.2133, 'learning_rate': 0.00014759473930370736, 'epoch': 2.06}
{'loss': 0.0235, 'learning_rate': 0.00014391965888473703, 'epoch': 2.12}
{'loss': 0.1029, 'learning_rate': 0.00014016954246529696, 'epoch': 2.18}
{'loss': 0.0134, 'learning_rate': 0.00013635079705638298, 'epoch': 2.24}
{'loss': 0.0334, 'learning_rate': 0.00013246994692046836, 'epoch': 2.29}
{'loss': 0.0686, 'learning_rate': 0.00012853362242491053, 'epoch': 2.35}
{'loss': 0.1879, 'learning_rate': 0.00012454854871407994, 'epoch': 2.41}
{'loss': 0.0515, 'learning_rate': 0.00012052153421956342, 'epoch': 2.47}
{'loss': 0.0536, 'learning_rate': 0.00011645945902807341, 'epoch': 2.53}
{'loss': 0.0595, 'learning_rate': 0.00011236926312693479, 'epoch': 2.59}
{'loss': 0.0714, 'learning_rate': 0.00010825793454723325, 'epoch': 2.65}
{'loss': 0.1455, 'learning_rate': 0.00010413249742488131, 'epoch': 2.71}
{'loss': 0.0655, 'learning_rate': 0.0001, 'epoch': 2.76}
{'loss': 0.0498, 'learning_rate': 9.586750257511867e-05, 'epoch': 2.82}
{'loss': 0.0635, 'learning_rate': 9.174206545276677e-05, 'epoch': 2.88}
{'loss': 0.075, 'learning_rate': 8.763073687306524e-05, 'epoch': 2.94}
{'loss': 0.079, 'learning_rate': 8.35405409719266e-05, 'epoch': 3.0}
{'loss': 0.0366, 'learning_rate': 7.947846578043659e-05, 'epoch': 3.06}
{'loss': 0.1427, 'learning_rate': 7.54514512859201e-05, 'epoch': 3.12}
{'loss': 0.0612, 'learning_rate': 7.146637757508949e-05, 'epoch': 3.18}
{'loss': 0.0401, 'learning_rate': 6.753005307953167e-05, 'epoch': 3.24}
{'loss': 0.0065, 'learning_rate': 6.3649202943617e-05, 'epoch': 3.29}
{'loss': 0.0302, 'learning_rate': 5.983045753470308e-05, 'epoch': 3.35}
{'loss': 0.0399, 'learning_rate': 5.608034111526298e-05, 'epoch': 3.41}
{'loss': 0.0241, 'learning_rate': 5.240526069629265e-05, 'epoch': 3.47}
{'loss': 0.0732, 'learning_rate': 4.8811495091039926e-05, 'epoch': 3.53}
{'loss': 0.0867, 'learning_rate': 4.530518418775733e-05, 'epoch': 3.59}
{'loss': 0.0454, 'learning_rate': 4.189231845980618e-05, 'epoch': 3.65}
{'loss': 0.0172, 'learning_rate': 3.857872873103322e-05, 'epoch': 3.71}
{'loss': 0.0124, 'learning_rate': 3.53700762139059e-05, 'epoch': 3.76}
{'loss': 0.0254, 'learning_rate': 3.227184283742591e-05, 'epoch': 3.82}
{'loss': 0.0799, 'learning_rate': 2.9289321881345254e-05, 'epoch': 3.88}
{'loss': 0.0466, 'learning_rate': 2.6427608932686843e-05, 'epoch': 3.94}
{'loss': 0.0185, 'learning_rate': 2.3691593180019366e-05, 'epoch': 4.0}
{'loss': 0.0042, 'learning_rate': 2.1085949060360654e-05, 'epoch': 4.06}
{'loss': 0.012, 'learning_rate': 1.861512827298051e-05, 'epoch': 4.12}
{'loss': 0.0279, 'learning_rate': 1.6283352173747145e-05, 'epoch': 4.18}
{'loss': 0.045, 'learning_rate': 1.4094604563011472e-05, 'epoch': 4.24}
{'loss': 0.0575, 'learning_rate': 1.2052624879351104e-05, 'epoch': 4.29}
{'loss': 0.0196, 'learning_rate': 1.0160901810802115e-05, 'epoch': 4.35}
{'loss': 0.0118, 'learning_rate': 8.422667334494249e-06, 'epoch': 4.41}
{'loss': 0.0071, 'learning_rate': 6.840891194872112e-06, 'epoch': 4.47}
{'loss': 0.0276, 'learning_rate': 5.418275829936537e-06, 'epoch': 4.53}
{'loss': 0.0353, 'learning_rate': 4.1572517541747294e-06, 'epoch': 4.59}
{'loss': 0.1348, 'learning_rate': 3.059973406066963e-06, 'epoch': 4.65}
{'loss': 0.0679, 'learning_rate': 2.128315467264552e-06, 'epoch': 4.71}
{'loss': 0.0589, 'learning_rate': 1.3638696597277679e-06, 'epoch': 4.76}
{'loss': 0.0682, 'learning_rate': 7.679420262954984e-07, 'epoch': 4.82}
{'loss': 0.0627, 'learning_rate': 3.415506993330153e-07, 'epoch': 4.88}
{'loss': 0.088, 'learning_rate': 8.542416126989805e-08, 'epoch': 4.94}
{'loss': 0.0182, 'learning_rate': 0.0, 'epoch': 5.0}
Training completed. Do not forget to share your model on huggingface.co/models =)
{
'train_runtime': 372.393,
'train_samples_per_second': 569.721,
'train_steps_per_second': 0.228,
'train_loss': 0.09256008372587317,
'epoch': 5.0
}
注意:训练的示例数从7643变为43410,因为序列现在是一个接一个地进行处理。观察结果:
.png?width=3441&height=807&name=tput-results-table%20(1).png)
打包的好处是显而易见的,它为微调提供了巨大的吞吐量和总体时间优势。
评估模型
我们可以用同样的方式轻松地执行验证数据集的评估:
trainer.evaluate()
并且观察输出,83%的评估准确性表明我们成功地训练了模型。
***** Running Evaluation *****
Num examples = 927
Batch size = 256
100% 4/4 [00:00<00:00, 36.42it/s]
{'roc_auc': 0.836179971362336}
使用Hugging Face进行高速推理
打包还可以用于高速批推理,涵盖了从微调模型到部署模型进行实时推理的工作流。
这里可以使用一个内置优化的充分抽象推理流水线,其源代码和模块可从Hugging Face Graphcore存储库获取。自定义PackkedBertTextClassificationPipeline非常适合优化更大的推理负载以实现高吞吐量。
下面的示例设定每个包最多12个序列(即理论速度提高12倍),以展示更高的潜在吞吐量。如前所述,在推理中,无需额外的工作或性能成本就能实现更高的打包因子,但在训练中却并非如此。
默认情况下,推理流水线将保留数据的顺序,以确保在对实时数据负载进行推理时可维护,并且输出可以轻松匹配回输入。
默认情况下,所需的参数是:
- 模型检查点(模型)
- 缓存存储目录(executable_cache_dir)
- 最大序列长度 (max_seq_length)
- 问题类型 (problem_type)
有些可选参数包括:
- 每包序列数(max_seq_per_pack——默认为6)
- 标签类别(label_categories——默认为索引)
- 微批尺寸(micro_batch_size——默认为1)
- 预训练的分词器(pretrained_tokenizer)的名称(当它并未展示在保存的模型中时,否则它将被默认为bert-base-uncased)
首先,我们概括一下标签类别对应的类名:
#Define each of the class names in category order for inference labelling
class_names = [
"admiration",
"amusement",
"anger",
"annoyance",
"approval",
"caring",
"confusion",
"curiosity",
"desire",
"disappointment",
"disapproval",
"disgust",
"embarrassment",
"excitement",
"fear",
"gratitude",
"grief",
"joy",
"love",
"nervousness",
"optimism",
"pride",
"realization",
"relief",
"remorse",
"sadness",
"surprise",
"neutral",
]
然后,我们可以使用所需的参数实例化流水线。
请注意,为推理传递IPU配置并不是绝对必要的,流水线将默认继承保存过的检查点的配置。然而,为了进一步展示使用并行性的优势,以数据并行方式使用4个IPU,我们在本示例中将增强的配置传递给流水线:
#Path to saved trained model checkpoint
model = "./"+f"{model_checkpoint}-{task}"
inference_boosted_ipu_config = IPUConfig.from_pretrained(model,
inference_device_iterations=32,
inference_replication_factor=4,
ipus_per_replica=1,
layers_per_ipu=[12]
)
#Instantiate the pipeline with all required options
pipeline = PackedBertTextClassificationPipeline(
model = model,
executable_cache_dir = executable_cache_dir,
problem_type='multi_label_classification',
max_seq_per_pack=12,
max_seq_length=max_seq_length,
ipu_config=inference_boosted_ipu_config,
micro_batch_size=8,
label_categories=class_names
)
以上几行是设置流水线所需的全部内容。接下来,作为大量数据的示例,我们将整个GoEmotions训练数据集的原始文本列直接传递到流水线中,以供其执行推理:
preds = pipeline.predict(dataset['train']['text'])
Packing efficiency (fraction of real tokens): 68.4612
Speed-up theoretical limit: 13.3547
Achieved speed-up over un-packed dataset: 9.14280
Runtime: Packed 43410 sequences in 0.001 seconds
Average packing factor: 9.142796967144061
Packed dataset creation time: 1.6288s
我们可以打印推理输出,以观察返回预测的内容:
print(f"Number of predictions: {len(preds['predictions'])}")
print(f"Preprocessing time: {preds['preprocessing_time']}s")
print(f"Postprocessing time: {preds['postprocessing_time']}s")
print(f"Throughput: {preds['throughput']} samples/s")
Number of predictions: 43410
Preprocessing time: 6.723727464675903s
Postprocessing time: 0.1985154151916504s
Throughput: 49017.46503352953 samples/s
推理输出显示每秒49017个样本的推理吞吐量,具有IPU加速和9.1的打包因子。与非打包版本相比,推理速度大约快9.1倍。回想一下,对于具有不同数据集倾向的数据集,将观察到吞吐量的不同改进。
让我们查看一个随机输出,看看流水线返回了什么。
print(f"Input:{dataset['train']['text'][16]}")
print(f"Output:{preds['predictions'][16]}")
Input:Thank you friend
Output:
{'admiration': 0.008711744,
'amusement': 0.0030106984,
'anger': 0.0032300074,
'annoyance': 0.0037541997,
'approval': 0.0056799226,
'caring': 0.0048773102,
'confusion': 0.0027520012,
'curiosity': 0.003843228,
'desire': 0.0020692206,
'disappointment': 0.0025953841,
'disapproval': 0.00305811,
'disgust': 0.0009967086,
'embarrassment': 0.0015079721,
'excitement': 0.0029756227,
'fear': 0.0018840467,
'gratitude': 0.90438044,
'grief': 0.001129419,
'joy': 0.0033982676,
'love': 0.0040671937,
'nervousness': 0.0016305067,
'optimism': 0.005975805,
'pride': 0.0019212064,
'realization': 0.0032426496,
'relief': 0.0016178179,
'remorse': 0.0053986902,
'sadness': 0.002555146,
'surprise': 0.003431616,
'neutral': 0.010305118
}
从上面可以看出,输出列出了所有类别的概率。对于该输入,最可能的类别预计是得分为0.903的“感恩”。
总结而言,使用打包进行微调和推理具有明显的优势,在优化上无需为了增加应用程序吞吐量而使用额外的硬件或存储。相反,它成功地“回收”了由过多填充数据集产生的计算浪费,使其在完全保持模型性能的同时特别节约时间。
亲自体验,免费试用
使用Paperspace在云端免费试用我们的PackedBERT notebook:
你也可以尝试我们的深入微调notebook,进一步了解如何为不同的数据集和任务实现打包。
其他有用的资源
详细了解用于BERT预训练的打包的原始开发,以及推进其实施的工作: