应用

  1. 我们将处理以下常见 NLP 任务:token classification 、掩码语言建模(如 BERT )、文本摘要、翻译、因果语言建模(如 GPT 系列模型 )、问答。

一、Token classification

  1. token classification 任务可以为文本中的单词或字符分配标签,如:

    • 实体命名识别 (NER):找出句子中的实体(如人物、地点或组织)。

    • 词性标注 (POS):将句子中的每个单词标记为对应的词性(如名词、动词、形容词等)。

    • 分块(chunking):找到属于同一实体的 Token (如,找出名词短语、动词短语等)。

1.1 加载数据集

  1. 查看数据集:

1.2 数据处理

  1. 我们的文本需要转换为 Token ID,然后模型才能理解它们。

    注意:单词 "lamb" 被分为两个子词 "la""##mb"。这导致了 token 序列和标签序列之间的不匹配:标签序列只有 9 个元素,而 token 序列有 12token 。我们需要扩展标签序列以匹配 token 序列 。这里有两条规则:

    • special token (如,[CLS], [SEP])的标签为 -100 。这是因为默认情况下 -100 是一个在我们将使用的损失函数(交叉熵)中被忽略的 index

    • 每个 token 都会获得与其所在单词相同的标签,因为它们是同一实体的一部分。对于单词内部但不在开头的 token ,我们将 B- 替换为 I- ,因为单词内部的 token 不是实体的开头。

  2. 为了预处理整个数据集,我们需要 tokenize 所有输入并在所有 label 上应用 align_labels_with_tokens() 。为了利用我们的快速分词器的速度优势,最好同时对大量文本进行 tokenize ,因此我们将编写一个处理样本列表的函数并使用带 batched = TrueDataset.map()方法。

1.3 使用 Trainer API 微调模型

a. 构建 mini-batch

  1. 前面的数据处理可以批量地将输入文本转换为 input_idstoken id 序列),但是我们还需要将样本拼接成 mini-batch 然而馈入模型。DataCollatorForTokenClassification 支持以相同的方式来填充输入文本和 label ,使得 input_idslabels 的长度保持相同。labelpadding 值为 -100,这样在损失计算中就可以忽略相应的预测。

    注意:input_idspadding 值为 0labelspadding 值为 -100

b. evaluation 指标

  1. 用于评估 Token 分类效果的传统框架是 seqeval 。要使用此指标,我们首先需要安装 seqeval 库:

    然后我们可以通过 load_metric() 函数加载它:

    这个评估框架以字符串而不是整数的格式传入 label ,因此在将 predictionlabel 传递给它之前需要对 prediction/label 进行解码。让我们看看它是如何工作的:

    首先,我们获得第一个训练样本的标签:

    然后我们可以通过更改 labels 来创建 ”虚拟” 的 prediction

    metric.compute 的输入是一组样本的 prediction (不仅仅是单个样本的 prediction )、以及一组样本的 label 。输出为:

    可以看到它返回了每个单独实体(如 "MISC"、"ORG")、以及整体的精度、召回率、以及 F1 分数。对于单独实体还返回出现的次数("number" ),对于整体还返回准确率。

  2. 为了让 Trainer 在每个 epoch 计算一个指标,我们需要定义一个 compute_metrics() 函数,该函数接受 predictionlabel ,并返回一个包含指标名称和值的字典。

    这个compute_metrics() 首先对 logits 执行 argmax 从而得到 prediction (这里无需采用 softmax,因为我们只需要找到概率最大的那个 token id 即可,也就是 logit 最大的那个 token id)。然后我们将 predictionlabel 从整数转换为字符串(注意删除 label = -100label 及其对应位置的 prediction)。然后我们将 prediction 字符串和 label 字符串传递给 metric.compute() 方法:

c. 定义模型

  1. 由于我们正在研究 Token 分类问题,因此我们将使用 AutoModelForTokenClassification 类。

    首先我们定义两个字典 id2labellabel2id ,其中包含从 label idlabel name的映射:

    然后我们加载预训练好的模型:

    注意,预训练好的模型必须和 AutoTokenizer 相匹配,这里都是用的 "bert-base-cased"

    创建模型会发出警告,提示某些权重未被使用(来自 pretrained-head 的权重),另外提示某些权重被随机初始化(来新分类头的权重),我们将要训练这个模型因此可以忽略这两种警告。

    我们可以查看模型配置:

  2. 然后我们需要定义训练参数,以及登录 Hugging Face (如果不需要把训练结果推送到 HuggingFace,则无需登录并且设置 push_to_hub=False )。

    • 登录:

    • 定义训练参数:

  3. 接下来我们构建 Trainer 并启动训练:

    建议使用 GPU 训练,CPU 速度太慢。在 MacBook Pro 2018 (型号 A1990)上训练速度:0.38 iteration/s,在 RTX 4090 上训练速度 26.48 iteration/s

    如果 push_to_hub=True ,那么训练期间每次保存模型时(这里是每个 epooch 保存一次),trainer 都会在后台将 checkpoint 上传到 HuggingFace Model Hub 。这样,你就能够在另一台机器上继续训练。并且训练完成之后,可以上传模型的最终版本:

    Trainer 还创建了一张包含所有评估结果的 Model Card 并上传。在此阶段,你可以使用 Hugging Face Model Hub 上的 inference widget 来测试该模型,并分享给其它人。

1.4 使用微调后的模型

  1. 通过指定模型名称来使用微调后的模型:

1.5 自定义训练过程

  1. 我们也可以自定义训练过程,从而代替 Trainer API ,这样可以对训练过程进行更精细的控制。

二、微调 Masked Language Model

  1. 首先为 masked language modeling 选择一个合适的预训练模型,如前面用到的 "bert-base-cased"

    现在我们来看看 BERT_Base 如何补全一个被掩码的单词:

2.1 数据集和数据处理

  1. 加载数据集:我们在 Large Movie Review Dataset: IMDb 上微调 BERT_Base 。该数据集有训练集、测试集、还有 unsupervised 等三个 split

  2. 数据处理:对于自回归语言建模、以及掩码语言建模,一个常见的预处理步骤是拼接所有样本,然后将整个语料库拆分为相同大小的 block 。我们还需要保留 word id 序列,以便后续用于全词掩码( whole word masking )。

    此外,我们删除 text 字段和 label 字段,因为不再需要。我们构建一个函数来执行这些:

    现在我们已经完成了 tokenization。下一步是将它们拼接在一起然后分块。块的大小怎么选择?这取决于 GPU 的显存大小。此外,还可以参考模型的最大上下文的长度,这可以通过 tokenizer.model_max_length 属性来判断:

    然后我们拼接文本并拆分为大小为 block_size 的块。

    以训练集为例,可以看到,样本数量要比原始的 25k 个样本要多。因为现在的样本是 contiguous token,而不是原始的情感分类样本。

    现在缺少关键的步骤:在输入的随机位置插入 [MASK] token 。这需要在训练过程中动态地插入,而不是静态地提前准备好。

2.2 使用 Trainer API 微调模型

  1. 如前所述,我们需要再训练过程中动态地在输入的随机位置插入 [MASK] token 。这需要一个特殊的 data collator 从而可以在训练过程中动态地随机掩码输入文本中的一些 token,即 DataCollatorForLanguageModeling 。我们需要向它传入 mlm_probability 参数从而指定 masked token 的占比。我们选择 15%,因为这是论文中常用的配置:

    随机掩码的一个副作用是,当使用 Trainer 时,我们的评估指标可能是随机的(每一次评估的结果可能各不相同),因为测试集使用的也是相同的 DataCollatorForLanguageModeling 。然而,我们可以利用 Accelerate 来自定义训练过程(而不是 Trainer 封装好的训练过程),从而在训练过程中冻结随机性。

  2. 全词掩码 whole word masking: WWM:全词掩码是掩码整个单词,而不仅是是掩码单词内的单个 token 。如果我们想使用全词掩码,我们需要自己构建一个 data collator 。此时,我们需要用到之前计算的 word_ids,它给出了每个 token 对应的 word id 。注意,除了与 [MASK] 对应的 label 以外,所有的其他 label 都是 -100

  3. 数据集缩小:为了演示的方便,我们将训练集缩小为数千个样本。

  4. 配置 Trainer :接下来我们可以登录 Hugging Face Hub (可选的,方式为在命令行中执行命令 huggingface-cli login)。

    默认情况下, Trainer 将删除不属于模型的 forward() 方法的列。这意味着, 如果你使用 WWM data_collator ,你还需要设置 remove_unused_columns = False,以确保我们不会在训练期间丢失 word_ids 列。

  5. 语言模型的困惑度( perplexity ):一个好的语言模型是为语法正确的句子分配高概率,为无意义的句子分配低概率。我们通过困惑度来衡量这种概率。困惑度有多种数学定义,这里我们采用交叉熵损失的指数。因此,我们可以通过 Trainer.evaluate() 函数计算测试集上的交叉熵损失,然后取结果的指数来计算预训练模型的困惑度:

    较低的困惑度分数意味着更好的语言模型。可以看到:模型困惑度降低了很多。这表明模型已经了解了一些关于电影评论领域的知识。

  6. 使用模型:现在可以通过 Transformerspipeline 来调用微调后的模型:

2.3 自定义训练过程

  1. DataCollatorForLanguageModeling 对每次评估过程采用随机掩码,因此每次训练运行时,我们都会看到困惑度分数的一些波动。消除这种随机性来源的一种方法是在整个测试集上应用一次掩码,然后使用 Transformers 中的默认 data collator

    注意,自定义训练过程人工创建了 dataloader,而 Trainer API 只需要传入 dataset 而无需创建 dataloader

    整体代码如下所示:

三、从头开始训练因果语言模型

  1. 这里我们将采用不同的方法并从头开始训练一个全新的因果语言模型。为了减小数据规模从而用于演示,我们将使用 Python 代码的子集专注于单行代码的补全(而不是补全完整的函数或类)。

3.1 数据集和数据处理

  1. 加载数据集:CodeParrot 数据集来自于 Google's BigQuery 数据集, 使用了大约 180 GBGitHub dump,包含大约 20MPython 文件。创建过程:

    为了演示的效果,我们进一步仅考虑与 Python 数据科学相关的子集。我们使用过滤函数:

    然后我们用这个过滤函数来流式地过滤数据集:

    这个加载数据集并过滤的耗时非常长,可能需要数个小时。过滤之后保留了大约 3% 的原始数据,仍然高达 6GB ,包含大约 600kpython 文件。HuggingFace 提供好了过滤后的数据集:

  2. Tokenization:第一步是对数据集进行 tokenization。我们的目标单行代码的补全,因此可以选择较短的上下文。这样做的好处是,我们可以更快地训练模型并且需要更少的内存。如果你需要更长的上下文(如,补全函数、类、或自动生成单元测试),那么需要设置较长的上下文。

    这里我们选择上下文为 128token (相比之下,GPT2 选择了 1024tokenGPT3 选择了 2048token )。大多数文档包含超过 128token,如果简单地进行数据截断,那么将丢弃大多数的数据。相反,我们使用 return_overflowing_tokens 来执行 tokenizeation 从而返回几个块,并且还使用 return_length 来返回每个块的长度。通常最后一个块会小于上下文大小,我们会去掉这些块从而免于填充,因为这里的数据量已经足够用了。

    可以看到:从两个样本中我们得到了 34 个块,其中:

    • outputs['input_ids'] 存放每个块的数据。

    • outputs['length'] 存放每个块的长度。可以看到,每个文档的末尾的块,它的长度都小于 128 (分别为 11741)。由于这种不足 128 的块的占比很小,因此我们可以将它们丢弃。

    • outputs['overflow_to_sample_mapping'] 存放每个块属于哪个样本。

    然后我们把上述代码封装到一个函数中,并在 Dataset.map() 中调用:

    我们现在有 16.70M 样本,每个样本有 128token ,总计相当于大约 2.1B tokens 。作为参考,OpenAIGPT-3Codex 模型分别在 30B100Btoken 上训练,其中 Codex 模型从 GPT-3 checkpoint 初始化。

3.2 使用 Trainer API 微调模型

  1. 我们初始化一个 GPT-2 模型。我们采用与 GPT-2 相同的配置,并确保词表规模与 tokenizer 规模相匹配,然后设置 bos_token_ideos_token_id 。利用该配置,我们加载一个新模型。注意,这是我们首次不使用 from_pretrained() 函数,因为我们实际上是在自己初始化模型:

    该模型有 124.2 M 参数。

  2. 在开始训练之前,我们需要设置一个负责创建 batchdata collator 。我们可以使用 DataCollatorForLanguageModeling ,它是专为语言建模而设计。除了 batchpadding ,它还负责创建语言模型的标签:在因果语言建模中,input 也用作 label (只是右移一个位置),并且这个 data collator 在训练期间动态地创建 label ,所以我们不需要复制 input_ids

    注意 DataCollatorForLanguageModeling 支持掩码语言建模 (Masked Language Model: MLM ) 和因果语言建模 (Causal Language Model: CLM )。默认情况下它为 MLM 准备数据,但我们可以通过设置 mlm=False 参数切换到 CLM

    用法示例:

  3. 接下来就是配置 TrainingArguments 并启动 Trainer 。我们将使用余弦学习率,进行一些 warmup ,设置有效 batch size = 256per_device_train_batch_size * gradient_accumulation_steps )。gradient_accumulation_steps 用于梯度累积,它通过多次前向传播和反向传播但是只有一次梯度更新,从而实现 large batch size 的效果。当我们使用Accelerate 手动创建训练循环时,我们将看到这一点。

  4. 使用模型:现在可以通过 Transformerspipeline 来调用微调后的模型:

3.3 自定义训练过程

  1. 有时我们想要完全控制训练循环,或者我们想要进行一些特殊的更改,这时我们可以利用 Accelerate 来进行自定义的训练过程。

  2. 众所周知,数学科学的 package 中有一些关键字,如 plt, pd, sk, fit, predict 等等。我们仅关注那些表示为单个 token 的关键字,并且我们还关注那些带有一个空格作为前缀的关键字版本。

    我们可以计算每个样本的损失,并计算每个样本中所有关键字的出现次数。然后我们以这个出现次数为权重来对样本的损失函数进行加权,使得模型更加注重那些具有多个关键字的样本。这是一个自定义的损失函数,它将输入序列、logits、以及我们刚刚选择的关键字 token 作为输入,然后输出关键字频率加权的损失函数:

  3. 加载数据集:

  4. 设置 weight-decay:我们对参数进行分组,以便优化器知道哪些将获得额外的 weight-decay。通常,所有的 bias 项和 LayerNorm weight 都不需要 weight-decay

  5. 评估函数:由于我们希望在训练期间定期地在验证集上评估模型,因此我们也为此编写一个函数。它只是运行 eval_dataloader 并收集跨进程的所有损失函数值:

    这个评估函数用于获取损失函数值、以及困惑度。

  6. 完整的训练过程:

四、文本摘要

  1. 文本摘要将长的文章压缩为摘要,这需要理解文章内容并生成捕获了文档主题的连贯的文本。

4.1 数据集和数据处理

  1. 加载数据集:我们使用 “多语言亚马逊评论语料库” 来创建一个双语的 summarizer 。该语料库由六种语言的亚马逊商品评论组成,通常用于对多语言分类器进行基准测试。然而,由于每条评论都附有一个简短的标题,我们可以使用标题作为我们模型学习的 target 摘要 。

    • 首先下载数据集,这里下载英语和西班牙语的子集:

      可以看到,对每种语言,train split200k 条评论、validation split5k 条评论、test split5k 条评论。我们感兴趣的评论信息在 review_bodyreview_title 字段。

      我们可以创建一个简单的函数来查看一些样本:

    • 然后,我们进行样本过滤。在单个 GPU 上训练所有 400k 条评论(两种语言,每种语言的训练集包含 200k 条评论)的摘要模型将花费太长时间,这里我们选择书籍(包括电子书)类目的评论。

    • 然后,我们需要将英语评论和西班牙语评论合并为一个 DatasetDict 对象:

      现在,train/validation/test split 都是英语和西班牙语的混合评论。

    • 现在,我们过滤掉太短的标题。如果reference 摘要(在这里就是标题)太短,则使得模型偏向于仅生成包含一两个单词的摘要。

  2. 预处理数据:现在需要对评论极其标题进行 tokenization 和编码。

    • 首先加载 tokenizer。这里我们使用 mt5-base 模型。

      特殊的 Unicode 字符 和序列结束符 </s> 表明我们正在处理 SentencePiece tokenizerSentencePiece tokenizer 基于 Unigram tokenization 算法,该算法对多语言语料库特别有用,因为它允许 SentencePiece 在不知道重音、标点符号以及没有空格分隔字符(例如中文)的情况下对文本进行 tokenization

    • 为了对文本进行 tokenization,我们必须处理与摘要相关的细节:因为 label 也是文本,它也可能超过模型的最大上下文大小。这意味着我们需要同时对评论和标题进行截断,确保不会将太长的输入传递给模型。Transformers 中的 tokenizer 提供了一个 as_target_tokenizer() 函数,从而允许你相对于 input 并行地对 label 进行 tokenize

4.2 评估方法

  1. 评估指标:衡量文本生成任务(如文本摘要、翻译)的性能并不那么简单。最常用的指标之一是 Recall-Oriented Understudy for Gisting Evaluation: ROUGE 得分。该指标背后的基本思想是:将生成的摘要与一组参考摘要(通常由人类创建)进行比较。具体而言,假设我们比较如下的两个摘要:

    比较它们的一种方法是计算重叠单词的数量,在这种情况下为 6 。但是,这有点粗糙,因此 ROUGE 是基于重叠的单词来计算 precisionrecall

    • recall:衡量生成的摘要召回了参考摘要(reference summary)中的多少内容。如果只是比较单词,那么 recall 为:

      (1)Recall=Number of overlapping wordsTotal number of words in reference summary
    • precision:衡量生成的摘要中有多少内容是和参考摘要有关。如果只是比较单词,那么 precision 为:

      (2)Recall=Number of overlapping wordsTotal number of words in generated summary

    在实践中,我们通常计算 precisionrecall,然后报告 F1-score 。我们可以安装 rouge_score package,并在 datasets 中调用该指标。

    rouge_score.compute() 会一次性计算所有指标。输出的含义如下:

    • 首先,rouge_score 计算了 precision, recall, F1-score 的置信区间,即 low/mid/high 属性。

    • 其次,rouge_score 在比较生成的摘要和参考摘要时,会考虑不同的粒度:rouge1unigram 粒度、rouge2bigram 粒度。rougeLrougeLsum 通过在生成的摘要和参考摘要之间查找最长公共子串,从而得到重叠的单词序列。其中,rougeLsum 表示指标是在整个摘要上计算的,而 rougeL 为单个句子的指标的均值。因为上述例子只有一个句子,因此 rougeLsumrougeL 的输出结果相同。

  2. 强大的 baseline:文本摘要的一个常见baseline 是简单地取一篇文章的前三个句子,通常称之为 lead-3 baseline 。我们可以使用句号(英文使用 ".")来断句,但这在 "U.S." 或者 "U.N." 之类的首字母缩略词上会失败。所以我们将使用 nltk 库,它包含一个更好的算法来处理这些情况。

    由于文本摘要任务的约定是用换行符来分隔每个摘要,因此我们这里用 "\n" 来拼接前三个句子。

    然后我们实现一个函数,该函数从数据集中提取 lead-3 摘要并计算 baselineROUGE 得分:

    然后我们可以使用这个函数来计算验证集的 ROUGE 分数:

    我们可以看到 rouge2 分数明显低于其他的 rouge 分数。 这可能反映了这样一个事实,即评论标题通常很简洁,因此 lead-3 baseline 过于冗长。

4.3 使用 Trainer API 微调模型

  1. 我们首先加载预训练模型。由于文本摘要是一个 seq-to-seq 的任务,我们可以使用 AutoModelForSeq2SeqLM 类加载模型:

    对于 seq-to-seq 任务,AutoModelForSeq2SeqLM 模型保留了所有的网络权重。相反,文本分类任务重,预训练模型的 head 被随机初始化的网络所替代。

  2. 然后我们定义超参数和其它参数。我们使用专用的 Seq2SeqTrainingArgumentsSeq2SeqTrainer 类。

    predict_with_generate=True 会告诉 Seq2SeqTrainer 在评估时调用模型的 generate() 方法来生成摘要。

  3. 然后我们为 Trainer 提供一个 compute_metrics() 函数,以便在训练期间评估模型。这里稍微有点复杂,因为我们需要在计算 ROUGE 分数之前将 outputlabel 解码为文本,从而提供给 rouge_score.compute() 来使用。此外,还需要利用 nltk 中的 sent_tokenize() 函数来用换行符分隔摘要的句子:

  4. 接下来我们需要为 seq-to-seq 任务定义一个 data collator 。在解码过程中,对于 mT5,我们需要将 label 右移一位从而作为 decoder 的输入。Transformers 提供了一个 DataCollatorForSeq2Seq ,它为我们动态地填充 inputlabel 。要实例化这个collator ,我们只需要提供 tokenizermodel

    我们看看这个 collator 在输入一个 mini batch 样本时会产生什么。

    首先移除所有的类型为字符串的列,因为 collator 不知道如何处理这些列:

    由于 collator 需要一个 dict 的列表,其中每个 dict 代表数据集中的一个样本,我们还需要在将数据传递给 data collator 之前将数据整理成预期的格式:

    如果某一个样本比另一个样本要短,那么它的 input_ids/attention_mask 右侧将被填充 [PAD] tokentoken ID0 )。 类似地,我们可以看到 labels 已用 -100 填充,以确保 pad token 被损失函数忽略。最后,我们可以看到一个新的 decoder_input_ids,它通过在开头插入 [PAD] token 将标签向右移动来形成。

  5. 现在开始实例化 Trainer 并进行训练了:

  6. 使用微调的模型:

    我们可以将测试集中的一些样本(模型还没有看到)馈入 pipeline ,从而了解生成的摘要的质量。

    我们实现一个简单的函数来一起显示评论、标题、以及生成的摘要:

4.4 自定义训练过程

  1. 使用 Accelerate 来微调 mT5 的过程,与微调文本分类模型非常相似。区别在于这里需要在训练期间显式生成摘要,并定义如何计算ROUGE 分数。

  2. 创建 dataloader:我们需要做的第一件事是为每个数据集的每个 split 创建一个DataLoader。 由于 PyTorch dataloader 需要batch 的张量,我们需要在数据集中将格式设置为torch

  3. 创建训练组件:

  4. 后处理:

    • 将生成的摘要进行断句(拆分为 "\n" 换行的句子),这是 ROUGE 需要的格式。

    • Hugging Face Hub 创建一个 repository 来存储模型。如果不需要上传,那么这一步不需要。

  5. 开始训练:(4090 显卡,模型大小 2.5G,训练期间占用内存 22.7G )

  6. 使用:

五、翻译

  1. 翻译是另一个 seq-to-seq 任务,它非常类似于文本摘要任务。你可以将我们将在此处学习到的一些内容迁移到其他的 seq-to-seq 问题。

5.1 数据集和数据处理

  1. 加载数据集:我们将使用 KDE4 数据集,该数据集是 KDE 应用程序本地化文件的数据集。该数据集有 92 种语言可用,这里我们选择英语和法语。

    我们有 210,173 对句子,但是我们需要创建自己的验证集:

    我们可以查看数据集的一个元素:

    我们使用的预训练模型已经在一个更大的法语和英语句子语料库上进行了预训练。我们看看这个预训练模型的效果:

  2. 数据预处理:所有文本都需要转换为 token ID

    但是,对于 target ,需要将 tokenizer 包装在上下文管理器 "as_target_tokenizer()"中,因为不同的语言需要不同的 tokenization

5.2 使用 Trainer API 微调模型

  1. 这里我们使用 Seq2SeqTrainer,它是 Trainer 的子类,它可以正确处理这种 seq-to-seq 的评估,并使用 generate() 方法来预测输出。

    • 首先我们加载一个 AutoModelForSeq2SeqLM 模型:

    • 然后我们使用 DataCollatorForSeq2Seq 来创建一个 data_collator ,它不仅预处理输入,也同时预处理 label

      现在我们来测试这个 data_collator

      可以看到,label 已经用 -100 填充到 batch 的最大长度。

  2. 评估指标:相比较于父类 TrainerSeq2SeqTrainer 的一个额外功能是,在评估或预测期间调用 generate() 方法从而进行评估。

    用于翻译任务的传统指标是 BLUE 分数,它评估翻译与 label 的接近程度。BLUE 分数不衡量翻译结果的语法正确性或可读性,而是使用统计规则来确保生成输出中的所有单词也出现在 label 中。BLUE 的一个缺点是,它需要确保文本已被 tokenization ,这使得比较使用不同 tokenizer 的模型之间的 BLUE 分数变得困难。因此,目前用于翻译任务的常用指标是 SacreBLUE,它通过标准化 tokenization 步骤解决这个缺点(以及其他的一些缺点)。

    该指标接受多个 acceptable labels,因为同一个句子通常有多个可接受的翻译。在我们的这个例子中,每个句子只有一个 label 。因此,预测结果是关于句子的一个列表,而 reference 也是关于句子的一个列表。

    这得到了 46.75BLUE 得分,看起来相当不错。如果我们尝试使用翻译模型中经常出现的两种糟糕的预测类型(大量重复、或者太短),我们将得到相当糟糕的BLEU 分数:

    为了从模型输出转换为文本从而计算 BLUE 得分,我们需要使用 tokenizer.batch_decode() 方法来解码。注意,我们需要清理 label 中的所有 -100

  3. 执行微调:

    • 首先我们创建 Seq2SeqTrainingArguments 的实例,其中 Seq2SeqTrainingArgumentsTrainingArguments 的子类。

    • 然后我们创建 Seq2SeqTrainer

    • 训练之前,我们首先评估模型在验证集上的 BLUE 分数:

    • 然后开始训练(模型大小 300M,训练显存消耗 15.7G):

    • 训练完成之后,我们再次评估模型:

      可以看到 BLUE 分有 19.12 分的提高。

5.3 自定义训练过程

  1. 自定义训练过程如下:

  2. 调用微调好的模型:

六、问答

  1. 这里将使用SQuAD 问答数据集微调一个 BERT 模型。

6.1 数据集和数据处理

  1. 下载数据:

    contextquestion 字段使用起来非常简单。但是 answers 字段有点棘手,因为它是一个字典并且两个字段都是列表。这是在评估过程中 squad metric 所期望的格式。answers 中的text 给出了答案的文本,answer 中的 answer_start 字段给出了答案文本在 context 中的起始位置。

    在训练期间,只有一种可能的答案。我们可以使用 Dataset.filter() 方法来确认这一点:

    在评估期间,每个问题都有几个可能得答案:

  2. 数据处理:困难的部分将是为答案生成 label,这将是答案对应于上下文中对应的开始位置和结束位置。

    • 首先,我们需要使用 tokenizer 将输入中的文本转换为模型可以理解的ID

      你也可以使用其它模型,只要它实现了 fast tokenizer 即可。

      现在我们检查 tokenization

      这个 tokenizer 将会插入 special token,从而形成如下格式的句子:

    • 然后,上下文可能太长,因此需要截断:

      可以看到,我们的样本被分为四个输入,每个输入都包含了整个问题、以及一部分的上下文。

    • 然后我们需要在上下文中找到答案的开始位置和结束位置。我们需要得到字符位置到 token 位置之间的映射:

      对于多个样本:

      这些信息将有助于我们将答案映射到对应的 label,其中:

      • 如果 label(0,0),则表明答案不在上下文的相应范围内。

      • 否则,如果 label(start_position, end_position) ,则表明答案在上下文的相应范围内,其中 start_position 是答案开始的 token indexend_position 是答案结束的 token index

      注意,这要求答案不能太长,使得答案必须位于拆分后的某个新样本的上下文中。

      现在我们验证该方法是否正确:

    • 现在开始准备预处理函数:

      然后将这个函数应用到 Dataset.map() 方法:

  3. 处理验证数据集:预处理验证数据会稍微容易一些,因为我们不需要生成 label (除非我们想计算验证集损失,但这个指标并不能真正帮助我们理解模型有多好)。需要注意的是:我们要将模型的预测结果解释为原始上下文的跨度。为此, 我们只需要存储 offset_mapping 、以及某种方式来将每个创建的新样本与原始样本相匹配。

    注意,我们仅保留 contextoffset_mapping ,而将 questionoffset_mapping 设置为 None 。这么做是为了后处理步骤做准备。

6.2 使用 Trainer API 微调模型

  1. 在这个例子中,compute_metrics() 函数是个难点。由于我们人工降所有样本填充到我们设置的最大长度,因此无需定义 data_collator 。困难的部分是将模型预测进行后处理,从而得到原始样本中的文本范围。

  2. 后处理:模型输出的是答案开始位置的 logit、答案结束位置的 logit 。通常而言,我们需要做如下的处理:

    • 首先,屏蔽掉上下文之外的 token 所对应的 logit,因为答案必须位于上下文中。

    • 然后,我们使用 softmax 将这些 logit 转换为概率。

    • 然后,我们通过获取对应的两个概率的乘积,从而为每个 (start_token, end_token) 组合赋值。

    • 最后,我们寻找有效的、答案分数最高的 (start_token, end_token) 组合。这里有效指的是,例如,start_token 必须位于 end_token 之前。

    在这里我们稍微改变下这个流程,因为我们在 compute_metrics() 函数中不需要计算实际得分,因此可以跳过 softmax 的计算,然后通过 start logitend logit 之和来获得 (start_token, end_token) 组合的得分。这里我们没有用乘积,因为 log(ab)=loga+logb

    此外,我们也没有对所有可能的 (start_token, end_token) 组合进行评分,而是对 top n_beststart_tokentop n_bestend_token 的组合进行评分,其中 n_best = 20

    为了检验该做法的合理性,我们使用一个预训练好的模型来生成一些 prediction

    由于 Trainer 将为我们提供的 predictionNumPy 数组格式,我们也进行这种格式转换:

    现在,我们需要在原始的 small_eval_set 中找到模型对每个样本所预测的答案。一个原始样本可能已经在 eval_set 中拆分为多个新样本,因此第一步是将 small_eval_set 中的原始样本映射到 eval_set 中相应的新样本:

    现在我们开始执行如前所示的处理流程:

  3. 评估指标:我们使用 load_metric 来加载 squad 的评估指标:

    这个指标预期我们提供:

    • 预测答案:一个关于字典的列表,其中字典格式为 {"id": 样本id, "prediction_text": 预测文本}

      注意:如果答案太长导致超过 max_answer_length,或者因为原始样本截断导致答案被截断到两个新样本中,那么这个样本就没有预测答案。

    • 真实答案:一个关于字典的列表,其中字典格式为 {"id": 样本id, "answers": 一组真实的参考答案}

    现在我们计算预测答案的得分:

  4. 现在把刚才所做的一切放在 compute_metrics() 函数中,我们将在 Trainer 中使用它。通常,compute_metrics() 函数只接收一个 eval_preds,它包含 logitslabels 的元组。这里我们需要更多输入,因为我们必须在截断后的新数据集中查找 offset

    compute_metrics() 函数几乎与前面的步骤相同,这里我们只是添加一个小检查,从而防止我们没有提出任何有效的答案(在这种情况下,我们预测一个空字符串)。

    然后我们检验下该函数:

  5. 模型微调:

    • 首先创建模型:

      我们收到一个警告,有些权重没有使用(来自 pretrained head)、另一些权重是随机初始化的(用于问答任务的)。这仅仅是因为该模型是用于微调。

    • 然后,如果我们需要将结果推送到 HuggingFace Hub,则需要登录:

      也可以不登录。

    • 之后,我们创建 TrainingArguments

      注意,由于 compute_metrics() 的函数前面,它不支持常规的 evaluation loop 。一种解决办法是创建 Trainer 的子类;另一种方法是不在训练过程中评估,而是在训练结束后在自定义 loop 中进行评估。这里我们选择第二种办法。这就是 Trainer API 的局限性,也体现了 Accelerate 库的亮点:完全自定义的 training loop

    • 然后我们创建 Trainer

    • 模型评估:Trainerpredict() 方法将返回一个元组,其中第一个元素将是模型的预测(这里 (start_logits, end_logits) 组合)。我们将其发送给 compute_metrics() 函数:

      很好!作为比较,BERT 文章中报告的该模型的 baseline 分数是80.888.5,所以我们应该是正确的。

    • 最后,如果需要的话,我们可以把模型上传到 HuggingFace Hub

  6. 使用微调的模型:在 pipeline 中使用微调好的模型很简单:

6.3 自定义训练过程

  1. 首先创建 dataloader

  2. 然后创建训练组件并进行训练: