# 翻译 [[翻译]]

{#if fw === 'pt'}

{:else}

{/if}

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

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

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

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

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

与前面几节一样，你可以使用以下代码找到我们将训练并上传到 Hub 的实际模型，并 [在这里](https://huggingface.co/huggingface-course/marian-finetuned-kde4-en-to-fr?text=This+plugin+allows+you+to+automatically+translate+web+pages+between+several+languages.) 查看模型输出的结果。

## 准备数据 [[准备数据]]

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

### KDE4 数据集 [[KDE4 数据集]]

像往常一样，我们使用 `load_dataset()` 函数下载数据集：

```py
from datasets import load_dataset

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

如果你想使用其他的语言对，你可以使用语言代码来设置你想使用的语言对。该数据集共有 92 种语言可用；你可以通过展开 [数据集卡片](https://huggingface.co/datasets/kde4) 上的语言标签来查看数据集支持的语言标签。

我们来看看数据集：

```py
raw_datasets
```

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

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

```py
split_datasets = raw_datasets["train"].train_test_split(train_size=0.9, seed=20)
split_datasets
```

```python out
DatasetDict({
    train: Dataset({
        features: ['id', 'translation'],
        num_rows: 189155
    })
    test: Dataset({
        features: ['id', 'translation'],
        num_rows: 21018
    })
})
```

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

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

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

```py
split_datasets["train"][1]["translation"]
```

```python out
{'en': 'Default to expanded threads',
 'fr': 'Par défaut, développer les fils de discussion'}
```

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

```py
from transformers import pipeline

model_checkpoint = "Helsinki-NLP/opus-mt-en-fr"
translator = pipeline("translation", model=model_checkpoint)
translator("Default to expanded threads")
```

```python out
[{'translation_text': 'Par défaut pour les threads élargis'}]
```

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

```py
split_datasets["train"][172]["translation"]
```

```python out
{'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."}
```

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

```py
translator(
    "Unable to import %1 using the OFX importer plugin. This file is not the correct format."
)
```

```python out
[{'translation_text': "Impossible d'importer %1 en utilisant le plugin d'importateur OFX. Ce fichier n'est pas le bon format."}]
```

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

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

### 处理数据 [[处理数据]]

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

```python
from transformers import AutoTokenizer

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

你也可以将 `model_checkpoint` 替换为你从 [Hub](https://huggingface.co/models) 中选择的其他模型，或者一个保存了预训练模型和 tokenizer 的本地文件夹。

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

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

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

```python
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
```

```python out
{'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 模型来说，效果不会很好。

```python
wrong_targets = tokenizer(fr_sentence)
print(tokenizer.convert_ids_to_tokens(wrong_targets["input_ids"]))
print(tokenizer.convert_ids_to_tokens(inputs["labels"]))
```

```python out
['▁Par', '▁dé', 'f', 'aut', ',', '▁dé', 've', 'lop', 'per', '▁les', '▁fil', 's', '▁de', '▁discussion', '']
['▁Par', '▁défaut', ',', '▁développer', '▁les', '▁fils', '▁de', '▁discussion', '']
```

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

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

```python
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。

> [!TIP]
> 💡 如果你正在使用 T5 模型（更具体地说，一个 `t5-xxx` checkpoint ），模型会期望文本输入有一个前缀指示目前的任务，比如 `translate: English to French:` 。

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

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

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

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

{#if fw === 'pt'}

## 使用 `Trainer` API 微调模型 [[使用 `Trainer` API 微调模型]]

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

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

```py
from transformers import AutoModelForSeq2SeqLM

model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)
```

{:else}

## 使用 Keras 微调模型 [[使用 Keras 微调模型]]

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

```py
from transformers import TFAutoModelForSeq2SeqLM

model = TFAutoModelForSeq2SeqLM.from_pretrained(model_checkpoint, from_pt=True)
```

💡 `Helsinki-NLP/opus-mt-en-fr` checkpoint 只有 PyTorch 的权重，所以如果你尝试使用 `from_pretrained()` 方法加载模型并且忘记设置`from_pt=True` 参数的时候，你会得到一个错误。当你设定 `from_pt=True` 时，🤗transormer 会自动下载并为你转换 PyTorch 权重。如你所见，使用🤗transormer 在两种框架之间切换非常简单。

{/if}

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

### 数据整理 [[数据整理]]

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

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

{#if fw === 'pt'}

```py
from transformers import DataCollatorForSeq2Seq

data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)
```

{:else}

```py
from transformers import DataCollatorForSeq2Seq

data_collator = DataCollatorForSeq2Seq(tokenizer, model=model, return_tensors="tf")
```

{/if}

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

```py
batch = data_collator([tokenized_datasets["train"][i] for i in range(1, 3)])
batch.keys()
```

```python out
dict_keys(['attention_mask', 'input_ids', 'labels', 'decoder_input_ids'])
```

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

```py
batch["labels"]
```

```python out
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，可以看到它们是标签经过偏移后的结果：

```py
batch["decoder_input_ids"]
```

```python out
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]])
```

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

```py
for i in range(1, 3):
    print(tokenized_datasets["train"][i]["labels"])
```

```python out
[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]
```

{#if fw === 'pt'}

把 `data_collator` 传递给 `Seq2SeqTrainer` 后就完成了数据整理。接下来，让我们看一下评估指标。
{:else}

我们现在可以使用 `data_collator` 将我们的每个数据集转换为 `tf.data.Dataset` ，这样就完成了数据整理：

```python
tf_train_dataset = model.prepare_tf_dataset(
    tokenized_datasets["train"],
    collate_fn=data_collator,
    shuffle=True,
    batch_size=32,
)
tf_eval_dataset = model.prepare_tf_dataset(
    tokenized_datasets["validation"],
    collate_fn=data_collator,
    shuffle=False,
    batch_size=16,
)
```

{/if}

### 评估指标 [[评估指标]]

{#if fw === 'pt'}

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

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

{/if}

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

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

```py
!pip install sacrebleu
```

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

```py
import evaluate

metric = evaluate.load("sacrebleu")
```

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

让我们尝试一个例子：

```py
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)
```

```python out
{'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” 论文](https://arxiv.org/pdf/1706.03762.pdf) 类似的英语和法语翻译任务中获得了 41.8 的 BLEU 分数！（关于其他指标的含义，例如 `counts` 和 `bp` ，可以参见 [SacreBLEU仓库](https://github.com/mjpost/sacrebleu/blob/078c440168c6adc89ba75fe6d63f0d922d42bcfe/sacrebleu/metrics/bleu.py#L74) ）另一方面，如果我们尝试将翻译模型中经常出现的两种糟糕的预测类型（大量重复或太短）输入给指标计算的函数，我们将得到相当糟糕的 BLEU 分数：

```py
predictions = ["This This This This"]
references = [
    [
        "This plugin allows you to automatically translate web pages between several languages."
    ]
]
metric.compute(predictions=predictions, references=references)
```

```python out
{'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}
```

```py
predictions = ["This plugin"]
references = [
    [
        "This plugin allows you to automatically translate web pages between several languages."
    ]
]
metric.compute(predictions=predictions, references=references)
```

```python out
{'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，越高越好。

{#if fw === 'tf'}

为了将模型的输出转化为评估指标可以使用的文本，我们将利用 `tokenizer.batch_decode()` 方法。因为 tokenizer 会自动处理填充 `tokens`，所以我们只需要清理所有标签中的 填充的`-100` 。让我们定义一个函数，这个函数会接收一个模型和一个数据集，并在其上计算 BLEU 指标。

我们还将使用一个显著提升性能的技巧 - 使用 [XLA](https://www.tensorflow.org/xla) ，TensorFlow 的线性代数加速编译器，编译我们的生成代码。XLA 对模型的计算图进行了各种优化，从而显著提升了速度和内存使用率。如 Hugging Face 的 [博客](https://huggingface.co/blog/tf-xla-generate) 所述，当我们的输入形状比较整齐时，XLA 能够很好地提高计算效率。因此，我们需要用填充整理器制作一个新的数据集，把输入补齐到 128 的倍数，然后我们使用 `@tf.function(jit_compile=True)` 装饰器装饰我们的生成函数，这将把整个函数标记为使用 XLA 编译。

```py
import numpy as np
import tensorflow as tf
from tqdm import tqdm

generation_data_collator = DataCollatorForSeq2Seq(
    tokenizer, model=model, return_tensors="tf", pad_to_multiple_of=128
)

tf_generate_dataset = model.prepare_tf_dataset(
    tokenized_datasets["validation"],
    collate_fn=generation_data_collator,
    shuffle=False,
    batch_size=8,
)

@tf.function(jit_compile=True)
def generate_with_xla(batch):
    return model.generate(
        input_ids=batch["input_ids"],
        attention_mask=batch["attention_mask"],
        max_new_tokens=128,
    )

def compute_metrics():
    all_preds = []
    all_labels = []
    sampled_dataset = tokenized_datasets["validation"].shuffle().select(range(200))
    tf_generate_dataset = sampled_dataset.to_tf_dataset(
        columns=["input_ids", "attention_mask", "labels"],
        collate_fn=data_collator,
        shuffle=False,
        batch_size=4,
    )
    for batch, labels in tqdm(tf_generate_dataset):
        predictions = generate_with_xla(batch)
        decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)
        labels = labels.numpy()
        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]
        all_preds.extend(decoded_preds)
        all_labels.extend(decoded_labels)

    result = metric.compute(predictions=all_preds, references=all_labels)
    return {"bleu": result["score"]}
```

{:else}

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

```py
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"]}
```

{/if}

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

### 微调模型 [[微调模型]]

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

```python
from huggingface_hub import notebook_login

notebook_login()
```

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

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

```bash
huggingface-cli login
```

{#if fw === 'tf'}

在我们开始之前，让我们看看我们在没有任何训练的情况下我们的模型效果怎么样。
```py
print(compute_metrics())
```

```
{'bleu': 33.26983701454733}
```

现在可以准备编译和训练模型了。运行 `tf.keras.mixed_precision.set_global_policy("mixed_ﬂoat16")` 后， Keras 将使用 ﬂoat16 精度进行训练，这样可以显著提高支持半精度 GPU（Nvidia 20xx/V100 或更高版本）的训练速度

```python
from transformers import create_optimizer
from transformers.keras_callbacks import PushToHubCallback
import tensorflow as tf

# 训练步数是数据集中的样本数量,除以 batch 大小,然后乘以总的 epoch 数。
# 注意这里的 tf_train_dataset 是 batch 形式的 tf.data.Dataset,
# 而不是原始的 Hugging Face Dataset ,所以使用 len() 计算它的长度已经是 num_samples // batch_size。

num_epochs = 3
num_train_steps = len(tf_train_dataset) * num_epochs

optimizer, schedule = create_optimizer(
    init_lr=5e-5,
    num_warmup_steps=0,
    num_train_steps=num_train_steps,
    weight_decay_rate=0.01,
)
model.compile(optimizer=optimizer)

# 使用 float16 混合精度进行训练
tf.keras.mixed_precision.set_global_policy("mixed_float16")
```

接下来就像我们在 [第 2 节](/course/chapter7/2) 中学到的，我们将定义一个 `PushToHubCallback` 回调函数，并拟合模型的时候添加该回调函数，这样就可以训练期间将我们的模型上传到 Hub。

```python
from transformers.keras_callbacks import PushToHubCallback

callback = PushToHubCallback(
    output_dir="marian-finetuned-kde4-en-to-fr", tokenizer=tokenizer
)

model.fit(
    tf_train_dataset,
    validation_data=tf_eval_dataset,
    callbacks=[callback],
    epochs=num_epochs,
)
```

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

> [!TIP]
> 💡如果正在使用的输出目录已经存在一个同名的文件夹，则它应该是目标推送仓库的在本地克隆在本地的版本。如果不是，当调用 `model.fit()` 时会收到错误，并需要设置一个新的路径。

最后，让我们看看训练结束后我们的模型的 BLEU 的分数：

```py
print(compute_metrics())
```

```
{'bleu': 57.334066271545865}
```

在这个阶段，你可以使用模型中心上的推理小部件来测试你的模型并与你的朋友分享。恭喜你，你已经成功地在翻译任务上微调了一个模型！

{:else}

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

```python
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` 组织](https://huggingface.co/huggingface-course) 时，我们在 `Seq2SeqTrainingArguments` 添加了 `hub_model_id="huggingface-course/marian-finetuned-kde4-en- to-fr"` 。默认情况下，该仓库将保存在你的账户中，并以你设置的输出目录命名，因此在我们的例子中它是 `"sgugger/marian-finetuned-kde4-en-to-fr"` 。

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

最后，我们将所有内容传递给 `Seq2SeqTrainer` ：

```python
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 分数，以确保我们的微调并未使情况变得更糟。这个命令需要一些时间，所以你可以在执行期间去喝杯咖啡：

```python
trainer.evaluate(max_length=max_length)
```

```python out
{'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 并不算太差，这反映了我们的模型已经擅长将英语句子翻译成法语句子。

接下来是训练，这也需要一些时间：

```python
trainer.train()
```

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

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

```py
trainer.evaluate(max_length=max_length)
```

```python out
{'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 为推理演示选择小部件的元数据，通常情况下我们不需要做额外的更改，因为它可以从模型类中推断出正确的小部件，但在这个示例中，它只能通过模型类推断这是一个序列到序列的问题，所以我们补充一下具体的模型类别。

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

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

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

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

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

{/if}

{#if fw === 'pt'}

## 自定义训练循环 [[自定义训练循环]]

我们现在来看一下完整的训练循环，这样你就可以轻松定制你需要的部分。它将与我们在 [第 2 节](https://chat.openai.com/course/chapter7/2) 和 [第 3 节](https://chat.openai.com/course/chapter3/4) 中做的非常相似。

### 准备训练所需的一切 [[准备训练所需的一切]]

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

```py
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
)
```

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

```py
model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)
```

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

```py
from torch.optim import AdamW

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

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

```py
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 的经典线性学习率调度：

```py
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()` 函数来查看：

```py
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
```

```python out
'sgugger/marian-finetuned-kde4-en-to-fr-accelerate'
```

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

```py
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` 对象所需要的字符串列表。：

```py
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 节](/course/chapter7/2) 与 [第三章](/course/chapter3) 中代码很相似，只是在评估部分有一些不同 —— 所以让我们重点关注一下这一点！

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

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

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

```py
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
        )
```

```python out
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](https://huggingface.co/huggingface-course/marian-finetuned-kde4-en-to-fr-accelerate) 上查看我们使用此代码训练的模型。如果你想测试对训练循环的任何调整，你可以直接通过编辑上面的代码来实现！

{/if}

## 使用微调后的模型 [[使用微调后的模型]]

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

```py
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")
```

```python out
[{'translation_text': 'Par défaut, développer les fils de discussion'}]
```

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

```py
translator(
    "Unable to import %1 using the OFX importer plugin. This file is not the correct format."
)
```

```python out
[{'translation_text': "Impossible d'importer %1 en utilisant le module externe d'importation OFX. Ce fichier n'est pas le bon format."}]
```

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

> [!TIP]
> ✏️ **轮到你了！** 把之前找到的包含单词“email”样本输入模型，会返回什么结果？

