BERT实战——(5)生成任务-机器翻译

引言

之前的分别介绍了使用 🤗 Transformers代码库中的模型开展one-class任务(文本分类多选问答问题)、class for each token任务(序列标注)以及copy from input任务(抽取式问答)。

这一篇以及下一篇将介绍如何使用 🤗 Transformers代码库中的模型来解决general sequence任务(关于什么是生成序列任务,回看之前的博客,定位词:general sequence)。这一篇为解决生成任务中的机器翻译问题

任务介绍

翻译任务,把一种语言信息转变成另一种语言信息。是典型的seq2seq任务,输入为一个序列,输出为不固定长度(由机器自行学习生成的序列应该多长)的序列。

比如输入一句中文,翻译为英文:

输入:我爱中国。
输出:I love China.

主要分为以下几个部分:

  1. 数据加载;
  2. 数据预处理;
  3. 微调预训练模型:使用transformer中的Seq2SeqTrainer接口对预训练模型进行微调(注意这里是Seq2SeqTrainer接口,之前的任务都是调用Trainer接口)。

前期准备

安装以下库:

pip install datasets transformers sacrebleu sentencepiece
#transformers==4.9.2
#datasets==1.11.0
#sacrebleu==1.5.1
#sentencepiece==0.1.96

数据加载

数据集介绍

我们使用WMT dataset数据集。这是翻译任务最常用的数据集之一。其中包括English/Romanian双语翻译。

加载数据

该数据的加载方式在transformers库中进行了封装,我们可以通过以下语句进行数据加载:

from datasets import load_dataset
raw_datasets = load_dataset("wmt16", "ro-en")

给定一个数据切分的key(train、validation或者test)和下标即可查看数据。

raw_datasets["train"][0]
# 我们可以看到一句英语en对应一句罗马尼亚语言ro
# {'translation': {'en': 'Membership of Parliament: see Minutes','ro': 'Componenţa Parlamentului: a se vedea procesul-verbal'}}

下面的函数将从数据集里随机选择几个例子进行展示:

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"])
translation
0 {'en': 'The Bulgarian gymnastics team won the gold medal at the traditional Grand Prix series competition in Thiais, France, which wrapped up on Sunday (March 30th).', 'ro': 'Echipa bulgară de gimnastică a câştigat medalia de aur la tradiţionala competiţie Grand Prix din Thiais, Franţa, care s-a încheiat duminică (30 martie).'}
1 {'en': 'Being on that committee, however, you will know that this was a very hot topic in negotiations between Norway and some Member States.', 'ro': 'Totuşi, făcând parte din această comisie, ştiţi că acesta a fost un subiect foarte aprins în negocierile dintre Norvegia şi unele state membre.'}
2 {'en': 'The overwhelming vote shows just this.', 'ro': 'Ceea ce demonstrează şi votul favorabil.'}
3 {'en': '[Photo illustration by Catherine Gurgenidze for Southeast European Times]', 'ro': '[Ilustraţii foto de Catherine Gurgenidze pentru Southeast European Times]'}
4 {'en': '(HU) Mr President, today the specific text of the agreement between the Hungarian Government and the European Commission has been formulated.', 'ro': '(HU) Domnule președinte, textul concret al acordului dintre guvernul ungar și Comisia Europeană a fost formulat astăzi.'}

数据预处理

在将数据喂入模型之前,我们需要对数据进行预处理。

仍然是两个数据预处理的基本流程:

  1. 分词;
  2. 转化成对应任务输入模型的格式;

Tokenizer用于上面两步数据预处理工作:Tokenizer首先对输入进行tokenize,然后将tokens转化为预模型中需要对应的token ID,再转化为模型需要的输入格式。

初始化Tokenizer

之前的博客已经介绍了一些Tokenizer的内容,并做了Tokenizer分词的示例,这里不再重复。use_fast=True指定使用fast版本的tokenizer。我们使用已经训练好的Helsinki-NLP/opus-mt-en-ro checkpoint来做翻译任务。

from transformers import AutoTokenizer
model_checkpoint = "Helsinki-NLP/opus-mt-en-ro"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, use_fast=True)

以使用的mBART模型为例,需要正确设置source语言和target语言。如果翻译的是其他双语语料,请查看这里进行配置:

if "mbart" in model_checkpoint:
tokenizer.src_lang = "en-XX"
tokenizer.tgt_lang = "ro-RO"

转化成对应任务输入模型的格式

模型的输入为待翻译的句子。

注意:为了给模型准备好翻译的targets,使用as_target_tokenizer来为targets设置tokenizer:

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 toke
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', '!', '</s>']

如果使用的是T5预训练模型的checkpoints,需要对特殊的前缀进行检查。T5使用特殊的前缀来告诉模型具体要做的任务("translate English to Romanian: ",具体前缀例子如下:

if model_checkpoint in ["t5-small", "t5-base", "t5-larg", "t5-3b", "t5-11b"]:
prefix = "translate English to Romanian: "
else:
prefix = ""

现在我们可以把上面的内容放在一起组成预处理函数preprocess_function。对样本进行预处理的时候,使用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。

接下来使用map函数对数据集datasets里面三个样本集合的所有样本进行预处理,将预处理函数preprocess_function应用到(map)所有样本上。参数batched=True可以批量对文本进行编码。这是为了充分利用前面加载fast_tokenizer的优势,它将使用多线程并发地处理批中的文本。

tokenized_datasets = raw_datasets.map(preprocess_function, batched=True)

微调预训练模型

数据已经准备好了,我们需要下载并加载预训练模型,然后微调预训练模型。

加载预训练模型

seq2seq任务,那么需要一个能解决这个任务的模型类。我们使用AutoModelForSeq2SeqLM 这个类

和之前几篇博客提到的加载方式相同不再赘述。

from transformers import AutoModelForSeq2SeqLM, 
model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint)

设定训练参数

为了能够得到一个Seq2SeqTrainer训练工具,我们还需要训练的设定/参数 Seq2SeqTrainingArguments。这个训练设定包含了能够定义训练过程的所有属性

由于数据集比较大,Seq2SeqTrainer训练时会同时不断保存模型,我们用save_total_limit=3参数控制至多保存3个模型。

from transformers import 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,
)

数据收集器data collator

接下来需要告诉Trainer如何从预处理的输入数据中构造batch。我们使用数据收集器DataCollatorForSeq2Seq,将经预处理的输入分batch再次处理后喂给模型。

from transformers import DataCollatorForSeq2Seq
data_collator = DataCollatorForSeq2Seq(tokenizer, model=model)

定义评估方法

我们使用'bleu'指标,利用metric.compute计算该指标对模型进行评估。

metric.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]}

将模型预测送入评估之前,还需要写postprocess_text函数做一些数据后处理:

import numpy as np
from datasets import load_metric
metric = load_metric("sacrebleu")

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

开始训练

将数据/模型/参数传入Trainer即可:

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
)

调用train方法开始训练:

trainer.train()

参考文献

4.6-生成任务-机器翻译.md

BERT相关——(7)将BERT应用到下游任务

transformers官方文档