使用 DPO 微调 Llama 2 (TRL)

1. 简介

Direct Preference Optimization(DPO) ,通过直接优化语言模型以符合人类偏好,无需显性奖励模型或强化学习。该算法隐式地优化与现有 RLHF 算法相同的目标(奖励最大化,带有 KL 散度约束),但易于实现且容易训练。
直接偏好优化 (Direct Preference Optimization,DPO) 法在 stack-exchange preference 数据集上微调最新的 Llama v2 7B 模型, stack-exchange preference 数据集中包含了各个 stack-exchange 门户上的各种问题及其排序后的回答。

stack-exchange preference 数据样式:
在这里插入图片描述

2. DPO 与 PPO

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

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

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

3. 使用 TRL 进行训练

一个典型的 RLHF 流水线通常包含以下几个环节:

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

DPO 训练直接消灭了奖励建模和 RL 这两个环节 (环节 3 和 4),直接根据标注好的偏好数据优化 DPO 目标。

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

  • prompt : 即推理时输入给模型的提示
  • chosen : 即针对给定提示的较优回答
  • rejected : 即针对给定提示的较劣回答或非给定提示的回答, 需要注意的是,单个提示可能对应于数据集数组中的多个响应,这在数据集的数组中反映了重复的条目。

数据例子:

dpo_dataset_dict = {
    "prompt": ["hello", "how are you",],
    "chosen": ["hi, nice to meet you", "I am fine",],
    "rejected": ["leave me alone", "I am not fine",],
}

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


from transformers import AutoModelForCausalLM
from datasets import load_dataset
from trl import SFTTrainer 

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()

4. 基于 Llama v2 进行实验

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

4.1 有监督微调

先用 TRLSFTTrainerSFT 数据子集上使用 QLoRA7B 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()
#trainer.save_model()
4.2 DPO 训练

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

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 方法对其进行训练。训练器(dpo_trainer)还会用评估数据集评估训练进度,并报告一些关键指标,例如可以选择通过 WandB 记录并显示隐式奖励。最后,我们可以将训练好的模型推送到 HuggingFace Hub。

4.3 Merging the adaptors

python examples/research_projects/stack_llama/scripts/merge_peft_adapter.py --base_model_name="meta-llama/Llama-2-7b-hf" --adapter_model_name="dpo/final_checkpoint/" --output_name="stack-llama-2"

4.4 Running the model

加载DPO训练步骤保存的经过DPO训练的LoRA适配器,并通过以下方式加载:

加载DPO训练步骤保存的经过DPO训练的LoRA适配器,并通过以下方式加载:

补充

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

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

  • rewards/chosen (较优回答的奖励) : 针对较优回答,策略模型与参考模型的对数概率二者之差的均值,按 beta 缩放。
  • rewards/rejected (较劣回答的奖励) : 针对较劣回答,策略模型与参考模型的对数概率二者之差的均值,按 beta 缩放。
  • rewards/accuracy (奖励准确率) : 较优回答的奖励大于相应较劣回答的奖励的频率的均值
  • rewards/margins (奖励余裕值) : 较优回答的奖励与相应较劣回答的奖励二者之差的均值。
    直观上讲,在训练过程中,希望余裕值增加并且准确率达到 1.0,换句话说,较优回答的奖励高于较劣回答的奖励 (或余裕值大于零)。随后还可以在评估数据集上计算这些指标。

stack_llama/scripts/merge_peft_adapter.py:

from dataclasses import dataclass, field
from typing import Optional

import torch
from peft import PeftConfig, PeftModel
from transformers import AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer, HfArgumentParser


@dataclass
class ScriptArguments:
    """
    The input names representing the Adapter and Base model fine-tuned with PEFT, and the output name representing the
    merged model.
    """

    adapter_model_name: Optional[str] = field(default=None, metadata={"help": "the adapter name"})
    base_model_name: Optional[str] = field(default=None, metadata={"help": "the base model name"})
    output_name: Optional[str] = field(default=None, metadata={"help": "the merged model name"})


parser = HfArgumentParser(ScriptArguments)
script_args = parser.parse_args_into_dataclasses()[0]
assert script_args.adapter_model_name is not None, "please provide the name of the Adapter you would like to merge"
assert script_args.base_model_name is not None, "please provide the name of the Base model"
assert script_args.output_name is not None, "please provide the output name of the merged model"

peft_config = PeftConfig.from_pretrained(script_args.adapter_model_name)
if peft_config.task_type == "SEQ_CLS":
    # The sequence classification task is used for the reward model in PPO
    model = AutoModelForSequenceClassification.from_pretrained(
        script_args.base_model_name, num_labels=1, torch_dtype=torch.bfloat16
    )
else:
    model = AutoModelForCausalLM.from_pretrained(
        script_args.base_model_name, return_dict=True, torch_dtype=torch.bfloat16
    )

tokenizer = AutoTokenizer.from_pretrained(script_args.base_model_name)

# Load the PEFT model
model = PeftModel.from_pretrained(model, script_args.adapter_model_name)
model.eval()

model = model.merge_and_unload()

model.save_pretrained(f"{script_args.output_name}")
tokenizer.save_pretrained(f"{script_args.output_name}")
model.push_to_hub(f"{script_args.output_name}", use_temp_dir=False)


参考
stack-exchange preference 数据集
trainer 文档
v4.40.2/src/transformers/trainer.py
sft_trainer.py
dpo_trainer.py
直接偏好优化(DPO):让语言模型拥有更好的奖励机制
使用 DPO 微调 Llama 2
Fine-tune Llama 2 with DPO
SFT 和 DPO 训练脚本的完整源代码

### 对Llama模型应用DPO微调 对于Llama模型而言,采用Direct Preference Optimization (DPO) 进行微调意味着直接基于人类偏好数据优化该模型,而无需通过奖励建模这一中间环节[^1]。具体实现过程中,主要关注于调整模型参数以更好地反映训练集中的偏好模式。 #### 准备工作环境 为了能够顺利实施DPO算法,在开始之前需确保已安装必要的库文件并加载预训练好的Llama模型: ```bash pip install transformers datasets torch accelerate ``` #### 加载与准备数据集 接下来要做的就是获取用于指导微调过程的人类反馈数据集,并对其进行适当处理以便后续使用: ```python from datasets import load_dataset dataset = load_dataset("path_to_preference_data") # 替换为实际路径或名称 train_set = dataset['train'] ``` #### 实施DPO微调流程 在此阶段,将定义一个循环来迭代遍历整个训练集合,每次更新都旨在使模型更贴近理想的行为方式。值得注意的是,这里并不涉及强化学习的具体机制,而是单纯依赖损失函数引导下的梯度下降方法完成参数修正操作。 ```python import torch from transformers import AutoModelForCausalLM, Trainer, TrainingArguments model_name_or_path = "meta-llama/Llama-2-7b-hf" tokenizer = AutoTokenizer.from_pretrained(model_name_or_path) model = AutoModelForCausalLM.from_pretrained(model_name_or_path) training_args = TrainingArguments( output_dir="./results", per_device_train_batch_size=8, num_train_epochs=3, logging_steps=10, save_strategy="epoch" ) def compute_loss(batch): outputs = model(**batch) logits = outputs.logits # 假设输入已经包含了正负样本对比信息 positive_indices = batch.get('positive_labels', None) negative_indices = batch.get('negative_labels', None) if positive_indices is not None and negative_indices is not None: pos_logits = logits.gather(1, positive_indices.unsqueeze(-1)).squeeze() neg_logits = logits.gather(1, negative_indices.unsqueeze(-1)).squeeze() loss_fn = torch.nn.MarginRankingLoss(margin=1.0) target = torch.ones_like(pos_logits) return loss_fn(pos_logits, neg_logits, target) else: raise ValueError("Batch must contain 'positive_labels' and 'negative_labels'") trainer = Trainer( model=model, args=training_args, train_dataset=train_set.select(range(10)), # 只取前十个作为示范用途 tokenizer=tokenizer, compute_loss=compute_loss ) trainer.train() ``` 上述代码片段展示了如何利用自定义`compute_loss()`函数计算针对每一对比较项之间的差异程度,并以此为基础构建最终的损失值。此做法有效简化了原本复杂的RLHF框架结构,使得整个系统更加易于理解和维护的同时也提高了效率。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值