LLM大模型: RLHF-DPO原理和源码解析

   1、前段时间国外某大学反向抄袭国内某团队的大模型闹得沸沸扬扬,国内被抄袭的大模型是MiniCPM,详细资料:https://github.com/OpenBMB/MiniCPM ; 能被国外同行抄袭,必定有过人之处,粗略看了一下https://github.com/OpenBMB/MiniCPM/blob/main/model/modeling_minicpm.py 模型文件,发现整个结构和llama类似,没啥特别的,如下:

         

    既然模型整体的结构和llama接近,没啥特别的,效果好就看整个训练策略了!作者详细介绍了训练策略:https://shengdinghu.notion.site/MiniCPM-c805a17c5c8046398914e47f0542095a  主要是从这5个方面优化的:

  • Hyper-parameters
  • Batch size
  • Learning Rate
  • Learning Rate Scheduler
  • Data Strategy

  所以要想大模型效果好,有个大的改进方向:

  • 改模型细节:比如llama用旋转位置编码代替绝对位置编码、采用flashAttention等
  • 改训练策略:比如MiniCPM(官方暂未公布实现代码)

    MiniCPM训练策略介绍的文章中指出:采用DPO对齐后,MiniCPM的得分甚至超过了llama2-70b-chat,效果很好啊!这个DPO又是啥了?

   

   2、目前市面上主流LLM,界面上都有反馈功能:觉得好的点赞,绝不不好的点倒赞!背后用的就是强化学习!现成已经实现的库在这里:https://github.com/huggingface/trl  

     (1)为便于理解,先举个例子看看。DPO的训练语料结构简单,一条语料只有3个字段,如下:

{
  "prompt": "What is the capital of China?",
  "chosen": "The capital of China is Beijing.",
  "rejected": "The capital of China is Shanghai."
}

  chosen是正确的(或则说符合用户偏好)的答案,reject则相反。DPO的目的就是让LLM的回答尽可能接近chosen答案!因为trl已经封装好了,使用起来也很简单,如下:

from datasets import Dataset
from transformers import GPT2Tokenizer, GPT2LMHeadModel, TrainingArguments
from trl import DPOTrainer

# 数据
data = [
    {
        "prompt": "What is the capital of China?",
        "chosen": "The capital of China is Beijing.",
        "rejected": "The capital of China is Shanghai."
    },
    {
        "prompt": "What is 2 + 2?",
        "chosen": "2 + 2 equals 4.",
        "rejected": "2 + 2 equals 5."
    }
]

# 将数据转换为 Hugging Face Dataset 格式
dataset = Dataset.from_list(data)

# 加载底座模型
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
model = GPT2LMHeadModel.from_pretrained("gpt2")
model_ref = GPT2LMHeadModel.from_pretrained("gpt2")  # 参考模型

# 添加pad_token
tokenizer.add_special_tokens({'pad_token': '[PAD]'})

# 数据预处理,转成token
def preprocess_function(examples):
    inputs = tokenizer(examples["prompt"], padding="max_length", truncation=True, max_length=512, return_tensors="pt")
    chosen_outputs = tokenizer(examples["chosen"], padding="max_length", truncation=True, max_length=512, return_tensors="pt")
    rejected_outputs = tokenizer(examples["rejected"], padding="max_length", truncation=True, max_length=512, return_tensors="pt")
    return {
        "input_ids": inputs["input_ids"],
        "attention_mask": inputs["attention_mask"],
        "chosen_ids": chosen_outputs["input_ids"],
        "rejected_ids": rejected_outputs["input_ids"]
    }

encoded_dataset = dataset.map(preprocess_function, batched=True)

# 设置训练参数
training_args = TrainingArguments(
    output_dir="/root/huggingface/DPO",
    eval_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=1,
    num_train_epochs=3,
    weight_decay=0.01,
)

# 初始化 DPOTrainer
trainer = DPOTrainer(
    model=model,
    beta=0.1,  # DPO损失中的温度超参数
    args=training_args,
    train_dataset=encoded_dataset,
    tokenizer=tokenizer,
)

# 在训练过程中手动使用参考模型
trainer.model_ref = model_ref

# 开始训练
trainer.train()

  由于trl已经封装成熟,整个流程简单、清晰:准备数据,转换数据,加载模型,调用DPO接口训练!流程和lora看起来几乎一摸一样,没啥本质区别!

    (2)传统的GPT大模型,原理是根据上文预测下一个token的概率,loss函数是cross entropy,通过这种auto regresison的方式完成语料的训练;DPO的核心思路是让LLM的回答往chosen靠近,远离reject的答案,从数学上讲是怎么实现的了?

   这里采用Bradley-Terry模型:x是prompt,y1是chosen回答,y2是reject回答,整个表达式就是y1的reward大于y2产生的reward:

        

   回答可能是负数,加上exp,上述表达式变成了:

        

   因为训练数据是批量的,不是单个,所以需要综合考虑所有训练语料的P值;同时为了方便处理exp,需要求ln,所以整个loss演变如下:

       

   其中Yw是chosen,Yl是reject,整个数学表达式看着复杂,其实数学意义很简单,从里往外看:

  • chosen的r(x,Yw)要比reject的r(x,Yl)高!
  • x、Yw、Yl从分布D中取出,然后所有的结果求期望
  • 因为loss是越小越好,所以前面加上负号

 (3)这样就完了?哪有这么简单!现在的思路很简单:用chosen和reject牵引基准模型的输出,尽可能靠近chosen。实际操作时,会遇到另一个问题:大量DPO语料训练后,基准模型的参数被改成了适应DPO的语料,自己原来训练语料相关的数据被灾难性遗忘【类似问题也存在于lora微调:微调数据不能全部用垂直领域的数据,还是要适当加一点通用领域的数据,避免基座的模型出现灾难性遗忘】。在DPO这里为了避免类似问题,是在loss上做了改进,如下:既要reward高,又要尽可能贴近原来的基座模型,两手都要抓,两手都要硬!

      

   全是数学推导了:

        

   详细的推导过程可以参考:https://www.bilibili.com/video/BV1GF4m1L7Nt/?spm_id_from=333.337.search-card.all.click&vd_source=241a5bcb1c13e6828e519dd1f78f35b2  最终的loss如下:

       

  和前面的那个loss比,只是让每个更新参数后的模型(policy model)的reward除以了使用基座模型(reference model)得到的reward,我个人觉得没有本质变化!

  (4)DPO的源码都在trl包里面了:dpo_loss计算的方法如下:

     

   (5)个人观点:深度神经网络用于提取特征维度,生成特征向量;强化学习用于指明发展演进方向,这点和人类从小的学习思路很接近,所以 深度学习+强化学习 = 人工智能!

 

参考:

1、https://www.bilibili.com/video/BV1vy4y1P7GT/?spm_id_from=333.788&vd_source=241a5bcb1c13e6828e519dd1f78f35b2   强化学习TRL包源码解读S2——PPO

2、https://shengdinghu.notion.site/MiniCPM-c805a17c5c8046398914e47f0542095a    MiniCPM:揭示端侧大语言模型的无限潜力

3、https://zhuanlan.zhihu.com/p/686664720  如何从零开始训练大模型(minicpm分享&讨论)

4、https://www.bilibili.com/video/BV1Lt421V7K6/?spm_id_from=333.337.search-card.all.click&vd_source=241a5bcb1c13e6828e519dd1f78f35b2    MiniCPM-2B-dpo-bf16MiniCPM-2B-dpo-fp32gradio webdemo演示系统及GPU占用情况

5、https://github.com/huggingface/trl/blob/main/trl/trainer/dpo_trainer.py  

6、https://www.bilibili.com/video/BV1GF4m1L7Nt/?spm_id_from=333.337.search-card.all.click&vd_source=241a5bcb1c13e6828e519dd1f78f35b2  DPO数学原理

posted @ 2024-06-17 23:32  第七子007  阅读(129)  评论(0编辑  收藏  举报