import torch
from torch.optim import AdamW
from transformers import 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()

几个步骤完成训练:

  1. 分词:这里tokenizer直接把输入的句子分为ids和attention_mask张量;
  2. 模型:使用同一个checkpoint加载,使用与训练时相同的safe_tensors以及分词方法;
  3. 标签:这里给输入的两个句子添加了一个labels;

但是数据量过少,没法很好的训练模型。

Hub中加载数据

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加载glue benchmark中的mrpc数据集。

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
>>>{'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)}

类似于使用字典的,可以通过索引、通过键的方式加载具体数据。使用.features加载数据集中的各个字段的格式,特别是标签的0/1表示的是什么具体含义。

预处理数据

from transformers import AutoTokenizer

checkpoint = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
raw_train_dataset = raw_datasets["train"]
train_dataset_slice = raw_train_dataset[15]
inputs1 = tokenizer(train_dataset_slice['sentence1'])
inputs2 = tokenizer(train_dataset_slice['sentence2'])
inputs = tokenizer(train_dataset_slice['sentence1'], train_dataset_slice['sentence2'])

print(inputs1)
print(inputs2)
print(inputs)

>>>{'input_ids': [101, 24049, 2001, 2087, 3728, 3026, 3580, 2343, 2005, 1996, 9722, 1004, 4132, 9340, 12439, 2964, 2449, 1012, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}  // inputs1
>>>{'input_ids': [101, 3026, 3580, 2343, 4388, 24049, 1010, 3839, 2132, 1997, 1996, 9722, 1998, 4132, 9340, 12439, 2964, 3131, 1010, 2097, 2599, 1996, 2047, 9178, 1012, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}  // inputs2
>>>{'input_ids': [101, 24049, 2001, 2087, 3728, 3026, 3580, 2343, 2005, 1996, 9722, 1004, 4132, 9340, 12439, 2964, 2449, 1012, 102, 3026, 3580, 2343, 4388, 24049, 1010, 3839, 2132, 1997, 1996, 9722, 1998, 4132, 9340, 12439, 2964, 3131, 1010, 2097, 2599, 1996, 2047, 9178, 1012, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]} //inputs

可以发现tokenizer可以同时解析两句话,以batch的形式进行分词。

[!NOTE]

需要注意的是,以batch形式进行分词时,token_type_ids中的0/1表示不同的两个句子。

sentences = tokenizer.convert_ids_to_tokens(inputs["input_ids"])

>>>sentences
>>>inputs['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]

通过将输入的tokens解析回句子,并拿着token_type_ids进行对照后,可以发现:

[CLS] sentence1 [SEP] sentence2 [SEP]

呈现上述形式,并且[CLS] sentence1 [SEP]对应第一句话(token_type_ids是0),sentence2 [SEP]对应第二句(token_type_ids是1)

那么更进一步的,可以直接使用

tokenized_dataset = tokenizer(
    raw_datasets["train"]["sentence1"],
    raw_datasets["train"]["sentence2"],
    padding=True,
    truncation=True,
)

但是这种方法会返回字典(包含键、input_ids、attention_mask 和 token_type_ids,以及List[List]值),要求有足够的内存来存储整个数据集时使用。

使用map加速处理
def tokenize_function(example):
    return tokenizer(example["sentence1"], example["sentence2"], truncation=True)

其中example是raw_train_datasets中的一项

这里不使用padding=True是因为这一步在构建批次时填充样本更好,这样我们只需填充该批次的最大长度,而不必填充整个数据集的最大长度

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

>>>tokenized_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
    })
})

#开发 使用Dataset.map()方法进行批量batched=True处理。

这种方法在定义字段后,解析多条往对应字段添加数据。

使用dynamic padding填充
from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

samples = tokenized_datasets["train"][:8]
samples = {k: v for k, v in samples.items() if k not in ["idx", "sentence1", "sentence2"]}
>>> [len(x) for x in samples["input_ids"]]
>>> [50, 59, 47, 67, 59, 50, 62, 32]

这里以8个为例,可以看到输入的input_idsd长度都是不一致的。

那么我们直接使用data_collator去对其进行批次长度填充

batch = data_collator(samples)
>>>{k: v.shape for k, v in batch.items()}
>>>{'attention_mask': torch.Size([8, 67]),
 'input_ids': torch.Size([8, 67]),
 'token_type_ids': torch.Size([8, 67]),
 'labels': torch.Size([8])}

实现按照批次最长长度完成动态填充。

更多推荐