简介

基于人类反馈的强化学习 (Reinforcement Learning from Human Feedback,RLHF) 事实上已成为 GPT-4 或 Claude 等 LLM 训练的最后一步,它可以确保语言模型的输出符合人类在闲聊或安全性等方面的期望。然而,它也给 NLP 引入了一些 RL 相关的复杂性: 既要构建一个好的奖励函数,并训练一个模型用以估计每个状态的价值 (value) ; 又要注意最终生成的 LLM 不能与原始模型相差太远,如果太远的话会使得模型容易产生乱码而非有意义的文本。该过程非常复杂,涉及到许多复杂的组件,而这些组件本身在训练过程中又是动态变化的,因此把它们料理好并不容易。

Rafailov、Sharma、Mitchell 等人最近发表了一篇论文 Direct Preference Optimization,论文提出将现有方法使用的基于强化学习的目标转换为可以通过简单的二元交叉熵损失直接优化的目标,这一做法大大简化了 LLM 的提纯过程。

本文介绍了直接偏好优化 (Direct Preference Optimization,DPO) 法,该方法现已集成至 TRL 库 中。同时,我们还展示了如何在 stack-exchange preference 数据集上微调最新的 Llama v2 7B 模型, stack-exchange preference 数据集中包含了各个 stack-exchange 门户上的各种问题及其排序后的回答。

DPO 与 PPO

在通过 RL 优化人类衍生偏好时,一直以来的传统做法是使用一个辅助奖励模型来微调目标模型,以通过 RL 机制最大化目标模型所能获得的奖励。直观上,我们使用奖励模型向待优化模型提供反馈,以促使它多生成高奖励输出,少生成低奖励输出。同时,我们使用冻结的参考模型来确保输出偏差不会太大,且继续保持输出的多样性。这通常需要在目标函数设计时,除了奖励最大化目标外再添加一个相对于参考模型的 KL 惩罚项,这样做有助于防止模型学习作弊或钻营奖励模型。

DPO 绕过了建模奖励函数这一步,这源于一个关键洞见: 从奖励函数到最优 RL 策略的分析映射。这个映射直观地度量了给定奖励函数与给定偏好数据的匹配程度。有了它,作者就可与将基于奖励和参考模型的 RL 损失直接转换为仅基于参考模型的损失,从而直接在偏好数据上优化语言模型!因此,DPO 从寻找最小化 RLHF 损失的最佳方案开始,通过改变参量的方式推导出一个 仅需 参考模型的损失!

有了它,我们可以直接优化该似然目标,而不需要奖励模型或繁琐的强化学习优化过程。

如何使用 TRL 进行训练

如前所述,一个典型的 RLHF 流水线通常包含以下几个环节:

  1. 有监督微调 (supervised fine-tuning,SFT)
  2. 用偏好标签标注数据
  3. 基于偏好数据训练奖励模型
  4. RL 优化

TRL 库包含了所有这些环节所需的工具程序。而 DPO 训练直接消灭了奖励建模和 RL 这两个环节 (环节 3 和 4),直接根据标注好的偏好数据优化 DPO 目标。

使用 DPO,我们仍然需要执行环节 1,但我们仅需在 TRL 中向 DPOTrainer 提供环节 2 准备好的偏好数据,而不再需要环节 3 和 4。标注好的偏好数据需要遵循特定的格式,它是一个含有以下 3 个键的字典:

  • prompt : 即推理时输入给模型的提示
  • chosen : 即针对给定提示的较优回答
  • rejected : 即针对给定提示的较劣回答或非给定提示的回答

例如,对于 stack-exchange preference 数据集,我们可以通过以下工具函数将数据集中的样本映射至上述字典格式并删除所有原始列:

def return_prompt_and_responses(samples) -> Dict[str, str, str]:
return {
"prompt": [
"Question: " + question + "\n\nAnswer: "
for question in samples["question"]
],
"chosen": samples["response_j"], # rated better than k
"rejected": samples["response_k"], # rated worse than j
} dataset = load_dataset(
"lvwerra/stack-exchange-paired",
split="train",
data_dir="data/rl"
)
original_columns = dataset.column_names dataset.map(
return_prompt_and_responses,
batched=True,
remove_columns=original_columns
)

一旦有了排序数据集,DPO 损失其实本质上就是一种有监督损失,其经由参考模型获得隐式奖励。因此,从上层来看,DPOTrainer 需要我们输入待优化的基础模型以及参考模型:

dpo_trainer = DPOTrainer(
model, # 经 SFT 的基础模型
model_ref, # 一般为经 SFT 的基础模型的一个拷贝
beta=0.1, # DPO 的温度超参
train_dataset=dataset, # 上文准备好的数据集
tokenizer=tokenizer, # 分词器
args=training_args, # 训练参数,如: batch size, 学习率等
)

其中,超参 beta 是 DPO 损失的温度,通常在 0.10.5 之间。它控制了我们对参考模型的关注程度,beta 越小,我们就越忽略参考模型。对训练器初始化后,我们就可以简单调用以下方法,使用给定的 training_args 在给定数据集上进行训练了:

dpo_trainer.train()

基于 Llama v2 进行实验

在 TRL 中实现 DPO 训练器的好处是,人们可以利用 TRL 及其依赖库 (如 Peft 和 Accelerate) 中已有的 LLM 相关功能。有了这些库,我们甚至可以使用 bitsandbytes 库提供的 QLoRA 技术 来训练 Llama v2 模型。

有监督微调

如上文所述,我们先用 TRL 的 SFTTrainer 在 SFT 数据子集上使用 QLoRA 对 7B Llama v2 模型进行有监督微调:

# load the base model in 4-bit quantization
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
) base_model = AutoModelForCausalLM.from_pretrained(
script_args.model_name, # "meta-llama/Llama-2-7b-hf"
quantization_config=bnb_config,
device_map={"": 0},
trust_remote_code=True,
use_auth_token=True,
)
base_model.config.use_cache = False # add LoRA layers on top of the quantized base model
peft_config = LoraConfig(
r=script_args.lora_r,
lora_alpha=script_args.lora_alpha,
lora_dropout=script_args.lora_dropout,
target_modules=["q_proj", "v_proj"],
bias="none",
task_type="CAUSAL_LM",
)
...
trainer = SFTTrainer(
model=base_model,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
peft_config=peft_config,
packing=True,
max_seq_length=None,
tokenizer=tokenizer,
args=training_args, # HF Trainer arguments
)
trainer.train()

DPO 训练

SFT 结束后,我们保存好生成的模型。接着,我们继续进行 DPO 训练,我们把 SFT 生成的模型作为 DPO 的基础模型和参考模型,并在上文生成的 stack-exchange preference 数据上,以 DPO 为目标函数训练模型。我们选择对模型进行 LoRa 微调,因此我们使用 Peft 的 AutoPeftModelForCausalLM 函数加载模型:

model = AutoPeftModelForCausalLM.from_pretrained(
script_args.model_name_or_path, # location of saved SFT model
low_cpu_mem_usage=True,
torch_dtype=torch.float16,
load_in_4bit=True,
is_trainable=True,
)
model_ref = AutoPeftModelForCausalLM.from_pretrained(
script_args.model_name_or_path, # same model as the main one
low_cpu_mem_usage=True,
torch_dtype=torch.float16,
load_in_4bit=True,
)
...
dpo_trainer = DPOTrainer(
model,
model_ref,
args=training_args,
beta=script_args.beta,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
tokenizer=tokenizer,
peft_config=peft_config,
)
dpo_trainer.train()
dpo_trainer.save_model()

可以看出,我们以 4 比特的方式加载模型,然后通过 peft_config 参数选择 QLora 方法对其进行训练。训练器还会用评估数据集评估训练进度,并报告一些关键指标,例如可以选择通过 WandB 记录并显示隐式奖励。最后,我们可以将训练好的模型推送到 HuggingFace Hub。

总结

SFT 和 DPO 训练脚本的完整源代码可在该目录 examples/stack_llama_2 处找到,训好的已合并模型也已上传至 HF Hub (见 此处)。

你可以在 这儿 找到我们的模型在训练过程的 WandB 日志,其中包含了 DPOTrainer 在训练和评估期间记录下来的以下奖励指标:

  • rewards/chosen (较优回答的奖励) : 针对较优回答,策略模型与参考模型的对数概率二者之差的均值,按 beta 缩放。
  • rewards/rejected (较劣回答的奖励) : 针对较劣回答,策略模型与参考模型的对数概率二者之差的均值,按 beta 缩放。
  • rewards/accuracy (奖励准确率) : 较优回答的奖励大于相应较劣回答的奖励的频率的均值
  • rewards/margins (奖励余裕值) : 较优回答的奖励与相应较劣回答的奖励二者之差的均值。

直观上讲,在训练过程中,我们希望余裕值增加并且准确率达到 1.0,换句话说,较优回答的奖励高于较劣回答的奖励 (或余裕值大于零)。随后,我们还可以在评估数据集上计算这些指标。

我们希望我们代码的发布可以降低读者的入门门槛,让大家可以在自己的数据集上尝试这种大语言模型对齐方法,我们迫不及待地想看到你会用它做哪些事情!如果你想试试我们训练出来的模型,可以玩玩这个 space: trl-lib/stack-llama


英文原文: https://hf.co/blog/dpo-trl

原文作者: Kashif Rasul, Younes Belkada, Leandro von Werra

译者: Matrix Yao (姚伟峰),英特尔深度学习工程师,工作方向为 transformer-family 模型在各模态数据上的应用及大规模模型的训练推理。

审校/排版: zhongdongy (阿东)

使用 DPO 微调 Llama 2的更多相关文章

  1. 解密Prompt系列6. lora指令微调扣细节-请冷静,1个小时真不够~

    上一章介绍了如何基于APE+SELF自动化构建指令微调样本.这一章咱就把微调跑起来,主要介绍以Lora为首的低参数微调原理,环境配置,微调代码,以及大模型训练中显存和耗时优化的相关技术细节 标题这样写 ...

  2. Caffe fine-tuning 微调网络

    转载请注明出处,楼燚(yì)航的blog,http://www.cnblogs.com/louyihang-loves-baiyan/ 目前呢,caffe,theano,torch是当下比较流行的De ...

  3. lecture14-RBM的堆叠、修改以及DBN的决策学习和微调

    这是Hinton的第14课,主要介绍了RBM和DBN的东西,这一课的课外读物有三篇论文<Self-taught learning- transfer learning from unlabele ...

  4. 微调Win8.1这台电脑

    从前有个笑话:一位朋友在办公室受到领导教育:“我说小王同志啊,虽然这电脑是你打了报告组织上买给你用的,可是你也不好这么狂妄嘛...”可怜的他只好把图标的名字改为“大家的电脑”. 想必大家已经知道这个笑 ...

  5. easyui源码翻译1.32--NumberSpinner(数字微调)

    前言 扩展自$.fn.spinner.defaults和$.fn.numberbox.defaults.使用$.fn.numberspinner.defaults重写默认值对象.下载该插件翻译源码 数 ...

  6. easyui源码翻译1.32--TimeSpinner(时间微调)

    前言 扩展自$.fn.spinner.defaults.使用$.fn.timespinner.defaults重写默认值对象.下载该插件翻译源码 时间微调组件的创建基于微调组件.它和数字微调类似,但是 ...

  7. TimeSpinner( 时间微调) 组件

    本节课重点了解 EasyUI 中 Spinner(微调)组件的使用方法,这个组件依赖于Spinner(微调)组件. 一. 加载方式//class 加载方式<input id="box& ...

  8. NumberSpinner( 数字微调) 组件

    本节课重点了解 EasyUI 中 Spinner(微调)组件的使用方法,这个组件依赖于Numberbox(数值输入框)和 Spinner(微调)组件. 一. 加载方式//class 加载方式<i ...

  9. Spinner( 微调) 组件

    本节课重点了解 EasyUI 中 Spinner(微调)组件的使用方法,这个组件依赖于ValidateBox(验证框)组件. 一. 加载方式Spinner(微调)组件是其他两款高级微调组件的基础组件, ...

  10. (原)torch中微调某层参数

    转载请注明出处: http://www.cnblogs.com/darkknightzh/p/6221664.html 参考网址: https://github.com/torch/nn/issues ...

随机推荐

  1. rt-thread Env 预处理配置方法

    简介 rt-thread 是我非常喜欢的一款RTOS,近期在使用Env更新工程的时候发现,keil MDK 中的预处理型号和器件型号不符. 这就导致我每次更新工程后都需要进入keil MDK手动修改一 ...

  2. rust程序设计(3)结构体相关概念和疑问

    结构体 // 如何定义结构体 struct User { active: bool, username: String, email: String, sign_in_count: u64, } // ...

  3. JAVA中的函数接口,你都用过吗

    公众号「架构成长指南」,专注于生产实践.云原生.分布式系统.大数据技术分享. 在这篇文章中,我们将通过示例来学习 Java 函数式接口. 函数式接口的特点 只包含一个抽象方法的接口称为函数式接口. 它 ...

  4. 管易云与网易互客对接集成发货单查询2.0=>编辑订单

    管易云与网易互客对接集成发货单查询2.0=>编辑订单 对接源平台:管易云 管易云是金蝶旗下专注提供电商企业管理软件服务的子品牌,先后开发了C-ERP.EC-OMS.EC-WMS.E店管家.BBC ...

  5. Docker 安装教程

    一.离线安装 一.CentOS 离线安装 一.下载地址 1.选择系统的型号,选择linux-CentOS 下载地址 2.上传文件到CentOS 服务器 #选择文件 rz 二.开始安装 1.解压压缩包 ...

  6. Kafka集群调优+能力探底

    一.前言 我们需要对4个规格的kafka能力进行探底,即其可以承载的最大吞吐:4个规格对应的单节点的配置如下: 标准版: 2C4G 铂金版: 4C8G 专业版: 8C16G 企业版: 16C32G 另 ...

  7. python数据类型元组、列表、集合、字典相互嵌套

    系统 Windows 10 专业工作站版22H2 软件 python-3.9.6-amd64.exe 拓展库: jupyter==1.0.0 notebook==7.0.6 1.元组嵌套 1.1 元组 ...

  8. 用JS实现简单的屏幕录像机

    本文将介绍如何用JS实现简单的屏幕录像机. 一.录制准备 创建一个按钮 <button id="recording-toggle">Start recording< ...

  9. 华企盾DSC无法从网页下载客户端(无法访问web端)

    解决方法1:服务器安装目录需要安装在英文目录,否则DSCApache.exe会启动不了,导致无法访问5580网页. 解决方法2:5580端口占用也会导致DSCApache.exe启动不了,可打开服务器 ...

  10. Spring 事务的实现原理

    在执行访问数据库相关的操作中,特别是针对数据的修改操作,由于对于数据的修改可能会出现异常,因此对于整个一组的数据修改实际上都不能算是生效的,在这种情况下,需要使用事务的 "回滚" ...