【LLM Course】Ch 3 - 微调模型

处理数据

从模型重新加载数据集

加载数据集

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
    })
})

利用 datasets 中的 load_datasets, 可以通过输入参数进行加载
这是加载后的 Datasets 对象的的详细信息:

  • 包含训练集, 验证集, 测试集
  • 每一个集合都包含 4 个列, 以及表示行数的变量

访问数据集中对象

1
2
3
4
5
6
7
8
9
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 .',
}

使用字典, 访问数据集中的一个对象, 可以查看详细信息

查看数据集信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 对应的详细信息

预处理数据集

从文字到 id

这里要把数据集中的数据从文本转换为数字

1
2
3
4
5
6
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"])

这样一来, 数据被转换成了 input_ids

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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],
}

这里我们传入两句话, 可以看到返回的 inputs 中含有 toekn_type_ids
这里是在告诉模型那些是第一句的 token, 哪些是第二句的

从 id 到 token

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
tokenizer.convert_ids_to_tokens(inputs["input_ids"])
--------------------------
[
    "[CLS]",
    "this",
    "is",
    "the",
    "first",
    "sentence",
    ".",
    "[SEP]",
    "this",
    "is",
    "the",
    "second",
    "one",
    ".",
    "[SEP]",
]

这里和之前讲的 decode 的区别:

方法 作用 输出 是否自动拼接 是否去特殊符号
convert_ids_to_tokens 把每个 token id 映射回 token 字符串(逐个映射) token 列表 ❌ 不会拼接成一句话 ❌ 不会自动去掉 [CLS]、[SEP]
decode 把 token id 列表直接还原成一句 完整的可读文本 字符串(句子) ✅自动拼接(会考虑空格、断词) ✅ 默认去掉特殊 token(可以关闭)

加载数据集

不推荐的方法
1
2
3
4
5
6
tokenized_dataset = tokenizer(
    raw_datasets["train"]["sentence1"],
    raw_datasets["train"]["sentence2"],
    padding=True,
    truncation=True,
)

这种方法会把数据集一次性加载进内存, 转换成 python 字典
可能导致内存爆炸

推荐方法: 使用datasets类的方法

HF Datasets 库的设计理念是:

  • 数据存在磁盘上,内存里只加载你当前处理的 batch。
  • 底层使用 Apache Arrow(一种高效的数据格式),支持按需读取。
    所以:
    如果直接用 Dataset.map,会:
  • 批量读取一小块数据
  • 预处理后再存回磁盘(或者高效缓存),不用全加载进内存
  • 只在训练或读取时才按需读取
    这样:
  • 不会爆内存
  • 处理效率更高
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def tokenize_function(example):
    return tokenizer(example["sentence1"], example["sentence2"], truncation=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
    })
})

动态填充

这里又是一个"填充"的概念, 只不过和之前对于 token 的填充不同, 这里是针对每一个 batch 的填充
用于解决句子长度不统一的问题

1
2
3
from transformers import DataCollatorWithPadding

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

实例化时需要指定 tokenizer

微调模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from datasets import load_dataset
from transformers import AutoTokenizer, DataCollatorWithPadding

raw_datasets = load_dataset("glue", "mrpc")
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 类, 它包含 Trainer 在训练和评估中使用的所有超参数
只需要提供一个参数: 用于保存模型 checkpoint 的目录

1
2
3
from transformers import TrainingArguments

training_args = TrainingArguments("test-trainer")

定义模型

1
2
3
from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained(checkpoint, num_labels=2)

定义 Trainer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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,
)

微调模型

1
trainer.train()

评估

获取预测结果

1
2
predictions = trainer.predict(tokenized_datasets["validation"])
print(predictions.predictions.shape, predictions.label_ids.shape)

函数接受一个EvalPrediction 对象
(它是一个带有 predictions 和 label_ids 字段的参数元组)
并将返回一个字符串映射到浮点数的字典(字符串是返回的指标名称,而浮点数是其值)

获取最大索引

1
2
3
import numpy as np

preds = np.argmax(predictions.predictions, axis=-1)

计算指标

1
2
3
4
5
6
import evaluate

metric = evaluate.load("glue", "mrpc")
metric.compute(predictions=preds, references=predictions.label_ids)
-------------------------------
{'accuracy': 0.8578431372549019, 'f1': 0.8996539792387542}

打包后得到的函数

1
2
3
4
5
def compute_metrics(eval_preds):
    metric = evaluate.load("glue", "mrpc")
    logits, labels = eval_preds
    predictions = np.argmax(logits, axis=-1)
    return metric.compute(predictions=predictions, references=labels)

最终 trainer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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,
)
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计