【Smol Course】2-偏好对齐
wbfwonderful Lv4

Preference Alignment

监督微调有助于模型学习任务,但偏好对齐鼓励输出符合人类的期望和价值观。

  • Direct Preference Optimization (DPO):直接偏好优化(DPO)通过使用偏好数据直接优化模型来简化偏好对齐。这种方法消除了对单独的奖励模型和复杂的强化学习的需要,使其比传统的基于人类反馈的强化学习(RLHF)更加稳定和高效。
  • Odds Ratio Preference Optimization (ORPO):ORPO在单个进程中引入了指令调优和偏好对齐的组合方法。它通过在 token 级别上将负对数似然损失与比值比项结合起来,修改了标准语言建模目标。该方法具有统一的单阶段训练过程、参考无模型架构和提高计算效率的特点。

DPO

DPO 将偏好对齐重新定义为人类偏好数据的分类问题。传统的 RLHF 方法需要训练一个单独的奖励模型,并使用复杂的强化学习算法(如 PPO)来校准模型输出。DPO 通过定义一个损失函数来简化这一过程,该损失函数可以根据首选输出和非首选输出直接优化模型的策略。

How DPO works

DPO 过程需要监督微调(SFT)来使模型适应目标域。这为通过在标准指令遵循数据集上进行训练来进行偏好学习奠定了基础。该模型在保持其一般能力的同时学习基本任务的完成。

接下来是偏好学习,其中模型在成对的输出上进行训练——一个是首选的,一个是非首选的。偏好对帮助模型理解哪种反应更符合人类的价值观和期望。

DPO 的核心创新在于其直接优化方法。DPO 不是训练单独的奖励模型,而是使用二元交叉熵损失来直接更新基于偏好数据的模型权重。这种简化的过程使训练更加稳定和高效,同时达到与传统 RLHF 相当或更好的结果。

Dataset

DPO 的数据集通常是通过将响应对注释为首选或非首选来创建的。通常包含 prompt、chosen 和 selected 三部分。

Prompt 包含用于生成 selected 和 Rejected 响应的提示。selected 和 Rejected 分别包含首选和非首选的响应。

Implementation with TRL

简单实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from trl import DPOConfig, DPOTrainer

# Define arguments
training_args = DPOConfig(
...
)

# Initialize trainer
trainer = DPOTrainer(
model,
train_dataset=dataset,
tokenizer=tokenizer,
...
)

# Train model
trainer.train()

Practice

导入包

1
2
3
4
5
import torch
import os
from transformers import AutoModelForCausalLM, AutoTokenizer
from datasets import load_dataset
from trl import DPOTrainer, DPOConfig

数据集

设置 cache_dir 字段即可将其下载到指定目录。后续可以直接加载使用。

1
dataset = load_dataset("argilla/ultrafeedback-binarized-preferences", split="train", cache_dir="D:\study\smol-course\data")

查看数据集,核心为 instruction、chosen_response 和 rejected_response。

Dataset({
    features: ['source', 'instruction', 'chosen_response', 'rejected_response', 'chosen_avg_rating', 'rejected_avg_rating', 'chosen_model'],
    num_rows: 63619
})

但是,chosen_response 和 rejected_response 只有回答,没有形成对话列表。所以需要定义处理数据集的函数:

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
def process_dataset(example):
chosen = [
{
'role': 'user',
'content': example['instruction']
},
{
'role': 'assistant',
'content': example['chosen_response']
}
]
rejected = [
{
'role': 'user',
'content': example['instruction']
},
{
'role': 'assistant',
'content': example['rejected_response']
}
]

return {'chosen': chosen, 'rejected': rejected, 'prompt': example["instruction"]}

dataset = dataset.map(process_dataset)

加载模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
model_name = "HuggingFaceTB/SmolLM2-135M-Instruct"

device = (
"cuda"
if torch.cuda.is_available()
else "mps" if torch.backends.mps.is_available() else "cpu"
)

# Model to fine-tune
model = AutoModelForCausalLM.from_pretrained(
pretrained_model_name_or_path=model_name,
torch_dtype=torch.float32,
).to(device)
model.config.use_cache = False
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

model, tokenizer = setup_chat_format(model=model, tokenizer=tokenizer)

# Set our name for the finetune to be saved &/ uploaded to
finetune_name = "SmolLM2-FT-DPO"
finetune_tags = ["smol-course", "module_1"]

注意:

  • model.config.use_cache = False:禁用 KV-cache(Key/Value Cache)。通常在训练/微调时禁用缓存(不然梯度计算会出错);而在推理时开启以提升生成速度。
  • tokenizer.pad_token = tokenizer.eos_token:将 pad_token 设置为 eos_token(结束符)。原因:有些模型(如 GPT)没有专门定义 pad token,训练时又需要对序列进行 padding,这种情况下通常将 pad token 和 eos token 设为同一个。否则训练时可能会出现缺失 pad_token 的错误。
  • 这里需要设置聊天模板:model, tokenizer = setup_chat_format(model=model, tokenizer=tokenizer)

训练

DPOConfig 参数

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Training arguments
training_args = DPOConfig(
# Training batch size per GPU
per_device_train_batch_size=4,
# Number of updates steps to accumulate before performing a backward/update pass
# Effective batch size = per_device_train_batch_size * gradient_accumulation_steps
gradient_accumulation_steps=4,
# Saves memory by not storing activations during forward pass
# Instead recomputes them during backward pass
gradient_checkpointing=True,
# Base learning rate for training
learning_rate=5e-5,
# Learning rate schedule - 'cosine' gradually decreases LR following cosine curve
lr_scheduler_type="cosine",
# Total number of training steps
max_steps=200,
# Disables model checkpointing during training
save_strategy="no",
# How often to log training metrics
logging_steps=1,
# Directory to save model outputs
output_dir="smol_dpo_output",
# Number of steps for learning rate warmup
warmup_steps=100,
# Use bfloat16 precision for faster training
bf16=True,
# Disable wandb/tensorboard logging
report_to="none",
# Keep all columns in dataset even if not used
remove_unused_columns=False,
# Enable MPS (Metal Performance Shaders) for Mac devices
use_mps_device=device == "mps",
# Model ID for HuggingFace Hub uploads
hub_model_id=finetune_name,
# DPO-specific temperature parameter that controls the strength of the preference model
# Lower values (like 0.1) make the model more conservative in following preferences
beta=0.1,
# Maximum length of the input prompt in tokens
max_prompt_length=1024,
# Maximum combined length of prompt + response in tokens
max_length=1536,
)
  • per_device_train_batch_size=4:每个设备(GPU/MPS)上的微调批大小是 4。若你使用多个 GPU 或 MPS,则总 batch size 会乘以设备数。
  • gradient_accumulation_steps=4:累积 4 个 mini-batch 的梯度再做一次反向传播。
    这样可以模拟 4×4=16 的有效 batch size,节省显存。
  • gradient_checkpointing=True:启用梯度检查点技术:节省内存,通过在反向传播时重算中间激活来换取少量计算开销。
  • learning_rate=5e-5:初始学习率。对于 LoRA + 微调一般推荐 1e-5 ~ 5e-5。
  • lr_scheduler_type=”cosine”:使用余弦退火学习率策略(cosine schedule):初期缓慢增大,后期缓慢降低(类似 U 形曲线)。
  • max_steps=200:总共进行 200 个训练 step。适合小规模调试或快速实验。
  • save_strategy=”no”:不保存中间模型 checkpoint(节省空间,适合调试)。
  • logging_steps=1:每 1 步打印一次日志(非常频繁,便于观察)。
  • output_dir=”smol_dpo_output”:模型微调输出目录。
  • warmup_steps=100:前 100 个 step 使用 warmup 策略逐渐提高学习率。可避免训练初期模型震荡。
  • bf16=True:使用 bfloat16 精度训练,适用于支持的设备(如 A100/H100)。比 fp16 更稳定,适合 DPO。
  • report_to=”none”:不启用日志追踪工具(如 WandB、TensorBoard)。
  • remove_unused_columns=False:保留所有输入字段,必要设置,因为 DPO 使用自定义字段(如 prompt, chosen, rejected)。
  • use_mps_device=device == “mps”:如果在 Mac 上使用 MPS 加速则设为 True。
  • hub_model_id=finetune_name:训练完成后模型上传到 Hugging Face Hub 的 ID 名(如 “my-org/my-dpo-model”)。
  • beta=0.1 DPO 特有参数:控制 DPO loss 中 policy 与 reference 的对比强度。越小(如 0.1),偏好控制越严格(保守);越大(如 1.0),更自由。
  • max_prompt_length=1024:输入的 prompt 最大长度(token 数)。超过则截断。
  • max_length=1536:prompt + 回复(chosen/rejected)拼接后的最大总长度。

DPOTrainer 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
trainer = DPOTrainer(
# The model to be trained
model=model,
# Training configuration from above
args=training_args,
# Dataset containing preferred/rejected response pairs
train_dataset=dataset,
# Tokenizer for processing inputs
processing_class=tokenizer,
# DPO-specific temperature parameter that controls the strength of the preference model
# Lower values (like 0.1) make the model more conservative in following preferences
# beta=0.1,
# Maximum length of the input prompt in tokens
# max_prompt_length=1024,
# Maximum combined length of prompt + response in tokens
# max_length=1536,
)
  • ref_model:如果没有参考模型,则默认和 model 相同
  • args:将 DPOConfig 传入
  • train_dataset:训练集。必须包含 prompt, chosen, rejected 三列
  • beta:控制 loss 中 π(chosen)/π(rejected) 的敏感程度。
    • beta = 0.1:趋于保守(更相信参考模型)
    • beta = 1.0:更自由(训练模型可偏离参考)

ORPO

ORPO (Odds Ratio Preference Optimization)是一种新颖的微调技术,它将微调和偏好对齐结合到一个统一的过程中。与 RLHF 或 DPO 等传统方法相比,这种组合方法在效率和性能方面具有优势。

与 DPO 等方法的对齐通常涉及两个单独的步骤:监督微调以使模型适应领域和格式,然后进行偏好对齐以与人类偏好保持一致。虽然 SFT 有效地使模型适应目标域,但它可能无意中增加产生期望和不期望响应的概率。ORPO 通过将这两个步骤集成到一个流程中来解决这一限制。对比如下:

image

如何工作

训练过程利用了一个类似于在 DPO 中使用的偏好数据集,其中每个训练示例包含一个输入提示以及两个响应:一个是首选的,另一个是拒绝的。与其他需要单独阶段和参考模型的校准方法不同,ORPO 将偏好校准直接集成到监督微调过程中。这种单片方法使其无需参考模型,计算效率更高,并且使用更少的 flop 来提高内存效率。

ORPO 通过结合两个主要组件创建了一个新目标:

  1. SFT 损失:语言建模中使用的标准负对数似然损失,它最大限度地提高了生成参考令牌的概率。这有助于维护模型的通用语言功能。
  2. odd rate 损失:一个新的组件,惩罚不希望的反应,而奖励首选的。这个损失函数使用比值比在令牌级别上有效地对比受欢迎和不受欢迎的响应。