本文参考资料是Hugging Face官网的课程,主要介绍了transformer
库的使用。
Transformer库中最基本的对象是pipeline(管道),将模型与其他必要预处理和后处理步骤组合起来,使我们可以直接输入任何文本并获得可理解的答案:
from transformers import pipeline
classifier = pipeline("sentiment-analysis")
classifier(
[
"This restaurant is awesome",
"I hate this so much!",
]
)
No model was supplied, defaulted to distilbert-base-uncased-finetuned-sst-2-english (https://huggingface.co/distilbert-base-uncased-finetuned-sst-2-english)
[{'label': 'POSITIVE', 'score': 0.9998743534088135},
{'label': 'NEGATIVE', 'score': 0.9994558691978455}]
我们在pipeline
中仅传入任务名称sentiment-analysis
(情感分析),此时,管道默认选择一个特定的预训练模型,该模型已经对英文情感分析进行了微调。第一次运行会下载并缓存该模型。
将一些文本传递到管道时涉及三个主要步骤:
目前可用的一些管道有:
我们也可以从 Hub 中针对特定任务来选择特定模型的管道 例如,情绪分析。
管道API可以处理不同的NLP任务,可以使用完整的编码器-解码器架构,也可以使用编码器或解码器,这取决于你要解决的具体任务:
模型 | 例子 | 任务 |
---|---|---|
Encoder | ALBERT, BERT, DistilBERT, ELECTRA, RoBERTa | 句子分类、命名实体识别、抽取式问答 |
Decoder | CTRL, GPT, GPT-2, Transformer XL | 文本生成 |
Encoder-decoder | BART, T5, Marian, mBART | 摘要生成、翻译、生成式问答 |
我们上面演示的例子是为特定任务编程的,不能执行她们的变体,在下一节中,我们将了解管道内部的内容以及如何自定义其行为。
让我们从一个完整的例子开始,看看当我们在第1节中执行以下代码时,幕后发生了什么:
from transformers import pipeline
classifier = pipeline("sentiment-analysis")
classifier(
[
"This restaurant is awesome",
"I hate this so much!",
]
)
[{'label': 'POSITIVE', 'score': 0.9998743534088135},
{'label': 'NEGATIVE', 'score': 0.9994558691978455}]
正如我们在前面看到的,这个管道将三个步骤组合在一起:预处理、传递输入到模型和后处理:
与其他神经网络一样,Transformer模型也不能直接处理原始文本,因此我们管道的第一步就是将文本输入转换为模型可以理解的数字。为此,我们使用了一个分词器tokenizer,它负责:
我们可以使用 AutoTokenizer
类及其 from_pretrained
方法,以保证所有这些预处理都以与模型预训练时完全相同的方式完成。
设定模型的checkpoint(检查点)名称,它会自动获取与模型的Tokenizer关联的数据并缓存它(所以它只在你第一次运行下面的代码时下载)。
由于情感分析管道的默认检查点是 distilbert-base-uncased-finetuned-sst-2-english
,我们可以运行以下命令得到我们需要的tokenizer:
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint) # 自动加载该模型训练时所用的分词器
有了分词器后,我们可以调用分词器来完成上面所说的过程:
raw_inputs = [
"I've been waiting for a HuggingFace course my whole life.",
"I hate this so much!",
]
# padding=True 填充输入序列,使得批次内序列长度一致
# truncation=True 截断过长的序列
# return_tensors="pt" 返回PyTorch 张量
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
print(inputs)
{
'input_ids': tensor([
[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102],
[ 101, 1045, 5223, 2023, 2061, 2172, 999, 102, 0, 0, 0, 0, 0, 0, 0, 0]
]),
'attention_mask': tensor([
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
])
}
可以看到,我们得到了两组张量。input_ids
就是分词后的标记转换为数字的形式,其中0
表示填充标记;而attention_mask
中非1
的标记是不需要注意的,这里是因为它们都是填充标记。
我们也可以像分词器一样下载我们的预训练模型, Transformers 提供了一个 AutoModel
类,它也有一个 from_pretrained
方法:
from transformers import AutoModel
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)
AutoModel
类可以从checkpoint实例化任何模型,而且这是一种比较好的实例化模型方法。
在上面的代码中,我们下载了之前在管道中使用的相同检查点,并用它实例化了一个模型。
但是这个架构只包含基本的Transformer模块:给定一些输入,它会输出隐藏状态。通常这些隐藏状态会作为模型另一部分的输入,即模型Head。
我们可以使用相同的模型架构执行不同的任务,但是每个任务都有与之关联的不同的模型head。
Model heads:将隐藏状态的高维向量(也就是logits向量)作为输入,并将它们投影到不同的维度上。 它们通常由一个或几个线性层组成:
从上图可以看到,输入经过嵌入层输出logits向量,产生句子的最终表征。
然后可以附加不同的模型Head,下面列举了部分:
以情感分析为例,我们需要一个带有序列分类的模型Head(能将句子分为正面或负面)。因此,我们实际上不会使用AutoModel
类,而是使用AutoModelForSequenceClassification
,因为它包含了我们想要的模型Head。
from transformers import AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)
模型head将我们之前看到的高维向量作为输入,并输出包含两个值(每个标签一个)的向量:
print(outputs.logits.shape)
torch.Size([2, 2])
由于我们只有两个句子和两个标签,因此我们从模型中得到的结果是 2 x 2 的形状。
我们从模型中获得的输出值本身不一定有意义,比如:
print(outputs.logits)
tensor([[-1.5607, 1.6123],
[ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)
我们的模型预测了第一个句子结果 [-1.5607, 1.6123]
和第二个句子的结果 [4.1692, -3.3464]
。但这些不是概率,而是 logits,即模型最后一层输出的原始非标准化分数。 要转换为概率,它们需要经过一个 SoftMax 层。所有 Transformers 模型都输出 logits,这是因为训练的损失函数一般会将最后一个激活函数(比如SoftMax)和实际的交叉熵损失函数相融合。文章Softmax回归中的数值稳定中有详细介绍。
import torch
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)
tensor([[4.0195e-02, 9.5980e-01],
[9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)
这次输出是可识别的概率分数。
要获得每个位置对应的标签,我们可以检查模型配置的 id2label 属性:
model.config.id2label
{0: 'NEGATIVE', 1: 'POSITIVE'}
现在我们可以得出结论,该模型预测了以下内容:
第一句:NEGATIVE:0.0402,POSITIVE:0.9598
第二句:NEGATIVE:0.9995,POSITIVE:0.0005
Trainer
可以让我们用自己的数据集微调预训练模型。
这里我们用官方提供的数据集来做。
MRPC 数据集是构成 GLUE 基准的 10 个数据集之一。而GLUE 基准是一种学术基准,用于衡量 ML 模型在 10 个不同文本分类任务中的性能。
Datasets库提供了一个非常简单的命令来下载和缓存Hub上的dataset。 我们可以像这样下载 MRPC 数据集:
from datasets import load_dataset
raw_datasets = load_dataset("glue", "mrpc")
raw_datasets
DatasetDict({
train: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 3668
})
validation: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 408
})
test: Dataset({
features: ['sentence1', 'sentence2', 'label', 'idx'],
num_rows: 1725
})
})
load_dataset 方法, 可以从不同的地方构建数据集
和字典一样,raw_datasets
可以通过索引访问其中的句子对:
raw_train_dataset = raw_datasets["train"]
raw_train_dataset[0]
{'idx': 0,
'label': 1,
'sentence1': 'Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
'sentence2': 'Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .'}
import pandas as pd
validation=pd.DataFrame(raw_datasets['validation'])
validation
raw_train_dataset.features
{'sentence1': Value(dtype='string', id=None),
'sentence2': Value(dtype='string', id=None),
'label': ClassLabel(num_classes=2, names=['not_equivalent', 'equivalent'], names_file=None, id=None),
'idx': Value(dtype='int32', id=None)}
label是 ClassLabel
类型,label=1
表示这对句子互为paraphrases,label=0
表示句子对意思不一致。
通过tokenizer可以将文本转换为模型能理解的数字。
from transformers import AutoTokenizer
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
让我们看一个示例:
inputs = tokenizer("This is the first sentence.", "This is the second one.")
inputs
{ 'input_ids': [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
所以将句子对列表传给tokenizer
,就可以对整个数据集进行分词处理。因此,预处理训练数据集的一种方法是:
tokenized_dataset = tokenizer(
raw_datasets["train"]["sentence1"],
raw_datasets["train"]["sentence2"],
padding=True,
truncation=True,
)
这种处理方法的缺点是处理之后tokenized_dataset
不再是一个dataset
格式,而是返回字典(带有我们的键:input_ids
、attention_mask
和 token_type_ids
,对应键值对的值)。
为了使我们的数据保持dataset
的格式,我们将使用更灵活的Dataset.map
方法。此方法可以完成更多的预处理而不仅仅是分词。 map
方法是对数据集中的每个元素应用同一个函数,所以让我们定义一个函数来对输入进行tokenize
预处理:
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
这个函数接受的是一个字典(就像我们dataset
的items
),返回的也是一个字典(有三个键:input_ids
、attention_mask
和 token_type_ids
)。
在tokenization
函数中省略了padding
参数,这是因为padding
到该批次中的最大长度时的效率,会高于所有序列都padding
到整个数据集的最大序列长度。 当输入序列长度很不一致时,这可以节省大量时间和处理能力!
以下是对整个数据集应用tokenization
方法。 我们在 map
调用中使用了 batched=True
,因此该函数一次应用于数据集的整个batch元素,而不是分别应用于每个元素。 这样预处理速度会更快。
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
tokenized_datasets
Datasets库应用这种处理的方式是向数据集添加新字段,如下所示:
DatasetDict({
train: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 3668
})
validation: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 408
})
test: Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 1725
})
})
最后,当我们将输入序列进行批处理时,要将所有输入序列填充到本批次最长序列的长度——我们称之为动态填充技术dynamic padding。
动态填充:即将每个批次的输入序列填充到一样的长度。具体内容放在最后。
数据预处理完成后,只需要几个简单的步骤来定义Trainer
的参数,就可以进行模型的基本训练循环了。
trainer
主要参数包括:
最后我们用代码总结:
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding
raw_datasets = load_dataset("glue", "mrpc") #MRPC判断两个句子是否互为paraphrases
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
def tokenize_function(example):
return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)#动态填充,即将每个批次的输入序列填充到一样的长度
Trainer
第一个参数是TrainingArguments
类,包含用于训练和评估的所有超参数。
from transformers import TrainingArguments
training_args = TrainingArguments("test-trainer")
接着,定义模型。
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)#标签数为2也就是二分类
有了模型之后,就可以定义一个训练器Trainer
了,将上面构建的所有对象传递给它进行模型微调:
from transformers import Trainer
trainer = Trainer(
model,
training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
tokenizer=tokenizer,
)
像上面这样传递tokenizer
时,参数data_collator
是之前定义的动态填充DataCollatorWithPadding
,所以此调用中的 data_collator=data_collator
行可以跳过。
要在我们的数据集上微调模型,我们只需要调用 Trainer
的 train
方法:
trainer.train()
The following columns in the training set don't have a corresponding argument in `BertForSequenceClassification.forward` and have been ignored: sentence2, idx, sentence1. If sentence2, idx, sentence1 are not expected by `BertForSequenceClassification.forward`, you can safely ignore this message.
/usr/local/lib/python3.7/dist-packages/transformers/optimization.py:309: FutureWarning: This implementation of AdamW is deprecated and will be removed in a future version. Use the PyTorch implementation torch.optim.AdamW instead, or set `no_deprecation_warning=True` to disable this warning
FutureWarning,
***** Running training *****
Num examples = 3668
Num Epochs = 3
Instantaneous batch size per device = 8
Total train batch size (w. parallel, distributed & accumulation) = 8
Gradient Accumulation steps = 1
Total optimization steps = 1377
[1377/1377 03:14, Epoch 3/3]
Step Training Loss
500 0.528600
1000 0.312000
Saving model checkpoint to test-trainer/checkpoint-500
Configuration saved in test-trainer/checkpoint-500/config.json
Model weights saved in test-trainer/checkpoint-500/pytorch_model.bin
tokenizer config file saved in test-trainer/checkpoint-500/tokenizer_config.json
Special tokens file saved in test-trainer/checkpoint-500/special_tokens_map.json
Saving model checkpoint to test-trainer/checkpoint-1000
Configuration saved in test-trainer/checkpoint-1000/config.json
Model weights saved in test-trainer/checkpoint-1000/pytorch_model.bin
tokenizer config file saved in test-trainer/checkpoint-1000/tokenizer_config.json
Special tokens file saved in test-trainer/checkpoint-1000/special_tokens_map.json
Training completed. Do not forget to share your model on huggingface.co/models =)
TrainOutput(global_step=1377, training_loss=0.35565512995834186, metrics={'train_runtime': 194.8661, 'train_samples_per_second': 56.47, 'train_steps_per_second': 7.066, 'total_flos': 406183858377360.0, 'train_loss': 0.35565512995834186, 'epoch': 3.0})
在colab上用了4分钟不到。
我们可以先看看验证集预处理后的结构:
tokenized_datasets["validation"]
Dataset({
features: ['attention_mask', 'idx', 'input_ids', 'label', 'sentence1', 'sentence2', 'token_type_ids'],
num_rows: 408
})
我们可以使用 Trainer.predict 命令获得模型的预测结果:
predictions = trainer.predict(tokenized_datasets["validation"])
print(predictions.predictions.shape, predictions.label_ids.shape)
(408, 2) (408,)
predict
方法输出一个具有三个字段的元组。
metrics={'train_runtime': 194.8661, 'train_samples_per_second': 56.47, 'train_steps_per_second': 7.066, 'total_flos': 406183858377360.0, 'train_loss': 0.35565512995834186, 'epoch': 3.0})
predictions
是一个二维数组,形状为 408 x 2(验证集408组数据,两个标签)。 要预测结果与标签进行比较,我们需要在predictions
第二个轴上取最大值的索引:
import numpy as np
preds = np.argmax(predictions.predictions, axis=-1)
同时,从上面训练过程可以看到:模型每 500 步报告一次训练损失。 但是,它不会告诉你模型的表现如何。 这是因为:
而如果我们将compute_metrics
函数写好并将其传递给Trainer
后,metrics
字段也将包含compute_metrics
返回的metrics
值。
现在看看如何构造compute_metrics
函数。这个函数:
为了构建我们的 compute_metric 函数,我们将依赖 Datasets 库中的metric。 通过 load_metric 函数,我们可以像加载数据集一样轻松加载与 MRPC 数据集关联的metric。
from datasets import load_metric
metric = load_metric("glue", "mrpc")
metric.compute(predictions=preds, references=predictions.label_ids)
{'accuracy': 0.8627450980392157, 'f1': 0.9027777777777778}
每次训练时model head的随机初始化可能会改变最终的metric值,所以这里的最终结果可能和你跑出的不一样。 acc和F1 是用于评估 GLUE 基准的 MRPC 数据集结果的两个指标。
将以上内容整合到一起,得到 compute_metrics 函数:
def compute_metrics(eval_preds):
metric = load_metric("glue", "mrpc")
logits, labels = eval_preds
predictions = np.argmax(logits, axis=-1)
return metric.compute(predictions=predictions, references=labels)
再设定每个epoch查看一次验证评估。所以下面就是我们设定compute_metrics参数之后的Trainer:
training_args = TrainingArguments("test-trainer", evaluation_strategy="epoch")
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)
trainer = Trainer(
model,
training_args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
tokenizer=tokenizer,
compute_metrics=compute_metrics
)
请注意,我们创建了一个新的 TrainingArguments,其evaluation_strategy 设置为“epoch”和一个新模型——否则,我们只会继续训练我们已经训练过的模型。 要启动新的训练运行,我们执行:
trainer.train()
下面我们来看一个实例,微调Transformer来做机器翻译任务。
对于机器翻译任务,我们将看到如何加载数据集、使用Trainer对模型进行微调。
选择一个预训练模型:
model_checkpoint = "Helsinki-NLP/opus-mt-en-ro"
数据加载和评测方式加载只需要简单使用load_dataset和load_metric即可。我们使用WMT数据集中的English/Romanian双语翻译。
from datasets import load_dataset, load_metric
raw_datasets = load_dataset("wmt16", "ro-en")
metric = load_metric("sacrebleu") # 需要先执行 pip install sacrebleu
这个datasets对象本身是一种DatasetDict数据结构. 对于训练集、验证集和测试集,只需要使用对应的key(train,validation,test)即可得到相应的数据。
raw_datasets
DatasetDict({
train: Dataset({
features: ['translation'],
num_rows: 610320
})
validation: Dataset({
features: ['translation'],
num_rows: 1999
})
test: Dataset({
features: ['translation'],
num_rows: 1999
})
})
下面随机选择几个样本进行展示:
import datasets
import random
import pandas as pd
from IPython.display import display, HTML
def show_random_elements(dataset, num_examples=5):
assert num_examples <= len(dataset), "Can't pick more elements than there are in the dataset."
picks = []
for _ in range(num_examples):
pick = random.randint(0, len(dataset)-1)
while pick in picks:
pick = random.randint(0, len(dataset)-1)
picks.append(pick)
df = pd.DataFrame(dataset[picks])
for column, typ in dataset.features.items():
if isinstance(typ, datasets.ClassLabel):
df[column] = df[column].transform(lambda i: typ.names[i])
display(HTML(df.to_html()))
show_random_elements(raw_datasets["train"])
我们使用compute方法来对比predictions和labels,从而计算得分。predictions和labels都需要是一个list:
fake_preds = ["hello there", "general kenobi"]
fake_labels = [["hello there"], ["general kenobi"]]
metric.compute(predictions=fake_preds, references=fake_labels)
{'bp': 1.0,
'counts': [4, 2, 0, 0],
'precisions': [100.0, 100.0, 0.0, 0.0],
'ref_len': 4,
'score': 0.0,
'sys_len': 4,
'totals': [4, 2, 0, 0]}
在将数据喂入模型之前,我们需要对数据进行预处理。预处理的工具叫Tokenizer。Tokenizer首先对输入进行tokenize,然后将tokens转化为预模型中需要对应的token ID,再转化为模型需要的输入格式。
为了达到数据预处理的目的,我们使用AutoTokenizer.from_pretrained
方法实例化我们的tokenizer,这样可以确保:
这个被下载的tokens vocabulary会被缓存起来,从而再次使用的时候不会重新下载。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
以我们使用的mBART模型为例,我们需要正确设置source语言和target语言:
if "mbart" in model_checkpoint:
tokenizer.src_lang = "en-XX"
tokenizer.tgt_lang = "ro-RO"
tokenizer既可以对单个文本进行预处理,也可以对一对文本进行预处理,tokenizer预处理后得到的数据满足预训练模型输入格式:
tokenizer("Hello, this one sentence!")
{'input_ids': [125, 778, 3, 63, 141, 9191, 23, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1]}
上面看到的token IDs也就是input_ids
一般来说随着预训练模型名字的不同而有所不同。原因是不同的预训练模型在预训练的时候设定了不同的规则。但只要tokenizer和model的名字一致,那么tokenizer预处理的输入格式就会满足model需求的。
注意:为了给模型准备好翻译的targets
,我们使用as_target_tokenizer
来控制targets
所对应的特殊token:
with tokenizer.as_target_tokenizer():
print(tokenizer("Hello, this one sentence!"))
model_input = tokenizer("Hello, this one sentence!")
tokens = tokenizer.convert_ids_to_tokens(model_input['input_ids'])
# 打印看一下special token
print('tokens: {}'.format(tokens))
{'input_ids': [10334, 1204, 3, 15, 8915, 27, 452, 59, 29579, 581, 23, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
tokens: ['▁Hel', 'lo', ',', '▁', 'this', '▁o', 'ne', '▁se', 'nten', 'ce', '!', '']
为了代码的兼容性,如果使用的是T5预训练模型的checkpoints,需要对特殊的前缀进行检查。T5使用特殊的前缀来告诉模型具体要做的任务,具体前缀例子如下:
if model_checkpoint in ["t5-small", "t5-base", "t5-larg", "t5-3b", "t5-11b"]:
prefix = "translate English to Romanian: "
else:
prefix = ""
现在我们可以把所有内容放在一起组成我们的预处理函数了。我们对样本进行预处理的时候,我们还会truncation=True
这个参数来确保我们超长的句子被截断。默认情况下,对与比较短的句子我们会自动padding。
max_input_length = 128
max_target_length = 128
source_lang = "en"
target_lang = "ro"
def preprocess_function(examples):
inputs = [prefix + ex[source_lang] for ex in examples["translation"]]
targets = [ex[target_lang] for ex in examples["translation"]]
model_inputs = tokenizer(inputs, max_length=max_input_length, truncation=True)
# Setup 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
以上的预处理函数可以处理一个样本,也可以处理多个样本exapmles。如果是处理多个样本,则返回的是多个样本被预处理之后的结果list。
preprocess_function(raw_datasets['train'][:2])
{'input_ids': [[393, 4462, 14, 1137, 53, 216, 28636, 0], [24385, 14, 28636, 14, 4646, 4622, 53, 216, 28636, 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], 'labels': [[42140, 494, 1750, 53, 8, 59, 903, 3543, 9, 15202, 0], [36199, 6612, 9, 15202, 122, 568, 35788, 21549, 53, 8, 59, 903, 3543, 9, 15202, 0]]}
接下来对数据集datasets里面的所有样本进行预处理,处理的方式是使用map函数,将预处理函数prepare_train_features
应用到(map)所有样本上。
tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)
这会花一定的诗句。不过,返回的结果会自动被缓存,避免下次处理的时候重新计算。
如果想清理这个缓存。清理的方式是使用load_from_cache_file=False
参数。
既然数据已经准备好了,现在我们需要下载并加载我们的预训练模型,然后微调预训练模型。既然我们是做seq2seq任务,那么我们需要一个能解决这个任务的模型类。我们使用AutoModelForSeq2SeqLM
这个类。和tokenizer相似,from_pretrained
方法同样可以帮助我们下载并加载模型,同时也会对模型进行缓存,就不会重复下载模型了。
from transformers import AutoModelForSeq2SeqLM, DataCollatorForSeq2Seq, Seq2SeqTrainingArguments, Seq2SeqTrainer
model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)
由于我们微调的任务是机器翻译,而我们加载的是预训练的seq2seq模型,所以不会提示我们加载模型的时候扔掉了一些不匹配的神经网络参数(比如:预训练语言模型的神经网络head被扔掉了,同时随机初始化了机器翻译的神经网络head)。
为了能够得到一个Seq2SeqTrainer训练工具,我们还需要3个要素,其中最重要的是训练的设定/参数Seq2SeqTrainingArguments
。这个训练设定包含了能够定义训练过程的所有属性:
batch_size = 16
args = Seq2SeqTrainingArguments(
"test-translation",
evaluation_strategy = "epoch",
learning_rate=2e-5,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
weight_decay=0.01,
save_total_limit=3,
num_train_epochs=1,
predict_with_generate=True,
fp16=False,
)
上面evaluation_strategy = "epoch"
参数告诉训练代码:我们每个epcoh会做一次验证评估。
由于我们的数据集比较大,同时Seq2SeqTrainer会不断保存模型,所以我们需要告诉它至多保存save_total_limit=3
个模型。
最后我们需要一个数据收集器data collator,将我们处理好的输入喂给模型。
data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)
设置好Seq2SeqTrainer
还剩最后一件事情,那就是我们需要定义好评估方法。我们使用metric来完成评估。将模型预测送入评估之前,我们也会做一些数据后处理:
import numpy as np
def postprocess_text(preds, labels):
preds = [pred.strip() for pred in preds]
labels = [[label.strip()] for label in labels]
return preds, labels
def compute_metrics(eval_preds):
preds, labels = eval_preds
if isinstance(preds, tuple):
preds = preds[0]
decoded_preds = tokenizer.batch_decode(preds, 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, decoded_labels = postprocess_text(decoded_preds, decoded_labels)
result = metric.compute(predictions=decoded_preds, references=decoded_labels)
result = {"bleu": result["score"]}
prediction_lens = [np.count_nonzero(pred != tokenizer.pad_token_id) for pred in preds]
result["gen_len"] = np.mean(prediction_lens)
result = {k: round(v, 4) for k, v in result.items()}
return result
最后将所有的参数/数据/模型传给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
)
调用train
方法进行微调训练。
trainer.train()
需要跑两个小时左右,这里就不看结果了。