# 处理数据

下面是我们用模型中心的数据在 PyTorch 上训练句子分类器的一个例子:

import torch
from transformers import AdamW, AutoTokenizer, AutoModelForSequenceClassification
# Same as before
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
sequences = [
    "I've been waiting for a HuggingFace course my whole life.",
    "This course is amazing!",
]
batch = tokenizer(sequences, padding=True, truncation=True, return_tensors="pt")
# This is new
batch["labels"] = torch.tensor([1, 1])
optimizer = AdamW(model.parameters())
loss = model(**batch).loss
loss.backward()
optimizer.step()
``
当然,仅仅用两句话训练模型不会产生很好的效果。为了获得更好的结果,您需要准备一个更大的数据集。
在本节中,我们将使用MRPC(微软研究释义语料库)数据集作为示例,该数据集由威廉·多兰和克里斯·布罗克特在这篇文章发布。该数据集由5801对句子组成,每个句子对带有一个标签,指示它们是否为同义(即,如果两个句子的意思相同)。我们在本章中选择了它,因为它是一个小数据集,所以很容易对它进行训练。
## 从模型中心(Hub)加载数据集
模型中心(hub)不只是包含模型;它也有许多不同语言的多个数据集。点击数据集的链接即可进行浏览。我们建议您在阅读本节后阅读一下加载和处理新的数据集这篇文章,这会让您对huggingface的darasets更加清晰。但现在,让我们使用MRPC数据集中的[GLUE 基准测试数据集](https://gluebenchmark.com/),它是构成MRPC数据集的10个数据集之一,这是一个学术基准,用于衡量机器学习模型在10个不同文本分类任务中的性能。
Datasets库提供了一个非常便捷的命令,可以在模型中心(hub)上下载和缓存数据集。我们可以通过以下的代码下载MRPC数据集
```python
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
    })
})

正如你所看到的,我们获得了一个 DatasetDict 对象,其中包含训练集、验证集和测试集。每一个集合都包含几个列 (sentence1, sentence2, label, and idx) 以及一个代表行数的变量,即每个集合中的行的个数(因此,训练集中有 3668 对句子,验证集中有 408 对,测试集中有 1725 对)。

默认情况下,此命令在下载数据集并缓存到~/.cache/huggingface/dataset. 回想一下第 2 章,您可以通过设置 HF_HOME 环境变量来自定义缓存的文件夹。

我们可以访问我们数据集中的每一个 raw_train_dataset 对象,如使用字典:

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 .'}

我们可以看到标签已经是整数了,所以我们不需要对标签做任何预处理。要知道哪个数字对应于哪个标签,我们可以查看 raw_train_dataset 的 features. 这将告诉我们每列的类型:

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(分类标签),使用整数建立起到类别标签的映射关系。0 对应于 not_equivalent,1 对应于 equivalent。

# 预处理数据集

为了预处理数据集,我们需要将文本转换为模型能够理解的数字。正如你在第二章上看到的那样

from transformers import AutoTokenizer
checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
tokenized_sentences_1 = tokenizer(raw_datasets["train"]["sentence1"])
tokenized_sentences_2 = tokenizer(raw_datasets["train"]["sentence2"])

然而,在两句话传递给模型,预测这两句话是否是同义之前。我们需要这两句话依次进行适当的预处理。幸运的是,标记器不仅仅可以输入单个句子还可以输入一组句子,并按照我们的 BERT 模型所期望的输入进行处理:

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

我们在第二章 讨论了输入词 id (input_ids) 和 注意力遮罩 (attention_mask) ,但我们在那个时候没有讨论类型标记 ID (token_type_ids)。在这个例子中,类型标记 ID (token_type_ids) 的作用就是告诉模型输入的哪一部分是第一句,哪一部分是第二句。

如果我们将 input_ids 中的 id 转换回文字:

tokenizer.convert_ids_to_tokens(inputs["input_ids"])
['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']

所以我们看到模型需要输入的形式是 [CLS] sentence1 [SEP] sentence2 [SEP] 。因此,当有两句话的时候。类型标记 ID(token_type_ids) 的值是:

['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']
[      0,      0,    0,     0,       0,          0,   0,       0,      1,    1,     1,        1,     1,   1,       1]

如您所见,输入中 [CLS] sentence1 [SEP] 它们的类型标记 ID 均为 0,而其他部分,对应于 sentence2 [SEP] ,所有的类型标记 ID 均为 1.

请注意,如果选择其他的 checkpoint,则不一定具有类型标记 ID (token_type_ids)(例如,如果使用 DistilBERT 模型,就不会返回它们)。只有当它在预训练期间使用过这一层,模型在构建时依赖它们,才会返回它们。

用类型标记 ID 对 BERT 进行预训练,并且使用第一章的遮罩语言模型,还有一个额外的应用类型,叫做下一句预测。这项任务的目标是建立成对句子之间关系的模型。

在下一个句子预测任务中,会给模型输入成对的句子(带有随机遮罩的标记),并被要求预测第二个句子是否紧跟第一个句子。为了提高模型的泛化能力,数据集中一半的两个句子在原始文档中挨在一起,另一半的两个句子来自两个不同的文档。

一般来说,你不需要担心是否有类型标记 ID (token_type_ids)。在您的输入中:只要您对标记器和模型使用相同的检查点,一切都会很好,因为标记器知道向其模型提供什么。

# 一份完整的代码 MRPC

from transformers import AutoModelForSequenceClassification
from transformers import Trainer, TrainingArguments
from datasets import load_metric
import numpy as np
from transformers import AutoTokenizer, DataCollatorWithPadding
import datasets
checkpoint = 'bert-base-cased'
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
raw_datasets = datasets.load_dataset('glue', 'mrpc')
def tokenize_function(sample):
    return tokenizer(sample['sentence1'], sample['sentence2'], truncation=True)
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
data_collator = DataCollatorWithPadding(tokenizer=tokenizer)
def compute_metrics(eval_preds):
    metric = load_metric("glue", "mrpc")
    logits, labels = eval_preds.predictions, eval_preds.label_ids
    # 上一行可以直接简写成:
    # logits, labels = eval_preds  因为它相当于一个 tuple
    predictions = np.argmax(logits, axis=-1)
    return metric.compute(predictions=predictions, references=labels)
training_args = TrainingArguments(output_dir='test_trainer', evaluation_strategy='epoch')  # 指定输出文件夹,没有会自动创建
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)  # new model
trainer = Trainer(
    model,
    training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,  # 在定义了 tokenizer 之后,其实这里的 data_collator 就不用再写了,会自动根据 tokenizer 创建
    tokenizer=tokenizer,
    compute_metrics=compute_metrics
)
trainer.train()

针对上面的问题,输入进入 model 的应该是什么呢?

model(torch.tensor([tokenizer(raw_datasets['train'][0]['sentence1'], raw_datasets['train'][0]['sentence2'], truncation=True, padding=True)['input_ids']]))

上面这种写法中,是没有传入 attention_masked 的,对比下面两个样本测试

之所以取两条数据,是因为这样得出来的就是二维的,如果只是取出一条的话,还得自己加一个维度。

model(torch.tensor(tokenizer(raw_datasets['train'][0:2]['sentence1'], raw_datasets['train'][0:2]['sentence2'], truncation=True, padding=True)['input_ids']))

结果为

SequenceClassifierOutput(loss=None, logits=tensor([[ 0.5339, -0.2458],
        [ 0.5840, -0.2698]], grad_fn=<AddmmBackward0>), hidden_states=None, attentions=None)

这两条数据为

raw_datasets['train'][0:2]
{'sentence1': ['Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .',
  "Yucaipa owned Dominick 's before selling the chain to Safeway in 1998 for $ 2.5 billion ."],
 'sentence2': ['Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .',
  "Yucaipa bought Dominick 's in 1995 for $ 693 million and sold it to Safeway for $ 1.8 billion in 1998 ."],
 'label': [1, 0],
 'idx': [0, 1]}

在 model 中传入 attention_mask 时

temp = tokenizer(raw_datasets['train'][0:2]['sentence1'], raw_datasets['train'][0:2]['sentence2'], truncation=True, padding=True)
print(temp.keys()) # dict_keys(['input_ids', 'token_type_ids', 'attention_mask'])
print(temp)
print(temp['input_ids'])
print(type(temp['input_ids'])) # <class 'list'>
print(type(temp['token_type_ids'])) # <class 'list'>
print(type(temp['attention_mask'])) # <class 'list'>
# 转为 tensor  <class 'torch.Tensor'>
temp['input_ids'] = torch.tensor(temp['input_ids'])
temp['token_type_ids'] = torch.tensor(temp['token_type_ids'])
temp['attention_mask'] = torch.tensor(temp['attention_mask'])
model(**temp)
SequenceClassifierOutput(loss=None, logits=tensor([[0.1645, 0.6985],
        [0.1670, 0.6946]], grad_fn=<AddmmBackward0>), hidden_states=None, attentions=None)

如果是在有 GPU 的环境下进行训练,则 model 会被转移到 GPU 上面,则要将样本数据也要转移到 GPU 上面去

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
temp.to(device)
'''
也可用下面的来代替上面的 temp.to(device)
temp['input_ids'] = temp['input_ids'].to(device)
temp['attention_mask'] = temp['attention_mask'].to(device)
temp['token_type_ids'] = temp['token_type_ids'].to(device)
'''
model.to(device)
model(**temp)

# 如何写 compute_metric 的代码

如果是

predictions = trainer.predict(tokenized_datasets['validation'])
print(predictions.predictions.shape)  # logits
# array([[-2.7887206,  3.1986978],
#       [ 2.5258656, -1.832253 ], ...], dtype=float32)
print(predictions.label_ids.shape) # array([1, 0, 0, 1, 0, 1, 0, 1, 1, 1, ...], dtype=int64)
print(predictions.metrics)

第一行改成 predictions = trainer.predict(tokenized_datasets['validation'][0:3]) ,就会出现报错:原因是 tokenized_datasets['validation'][0:3] 的类型是 <class 'dict'> ,而 tokenized_datasets['validation'] 的类型是 <class 'datasets.arrow_dataset.Dataset'>

更改代码为如下就可以了:

from datasets import Dataset
x = tokenized_datasets['validation'][0:2]
y = Dataset.from_dict(x)
print(type(y))
predictions = trainer.predict(y)
print(predictions.predictions.shape)  # logits
# array([[-2.7887206,  3.1986978],
#       [ 2.5258656, -1.832253 ], ...], dtype=float32)
print(predictions.label_ids.shape) # array([1, 0, 0, 1, 0, 1, 0, 1, 1, 1, ...], dtype=int64)
print(predictions.metrics)

值得注意的是,上面得到的结果 predictions.predictionsmodel(**temp) 得到的结果是相同的。

# 下面探索 label

将数据集中的 label 列变成 predict 中的 label_ids,可能数据类型不同,但是数据是相同的

from datasets import Dataset
x = tokenized_datasets['validation'][0:2]
y = Dataset.from_dict(x)
print(type(y))
predictions = trainer.predict(y)
print(predictions.predictions.shape)  # logits
# array([[-2.7887206,  3.1986978],
#       [ 2.5258656, -1.832253 ], ...], dtype=float32)
print(predictions.label_ids.shape) # array([1, 0, 0, 1, 0, 1, 0, 1, 1, 1, ...], dtype=int64)
print(predictions.metrics)
# 输出 label
print(predictions.label_ids)
print(x['label'])

# 总结

from datasets import Dataset
x = tokenized_datasets['validation'][0:2]
y = Dataset.from_dict(x)
print(type(y))
predictions = trainer.predict(y)
print(predictions.predictions.shape)  # logits
# array([[-2.7887206,  3.1986978],
#       [ 2.5258656, -1.832253 ], ...], dtype=float32)
print(predictions.label_ids.shape) # array([1, 0, 0, 1, 0, 1, 0, 1, 1, 1, ...], dtype=int64)
# 这里就计算出来指标了,可以直接查看
print(predictions.metrics)
metric = load_metric("glue", "mrpc")
metric.compute(predictions=np.argmax(predictions.predictions, axis=-1), references=predictions.label_ids)

如果是对 temp 的话

res = model(**temp)
metric = load_metric("glue", "mrpc")
# 要将 res.logits 转移到 cpu 上,
metric.compute(predictions=np.argmax(res.logits.cpu().detach().numpy(), axis=-1), references=tokenized_datasets['validation']['label'][0:2])

detach () 函数的用法:https://blog.csdn.net/Hodors/article/details/119248838

现在的问题是,在换 head 之后,模型的输出结果和 labels 指定的形式不一定是一样的,这会对 compute_metric 造成影响,解决方案是:

在训练模型前(此时 head 中的权重是随机初始化的),先

# cola

在上面的代码中,使用的都是 AutoModelFor... ,但是查看官网发现,是存在着 BertFor... 这样的类存在的,目前尚不清楚两者的区别。

对于 AutoModelForSequenceClassification ,模型的输出并不是非 0 即 1,对于输入的每个句子都会得到一个浮点数的结果,如果目标是二分类的话,最后使用 argmax(..., axis=-1) ,得到两个句子中的 0 或 1。

如果目标是类似 stsb 的 1-5 呢?该怎么处理输出的结果呢?

from transformers import AutoModelForSequenceClassification
from transformers import Trainer, TrainingArguments
from datasets import load_metric
import numpy as np
from transformers import AutoTokenizer, DataCollatorWithPadding
import datasets
checkpoint = 'bert-base-cased'
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
raw_datasets = datasets.load_dataset('glue', 'cola')
# 查看数据集的基本信息
raw_datasets['train'].features
def tokenize_function(sample):
    return tokenizer(sample['sentence'], truncation=True)
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True)
model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)  # new model
def compute_metrics(eval_preds):
    metric = load_metric("glue", "cola")
    logits, labels = eval_preds.predictions, eval_preds.label_ids
    # 上一行可以直接简写成:
    # logits, labels = eval_preds  因为它相当于一个 tuple
    predictions = np.argmax(logits, axis=-1)
    return metric.compute(predictions=predictions, references=labels)
training_args = TrainingArguments(output_dir='test_trainer', evaluation_strategy='epoch')  # 指定输出文件夹,没有会自动创建
trainer = Trainer(
    model,
    training_args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    # data_collator=data_collator,  # 在定义了 tokenizer 之后,其实这里的 data_collator 就不用再写了,会自动根据 tokenizer 创建
    tokenizer=tokenizer,
    compute_metrics=compute_metrics
)
trainer.train()

# 补充

# collate