翻译
现在让我们深入了解翻译。这又是另一个序列到序列任务,这意味着它是一个可以被表述为从一个序列到另一个序列的问题。从这个意义上说,这个问题与摘要非常接近,你可以将我们在这里看到的应用到其他序列到序列问题,例如
- 风格转换:创建一个模型来翻译以特定风格编写的文本到另一种风格(例如,正式到非正式或莎士比亚式英语到现代英语)
- 生成式问答:创建一个模型,在给定上下文的条件下生成问题的答案
如果你拥有两种(或更多)语言的大量文本语料库,你可以像我们在因果语言建模部分中做的那样,从头开始训练一个新的翻译模型。然而,微调现有的翻译模型会更快,无论是像 mT5 或 mBART 这样的多语言模型,你想要将其微调到特定语言对,还是专门针对从一种语言到另一种语言翻译的模型,你想要将其微调到你的特定语料库。
在本节中,我们将微调一个 Marian 模型,该模型预训练为将英语翻译成法语(因为很多 Hugging Face 员工都讲这两种语言),并使用KDE4 数据集进行训练,该数据集是 KDE 应用程序的本地化文件数据集。我们将使用的模型已经使用来自Opus 数据集的大量法语和英语文本语料库进行了预训练,该数据集实际上包含 KDE4 数据集。但即使我们使用的预训练模型在预训练期间看到了这些数据,我们也会看到在微调之后,我们可以获得一个更好的模型版本。
完成之后,我们将拥有一个能够像下面这样进行预测的模型
与前面的章节一样,你可以使用下面的代码找到我们将要训练并上传到 Hub 的实际模型,并仔细检查其预测在这里。
准备数据
为了从头开始微调或训练翻译模型,我们需要一个适合该任务的数据集。如前所述,在本节中,我们将使用KDE4 数据集,但只要你拥有想要从中翻译和翻译到的两种语言的句子对,就可以轻松地将代码改编为使用你自己的数据。如果你需要回顾如何将自定义数据加载到 Dataset
中,请参阅第 5 章。
KDE4 数据集
像往常一样,我们使用 load_dataset()
函数下载我们的数据集
from datasets import load_dataset
raw_datasets = load_dataset("kde4", lang1="en", lang2="fr")
如果你想使用其他语言对,你可以通过它们的代码指定它们。这个数据集总共有 92 种语言可用;你可以在其数据集卡片上展开语言标签查看所有语言。
让我们看一下数据集
raw_datasets
DatasetDict({
train: Dataset({
features: ['id', 'translation'],
num_rows: 210173
})
})
我们有 210,173 对句子,但只有一个拆分,因此我们需要创建自己的验证集。正如我们在第 5 章中所看到的,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" 这个词中看到,这个词并非官方的法语词,但大多数母语人士都能理解它,并且不会费心去翻译。在本数据集中,这个词被翻译成了更官方的法语 "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"。在训练数据集中找到第一个使用这个单词的样本。它是如何翻译的?预训练模型如何翻译相同的英语句子?
处理数据
现在你应该知道流程了:所有文本都需要转换为一组词元 ID,以便模型能够理解它们。为了完成此任务,我们需要对输入和目标进行分词。我们的首要任务是创建tokenizer
对象。如前所述,我们将使用 Marian 英语到法语预训练模型。如果你想使用其他语言对进行尝试,请确保修改模型检查点。 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,或者是你保存了预训练模型和分词器的本地文件夹。
💡如果你使用的是多语言分词器,例如 mBART、mBART-50 或 M2M100,你需要通过设置tokenizer.src_lang
和tokenizer.tgt_lang
为正确的值,在分词器中设置输入和目标的语言代码。
我们数据的准备非常简单。只需记住一点:你需要确保分词器使用输出语言(此处为法语)处理目标。你可以通过将目标传递给分词器__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]}
正如我们所看到的,输出包含与英语句子相关的输入 ID,而与法语句子相关的 ID 存储在labels
字段中。如果你忘记指出你正在对标签进行分词,它们将被输入分词器分词,对于 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>']
正如我们所看到的,使用英语分词器对法语句子进行预处理会导致更多的词元,因为分词器不知道任何法语单词(除了英语中也出现的单词,例如“discussion”)。
由于inputs
是一个包含我们常用键(输入 ID、注意力掩码等)的字典,因此最后一步是定义我们将应用于数据集的预处理函数
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
检查点中的一个),该模型将期望文本输入带有指示当前任务的前缀,例如translate: English to French:
。
⚠️我们不关注目标的注意力掩码,因为模型不需要它。相反,与填充词元对应的标签应设置为-100
,这样它们在损失计算中就会被忽略。这将由我们稍后的数据整理器完成,因为我们正在应用动态填充,但如果你在这里使用填充,则应调整预处理函数,将所有与填充词元对应的标签设置为-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)
请注意,这次我们使用的是一个在翻译任务上训练的模型,它实际上已经可以使用了,因此不会出现缺少权重或新初始化权重的警告。
数据整理
我们需要一个数据整理器来处理动态批处理的填充。在这个例子中,我们不能像在第 3 章中那样简单地使用DataCollatorWithPadding
,因为它只填充输入(输入 ID、注意力掩码和词元类型 ID)。我们的标签也应该填充到标签中遇到的最大长度。并且,如前所述,用于填充标签的填充值应为-100
,而不是分词器的填充词元,以确保这些填充值在损失计算中被忽略。
所有这些都是由DataCollatorForSeq2Seq
完成的。与DataCollatorWithPadding
一样,它接受用于预处理输入的tokenizer
,但它也接受model
。这是因为这个数据整理器还将负责准备解码器输入 ID,这些 ID 是标签的移位版本,在开头有一个特殊词元。由于这种移位对于不同的架构略有不同,因此DataCollatorForSeq2Seq
需要知道model
对象
from transformers import DataCollatorForSeq2Seq
data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)
为了对几个样本进行测试,我们只需在我们的分词训练集中的示例列表上调用它
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["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]])
我们也可以看看解码器输入 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
。接下来,让我们看看指标。
指标
Seq2SeqTrainer
在其超类Trainer
中添加的功能是能够在评估或预测期间使用generate()
方法。在训练期间,模型将使用decoder_input_ids
和注意力掩码,确保它不使用它正在尝试预测的词元之后的词元,以加快训练速度。在推断期间,我们将无法使用这些词元,因为我们没有标签,因此最好在相同的设置下评估我们的模型。
正如我们在第 1 章中看到的,解码器通过逐个预测词元来执行推断 - 这由 🤗 Transformers 背后的generate()
方法实现。如果我们设置predict_with_generate=True
,Seq2SeqTrainer
将允许我们使用该方法进行评估。
传统上用于翻译的指标是BLEU 分数,它在2002 年 Kishore Papineni 等人发表的一篇文章中被提出。BLEU 分数评估翻译与标签的接近程度。它不衡量模型生成的输出的可理解性或语法正确性,而是使用统计规则来确保生成的输出中的所有词元也出现在目标中。此外,还有一些规则会惩罚相同词元的重复,如果它们没有在目标中重复(以避免模型输出诸如"the the the the the"
之类的句子)以及比目标中句子更短的输出句子(以避免模型输出诸如"the"
之类的句子)。
BLEU 的一个弱点是它期望文本已经被分词,这使得很难比较使用不同分词器的模型之间的分数。因此,如今用于基准测试翻译模型的最常用指标是SacreBLEU,它通过标准化分词步骤解决了这个弱点(以及其他弱点)。要使用这个指标,我们首先需要安装 SacreBLEU 库
!pip install sacrebleu
然后我们可以像我们在第 3 章中所做的那样,通过evaluate.load()
加载它
import evaluate
metric = evaluate.load("sacrebleu")
这个指标将接受文本作为输入和目标。它被设计为接受多个可接受的目标,因为同一个句子的翻译通常有多个可接受的版本 - 我们正在使用的 dataset 只提供一个,但 NLP 中很常见的是, dataset 提供多个句子作为标签。因此,预测应该是一个句子列表,但参考应该是一个句子列表的列表。
让我们尝试一个例子
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 模型在英语到法语的类似翻译任务上取得了 41.8 的 BLEU 分数!(有关各个指标(如counts
和bp
)的更多信息,请参阅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()
方法。我们只需要清理标签中的所有-100
(分词器会自动对填充标记执行相同的操作)。
import numpy as np
def compute_metrics(eval_preds):
preds, labels = eval_preds
# In case the model returns more than the prediction logits
if isinstance(preds, tuple):
preds = preds[0]
decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)
# Replace -100s in the labels as we can't decode them
labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
# Some simple post-processing
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,以便能够将您的结果上传到模型中心。笔记本中有一个便利函数可以帮助您完成此操作。
from huggingface_hub import notebook_login
notebook_login()
这将显示一个窗口,您可以在其中输入您的Hugging Face登录凭据。
如果您不在笔记本中工作,请在您的终端中键入以下代码行
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
,这可以加快现代GPU上的训练速度。 - 我们设置
predict_with_generate=True
,如上所述。 - 我们使用
push_to_hub=True
在每个迭代结束时将模型上传到中心。
请注意,您可以使用hub_model_id
参数指定要推送到其中的存储库的完整名称(特别是,您必须使用此参数才能推送到组织)。例如,当我们将模型推送到huggingface-course
组织时,我们添加了hub_model_id="huggingface-course/marian-finetuned-kde4-en-to-fr"
到Seq2SeqTrainingArguments
。默认情况下,使用的存储库将位于您的命名空间中,并以您设置的输出目录命名,因此在我们的例子中,它将是"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,
)
在训练之前,我们将首先查看模型的得分,以双重检查我们在微调时没有使事情变得更糟。此命令需要一些时间,因此您可以在执行时去喝一杯咖啡。
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}
39 的 BLEU 分数还不错,这反映了我们的模型已经很擅长将英语句子翻译成法语句子。
接下来是训练,这也需要一些时间。
trainer.train()
请注意,在训练进行时,每次模型被保存(这里,每个迭代)时,它都会在后台上传到中心。这样,您就可以在需要时在另一台机器上恢复训练。
训练完成后,我们再次评估模型 - 希望我们会看到 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
还会草拟一个包含所有评估结果的模型卡并上传它。此模型卡包含元数据,有助于模型中心为推理演示选择小部件。通常情况下,不需要说任何话,因为它可以从模型类推断出正确的小部件,但在这种情况下,相同的模型类可以用于各种序列到序列问题,因此我们指定它是一个翻译模型。
trainer.push_to_hub(tags="translation", commit_message="Training complete")
此命令将返回它刚刚执行的提交的 URL,如果您想查看它。
'https://huggingface.co/sgugger/marian-finetuned-kde4-en-to-fr/commit/3601d621e3baae2bc63d3311452535f8f58f6ef3'
在这个阶段,您可以使用模型中心上的推理小部件来测试您的模型并与您的朋友分享。您已经成功地对一个模型进行了翻译任务的微调 - 祝贺您!
如果您想更深入地了解训练循环,我们将向您展示如何使用 🤗 Accelerate 做同样的事情。
自定义训练循环
现在让我们看看完整的训练循环,这样您就可以轻松地自定义您需要的部分。它看起来很像我们在第二节和第三章中所做的。
准备所有训练所需内容
您已经多次看到这些内容,因此我们将快速浏览代码。首先,我们将从我们的数据集构建DataLoader
,在将数据集设置为"torch"
格式之后,以便我们获得 PyTorch 张量。
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 笔记本中使用 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,
)
最后,为了将我们的模型推送到中心,我们需要在一个工作文件夹中创建一个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
中保存的任何内容。这将有助于我们在每个迭代结束时上传中间模型。
训练循环
我们现在准备好编写完整的训练循环了。为了简化它的评估部分,我们定义了这个postprocess()
函数,它接受预测和标签,并将它们转换为我们的metric
对象期望的字符串列表。
def postprocess(predictions, labels):
predictions = predictions.cpu().numpy()
labels = labels.cpu().numpy()
decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True)
# Replace -100 in the labels as we can't decode them.
labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
# Some simple post-processing
decoded_preds = [pred.strip() for pred in decoded_preds]
decoded_labels = [[label.strip()] for label in decoded_labels]
return decoded_preds, decoded_labels
训练循环看起来很像第二节和第三章中的循环,评估部分有一些区别 - 因此让我们关注这些!
首先要注意的是,我们使用generate()
方法来计算预测,但这在我们基本模型上的方法,而不是prepare()
方法中创建的封装模型 🤗 Accelerate。这就是为什么我们首先解开模型,然后调用此方法的原因。
第二件事是,就像令牌分类一样,两个进程可能已经将输入和标签填充到不同的形状,因此我们在调用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):
# Training
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)
# Evaluation
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"]
# Necessary to pad predictions and labels for being gathered
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}")
# Save and upload
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
完成此操作后,您应该有一个模型,其结果与使用Seq2SeqTrainer
训练的模型非常相似。您可以在huggingface-course/marian-finetuned-kde4-en-to-fr-accelerate中查看我们使用此代码训练的模型。如果您想测试对训练循环的任何调整,您可以通过编辑上面显示的代码直接实现它们!
使用微调模型
我们已经向您展示了如何在模型中心使用我们微调的模型以及推理小部件。要将其在pipeline
中本地使用,我们只需指定正确的模型标识符。
from transformers import pipeline
# Replace this with your own 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."}]
另一个领域适应的绝佳示例!
✏️ 轮到你了! 模型在您之前识别的包含“电子邮件”一词的样本中返回了什么?