《Hands-On Large Language Models》第12章 生成模型的微调

\(\text{12}\) 生成模型的微调

Fine-Tuning Generation Models

在本章中,我们将采用一个预训练文本生成模型,并介绍微调它的过程。这一微调步骤是产生高质量模型的关键,也是我们工具箱中用于使模型适应特定期望行为的重要工具。微调使我们能够将模型适应特定的数据集或领域

在本章中,我们将指导您了解微调文本生成模型最常用的两种方法有监督微调 (\(\text{supervised fine-tuning}\)) 和偏好调整 (\(\text{preference tuning}\))。我们将探索微调预训练文本生成模型的变革潜力,使其成为您应用程序的更有效工具。

大语言模型 (\(\text{LLM}\)) 的三个训练步骤:预训练、有监督微调和偏好调整

The Three LLM Training Steps: Pretraining, Supervised Fine-Tuning, and Preference Tuning

创建高质量大语言模型 (\(\text{LLM}\)) 通常有三个常见步骤

  1. 语言建模

    \(\text{Language modeling}\)

    创建高质量 \(\text{LLM}\)第一步是使用一个或多个海量文本数据集对其进行预训练(图 \(\text{12}-1\))。在训练过程中,它尝试预测下一个词元 (\(\text{token}\)),以准确学习文本中发现的语言和语义表征。正如我们在第 \(\text{3}\) 章和第 \(\text{11}\) 章中看到的那样,这被称为语言建模,是一种自监督方法

    F12.1

这会产生一个基础模型 (\(\text{base model}\)),通常也称为预训练模型 (\(\text{pretrained}\)) 或基础模型 (\(\text{foundation model}\))。基础模型是训练过程中的一个关键产物,但终端用户处理起来比较困难。这就是为什么下一步很重要

  1. 微调 \(\text{1}\) (有监督微调)

    \(\text{Fine-tuning 1 (supervised fine-tuning)}\)

    如果 \(\text{LLM}\)指令反应良好并尝试遵循它们,它们就会更有用。当人类要求模型写一篇文章时,他们期望模型生成文章,而不是列出其他指令(这可能是基础模型会做的事情)。

    通过有监督微调 (\(\text{SFT}\)),我们可以使基础模型适应遵循指令。在此微调过程中,基础模型的参数会更新,使其更符合我们的目标任务,例如遵循指令。与预训练模型一样,它也是使用下一个词元预测进行训练,但它不是仅预测下一个词元,而是基于用户输入进行预测(图 \(\text{12}-2\))。

F12.1

\(\text{SFT}\) 也可用于其他任务,例如分类,但通常用于将基础生成模型转换为指令(或聊天)生成模型

  1. 微调 \(\text{2}\) (偏好调整)

    \(\text{Fine-tuning 2 (preference tuning)}\)

    最后一步进一步提高模型的质量,使其更符合 \(\text{AI}\) 安全或人类偏好的预期行为。这被称为偏好调整 (\(\text{preference tuning}\))。偏好调整是微调的一种形式,顾名思义,它使模型的输出与我们的偏好对齐,这些偏好由我们提供给它的数据定义。与 \(\text{SFT}\) 一样,它可以改进原始模型,但增加了在训练过程中提炼输出偏好的好处。这三个步骤如图 \(\text{12}-3\) 所示,展示了从一个未经训练的架构开始到以一个经过偏好调整的 \(\text{LLM}\) 结束的整个过程。

F12.1

在本章中,我们使用一个已经在海量数据集上训练过的基础模型,并探索如何使用这两种微调策略对其进行微调。对于每种方法,我们都从理论基础开始,然后将其应用于实践

有监督微调

Supervised Fine-Tuning (SFT)

大型数据集预训练模型的目的是使其能够再现语言及其含义。在这个过程中,模型学习完成输入短语,如图 \(\text{12}-4\) 所示。

F12.1

这个例子也说明模型没有经过遵循指令的训练,而是会尝试完成一个问题而不是回答它(图 \(\text{12}-5\))。

F12.1

我们可以使用这个基础模型,并通过微调将其适应某些用例,例如遵循指令

完全微调

Full Fine-Tuning

最常见的微调过程是完全微调 (\(\text{full fine-tuning}\))。与预训练 \(\text{LLM}\) 一样,这个过程涉及更新模型的所有参数,使其与您的目标任务保持一致。主要的区别在于我们现在使用更小但有标签的数据集,而预训练过程是在没有标签的大型数据集上完成的(图 \(\text{12}-6\))。

F12.1

您可以将任何有标签的数据用于完全微调,这也使其成为学习特定领域表征的绝佳技术。为了让我们的 \(\text{LLM}\) 遵循指令,我们将需要问题-回答数据。如图 \(\text{12}-7\) 所示,这些数据是用户查询及其对应的答案

F12.1

完全微调过程中,模型接收输入(指令),并对输出(回答)应用下一个词元预测。反过来,它将遵循指令,而不是生成新的问题。

参数高效微调

Parameter-Efficient Fine-Tuning (PEFT)

更新模型的所有参数具有提高其性能的巨大潜力,但也伴随着几个缺点。它训练成本高昂训练时间缓慢,并且需要大量的存储空间。为了解决这些问题,人们将注意力转向了参数高效微调 (\(\text{PEFT}\)) 替代方案,这些方案专注于以更高的计算效率微调预训练模型

适配器

Adapters

适配器是许多基于 \(\text{PEFT}\) 的技术的核心组件。该方法提出了 \(\text{Transformer}\) 内部的一组附加的模块化组件,可以对其进行微调提高模型在特定任务上的性能,而无需微调所有模型权重。这节省了大量的时间和计算资源。

适配器在论文《Parameter-efficient transfer learning for NLP》中得到了描述,该论文表明,仅微调 \(\text{BERT}\) \(\text{3.6\%}\) 的参数即可在任务上产生与微调所有模型权重相当的性能。在 \(\text{GLUE}\) 基准测试中,作者表明他们的性能达到了完全微调性能的 \(\text{0.4\%}\) 以内。在一\(\text{Transformer}\)中,该论文提出的架构将适配器放置在注意力层前馈神经网络之后,如图 \(\text{12}-8\) 所示。

F12.1

然而,仅更改一个 \(\text{Transformer}\) 块是不够的,因此这些组件是模型中每个块的一部分,如图 \(\text{12}-9\) 所示。

F12.1

像这样查看模型中所有的适配器组件,使我们能够看到单个适配器,如图 \(\text{12}-10\) 所示,它是跨越模型所有块的这些组件的集合适配器 \(\text{1}\) 可以是医学文本分类的专家,而适配器 \(\text{2}\) 可以专注于命名实体识别 (\(\text{NER}\))。您可以从 https://oreil.ly/XraXg 下载专业适配器。

F12.1

论文《AdapterHub: A framework for adapting transformers》引入了 \(\text{Adapter Hub}\) 作为共享适配器的中央存储库。许多早期的适配器更侧重于 \(\text{BERT}\) 架构。最近,这个概念已被应用于文本生成 \(\text{Transformer}\) 模型中,例如论文《LLaMA-Adapter: Efficient fine-tuning of language models with zero-init attention》

低秩适应

Low-Rank Adaptation (LoRA)

作为适配器的一种替代方案,低秩适应 (\(\text{LoRA}\)) 被引入,并且在撰写本文时是一种广泛使用且有效\(\text{PEFT}\) 技术。\(\text{LoRA}\) 是一种(像适配器一样)只需要更新一小组参数的技术。如图 \(\text{12}-11\) 所示,它创建了基础模型的一个小子集进行微调,而不是向模型添加层

F12.1

适配器 (\(\text{adapters}\)) 类似,这个子集使得微调快得多,因为我们只需要更新基础模型的一小部分。我们通过用较小的矩阵近似伴随原始 \(\text{LLM}\)大矩阵,从而创建这个参数子集。然后,我们可以使用这些较小的矩阵作为替代品,并微调它们而不是原始的大矩阵。以我们在图 \(\text{12}-12\) 中看到的 \(\text{10} \times \text{10}\) 矩阵为例。

F12.1

我们可以设计出两个较小的矩阵,当它们相乘时,可以重构一个 \(\text{10} \times \text{10}\) 矩阵。这是一个主要的效率胜利,因为我们现在只有 \(\text{20}\) 个权重 (\(\text{10}\)\(\text{10}\)),而不是使用 \(\text{100}\) 个权重 (\(\text{10}\) 乘以 \(\text{10}\)),如图 \(\text{12}-13\) 所示。

F12.1

在训练过程中,我们只需要更新这些较小的矩阵,而不需要更新完整的权重变化。然后,更新后的变化矩阵(较小的矩阵)会与完整的(冻结的)权重结合,如图 \(\text{12}-14\) 所示。

F12.1

但您可能会怀疑性能会下降。您猜对了。但这种权衡在什么地方是合理的呢?

《Intrinsic dimensionality explains the effectiveness of language model fine-tuning》这样的论文表明,语言模型“具有非常低的内在维度。”这意味着我们可以找到小的秩 (\(\text{ranks}\)) 来近似 \(\text{LLM}\)即使是巨大的矩阵。例如,像 \(\text{GPT-3}\) 这样的 \(\text{175B}\) 模型,在其 \(\text{96}\)\(\text{Transformer}\)中的每个块中都会有一个 \(\text{12,288} \times \text{12,288}\) 的权重矩阵。这是 \(\text{1.5}\) 亿个参数。如果我们可以成功地将该矩阵适应到秩 \(\text{8}\),那么每个块将只需要两个 \(\text{12,288} \times \text{8}\) 的矩阵,从而产生 \(\text{197K}\) 个参数。正如之前引用的 \(\text{LoRA}\) 论文中进一步解释的那样,这在速度存储计算方面节省了大量资源。

这种较小的表征非常灵活,您可以选择基础模型的哪些部分进行微调。例如,我们可以只微调每个 \(\text{Transformer}\) 层中的 \(\text{Query}\)\(\text{Value}\) 权重矩阵

压缩模型以实现(更)高效的训练

Compressing the model for (more) efficient training

我们可以通过在将模型的原始权重投影到较小的矩阵之前减少其内存需求,从而使 \(\text{LoRA}\) 更加高效\(\text{LLM}\) 的权重是具有给定精度的数值,可以用浮点数(如 \(\text{float64}\)\(\text{float32}\)来表示。如图 \(\text{12}-15\) 所示,如果我们降低表示一个值的位数,我们得到的结果准确性就会降低。然而,如果降低位数,我们也会降低该模型的内存需求

F12.1

通过量化 (\(\text{quantization}\)),我们的目标是在准确表示原始权重值的同时降低位数。然而,如图 \(\text{12}-16\) 所示,当直接将更高精度值映射到更低精度值时,多个更高精度值最终可能被同一个更低精度值表示

F12.1

相反,\(\text{LoRA}\) 的量化版本 \(\text{QLoRA}\) 的作者找到了一种方法,可以在不与原始权重产生太大差异的情况下,从较高的位数转换到较低的值,反之亦然。

他们使用了块状量化 (\(\text{blockwise quantization}\)) 将某些块的较高精度值映射到较低精度值。不是直接将较高精度映射到较低精度值,而是创建额外的块允许量化相似的权重。如图 \(\text{12}-17\) 所示,这使得值可以用较低的精度准确表示

F12.1

神经网络的一个很好的特性是它们的值通常呈正态分布\(\text{-1}\)\(\text{1}\) 之间。正如\(\text{12}-18\) 所示,这种特性允许我们根据权重的相对密度将原始权重分箱到较低的位数权重之间的映射效率更高,因为它考虑了权重的相对频率。这也减少了异常值的问题

F12.1

结合块状量化 (\(\text{blockwise quantization}\)),这种归一化过程允许低精度值准确表示高精度值,而大语言模型的性能只会略有下降。因此,我们可以从 \(\text{16}\) 位浮点表示转换为区区 \(\text{4}\) 位归一化浮点表示\(\text{4}\) 位表示可以显著减少 \(\text{LLM}\) 在训练期间的内存需求。请注意,\(\text{LLM}\) 的量化通常也有助于推理,因为量化后的 \(\text{LLM}\) 体积更小,因此需要的 \(\text{VRAM}\) 也更少

还有更多优雅的方法可以进一步优化这一点,例如双重量化 (\(\text{double quantization}\)) 和分页优化器 (\(\text{paged optimizers}\)),您可以在前面讨论的 \(\text{QLoRA}\) 论文中阅读更多相关内容。如需完整的、高度可视化的量化指南,请参阅这篇博客文章

使用 \(\text{QLoRA}\) 进行指令调整

Instruction Tuning with QLoRA

现在我们已经探索了 \(\text{QLoRA}\) 的工作原理,让我们将这些知识付诸实践!在本节中,我们将微调 \(\text{Llama}\) 的一个完全开源和更小版本 \(\text{TinyLlama}\),以使用 \(\text{QLoRA}\) 流程遵循指令。将该模型视为一个基础或预训练模型,它经过语言建模训练,但尚不能遵循指令

模板化指令数据

Templating Instruction Data

为了让 \(\text{LLM}\) 遵循指令,我们需要准备遵循聊天模板的指令数据。正如\(\text{12}-19\) 所示,这个聊天模板区分了 \(\text{LLM}\) 生成的内容用户生成的内容

F12.1

我们选择在整个示例中使用这个聊天模板,因为 \(\text{TinyLlama}\) 的聊天版本使用了相同的格式。我们使用的数据是 \(\text{UltraChat}\) 数据集的一个小规模子集。该数据集是原始 \(\text{UltraChat}\) 数据集的过滤版本,其中包含\(\text{200,000}\) 个用户和 \(\text{LLM}\) 之间的对话

我们创建一个名为 \(\text{format\_prompt}\) 的函数,以确保对话遵循此模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from transformers import AutoTokenizer
from datasets import load_dataset
# Load a tokenizer to use its chat template
template_tokenizer = AutoTokenizer.from_pretrained(
"TinyLlama/TinyLlama-1.1BChat-v1.0"
)
def format_prompt(example):
"""Format the prompt to using the <|user|> template TinyLLama is using"""
# Format answers
chat = example["messages"]
prompt = template_tokenizer.apply_chat_template(chat, tokenize=False)
return {"text": prompt}
# Load and format the data using the template TinyLLama is using
dataset = (
load_dataset("HuggingFaceH4/ultrachat_200k", split="test_sft")
.shuffle(seed=42)
.select(range(3_000))
)
dataset = dataset.map(format_prompt)

我们选择了 \(\text{3,000}\) 个文档的子集减少训练时间,但您可以增加这个值以获得更准确的结果

使用 \(\text{"text"}\),我们可以探索这些格式化的提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Example of formatted prompt
print(dataset["text"][2576])
<|user|>
Given the text: Knock, knock. Who's there? Hike.
Can you continue the joke based on the given text material "Knock, knock.
Who's there? Hike"?</s>
<|assistant|>
Sure! Knock, knock. Who's there? Hike. Hike who? Hike up your pants, it's cold
outside!</s>
<|user|>

Can you tell me another knock-knock joke based on the same text material
"Knock, knock. Who's there? Hike"?</s>
<|assistant|>
Of course! Knock, knock. Who's there? Hike. Hike who? Hike your way over here
and let's go for a walk!</s>

模型量化

Model Quantization

现在我们有了数据,就可以开始加载我们的模型了。这就是我们应用 \(\text{QLoRA}\) 中的 \(\text{Q}\),即量化 (\(\text{quantization}\)) 的地方。我们使用 \(\text{bitsandbytes}\)将预训练模型压缩为 \(\text{4}\) 位表示

\(\text{BitsAndBytesConfig}\) 中,您可以定义量化方案。我们遵循原始 \(\text{QLoRA}\) 论文中使用的步骤,以 \(\text{4}\)加载模型(\(\text{load\_in\_4bit}\)),使用归一化浮点表示\(\text{bnb\_4bit\_quant\_type}\))和双重量化\(\text{bnb\_4bit\_use\_double\_quant}\)):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_name = "TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T"
# 4-bit quantization configuration - Q in QLoRA
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # Use 4-bit precision model loading
bnb_4bit_quant_type="nf4", # Quantization type
bnb_4bit_compute_dtype="float16", # Compute dtype
bnb_4bit_use_double_quant=True, # Apply nested quantization
)
# Load the model to train on the GPU
model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map="auto",
# Leave this out for regular SFT
quantization_config=bnb_config,
)
model.config.use_cache = False
model.config.pretraining_tp = 1
# Load LLaMA tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = "<PAD>"
tokenizer.padding_side = "left"

这种量化过程使我们能够减小原始模型的尺寸,同时保留大部分原始权重的精度。现在加载模型只需要 \(\sim 1 GB\) 的显存 (\(\text{VRAM}\)),而不进行量化则需要 \(\sim 4 GB\) 的显存。请注意,在微调期间,将需要更多的显存,因此它不会限制在加载模型所需的 \(\sim 1 GB\) 显存上。

\(\text{LoRA}\) 配置

LoRA Configuration

接下来,我们需要使用 \(\text{peft}\)来定义我们的 \(\text{LoRA}\) 配置,它代表了微调过程的超参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from peft import LoraConfig, prepare_model_for_kbit_training, get_peft_model
# Prepare LoRA Configuration
peft_config = LoraConfig(
lora_alpha=32, # LoRA Scaling
lora_dropout=0.1, # Dropout for LoRA Layers
r=64, # Rank
bias="none",
task_type="CAUSAL_LM",
target_modules= # Layers to target
["k_proj", "gate_proj", "v_proj", "up_proj", "q_proj", "o_proj",
"down_proj"]
)
# Prepare model for training
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, peft_config)

有几个参数值得一提:

  • \(\text{r}\) 这是压缩矩阵的秩(回想图 \(\text{12}-13\) 中的内容)。增加这个值也会增加压缩矩阵的大小,从而降低压缩率,进而提高代表能力。该值通常在 \(\text{4}\)\(\text{64}\) 之间。
  • \(\text{lora\_alpha}\) 控制添加到原始权重的变化量。本质上,它平衡了原始模型的知识与新任务的知识。一个经验法则是选择 \(\text{r}\) 两倍大小的值
  • \(\text{target\_modules}\) 控制要针对哪些层\(\text{LoRA}\) 过程可以选择忽略特定层,例如特定的投影层。这可以加快训练速度,但会降低性能,反之亦然。

尝试调整这些参数是一个有价值的实验,可以直观地理解哪些值有效,哪些值无效。您可以在 \(\text{Sebastian Raschka}\)\(\text{Ahead of AI}\) 时事通讯中找到关于 \(\text{LoRA}\) 微调的额外技巧的精彩资源。

本示例展示了高效微调模型的形式。如果您想进行完全微调 (\(\text{full fine-tuning}\)),您可以在加载模型时移除 \(\text{quantization\_config}\) 参数,并跳过 \(\text{peft\_config}\) 的创建。通过移除这些内容,我们将从“使用 \(\text{QLoRA}\) 进行指令调整”转变为“完全指令调整” (\(\text{full instruction tuning}\))。

训练配置

Training Configuration

最后,我们需要像\(\text{11}\)中那样配置我们的训练参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from transformers import TrainingArguments
output_dir = "./results"
# Training arguments
training_arguments = TrainingArguments(
output_dir=output_dir,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
optim="paged_adamw_32bit",
learning_rate=2e-4,
lr_scheduler_type="cosine",
num_train_epochs=1,
logging_steps=10,
fp16=True,
gradient_checkpointing=True
)

有几个参数值得一提:

  • \(\text{num\_train\_epochs}\) 总训练轮数。较高的值往往会降低性能,因此我们通常倾向于保持较低
  • \(\text{learning\_rate}\) 决定了权重更新的每一步的大小\(\text{QLoRA}\) 的作者发现较高的学习率更适用于更大的模型\(>\) \(\text{33B}\) 参数)。
  • \(\text{lr\_scheduler\_type}\) 一种基于余弦的调度器,用于动态调整学习率。它将从零开始线性增加学习率,直到达到设定的值。之后,学习率将遵循余弦函数的值进行衰减
  • \(\text{optim}\) 原始 \(\text{QLoRA}\) 论文中使用的分页优化器 (\(\text{paged optimizers}\))。

优化这些参数是一项困难的任务,并且没有固定的指导方针。它需要进行实验才能找出最适合特定数据集、模型大小和目标任务的方案

尽管本节描述的是指令调整 (\(\text{instruction tuning}\)),我们也可以使用 \(\text{QLoRA}\) 来微调一个指令模型。例如,我们可以微调一个聊天模型来生成特定的 \(\text{SQL}\) 代码,或创建遵循特定格式的 \(\text{JSON}\) 输出。只要您有可用的数据(带有适当的查询-回答项),\(\text{QLoRA}\) 就是一种极好的技术,可以推动现有聊天模型更适合您的用例

训练

Training

现在我们已经准备好所有的模型和参数,就可以开始微调我们的模型了。我们加载 \(\text{SFTTrainer}\) 并简单地运行 \(\text{trainer.train()}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from trl import SFTTrainer
# Set supervised fine-tuning parameters
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
dataset_text_field="text",
tokenizer=tokenizer,
args=training_arguments,
max_seq_length=512,
# Leave this out for regular SFT
peft_config=peft_config,
)
# Train model
trainer.train()
# Save QLoRA weights
trainer.model.save_pretrained("TinyLlama-1.1B-qlora")

在训练过程中,损失将根据 \(\text{logging\_steps}\) 参数\(\text{10}\) 步打印一次。如果您使用的是 \(\text{Google Colab}\) 提供的免费 \(\text{GPU}\)(在撰写本文时是 \(\text{Tesla T4}\)),那么训练可能需要长达一个小时。是时候休息一下了!

合并权重

Merge Weights

在训练完我们的 \(\text{QLoRA}\) 权重后,我们仍然需要将它们与原始权重结合起来才能使用。我们\(\text{16}\)(而不是量化后的 \(\text{4}\) 位)重新加载模型以合并权重。尽管 \(\text{tokenizer}\) 在训练期间没有更新,但我们将其保存到与模型相同的文件夹中,以便于访问:

1
2
3
4
5
6
7
8
from peft import AutoPeftModelForCausalLM
model = AutoPeftModelForCausalLM.from_pretrained(
"TinyLlama-1.1B-qlora",
low_cpu_mem_usage=True,
device_map="auto",
)
# Merge LoRA and base model
merged_model = model.merge_and_unload()

适配器与基础模型合并后,我们就可以使用我们之前定义的提示模板来使用它:

1
2
3
4
5
6
7
8
9
from transformers import pipeline
# Use our predefined prompt template
prompt = """<|user|>
Tell me something about Large Language Models.</s>
<|assistant|>
"""
# Run our instruction-tuned model
pipe = pipeline(task="text-generation", model=merged_model, tokenizer=tokenizer)
print(pipe(prompt)[0]["generated_text"])
1
2
3
Large Language Models (LLMs) are artificial intelligence (AI) models that
learn language and understand what it means to say things in a particular
language. They are trained on huge amounts of text…

汇总输出显示模型现在密切遵循我们的指令,这在基础模型中是不可能的。

评估生成模型

Evaluating Generative Models

评估生成模型提出了一个重大挑战。生成模型用于许多不同的用例,这使得依赖单一指标进行判断成为一个挑战。与更专业的模型不同,生成模型解决数学问题的能力不保证在解决编码问题上也能成功。

与此同时,评估这些模型至关重要,尤其是在一致性很重要的生产环境中。鉴于它们的概率性质,生成模型不一定会产生一致的输出;因此需要稳健的评估

在本节中,我们将探索几种常见的评估方法,但我们想要强调当前缺乏黄金标准没有一个指标适用于所有用例的完美衡量标准

词汇级别指标

Word-Level Metrics

一种常见的比较生成模型的指标类别词汇级别评估 (\(\text{word-level evaluation}\))。这些经典技术词元(集)级别比较参考数据集和生成的词元。常见的词汇级别指标包括困惑度 (\(\text{perplexity}\))、\(\text{ROUGE}\)\(\text{BLEU}\)\(\text{BERTScore}\)

值得注意的是困惑度,它衡量语言模型预测文本的准确程度。给定输入文本,模型预测下一个词元的可能性。使用困惑度,我们假设如果模型赋予下一个词元高概率,则性能更好。换句话说,当呈现一篇写得好的文档时,模型不应该感到“困惑”

如图 \(\text{12}-20\) 所示,当呈现输入 “\(\text{When a measure becomes a}\)” 时,模型被问及“\(\text{target}\)” 作为下一个词的可能性有多大。

F12.1

尽管困惑度以及其他词汇级别指标理解模型置信度的有用指标,但它们并非完美的衡量标准。它们没有考虑到生成文本的一致性、流畅性、创造性,甚至正确性

基准测试

Benchmarks

评估生成模型语言生成和理解任务上性能的一种常用方法是使用众所周知的公共基准测试 (\(\text{benchmarks}\)),例如 \(\text{MMLU}\)\(\text{GLUE}\)\(\text{TruthfulQA}\)\(\text{GSM8k}\)\(\text{HellaSwag}\)。这些基准测试为我们提供了基础语言理解以及复杂分析回答(例如数学问题)的信息

除了自然语言任务,一些模型专注于其他领域,例如编程。这些模型通常在不同的基准测试上进行评估,例如 \(\text{HumanEval}\),它包含具有挑战性的编程任务供模型解决。\(\text{12}-1\) 概述了生成模型的常见公共基准测试

F12.1

基准测试了解模型在各种任务上表现如何的好方法。公共基准测试的一个缺点是,模型可能会过度拟合 (\(\text{overfitted}\)) 这些基准测试以生成最佳回复。此外,这些仍然是广泛的基准测试,可能无法涵盖非常具体的用例。最后,另一个缺点是,有些基准测试需要强大的 \(\text{GPU}\) 才能计算,并且运行时间长(超过数小时),这使得迭代变得困难

排行榜

Leaderboards

有如此多不同的基准测试,很难选择哪个基准测试最适合您的模型。每当模型发布时,您通常会看到它在多个基准测试上进行评估,以展示其全面表现

因此,开发了包含多个基准测试的排行榜 (\(\text{leaderboards}\))。一个常见的排行榜是 \(\text{Open LLM Leaderboard}\),在撰写本文时,它包括六个基准测试,其中包括 \(\text{HellaSwag}\)\(\text{MMLU}\)\(\text{TruthfulQA}\)\(\text{GSM8k}\)。位居排行榜榜首的模型(假设它们没有在数据上过度拟合)通常被认为是“最好的”模型。然而,由于这些排行榜通常包含公开可用的基准测试,因此存在在排行榜上过度拟合的风险

自动化评估

Automated Evaluation

评估生成输出的一部分是其文本的质量。例如,即使两个模型对一个问题给出了相同的正确答案,它们得出该答案的方式也可能不同。这通常不仅仅是最终答案的问题,还关乎其构造。类似地,尽管两个摘要可能相似,但其中一个可能明显短于另一个,这对于一个好的摘要来说通常很重要

为了评估生成文本的质量不仅仅是最终答案的正确性,引入了\(\text{LLM}\) 即评审” (\(\text{LLM-as-a-judge}\)) 的方法。实质上,一个单独的 \(\text{LLM}\) 被要求判断待评估 \(\text{LLM}\) 的质量。这种方法的一个有趣的变体成对比较 (\(\text{pairwise comparison}\))。两个不同的 \(\text{LLM}\) 将生成一个问题的答案,而第三个 \(\text{LLM}\) 将充当评委来裁定哪个更好

因此,这种方法论允许对开放式问题进行自动化评估。一个主要优势是,随着 \(\text{LLM}\) 的改进,它们判断输出质量的能力也会提高。换句话说,这种评估方法与该领域共同发展

人工评估

Human Evaluation

尽管基准测试很重要,但评估的黄金标准通常被认为是人工评估 (\(\text{human evaluation}\))。即使一个 \(\text{LLM}\)广泛的基准测试上得分很高,它仍可能在领域特定的任务上得分不高。此外,基准测试并不能完全捕捉人类偏好,而之前讨论的所有方法都仅仅是人类偏好的替代指标

一个基于人工评估技术的绝佳示例\(\text{Chatbot Arena}\)(https://lmarena.ai/)。当您进入这个排行榜时,您会看到两个(匿名)\(\text{LLM}\),您可以与之互动。您提出的任何问题或提示都将发送给这两个模型,并且您将收到它们的输出。然后,您可以决定您更喜欢哪个输出。这个过程允许社区对他们更喜欢的模型进行投票,而不知道呈现给他们的是哪个模型只有在您投票后,您才会看到哪个模型生成了哪个文本

在撰写本文时,这种方法已经产生了 \(\text{800,000}\) 多次人工投票,用于计算排行榜。这些投票用于根据 \(\text{LLM}\) 的胜率来计算其相对技能水平。例如,如果一个低排名的 \(\text{LLM}\) 击败了一个高排名的 \(\text{LLM}\),它的排名就会显著变化。在国际象棋中,这被称为 \(\text{Elo}\) 等级分系统

因此,这种方法论使用了众包投票(crowdsourced vote),这有助于我们了解 \(\text{LLM}\) 的质量。然而,它仍然是广泛用户群体的聚合意见可能与您的用例不相关

因此,没有一种完美的 \(\text{LLM}\) 评估方法。所有提到的方法论和基准测试都提供了重要但有限的评估视角。我们的建议是根据预期的用例来评估您的 \(\text{LLM}\)。对于编码\(\text{HumanEval}\) 会比 \(\text{GSM8k}\) 更合乎逻辑。

但最重要的是,我们相信您是最好的评估者人工评估仍然是黄金标准,因为最终由您决定 \(\text{LLM}\) 是否适用于您预期的用例。与本章中的示例一样,我们强烈建议您也尝试这些模型,并自己设计一些问题。例如,本书的作者是阿拉伯人 (\(\text{Jay Alammar}\)) 和荷兰人 (\(\text{Maarten Grootendorst}\)),当我们接触新模型时,我们经常用我们的母语提问

关于这个主题的最后一点是我们珍视的一句话

当一个衡量标准变成一个目标时,它就不再是一个好的衡量标准了。古德哈特定律 (\(\text{Goodhart’s Law}\))

\(\text{LLM}\) 的背景下,当使用特定的基准测试时,我们倾向于为该基准测试进行优化,而不顾后果。例如,如果我们纯粹专注于优化生成语法正确的句子,模型可能会学会只输出一个句子:“\(\text{This is a sentence.}\)”(这是一个句子。)它在语法上是正确的,但没有告诉你任何关于其语言理解能力的信息。因此,模型可能擅长某个特定的基准测试,但可能会牺牲其他有用的能力

偏好调整/对齐/强化学习自人类反馈

Preference-Tuning / Alignment / RLHF

尽管我们的模型现在可以遵循指令,但我们可以通过最后的训练阶段进一步改进其行为,使其与我们期望它在不同场景下的行为保持一致。例如,当被问到“\(\text{What is an LLM?}\)”(什么是 \(\text{LLM}\)?)时,我们可能更喜欢一个详细描述 \(\text{LLM}\) 内部结构的答案,而不是没有进一步解释的“\(\text{It is a large language model}\)”(它是一个大型语言模型)这样的答案。我们究竟如何将我们(人类)对一个答案的偏好(优于另一个答案)与 \(\text{LLM}\) 的输出对齐呢?

首先,回想一下 \(\text{LLM}\) 接受一个提示并输出一个生成结果,如图 \(\text{12}-21\) 所示。

F12.1

我们可以要求一个人偏好评估者 \(\text{preference evaluator}\)评估该模型生成的质量。假设他们给它分配了某个分数,比如 \(\text{4}\) 分(参见图 \(\text{12}-22\))。

F12.1

\(\text{12}-23\) 显示了基于该分数更新模型偏好调整步骤

  • 如果分数很高,则更新模型鼓励它生成更多此类生成结果
  • 如果分数很低,则更新模型阻止此类生成结果。

F12.1

一如既往,我们需要许多训练示例。那么,我们能否将偏好评估自动化呢?是的,我们可以通过训练一个不同的模型,称为奖励模型 (\(\text{reward model}\)),来实现。

使用奖励模型实现偏好评估自动化

Automating Preference Evaluation Using Reward Models

为了实现偏好评估的自动化,我们需要在偏好调整步骤之前增加一个步骤,即训练一个奖励模型 (\(\text{reward model}\)),如图 \(\text{12}-24\) 所示。

F12.1

\(\text{12}-25\) 显示,为了创建奖励模型,我们复制了经过指令调整的模型 (\(\text{instruction-tuned model}\)),并对其进行了微小改动,使其不再生成文本,而是输出一个单一的分数

F12.1

奖励模型的输入和输出

The Inputs and Outputs of a Reward Model

我们期望这个奖励模型的工作方式是:我们给它一个提示和一个生成结果,它会输出一个单一的数字,表明该生成结果对该提示的偏好/质量。图 \(\text{12}-26\) 显示了奖励模型生成这个单一数字的过程。

F12.1

训练奖励模型

Training a Reward Model

我们不能直接使用奖励模型。它需要先经过训练才能正确地给生成结果评分。因此,让我们获取一个模型可以学习的偏好数据集

奖励模型训练数据集

Reward model training dataset

偏好数据集的一种常见形式是,一个训练示例包含一个提示一个被接受的生成结果 (\(\text{accepted generation}\)) 和一个被拒绝的生成结果 (\(\text{rejected generation}\))。(细微差别:它不总是好与坏的生成结果;它可能是两个生成结果都很好,但其中一个比另一个更好)。图 \(\text{12}-27\) 显示了一个包含两个训练示例的偏好训练集。

F12.1

生成偏好数据的一种方法是\(\text{LLM}\) 呈现一个提示,让它生成两个不同的结果。如图 \(\text{12}-28\) 所示,我们可以询问人类标注者他们更喜欢哪一个

F12.1

奖励模型训练步骤 (\(\text{Reward model training step}\))

现在我们有了偏好训练数据集,我们就可以继续训练奖励模型了。

一个简单的步骤是我们使用奖励模型来

  1. 被接受的生成结果评分。
  2. 被拒绝的生成结果评分。

\(\text{12}-29\) 显示了训练目标确保被接受的生成结果得分高于被拒绝的生成结果

F12.1

当我们将所有内容组合在一起时(如图 \(\text{12}-30\) 所示),我们就得到了偏好调整的三个阶段

  1. 收集偏好数据
  2. 训练奖励模型
  3. 使用奖励模型微调 \(\text{LLM}\)(作为偏好评估者)

F12.1

奖励模型是一个绝妙的想法,可以进一步扩展和发展。例如,\(\text{Llama 2}\) 训练了两个奖励模型:一个对有用性进行评分,另一个对安全性进行评分(图 \(\text{12}-31\))。

F12.1

使用训练好的奖励模型来微调 \(\text{LLM}\) 的一种常用方法近端策略优化 (\(\text{Proximal Policy Optimization, PPO}\))\(\text{PPO}\) 是一种流行的强化学习技术,它通过确保 \(\text{LLM}\) 不会偏离预期奖励太多优化经过指令调整的 \(\text{LLM}\)。它甚至被用于训练 \(\text{2022}\)\(\text{11}\) 月发布的最初 \(\text{ChatGPT}\)

无需训练奖励模型

Training No Reward Model

\(\text{PPO}\) 的一个缺点是它是一种复杂的方法,需要训练至少两个模型奖励模型\(\text{LLM}\),这可能比必要的更昂贵

直接偏好优化 (\(\text{Direct Preference Optimization, DPO}\))\(\text{PPO}\)一种替代方案,它摒弃了基于强化学习的过程我们不再使用奖励模型来判断生成结果的质量,而是\(\text{LLM}\) 本身来做这件事。如图 \(\text{12}-32\) 所示,我们使用 \(\text{LLM}\) 的一个副本作为参考模型 (\(\text{reference model}\)),来判断被接受的生成结果和被拒绝的生成结果的质量参考模型可训练模型 (\(\text{trainable model}\)) 之间的偏移

F12.1

通过在训练期间计算这种偏移,我们可以通过跟踪参考模型和可训练模型之间的差异,来优化被接受的生成结果的似然性(优于被拒绝的生成结果)。

个人注:参考模型 是为了防止模型在对齐过程中偏离得太远,从而避免生成无意义的文本。

为了计算这种偏移及其相关分数,需要从两个模型中提取被拒绝生成结果和被接受生成结果的对数概率 (\(\text{log probabilities}\))。如图 \(\text{12}-33\) 所示,这个过程在词元级别 (\(\text{token level}\)) 进行,其中概率被组合起来计算参考模型和可训练模型之间的偏移

F12.1

个人注:你看到的那两组输出(上面是 “I got no clue !”,下面是 “I have no idea !”)分别对应模型生成的两种候选回答,而图中列出 "I", "have", "no", "idea", "!" 的 token,其实是为了 展示模型在“较优回答”这条序列上的逐词概率变化

利用这些分数,我们可以优化可训练模型的参数,使其对生成被接受的生成结果更有信心,而对生成被拒绝的生成结果信心不足。与 \(\text{PPO}\) 相比,作者发现 \(\text{DPO}\) 在训练期间更稳定更准确。由于其稳定性,我们将使用它作为我们偏好调整之前经过指令调整的模型的主要模型

使用 \(\text{DPO}\) 进行偏好调整

Preference Tuning with DPO

当我们使用 \(\text{Hugging Face}\) 堆栈时,偏好调整与我们之前介绍的指令调整非常相似,只有一些细微差别。我们将继续使用 \(\text{TinyLlama}\),但这次是一个经过指令调整的版本,它首先使用完全微调进行训练,然后使用 \(\text{DPO}\) 进一步对齐。与我们最初经过指令调整的模型相比,这个 \(\text{LLM}\) 是在更大的数据集上进行训练的。

在本节中,我们将演示如何使用 \(\text{DPO}\) 和基于奖励的数据集进一步对齐该模型

模板化对齐数据

Templating Alignment Data

我们将使用一个对于每个提示都包含一个被接受的生成结果和一个被拒绝的生成结果的数据集。该数据集部分由 \(\text{ChatGPT}\) 生成,并附带了关于哪个输出应该被接受、哪个应该被拒绝的评分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from datasets import load_dataset
def format_prompt(example):
"""Format the prompt to using the <|user|> template TinyLLama is using"""
# Format answers
system = "<|system|>\n" + example["system"] + "</s>\n"
prompt = "<|user|>\n" + example["input"] + "</s>\n<|assistant|>\n"
chosen = example["chosen"] + "</s>\n"
rejected = example["rejected"] + "</s>\n"
return {
"prompt": system + prompt,
"chosen": chosen,
"rejected": rejected,
}
# Apply formatting to the dataset and select relatively short answers
dpo_dataset = load_dataset(
"argilla/distilabel-intel-orca-dpo-pairs", split="train"
)
dpo_dataset = dpo_dataset.filter(
lambda r:
r["status"] != "tie" and
r["chosen_score"] >= 8 and
not r["in_gsm8k_train"]
)
dpo_dataset = dpo_dataset.map(
format_prompt, remove_columns=dpo_dataset.column_names
)
dpo_dataset

请注意,我们应用了额外的过滤,将数据的规模从原来的 \(\text{13,000}\) 个示例进一步减少到大约 \(\text{6,000}\) 个示例

模型量化

Model Quantization

我们加载基础模型,并加载我们先前创建的 \(\text{LoRA}\)。和以前一样,我们对模型进行量化减少训练所需的 \(\text{VRAM}\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from peft import AutoPeftModelForCausalLM
from transformers import BitsAndBytesConfig, AutoTokenizer
# 4-bit quantization configuration - Q in QLoRA
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # Use 4-bit precision model loading

bnb_4bit_quant_type="nf4", # Quantization type
bnb_4bit_compute_dtype="float16", # Compute dtype
bnb_4bit_use_double_quant=True, # Apply nested quantization
)
# Merge LoRA and base model
model = AutoPeftModelForCausalLM.from_pretrained(
"TinyLlama-1.1B-qlora",
low_cpu_mem_usage=True,
device_map="auto",
quantization_config=bnb_config,
)
merged_model = model.merge_and_unload()
# Load LLaMA tokenizer
model_name = "TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T"
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = "<PAD>"
tokenizer.padding_side = "left"

接下来,我们使用与之前相同的 \(\text{LoRA}\) 配置来执行 \(\text{DPO}\) 训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from peft import LoraConfig, prepare_model_for_kbit_training, get_peft_model
# Prepare LoRA configuration
peft_config = LoraConfig(
lora_alpha=32, # LoRA Scaling
lora_dropout=0.1, # Dropout for LoRA Layers
r=64, # Rank
bias="none",
task_type="CAUSAL_LM",
target_modules= # Layers to target
["k_proj", "gate_proj", "v_proj", "up_proj", "q_proj", "o_proj",
"down_proj"]
)
# prepare model for training
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, peft_config)

训练配置

Training Configuration

为了简单起见,我们将使用与之前相同的训练参数,但有一个区别。为了说明目的,我们运行 \(\text{200}\),而不是运行一个 \(\text{epoch}\)(可能需要长达两小时)。此外,我们添加了 \(\text{warmup\_ratio}\) 参数,它在\(\text{10\%}\) 的步骤中将学习率\(\text{0}\) 增加到我们设定的 \(\text{learning\_rate}\) 值。通过在开始时保持较小的学习率(即预热期 \(\text{warmup period}\)),我们允许模型在应用较大的学习率之前调整以适应数据,从而避免有害的发散 (\(\text{divergence}\)):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from trl import DPOConfig
output_dir = "./results"
# Training arguments
training_arguments = DPOConfig(
output_dir=output_dir,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
optim="paged_adamw_32bit",
learning_rate=1e-5,
lr_scheduler_type="cosine",
max_steps=200,
logging_steps=10,
fp16=True,
gradient_checkpointing=True,
warmup_ratio=0.1
)

训练

Training

现在我们已经准备好所有的模型和参数,就可以开始微调我们的模型了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from trl import DPOTrainer
# Create DPO trainer
dpo_trainer = DPOTrainer(
model,
args=training_arguments,
train_dataset=dpo_dataset,
tokenizer=tokenizer,
peft_config=peft_config,
beta=0.1,
max_prompt_length=512,
max_length=512,
)
# Fine-tune model with DPO
dpo_trainer.train()
# Save adapter
dpo_trainer.model.save_pretrained("TinyLlama-1.1B-dpo-qlora")

我们创建了第二个适配器。为了合并这两个适配器,我们迭代地将适配器与基础模型合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from peft import PeftModel
# Merge LoRA and base model
model = AutoPeftModelForCausalLM.from_pretrained(
"TinyLlama-1.1B-qlora",

low_cpu_mem_usage=True,
device_map="auto",
)
sft_model = model.merge_and_unload()
# Merge DPO LoRA and SFT model
dpo_model = PeftModel.from_pretrained(
sft_model,
"TinyLlama-1.1B-dpo-qlora",
device_map="auto",
)
dpo_model = dpo_model.merge_and_unload()

这种 \(\text{SFT}\) + \(\text{DPO}\) 的组合首先微调模型以执行基本聊天,然后将其答案与人类偏好对齐绝佳方式。然而,它确实需要付出代价,因为我们需要执行两个训练循环,并且可能需要调整两个过程中的参数

自从 \(\text{DPO}\) 发布以来,新的对齐偏好的方法不断被开发出来。值得注意的是赔率比偏好优化 (\(\text{Odds Ratio Preference Optimization, ORPO}\)),这是一个\(\text{SFT}\)\(\text{DPO}\) 结合到一个单一训练过程中的流程。它消除了执行两个独立训练循环的需要进一步简化了训练过程,同时允许使用 \(\text{QLoRA}\)

总结

在本章中,我们探索了微调预训练 \(\text{LLM}\) 的不同步骤。我们通过低秩适应 (\(\text{LoRA}\)) 技术,利用参数高效微调 (\(\text{PEFT}\)) 来执行微调。我们解释了如何通过量化 (\(\text{quantization}\)) 扩展 \(\text{LoRA}\),量化是一种在表示模型和适配器的参数时减少内存限制的技术

我们探索的微调过程包含两个步骤

第一步中,我们使用指令数据预训练 \(\text{LLM}\) 执行了有监督微调 (\(\text{supervised fine-tuning}\)),这通常称为指令调整 (\(\text{instruction tuning}\))。这产生了一个具有聊天式行为并能密切遵循指令的模型

第二步中,我们通过使用对齐数据(即表示哪种类型的答案优于其他答案的数据)对其进行微调,进一步改进了模型。这个过程被称为偏好调整 (\(\text{preference tuning}\)),它将人类偏好提炼到先前经过指令调整的模型中。

总而言之,本章展示了微调预训练 \(\text{LLM}\) 的两个主要步骤,以及这如何能够产生更准确、信息更丰富的输出

Afterword

感谢所有加入我们,一同完成这次穿越大型语言模型世界的迷人旅程的读者。我们感激您致力于学习这些功能强大、彻底革新了语言处理领域的模型。

在整本书中,我们看到了 \(\text{LLM}\) 的工作原理,以及如何利用它们来创建各种应用,从简单的聊天机器人到更复杂的系统,例如搜索引擎。我们还探索了各种方法,用于针对特定任务(包括分类、生成和语言表示)对预训练的 \(\text{LLM}\) 进行微调。通过掌握这些技术,读者将能够释放 \(\text{LLM}\) 的潜力,并创建可从其能力中受益的创新解决方案。这些知识将使读者能够保持领先地位,并适应该领域的新发展。

在本书即将结束之际,我们要强调,我们对 \(\text{LLM}\) 的探索仅仅是一个开始。未来还有更多激动人心的发展,我们鼓励您继续关注该领域的进展。为了协助这个过程,请继续关注本书的存储库,我们会在其中持续添加资源。

我们希望通过阅读本书,您对 \(\text{LLM}\) 如何应用于各种应用程序以及它们如何具备改变各行各业的潜力,有了更深入的理解。

以本书为指导,我们相信您将有充足的准备来驾驭 \(\text{LLM}\) 这一令人兴奋的领域,并为这个快速发展的领域做出有意义的贡献。

书籍各章的机翻md文件:
《Hands-On Large Language Models》目录及前言
《Hands-On Large Language Models》第1章 大型语言模型简介
《Hands-On Large Language Models》第2章 词元与嵌入
《Hands-On Large Language Models》第3章 深入了解大型语言模型
《Hands-On Large Language Models》第4章 文本分类
《Hands-On Large Language Models》第5章 文本聚类和主题建模
《Hands-On Large Language Models》第6章 提示工程
《Hands-On Large Language Models》第7章 高级文本生成技术与工具
《Hands-On Large Language Models》第8章 语义搜索与检索增强生成
《Hands-On Large Language Models》第9章 多模态大型语言模型
《Hands-On Large Language Models》第10章 创建文本嵌入模型
《Hands-On Large Language Models》第11章 微调用于分类的表征模型
《Hands-On Large Language Models》第12章 生成模型的微调