使用 ORPO 微调 Llama 3
ORPO算是近期微调领域一个让人眼前一亮的方案——它把监督式微调和偏好对齐这两个原本分开的阶段,直接揉进了同一个训练过程。这带来的最直接好处就是:训练所需的计算量和时间都明显降下来了。更关键的是,从实际跑下来的结果看,不管是在不同参数规模的模型上,还是各种评测基准里,ORPO的表现都优于其他对齐方法。
下面,我们就通过一个具体案例来演示如何用TRL库和ORPO对最新的Llama 3 8B模型进行微调。配套的代码已经放在了Google Colab和GitHub上(LLM课程中都能找到)。

ORPO 到底怎么工作的?
指令微调和偏好对齐,一直是让大语言模型适配具体任务的两大基本功。传统做法通常分两步走:先通过
监督式微调(SFT)
偏好对齐方法

但研究人员后来发现这套流程有个麻烦的地方:SFT虽然能把模型往目标领域里带,可它同时也悄悄
提高了模型生成那些糟糕答案的概率

ORPO(由Hong和Lee在2024年提出)提供了一种很巧妙的解决办法:它把指令调整和偏好对齐整合成了一个端到端的训练流程。具体来说,ORPO在标准的语言建模目标上做了改动,把负对数似然损失和一个叫“优势比”(Odds Ratio, OR)的惩罚项结合在了一起。这个OR损失会对那些被拒绝的回答施加轻微的惩罚,同时对偏好的回答给予更强的奖励。这样一来,模型在学会完成任务的同时,也自然而然地跟上了人类的偏好。
目前,ORPO已经落地到了几个主流微调库上,比如TRL、Axolotl和LLaMA-Factory,开发者直接拿来用就行。
用 ORPO 微调 Llama 3 的实操
Llama 3是Meta最新推出的LLM系列,训练数据量是
15万亿个token
上下文长度也提升到了8,192个token(Llama 2是4,096),而且通过RoPE还有望扩展到32K。另外,这次还换了一个新的分词器,词表大小到了128K token,同样一段文本编码后token数量能减少15%。这也解释了为什么参数从7B涨到了8B。
准备数据集
ORPO需要一份偏好数据集,里面包含三样东西:一条提示(prompt)、一个被选中的好答案(chosen)、还有一个被拒绝的答案(rejected)。这次我们用 mlabonne/orpo-dpo-mix-40k,它是下面这几个高质量DPO数据集的混合物:
argilla/distilabel-capybara-dpo-7k-binarized:高分答案(≥5)摘了2,882条argilla/distilabel-intel-orca-dpo-pairs:高分答案(≥9)且不在GSM8K范围内的,凑了2,299条argilla/ultrafeedback-binarized-preferences-cleaned:高分答案(≥5),22,799条argilla/distilabel-math-preference-dpo:高分答案(≥9),2,181条unalignment/toxic-dpo-v0.2(541条)M4-ai/prm_dpo_pairs_cleaned(7,958条)jondurbin/truthy-dpo-v0.1(1,016条)
这里要感谢 argilla、unalignment、M4-ai 和 jondurbin 贡献了这些源数据。
安装与环境配置
先把需要的库都装上:
pip install -U transformers datasets accelerate peft trl bitsandbytes wandb接着导入库并登录W&B(这一步可选):
import gc
import os
import torch
import wandb
from datasets import load_dataset
from google.colab import userdata
from peft import LoraConfig, PeftModel, prepare_model_for_kbit_training
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
pipeline,
)
from trl import ORPOConfig, ORPOTrainer, setup_chat_format
wb_token = userdata.get('wandb')
wandb.login(key=wb_token)如果你的GPU支持,还可以开Flash Attention,能显著提速:
if torch.cuda.get_device_capability()[0] >= 8:
!pip install -qqq flash-attn
attn_implementation = "flash_attention_2"
torch_dtype = torch.bfloat16
else:
attn_implementation = "eager"
torch_dtype = torch.float16加载模型并配置PEFT
接下来,用bitsandbytes先把Llama 3 8B以4位精度加载进来,然后用PEFT配一套LoRA参数。这里用 setup_chat_format() 来顺手调整模型和分词器,让它支持ChatML——这个函数会自动套用聊天模板、添加特殊标记,同时把模型的嵌入层大小调到和新词表一致。
有一点要注意:访问 meta-llama/Meta-Llama-3-8B 需要提前向Meta提交申请,并在Hugging Face上登录。如果嫌麻烦,也可以换成非门控的版本,比如 NousResearch/Meta-Llama-3-8B。
# Model
base_model = "meta-llama/Meta-Llama-3-8B"
new_model = "OrpoLlama-3-8B"
# QLoRA config
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch_dtype,
bnb_4bit_use_double_quant=True,
)
# LoRA config
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=['up_proj', 'down_proj', 'gate_proj', 'k_proj', 'q_proj', 'v_proj', 'o_proj']
)
# Load tokenizer
tokenizer = AutoTokenizer.from_pretrained(base_model)
# Load model
model = AutoModelForCausalLM.from_pretrained(
base_model,
quantization_config=bnb_config,
device_map="auto",
attn_implementation=attn_implementation
)
model, tokenizer = setup_chat_format(model, tokenizer)
model = prepare_model_for_kbit_training(model)处理数据集
模型就位后,开始处理数据。加载 mlabonne/orpo-dpo-mix-40k,然后调用 apply_chat_template() 把“chosen”和“rejected”两列转成ChatML格式。注意,为了演示方便,这里只取了前1,000条——跑完整的数据集时间会长很多。
dataset_name = "mlabonne/orpo-dpo-mix-40k"
dataset = load_dataset(dataset_name, split="all")
dataset = dataset.shuffle(seed=42).select(range(100))
def format_chat_template(row):
row["chosen"] = tokenizer.apply_chat_template(row["chosen"], tokenize=False)
row["rejected"] = tokenizer.apply_chat_template(row["rejected"], tokenize=False)
return row
dataset = dataset.map(
format_chat_template,
num_proc= os.cpu_count(),
)
dataset = dataset.train_test_split(test_size=0.01)训练配置与超参数
训练前,有几个关键超参数得特别注意:
- :相比传统的SFT甚至DPO,ORPO用的学习率要低得多。这里的8e-6来自原始论文,大致对应SFT的1e-5和DPO的5e-6。如果做真正的全量微调,建议提到1e-6左右试试。
learning_rate
- :对应论文中的 \(\lambda\),默认值0.1。原始论文的附录有消融实验,展示了不同beta值的影响。
beta
其他参数比如max_length和batch size,都按尽量吃满显存来设(这套配置大概用20 GB)。理想情况下,模型应该训练3-5个epoch,这里只跑1个。
orpo_args = ORPOConfig(
learning_rate=8e-6,
beta=0.1,
lr_scheduler_type="linear",
max_length=1024,
max_prompt_length=512,
per_device_train_batch_size=2,
per_device_eval_batch_size=2,
gradient_accumulation_steps=4,
optim="paged_adamw_8bit",
num_train_epochs=1,
evaluation_strategy="steps",
eval_steps=0.2,
logging_steps=1,
warmup_steps=10,
report_to="wandb",
output_dir="./results/",
)
trainer = ORPOTrainer(
model=model,
args=orpo_args,
train_dataset=dataset["train"],
eval_dataset=dataset["test"],
peft_config=peft_config,
tokenizer=tokenizer,
)
trainer.train()
trainer.sa ve_model(new_model)在L4 GPU上,训练这1,000条样本大约需要2小时。看看W&B上的曲线:
虽然损失是在下降的,但chosen和rejected之间的差距还不够明显——平均边际和准确率都只比零和0.5高一点点。原始论文里,作者在Anthropic/hh-rlhf数据集(161k条)上跑了10个epoch,比我们这轮演示长得多。他们也用Llama 3做过实验,日志数据已经公开发布了。
最后一步,把QLoRA适配器和基础模型合并,然后推到Hugging Face Hub:
# Flush memory
del trainer, model
gc.collect()
torch.cuda.empty_cache()
# Reload tokenizer and model
tokenizer = AutoTokenizer.from_pretrained(base_model)
model = AutoModelForCausalLM.from_pretrained(
base_model,
low_cpu_mem_usage=True,
return_dict=True,
torch_dtype=torch.float16,
device_map="auto",
)
model, tokenizer = setup_chat_format(model, tokenizer)
# Merge adapter with base model
model = PeftModel.from_pretrained(model, new_model)
model = model.merge_and_unload()
model.push_to_hub(new_model, use_temp_dir=False)
tokenizer.push_to_hub(new_model, use_temp_dir=False)恭喜,到这里我们就完成了对Llama 3的一次快速微调,产出的模型是 mlabonne/OrpoLlama-3-8B。你可以在Hugging Face Space上直接体验它。
虽然这次训练不够充分(从W&B曲线也能看出来),但用LLM AutoEval在Nous的基准套件上跑了一下,结果还挺让人意外的:
ORPO微调后的模型在每一个基准上都有提升,超过了基础版本。这很鼓舞人心,也意味着如果对整个40k数据集做完整微调,效果应该会更可观。
对开源社区来说,现在确实是个好时候。越来越多高质量的开源权重模型在发布,闭源和开源之间的差距在慢慢缩小。而微调,正是让模型在自己场景里发挥最大价值的关键工具。
总结
这篇内容我们聊了ORPO是怎么把SFT和偏好对齐合成一步的,然后通过TRL在自定义数据集上对Llama 3 8B做了一次实战微调。最终模型的结果让人眼前一亮,也展示了ORPO作为微调新范式的潜力。
文章到这里就差不多了,建议你亲自跑一下Colab上的笔记本。下次我们会专门聊聊怎么构造高质量的数据集——这件事其实比模型微调本身还容易被忽略。
-
- 关于宇宙的好的网名有哪些
- 角色扮演 | 1
- 网名