NLP Course documentation

翻译

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

翻译

Open In Colab Open In Studio Lab

现在让我们深入研究翻译。这是另一个 sequence-to-sequence 任务 ,着这是一个可以表述为输入是一个序列输出另一个序列的问题。从这个意义上说,这个问题非常类似 文本摘要 ,并且你可以将我们将在此处学习到的一些技巧迁移到其他的序列到序列问题,例如:

  • 风格迁移 创建一个模型将某种风格迁移到一段文本(例如,正式的风格迁移到休闲的风格,或从莎士比亚英语迁移到现代英语)。
  • 生成问题的回答 创建一个模型,在给定上下文的情况下生成问题的答案。

如果你有足够大的两种(或更多)语言的文本语料库,你可以从头开始训练一个新的翻译模型,就像我们在 因果语言建模 部分中所做的那样。然而,微调现有的翻译模型会更快,无论是从像 mT5 或 mBART 这样的多语言模型微调到特定的语言对,还是从特定语料库的一种语言到另一种语言的专用翻译模型。

在这一节中,我们将在 KDE4 数据集 上微调一个预训练的 Marian 模型,用来把英语翻译成法语的(因为很多 Hugging Face 的员工都会说这两种语言)。KDE4 数据集是一个 KDE 应用 本地化的数据集。我们将使用的模型已经在从 Opus 数据集 (实际上包含 KDE4 数据集)中提取的法语和英语文本的大型语料库上进行了预先训练。不过,即使我们使用的预训练模型在其预训练期间使用了这部分数据集,我们也会看到,经过微调后,我们可以得到一个更好的版本。

完成后,我们将拥有一个模型,可以进行这样的翻译:

One-hot encoded labels for question answering.

与前面几节一样,你可以使用以下代码找到我们将训练并上传到 Hub 的实际模型,并 在这里 查看模型输出的结果。

准备数据

为了从头开始微调或训练翻译模型,我们需要一个适合该任务的数据集。如前所述,我们将使用 KDE4 数据集 。只要数据集中有互译的两种语言的句子对,就可以很容易地调整本节的代码以使用自己的数据集进行微调。如果你需要复习如何将自定义数据加载到 Dataset ,可以复习一下 第五章

KDE4 数据集

像往常一样,我们使用 load_dataset() 函数下载数据集:

from datasets import load_dataset

raw_datasets = load_dataset("kde4", lang1="en", lang2="fr")

如果你想使用其他的语言对,你可以使用语言代码来设置你想使用的语言对。该数据集共有 92 种语言可用;你可以通过展开 数据集卡片 上的语言标签来查看数据集支持的语言标签。

Language available for the KDE4 dataset.

我们来看看数据集:

raw_datasets
DatasetDict({
    train: Dataset({
        features: ['id', 'translation'],
        num_rows: 210173
    })
})

我们下载的数据集有 210,173 对句子,在一次训练过程中,除了训练集,我们也需要创建自己的验证集。正如我们在 第五章 学的的那样, Dataset 有一个 train_test_split() 方法可以帮助我们。我们将设置一个固定的随机数种子以保证结果可以复现:

split_datasets = raw_datasets["train"].train_test_split(train_size=0.9, seed=20)
split_datasets
DatasetDict({
    train: Dataset({
        features: ['id', 'translation'],
        num_rows: 189155
    })
    test: Dataset({
        features: ['id', 'translation'],
        num_rows: 21018
    })
})

我们可以像下面这样将 test 键重命名为 validation

split_datasets["validation"] = split_datasets.pop("test")

现在让我们看一下数据集的一个元素:

split_datasets["train"][1]["translation"]
{'en': 'Default to expanded threads',
 'fr': 'Par défaut, développer les fils de discussion'}

我们得到一个包含我们选择的两种语言的两个句子的字典。这个充满技术计算机科学术语的数据集的一个特殊之处在于它们都完全用法语翻译。然而现实中,法国工程师在交谈时,大多数计算机科学专用词汇都用英语表述。例如,“threads”这个词很可能出现在法语句子中,尤其是在技术对话中;但在这个数据集中,它被翻译成更准确的“fils de Discussion”。我们使用的预训练模型已经在一个更大的法语和英语句子语料库上进行了预训练,所以输出的是原始的英语表达:

from transformers import pipeline

model_checkpoint = "Helsinki-NLP/opus-mt-en-fr"
translator = pipeline("translation", model=model_checkpoint)
translator("Default to expanded threads")
[{'translation_text': 'Par défaut pour les threads élargis'}]

这种情况的另一个例子可以在“plugin”这个词上看到,它并非正式的法语词汇,但大多数母语是法语的人都能够看懂并且不会去翻译它。不过,在 KDE4 数据集中,这个词被翻译成了更正式的法语词汇“module d’extension”:

split_datasets["train"][172]["translation"]
{'en': 'Unable to import %1 using the OFX importer plugin. This file is not the correct format.',
 'fr': "Impossible d'importer %1 en utilisant le module d'extension d'importation OFX. Ce fichier n'a pas un format correct."}

然而,我们的预训练模型坚持使用简练而熟悉的英文单词:

translator(
    "Unable to import %1 using the OFX importer plugin. This file is not the correct format."
)
[{'translation_text': "Impossible d'importer %1 en utilisant le plugin d'importateur OFX. Ce fichier n'est pas le bon format."}]

看看我们的微调模型是否能学习到数据集的这些特殊特性。(剧透警告:它能)。

✏️ 轮到你了! 另一个在法语中经常使用的英语单词是“email”。在训练数据集中找到使用这个词的第一个样本。在数据集中它是如何翻译的?预训练模型如何翻译同一个英文句子?

处理数据

你现在应该可以预测我们的下一步该做些什么了:将所有文本转换为 token IDs 的集合,这样模型才可以理解它们。对于这个任务,我们需要同时对原始文本和翻译后的文本同时进行 tokenize。首先,我们需要创建 tokenizer 对象。如前所述,我们将使用 Marian 英语到法语的预训练模型。如果你使用下面的代码微调另一对语言,请记得更改下面代码中的 checkpoint。 Helsinki-NLP 组织提供了超过一千个多语言模型。

from transformers import AutoTokenizer

model_checkpoint = "Helsinki-NLP/opus-mt-en-fr"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, return_tensors="pt")

你也可以将 model_checkpoint 替换为你从 Hub 中选择的其他模型,或者一个保存了预训练模型和 tokenizer 的本地文件夹。

💡 如果你在使用一个多语言的 tokenizer,比如 mBART,mBART-50,或者 M2M100,你需要通过设置 tokenizer.src_langtokenizer.tgt_lang 来在 tokenizer 中指定输入和目标的语言代码。

我们的数据准备相当简单。只有一点要记住;你需要确保 tokenizer 处理的目标是输出语言(在这里是法语)。你可以通过将目标语言传递给 tokenizer 的 __call__ 方法的 text_targets 参数来完成此操作。

为了演示设置的方法,让我们处理训练集中的一个样本:

en_sentence = split_datasets["train"][1]["translation"]["en"]
fr_sentence = split_datasets["train"][1]["translation"]["fr"]

inputs = tokenizer(en_sentence, text_target=fr_sentence)
inputs
{'input_ids': [47591, 12, 9842, 19634, 9, 0], 'attention_mask': [1, 1, 1, 1, 1, 1], 'labels': [577, 5891, 2, 3184, 16, 2542, 5, 1710, 0]}

我们可以看到,输出包含了与英语句子的 inputs IDs,而与法语句子的 IDs 存储在 labels 字段中。如果你忘记设置 labels 的 tokenizer,默认情况下 labels 将由输入的 tokenizer(语言类型不一样) 进行 tokenize,对于 Marian 模型来说,效果不会很好。

wrong_targets = tokenizer(fr_sentence)
print(tokenizer.convert_ids_to_tokens(wrong_targets["input_ids"]))
print(tokenizer.convert_ids_to_tokens(inputs["labels"]))
['▁Par', '▁dé', 'f', 'aut', ',', '▁dé', 've', 'lop', 'per', '▁les', '▁fil', 's', '▁de', '▁discussion', '</s>']
['▁Par', '▁défaut', ',', '▁développer', '▁les', '▁fils', '▁de', '▁discussion', '</s>']

如你所见,如果用英语的 tokenizer 来预处理法语句子,会产生更多的 tokens,因为这个 tokenizer 不认识任何法语单词(除了那些在英语里也出现的,比如“discussion”)。

最后一步是定义我们数据集的预处理函数:

max_length = 128


def preprocess_function(examples):
    inputs = [ex["en"] for ex in examples["translation"]]
    targets = [ex["fr"] for ex in examples["translation"]]
    model_inputs = tokenizer(
        inputs, text_target=targets, max_length=max_length, truncation=True
    )
    return model_inputs

请注意,上述代码也为输入和输出设置了相同的最大长度。由于要处理的文本看起来很短,因此在这里将最大长度设置为 128。

💡 如果你正在使用 T5 模型(更具体地说,一个 t5-xxx checkpoint ),模型会期望文本输入有一个前缀指示目前的任务,比如 translate: English to French:

⚠️ 我们不需要对待遇测的目标设置注意力掩码,因为模型序列到序列的不会需要它。不过,我们应该将填充(padding) token 对应的标签设置为 -100 ,以便在 loss 计算中忽略它们。由于我们正在使用动态填充,这将在稍后由我们的数据整理器完成,但是如果你在此处就打算进行填充,你应该调整预处理函数,将所有填充(padding) token 对应的标签设置为 -100

我们现在可以一次性使用上述预处理处理数据集的所有数据。

tokenized_datasets = split_datasets.map(
    preprocess_function,
    batched=True,
    remove_columns=split_datasets["train"].column_names,
)

现在数据已经过预处理,我们准备好微调我们的预训练模型了!

使用 Trainer API 微调模型

使用 Trainer 的代码将与以前相同,只是稍作改动:我们在这里将使用 Seq2SeqTrainer ,它是 Trainer 的子类,它使用 generate() 方法来预测输入的输出结果,并且可以正确处理这种序列到序列的评估。当我们讨论评估指标时,我们将更详细地探讨这一点。

首先,我们需要一个模型来进行微调。我们将使用常用的 AutoModel API:

from transformers import AutoModelForSeq2SeqLM

model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)

注意,这次我们使用的是一个已经在翻译任务上进行过训练的模型,实际上已经可以直接使用了,所以没有收到关于缺少权重或重新初始化的权重的警告。

数据整理

在这个任务中,我们需要一个数据整理器来动态批处理填充。因此,我们不能像 第三章 那样直接使用 DataCollatorWithPadding ,因为它只填充输入的部分(inputs ID、注意掩码和 token 类型 ID)。我们的标签也应该被填充到所有标签中最大的长度。而且,如前所述,用于填充标签的填充值应为 -100 ,而不是 tokenizer 默认的的填充 token,这样才可以在确保在损失计算中忽略这些填充值。

上述的这些需求都可以由 DataCollatorForSeq2Seq 完成。它与 DataCollatorWithPadding 一样,它接收用于预处理输入的 tokenizer ,同时它也接收一个 model 参数。这是因为数据整理器还将负责准备解码器 inputs ID,它们是标签偏移之后形成的,开头带有特殊 token 。由于对于不同的模型架构有稍微不同的偏移方式,所以 DataCollatorForSeq2Seq 还需要接收 model 对象:

from transformers import DataCollatorForSeq2Seq

data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)

为了在几个样本上进行测试,我们在已经完成 tokenize 的训练集中的部分数据上调用它,测试一下其功能:

batch = data_collator([tokenized_datasets["train"][i] for i in range(1, 3)])
batch.keys()
dict_keys(['attention_mask', 'input_ids', 'labels', 'decoder_input_ids'])

我们可以检查我们的标签是否已经用 -100 填充到 batch 的最大长度:

batch["labels"]
tensor([[  577,  5891,     2,  3184,    16,  2542,     5,  1710,     0,  -100,
          -100,  -100,  -100,  -100,  -100,  -100],
        [ 1211,     3,    49,  9409,  1211,     3, 29140,   817,  3124,   817,
           550,  7032,  5821,  7907, 12649,     0]])

我们还可以查看解码器的 inputs ID,可以看到它们是标签经过偏移后的结果:

batch["decoder_input_ids"]
tensor([[59513,   577,  5891,     2,  3184,    16,  2542,     5,  1710,     0,
         59513, 59513, 59513, 59513, 59513, 59513],
        [59513,  1211,     3,    49,  9409,  1211,     3, 29140,   817,  3124,
           817,   550,  7032,  5821,  7907, 12649]])

以下是我们数据集中第一个和第二个元素的标签:

for i in range(1, 3):
    print(tokenized_datasets["train"][i]["labels"])
[577, 5891, 2, 3184, 16, 2542, 5, 1710, 0]
[1211, 3, 49, 9409, 1211, 3, 29140, 817, 3124, 817, 550, 7032, 5821, 7907, 12649, 0]

data_collator 传递给 Seq2SeqTrainer 后就完成了数据整理。接下来,让我们看一下评估指标。

评估指标

Seq2SeqTrainerTrainer 类的一个子类,它的主要增强特性是在评估或预测时使用 generate() 方法。在训练过程中,模型会利用 decoder_input_ids 和一个特殊的注意力掩码来加速训练。这种方法允许模型在预测下一个token时看到部分目标序列,但确保它不会使用预测token之后的信息。这种优化策略显著提高了训练效率。然而,在实际的推理过程中,我们没有真实的标签值,因此无法生成 decoder_input_ids 和相应的注意力掩码。这意味着我们无法在推理时使用这种训练时的优化方法。

为了确保评估结果能够准确反映模型在实际使用中的表现,我们应该在评估阶段模拟真实推理的条件。这意味着我们需要使用在 第一章 中介绍的 🤗 Transformers 库中的 generate() 方法。该方法能够逐个生成token,真实地模拟推理过程,而不是依赖于训练时的优化技巧。要启用这个功能,我们需要在训练时添加 predict_with_generate=True 参数。这样做可以确保我们的评估结果更加接近模型在实际应用中的表现。

用于翻译的传统指标是 BLEU 分数 ,它最初由 Kishore Papineni 等人在 2002 年的 一篇文章 中引入。BLEU 分数评估翻译与参考翻译的接近程度。它不衡量模型生成输出的可理解性或语法正确性,而是使用统计规则来确保生成输出中的所有单词也出现在参考的输出中。此外,还有一些规则对重复的词进行惩罚,如果这些词在输出中重复出现(模型输出像“the the the the the”这样的句子);或者输出的句子长度比目标中的短(模型输出像“the”这样的句子)都会被惩罚。

BLEU 的一个缺点是的输入是已分词的文本,这使得比较使用不同分词器的模型之间的分数变得困难。因此,当今用于评估翻译模型的最常用指标是 SacreBLEU ,它通过标准化的分词步骤解决了这个缺点(和其他的一些缺点)。要使用此指标,我们首先需要安装 SacreBLEU 库:

!pip install sacrebleu

然后我们可以就像在 第三章 那样通过 evaluate.load() 加载它

import evaluate

metric = evaluate.load("sacrebleu")

SacreBLEU 指标中待评估的预测和参考的目标译文输入的格式都是文本。它的设计是为了支持多个参考翻译,因为同一句话通常有多种可接受的翻译——虽然我们使用的数据集只提供一个,但在 NLP 中找到将多个句子作为标签的数据集是很常见的。因此,预测结果应该是一个句子列表,而参考应该是一个句子列表的列表。

让我们尝试一个例子:

predictions = [
    "This plugin lets you translate web pages between several languages automatically."
]
references = [
    [
        "This plugin allows you to automatically translate web pages between several languages."
    ]
]
metric.compute(predictions=predictions, references=references)
{'score': 46.750469682990165,
 'counts': [11, 6, 4, 3],
 'totals': [12, 11, 10, 9],
 'precisions': [91.67, 54.54, 40.0, 33.33],
 'bp': 0.9200444146293233,
 'sys_len': 12,
 'ref_len': 13}

达到了 46.75 的 BLEU 分数,这是相当不错的——作为参考,原始 Transformer 模型在 “Attention Is All You Need” 论文 类似的英语和法语翻译任务中获得了 41.8 的 BLEU 分数!(关于其他指标的含义,例如 countsbp ,可以参见 SacreBLEU仓库 )另一方面,如果我们尝试将翻译模型中经常出现的两种糟糕的预测类型(大量重复或太短)输入给指标计算的函数,我们将得到相当糟糕的 BLEU 分数:

predictions = ["This This This This"]
references = [
    [
        "This plugin allows you to automatically translate web pages between several languages."
    ]
]
metric.compute(predictions=predictions, references=references)
{'score': 1.683602693167689,
 'counts': [1, 0, 0, 0],
 'totals': [4, 3, 2, 1],
 'precisions': [25.0, 16.67, 12.5, 12.5],
 'bp': 0.10539922456186433,
 'sys_len': 4,
 'ref_len': 13}
predictions = ["This plugin"]
references = [
    [
        "This plugin allows you to automatically translate web pages between several languages."
    ]
]
metric.compute(predictions=predictions, references=references)
{'score': 0.0,
 'counts': [2, 1, 0, 0],
 'totals': [2, 1, 0, 0],
 'precisions': [100.0, 100.0, 0.0, 0.0],
 'bp': 0.004086771438464067,
 'sys_len': 2,
 'ref_len': 13}

分数可以从 0 到 100,越高越好。

为了将模型的输出转化为评估指标可以使用的文本,我们将使用 tokenizer.batch_decode() 方法。因为 tokenizer 会自动处理填充的 tokens,所以我们只需要清理标签中的所有 -100 token:

import numpy as np


def compute_metrics(eval_preds):
    preds, labels = eval_preds
    # 如果模型返回的内容超过了预测的logits
    if isinstance(preds, tuple):
        preds = preds[0]

    decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)

    # 由于我们无法解码 -100,因此将标签中的 -100 替换掉
    labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
    decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)

    # 一些简单的后处理
    decoded_preds = [pred.strip() for pred in decoded_preds]
    decoded_labels = [[label.strip()] for label in decoded_labels]

    result = metric.compute(predictions=decoded_preds, references=decoded_labels)
    return {"bleu": result["score"]}

现在这已经完成了,我们已经准备好微调我们的模型了!

微调模型

第一步是登录 Hugging Face,这样你就可以在训练过程中将结果上传到 Hub中。有一个方便的功能可以帮助你在 notebook 中完成登陆:

from huggingface_hub import notebook_login

notebook_login()

这将显示一个小部件,你可以在其中输入你的 Hugging Face 登录凭据。

如果你不是在 notebook 上运行代码,可以在终端中输入以下命令:

huggingface-cli login

完成这些步骤之后,我们就可以定义我们的 Seq2SeqTrainingArguments 了。与 Trainer 一样,它是 TrainingArguments 的子类,其中包含更多可以设置的字段:

from transformers import Seq2SeqTrainingArguments

args = Seq2SeqTrainingArguments(
    f"marian-finetuned-kde4-en-to-fr",
    evaluation_strategy="no",
    save_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=32,
    per_device_eval_batch_size=64,
    weight_decay=0.01,
    save_total_limit=3,
    num_train_epochs=3,
    predict_with_generate=True,
    fp16=True,
    push_to_hub=True,
)

除了通常的超参数(如学习率、训练轮数、批次大小和一些权重衰减)之外,这里的部分参数与我们在前面章节看到的有一些不同:

  • 我们没有设置定期进行评估,因为评估需要耗费一定的时间;我们将只在训练开始之前和结束之后评估我们的模型一次。
  • 我们设置 fp16=True ,这可以加快在支持 fp16 的 GPU 上的训练速度。
  • 和之前我们讨论的一样,我们设置 predict_with_generate=True
  • 我们设置了 push_to_hub=True ,在每个 epoch 结束时将模型上传到 Hub。

请注意,你可以使用 hub_model_id 参数指定要推送到的存储库的名称(当你想把模型推送到指定的组织的时候,就必须使用此参数)。例如,当我们将模型推送到 huggingface-course 组织 时,我们在 Seq2SeqTrainingArguments 添加了 hub_model_id="huggingface-course/marian-finetuned-kde4-en- to-fr" 。默认情况下,该仓库将保存在你的账户中,并以你设置的输出目录命名,因此在我们的例子中它是 "sgugger/marian-finetuned-kde4-en-to-fr"

💡如果你使用的输出目录已经存在一个同名的文件夹,则它应该是推送的仓库克隆在本地的版本。如果不是,你将在定义你的 Seq2SeqTrainer 名称时会遇到错误,并且需要设置一个新名称。

最后,我们将所有内容传递给 Seq2SeqTrainer

from transformers import Seq2SeqTrainer

trainer = Seq2SeqTrainer(
    model,
    args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    tokenizer=tokenizer,
    compute_metrics=compute_metrics,
)

在开始训练之前,我们先查看一下我们的模型目前的 BLEU 分数,以确保我们的微调并未使情况变得更糟。这个命令需要一些时间,所以你可以在执行期间去喝杯咖啡:

trainer.evaluate(max_length=max_length)
{'eval_loss': 1.6964408159255981,
 'eval_bleu': 39.26865061007616,
 'eval_runtime': 965.8884,
 'eval_samples_per_second': 21.76,
 'eval_steps_per_second': 0.341}

BLEU 得分为 39 并不算太差,这反映了我们的模型已经擅长将英语句子翻译成法语句子。

接下来是训练,这也需要一些时间:

trainer.train()

请注意,在训练过程中,每当保存模型时(这里是每个 epoch),它都会在后台将模型上传到 Hub。这样,如有必要,你将能够在另一台机器上继续你的训练。

训练完成后,我们再次评估我们的模型——希望我们会看到 BLEU 分数有所提高!

trainer.evaluate(max_length=max_length)
{'eval_loss': 0.8558505773544312,
 'eval_bleu': 52.94161337775576,
 'eval_runtime': 714.2576,
 'eval_samples_per_second': 29.426,
 'eval_steps_per_second': 0.461,
 'epoch': 3.0}

可以看到近 14 点的改进,这很棒!

最后,我们使用 push_to_hub() 方法来确保我们上传了模型最新的版本。 Trainer 还创建了一张包含所有评估结果的模型卡片并上传到 Hub 。这个模型卡片包含了可以帮助 Hub 为推理演示选择小部件的元数据,通常情况下我们不需要做额外的更改,因为它可以从模型类中推断出正确的小部件,但在这个示例中,它只能通过模型类推断这是一个序列到序列的问题,所以我们补充一下具体的模型类别。

trainer.push_to_hub(tags="translation", commit_message="Training complete")

如果你想检查命令执行的结果,此命令将返回它刚刚执行的提交的 URL,你可以打开 url 进行检查:

'https://huggingface.co./sgugger/marian-finetuned-kde4-en-to-fr/commit/3601d621e3baae2bc63d3311452535f8f58f6ef3'

在此阶段,你可以在 Model Hub 上使用推理小部件来测试你的模型,并与你的朋友分享。你已经成功地在翻译任务上进行了模型的微调,恭喜你!

如果你想更深入地了解训练循环,我们现在将向你展示如何使用 🤗 Accelerate 做同样的事情。

自定义训练循环

我们现在来看一下完整的训练循环,这样你就可以轻松定制你需要的部分。它将与我们在 第 2 节第 3 节 中做的非常相似。

准备训练所需的一切

由于这里的步骤在之前的章节已经出现过很多次,因此这里只做简略说明。首先,我们将数据集设置为 torch 格式,这样可以将数据集的格式转换为 PyTorch 张量,然后我们用数据集构建 DataLoader

from torch.utils.data import DataLoader

tokenized_datasets.set_format("torch")
train_dataloader = DataLoader(
    tokenized_datasets["train"],
    shuffle=True,
    collate_fn=data_collator,
    batch_size=8,
)
eval_dataloader = DataLoader(
    tokenized_datasets["validation"], collate_fn=data_collator, batch_size=8
)

接下来我们重新实例化我们的模型,以确保我们不会继续上一节的微调,而是再次从预训练模型开始重新训练:

model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)

然后我们需要一个优化器:

from transformers import AdamW

optimizer = AdamW(model.parameters(), lr=2e-5)

准备好这些对象,我们就可以将它们发送到 accelerator.prepare() 方法中。请记住,如果你想在 Colab Notebook 上使用 TPU 进行训练,你需要将所有这些代码移动到一个训练函数中,并且这个训练函数不应该包含实例化 Accelerator 的代码。换句话说,Accelerator 的实例化应该在这个函数之外进行。这么做的原因是,TPU 在 Colab 中的工作方式有些特殊。TPU 运行时会重新执行整个单元格的代码,因此如果 Accelerator 的实例化在训练函数内部,它可能会被多次实例化,导致错误。

from accelerate import Accelerator

accelerator = Accelerator()
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
    model, optimizer, train_dataloader, eval_dataloader
)

现在我们已经将我们的 train_dataloader 发送到 accelerator.prepare() 方法中了,现在我们可以使用它的长度来计算训练步骤的数量。请记住,我们应该始终在准备好数据加载器后再执行此操作,因为更改数据加载器会改变 DataLoader 的长度。然后,我们使用学习率衰减到 0 的经典线性学习率调度:

from transformers import get_scheduler

num_train_epochs = 3
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch

lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)

最后,为了将我们的模型推送到 Hugging Face Hub,我们需要在一个工作文件夹中创建一个 Repository 对象。如果你尚未登录 Hugging Face,请先进行登录。我们将根据模型 ID 来确定仓库名称。你可以使用自己选择的名称替换 repo_name,但请确保包含你的用户名。如果你不确定当前的用户名,可以使用get_full_repo_name() 函数来查看:

from huggingface_hub import Repository, get_full_repo_name

model_name = "marian-finetuned-kde4-en-to-fr-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
'sgugger/marian-finetuned-kde4-en-to-fr-accelerate'

然后我们可以在本地文件夹中克隆该存储库。如果已经存在一个同名的文件夹,这个本地文件夹应该是我们正在使用的存储库克隆到本地的版本:

output_dir = "marian-finetuned-kde4-en-to-fr-accelerate"
repo = Repository(output_dir, clone_from=repo_name)

现在,我们可以通过调用 repo.push_to_hub() 方法上传我们在 output_dir 中保存的所有文件。这将帮助我们在每个 epoch 结束时上传中间模型。

训练循环

我们现在准备编写完整的训练循环。为了简化其评估部分,我们定义了这个 postprocess() 函数用于接收预测值和参考翻译对于的标签值,并将其转换为 metric 对象所需要的字符串列表。:

def postprocess(predictions, labels):
    predictions = predictions.cpu().numpy()
    labels = labels.cpu().numpy()

    decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)

    # 替换标签中的 -100,因为我们无法解码它们。
    labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
    decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)

    # 一些简单的后处理
    decoded_preds = [pred.strip() for pred in decoded_preds]
    decoded_labels = [[label.strip()] for label in decoded_labels]
    return decoded_preds, decoded_labels

训练循环看起来和本章 第 2 节第三章 中代码很相似,只是在评估部分有一些不同 —— 所以让我们重点关注一下这一点!

首先要注意的是,我们使用 generate() 方法来计算预测,但这是我们基础模型上的一个方法,而不是🤗 Accelerate 在 prepare() 方法中创建的封装模型。这就是为什么我们首先 unwrap_model ,然后调用此方法。

首先要注意的是,我们用来计算预测的 generate() 函数是基础模型上的一个方法,而不是🤗 Accelerate 在 prepare() 函数中创建的封装模型。这就是在调用此函数之前先调用unwrap_model,的原因。

第二个要注意的是,就像 token 分类 一样,在训练和评估这两个过程可能以不同的形状对输入和标签进行了填充,所以我们在调用 gather() 函数之前使用 accelerator.pad_across_processes() 方法,使预测和标签具有相同的形状。如果我们不这么做,那么评估的过程将会出错或被永远挂起。

from tqdm.auto import tqdm
import torch

progress_bar = tqdm(range(num_training_steps))

for epoch in range(num_train_epochs):
    # 训练
    model.train()
    for batch in train_dataloader:
        outputs = model(**batch)
        loss = outputs.loss
        accelerator.backward(loss)

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

    # 评估
    model.eval()
    for batch in tqdm(eval_dataloader):
        with torch.no_grad():
            generated_tokens = accelerator.unwrap_model(model).generate(
                batch["input_ids"],
                attention_mask=batch["attention_mask"],
                max_length=128,
            )
        labels = batch["labels"]

        # 需要填充预测和标签才能调用gather()
        generated_tokens = accelerator.pad_across_processes(
            generated_tokens, dim=1, pad_index=tokenizer.pad_token_id
        )
        labels = accelerator.pad_across_processes(labels, dim=1, pad_index=-100)

        predictions_gathered = accelerator.gather(generated_tokens)
        labels_gathered = accelerator.gather(labels)

        decoded_preds, decoded_labels = postprocess(predictions_gathered, labels_gathered)
        metric.add_batch(predictions=decoded_preds, references=decoded_labels)

    results = metric.compute()
    print(f"epoch {epoch}, BLEU score: {results['score']:.2f}")

    # 保存和上传
    accelerator.wait_for_everyone()
    unwrapped_model = accelerator.unwrap_model(model)
    unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
    if accelerator.is_main_process:
        tokenizer.save_pretrained(output_dir)
        repo.push_to_hub(
            commit_message=f"Training in progress epoch {epoch}", blocking=False
        )
epoch 0, BLEU score: 53.47
epoch 1, BLEU score: 54.24
epoch 2, BLEU score: 54.44

训练完成之后,你就有了一个模型,最终的 BLEU 分数应该与 Seq2SeqTrainer 训练的模型非常相似。你可以在 huggingface-course/marian-finetuned-kde4-en-to-fr-accelerate 上查看我们使用此代码训练的模型。如果你想测试对训练循环的任何调整,你可以直接通过编辑上面的代码来实现!

使用微调后的模型

我们已经向你展示了如何在模型 Hub 上使用我们微调的模型。要在本地的 pipeline 中使用它,我们只需要指定正确的模型标识符:

from transformers import pipeline

# 将其替换成你自己的 checkpoint
model_checkpoint = "huggingface-course/marian-finetuned-kde4-en-to-fr"
translator = pipeline("translation", model=model_checkpoint)
translator("Default to expanded threads")
[{'translation_text': 'Par défaut, développer les fils de discussion'}]

和预想的一样,我们的预训练模型适应了我们微调它的语料库,没有保留英语单词“threads”,而是将它翻译成官方的法语版本。对于“plugin”也是如此:

translator(
    "Unable to import %1 using the OFX importer plugin. This file is not the correct format."
)
[{'translation_text': "Impossible d'importer %1 en utilisant le module externe d'importation OFX. Ce fichier n'est pas le bon format."}]

这是另一个领域适应的好例子!

✏️ 轮到你了! 把之前找到的包含单词“email”样本输入模型,会返回什么结果?

< > Update on GitHub