添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Hugging Face教程 - 7.3、使用huggingface做主流NLP训练任务(文本翻译模型,也可以是序列到序列模型)

文本翻译

下面我们看下如何使用transformers库来进行翻译模型的训练及相关操作。可以认为翻译是另外一种 sequence-to-sequence seq2seq任务 ,该任务将一个序列通过某种变换成另外一个序列。这个问题和 摘要 任务非常相似(摘要是将一个较长的文本转换为较短的文本)。本节也会介绍一些其他的seq2seq问题。

  • 风格转换Style transfer : 创建一个模型,将一种放歌的文本转换为另外一种风格的文本(例如将传统或莎士比亚风格的英语转换为现代英语)
  • 生成式问答 : 创建一个模型,根据一段文本,来生成一个问题的答案。

如果读者有海量两个成对或多个成对的文本语料时,你就可以训练类似 causal language modeling 这种自编码模式的新型翻译模型(ChatGPT就是这么做的)。这样的模型速度会比较快,但是如果我们要微调一个现有的翻译模型,例如将多语言翻译模型如mT5或mBART微调为只对一个指定语言对进行翻译,或者将一个指定翻译模型,将一种语言翻译为另外一种你语料库对应的语言。(这一段好神奇,但是GPT的神奇能力相比大家在ChatGPT上已经看到了)

在本节中,我们微调一个预训练的Marian模型,实现从英文到法文的翻译(Hugging Face的很多雇员大多使用这两种语言)。数据集为 KDE4 dataset ,该数据集来自于 KDE apps 。该模型已经在大型法语和英文语料库上进行了预训练,该语料库为 Opus dataset ,该数据集包含KDE4数据集。尽管我们的模型已经海量数据集上进行了预训练,但是我们依然可以通过微调获得更好的效果。

一旦完成了训练,我们可以通过gradio来构建我们的应用,具体参考huggingface上例子。

​ 与前面章节类似,我们训练的模型和上传的模型,可以查看链接 here .

准备数据

为了微调或从头训练一个翻译模型,我们需要一个合适的数据集(啥才能称为合适?)。如前所述,我们在本节中使用 KDE4数据集 ,另外通过微调下面的代码可以很方便使用读者自定义的数据集,只要该数据集是按照成对的翻译文本出现即可(当然要满足一定的结构,具体编码时就知道了)。如果想了解更多关于自定义数据集加载的知识,可以复习 第5章 内容。

KDE4数据集

通常,可以使用 load_dataset() 函数加载数据集。

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

如果想获得另外的语言对,那么可以指定参数 lang1 lang2 为其他值。在kde4数据集中有92种语言可选,具体可参考链接 dataset card

上面代码完成数据集的加载和缓存,下面看下加载后的数据集情况。

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

语句对有210,173个,但是只有一个 train split,因此我们需要手工创建验证集,具体可看 第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 的split更名为 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"经常在法文句子中出现,尤其在技术博客中;但是在这个数据集中,"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 在法语中也是常用的,可以看下训练数据集中是否有这样的例子。它是如何翻译成法文的?以及观察预训练模型是如何翻译带有 email 单词的英文文本的。

准备数据

输入到模型的数据都必须是整数,也就是需要将文本转换为token IDs的集合,模型才能够进行计算,并得到结果。对于翻译任务,需要对输入和标签都进行分词处理。我们首先创建一个 tokenizer 对象。这里我们使用一个已经在英文转法文数据集上做过预训练的模型。如果需要处理其它的语言,则要相应的调整你的模型 checkpoint。 Helsinki-NLP 这个开源组织提供了很多的语言对预训练模型(可以打开链接看看)。

 from transformers import AutoTokenizer
 model_checkpoint = "Helsinki-NLP/opus-mt-en-fr"
 tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, return_tensors="tf")

可以从 Hub 替换你需要的 model_checkpoint ,或者从本地加载你自己预训练的模型和分词器。

如果使用多语言分词器,例如mBART、mBART-50或M2M100,你需要通过设置 tokenizer.src_lang tokenizer.tgt_lang 来指定输入和输出的分词器。

进行分词的时候,需要注意的是,在对输入进行分词时,就和前面章节的操作类似。但是对于目标文本进行分词时,需要加上上下文管理器 as_target_tokenizer() 。(当然,如果输入和输出是同一种语言的话,就不需要了,因为输入和标签用的是同一种分词器)

Python中的上下文管理器,使用 with 关键词(在操作磁盘文件的时候经常用到),如下所示:

 with open(file_path) as f:
     content = f.read()

上面已经隐含了文件的打开和关闭操作(代码可读性提高了)。打开文件的句柄 f with 包含的语句片段有效,离开后会关闭句柄并释放资源。

在本例中,我们在对输入进行分词操作时,调用 with 改变分词器为法文分词器,在法文文本分词结束后回到英文分词器,代码如下所示(下面为对一个文本对进行操作的例子)。

 en_sentence = split_datasets["train"][1]["translation"]["en"]
 fr_sentence = split_datasets["train"][1]["translation"]["fr"]
 inputs = tokenizer(en_sentence)
 with tokenizer.as_target_tokenizer():
     targets = tokenizer(fr_sentence)

如果没有使用 with 进行分词器转换,那么对目标也会使用英文分词器,结果如下(不是法文分词,同是拉丁语系,可能还好,但是如果不同语系,也许会得到很差的结果,也许吧)。

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

如上所属,英文分词会把法语句子分词为非常多的token,而且有些法文单词没有被分词器认出来。

inputs targets 是python字典,包含 input_ids attention_mask token_type_ids 等键,需要设置 labels 键给模型输入。这个事情在预处理函数中完成,如下所示。

 max_input_length = 128
 max_target_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, max_length=max_input_length, truncation=True)
     # Set up the tokenizer for targets
     with tokenizer.as_target_tokenizer():
         labels = tokenizer(targets, max_length=max_target_length, truncation=True)
     model_inputs["labels"] = labels["input_ids"]
     return model_inputs

注意这里将输入和输出设置了相同的最大token长度,因为我们数据集中的文本都比较短,这里使用128。

提示:如果使用T5模型,模型希望输入文本提供一个前缀,对于翻译模型,添加 translate: English to French:
提示:这里不关注标签的掩码 attention_mask ,因为模型不需要(但是计算标签准确率的时候,可以用这个mask)。不用 attention_mask 的一点是,我们在设置标签的时候,会将padding部分使用-100代替,-100在计算损失函数的时候会忽略掉这些padding部分。这部分操作会在后面的data collator函数中完成,我们在data collator中进行动态padding,即只针对一个batch进行padding。如果在上面的预处理函数中进行padding,则必须对labels中对应的padding部分进行赋值-100操作(这是静态padding了)。

下面使用 map 函数完成对数据集中所有数据的预处理。

 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)

这里我们使用一个在翻译任务已经预训练的模型,因此加载后不会警告说有新初始化的权重。

Data collation

在动态batching中我们需要一个data collator完成padding。这里不适用 DataCollatorWithPadding 来进行补齐操作,因为这个函数仅对输入的键(包括 input_ids, attention_mask, token_type_ids )进行补齐,不会对 labels 进行补齐操作。还有在对 labels 进行补齐操作时,使用的是-100而不是分词器的pad_token,这么做到的目的是在计算损失函数的时候忽略掉这些padding token。

DataCollatorForSeq2Seq 完成上述操作(还真是方便,但是建议读者还是进入源代码,看下具体的操作,这样使用的时候会更有信心)。预 DataCollatorWithPadding 类似, DataCollatorForSeq2Seq 使用 tokenizer 分词器来预处理输入,但是它也会适应 model 。这是因为data_collator需要准备好解码器的输入ids,这个输入ids是标签的右移版本,在第一个位置上添加一个特殊的token。因为不同模型会有不同的移动方式,因此 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'])

可以发现,我们的 labels 使用 -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]])

然后看下解码器的输入,和上面的标签对比发现,其是标签的一个平移,且在第一个位置添加 59513 这个token 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]

后面会向 Seq2SeqTrainer 传入 data_collator 函数。下面我们看下指标计算。

指标

Seq2SeqTrainer Trainer 的基础添加了使用 generate() 函数来进行评估和预测。在训练期间,模型使用带下三角注意力掩码的 decoder_input_ids 来保证预测下一个token时,只能看到该token之前的token(并行计算,来加速训练)。而在推理阶段,不会使用这个操作,因为我们无法实现知道预测标签,因此我们使用相同的设置来评估模型效果。

正如 第1章 所述,解码器是一个token接一个token进行序贯预测的。在Transformers库中,定义了 generate() 函数来完成这个操作。如果我们设置参数 predict_with_generate=True ,那么我们 Seq2SeqTrainer 运行我们使用该函数完成模型评估。

翻译任务的一个经典指标是 BLEU得分 ,由Kishore Papineni et al. 在 a 2002 article 这篇论文中提出。BLEU得分计算翻译结果和标签之间的相似关系。它无法衡量模型生成结果是否够典雅或是否存在语法问题,而是使用统计规则来计算在生成文本中出现单词是否也出现在标签中。另外,对于在标签中单词重复出现进行一定的惩罚(防止模型输出类似 "the the the the the" ),以及如果输入句子长度比标签短,也会进行惩罚(防止输出句子只有一个单词,例如 the )。

BLEU指标的一个缺点是希望文本已经分词了,这使得对于使用不同分词器的模型之间计算得分变得困难。一般我们使用 SacreBLEU 作为翻译结果的指标,克服了上述缺点。为了使用这个指标,首先需要安装SacreBLEU库,如下。

 !pip install sacrebleu

第3章 类似,我们使用函数 evaluate.load() 加载指标。

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

指标使用原始输入和目标文本作为计算输入,该函数可以接受多个标签语句,这和常识想通(一个句子可以被翻译成多个意思接近的句子)。我们使用的数据集只提供了一个翻译结果,但是其他数据集有时候会提供多个标签语句。因此预测值必须是一个语句序列,但是标签必须是一个语句序列的序列(有两个中括号)。

Let’s try an example:

 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}

BLEU得分是46.75,结果还不错(满分是100,难道和目标检测mAP一样,40多就很不错了!)。在论文 “Attention Is All You Need” 中首次提出transformer架构,在翻译任务上达到了41.8的BLEU得分(这个得分就已经不错了)。上面指标结果中,例如 counts bp ,可以查看链接 SacreBLEU repository 。上面是测试翻译结果较好的BLEU,下面我们看下翻译交叉的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,0最差,100最好。

为了将模型输出转换为文本来计算指标,我们使用函数 tokenizer.batch_decode() 。同时,需要将标签中的 -100 也给替换为补齐用的token。

 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 Hub上。下面是登录代码。

 from huggingface_hub import notebook_login
 notebook_login()

上面的代码返回一个登录界面,可以输入用户名和密码。

如果不是在notebook上运行代码,可以在命令行下运行下面的命令。

 huggingface-cli login

登录之后,定义参数实例 Seq2SeqTrainingArguments 。该类对应序列到序列任务。在 Seq2SeqTrainer 的参数用上面的类来定义,如下。

 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,
 )

TrainingArguments 的常见参数(常见参数有学习率、训练轮数、批大小和一些权重衰减)不同,有一些不一样的参数,如下:

  • 没有设置常规验证机制,我们在整个模型训练完成之后再进行验证;
  • 设置 fp16=True , 用来加速GPU的训练;
  • 设置 predict_with_generate=True ,预测的时候使用 generate() 函数;
  • 设置 push_to_hub=True ,在每个epoch结束后将模型上传到Hub上。

注意这里可以通过设置参数 hub_model_id 来指定上传到Hub的repo名称(当然也可以在repo名前加上公司或组织名称)。例如,我们上传模型到 huggingface-course organization ,可以在 Seq2SeqTrainingArguments 中设置参数 hub_model_id="huggingface-course/marian-finetuned-kde4-en-to-fr" 。默认情况下,一个repo使用你的namespace和输出文件夹的名称作为上传repo的名称,因此默认情况下你的上传模型名称为 "sgugger/marian-finetuned-kde4-en-to-fr"

如果输出文件夹已经存在,那么定义 Seq2SeqTrainer 时会报错。纠正方法就是重新克隆一个repo到本地或者直接删除。

最后,将参数传入 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_target_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()

另外登录的好处就是,在训练的过程,包含模型的整个训练状态已经上传到Hub上,你可以在其他机器上继续训练。

训练完成后,再次进行验证,我们可以看到BLEU得分提高了不少。

 trainer.evaluate(max_length=max_target_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}

BLEU有14点的提升,很棒!

最后,调用函数 push_to_hub() 来将最终版本的模型上传到Hub上。 Trainer 提供了一个默认的模型卡片(也就是上传Hub后,看到的Readme),其中包含验证数据集的验证结果。同时也会在模型网页右侧,给出模型的推理demo,可以在这个demo上玩一下该模型。一般情况下,在模型类的界面上没有必要做什么操作,但是本例中,需要指定上传模型的标签为翻译模型(因为类似的模型可以应用在所有类型的Seq2Seq问题上)。

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

上述代码会返回commit的URL,方便我们定位模型网页。

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

完成上述步骤后,可以在Model Hub的demo上测试模型,并将网页进行分享。到目前为止,已经完成了翻译任务的微调(矮油,不错哟!)。

如果读者向深入训练过程,可以使用accelerate库方便的定制训练过程。

定制训练过程(使用accelerate库)

下面介绍使用accelerate库来实现定制的训练过程,适合喜欢定制训练过程的小伙伴。

准备训练要素

前面使用 Trainer 进行训练已经准备好了训练要素,这里简要介绍下。首先基于数据集构建 DataLoader ,然后将数据转换为Pytorch格式(由data_collator实现)。

 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
 )

下面重初始化模型,来确保不是训练 Trainer 之后的模型(为了观察效果,微调最初下载的模型)。

 model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)

然后设置优化器(最近出的Lion听说不错,对很大的batch_size有用,对32左右的batch_size不知道效果是否会比adamw好)

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

准备好上面的 DataLoader 和优化器后,可以将这些要素送入函数 accelerator.prepare() 来做下wrapper。如果要在Colab上TPU进行训练,则需要将所有的代码封装为一个训练函数,并且不要在notebook的cell中执行任何 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() ,我们可以使用 train_dataloader 来计算整体的训练步数( len(train_dataloader) 是一个epoch的训练步数)。需要注意的是在计算训练步数,要在 accelerate.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,
 )

下步依然是huggingface的老操作了,登录Hub,为了将训练过程和模型参数上传(如果wandb访问过慢的话,这个其实也不错)。创建repo,然后设置名称。可以通过函数 get_full_repo_name() 来获取repo的全名。

 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'

然后将repo克隆到本地。如果文件夹已经存在,那么该文件夹必须是之前从hub上克隆下来的。

 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 函数需要,也就是sacrebleu的输入是两个字符串)

 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

训练过程与之前章节类似(主要是 model(**batch) -> loss -> loss.backward() -> optimizer.step() ,即模型前馈和反馈过程,这是核心内容),下面主要介绍不同的地方。

第一个不同就是我们使用 generate() 函数来生成预测,这是我们基础模型的一个函数,而不是Accelerate库wrapper之后的模型的一个函数。因此在调用 generate() 函数之前,需要进行 unwrapper 操作。

第二个不同就是,类似token分类任务,需要同时对输入和输出进行补齐操作(这个好理解,就是一批的输入和输出数据都存在长度不相同的情况)。这里在 gather() 函数之前调用 accelerate.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)
     # 评估过程,使用generate解码很慢,因此如果评估数据集很大,那么就评估过程会非常慢
     # 具体的体验可以参考uer-py中的一些模型训练过程
     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之前进行pad,这一步不做代码运行时会报错
         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 。并且要测试其他的技巧,那么可以在上面代码基础进行修改。(上述代码是核心主干部分)

使用微调模型

前面已经介绍了如何在Hub上使用微调模型进行推理操作。更优雅的方法是使用 pipeline (这个工厂流水线的单词还真是上头!),需要指定模型的ID,具体如下。

 from transformers import pipeline
 # 可以替换model_checkpoint为你自己的translation模型ID
 model_checkpoint = "huggingface-course/marian-finetuned-kde4-en-to-fr"