RLHF平替算法DPO篇
来源:AiGC面试宝典 日期:2024年05月06日
目录
- 一、DPO vs RLHF
- 二、介绍一下DPO的损失函数
- 三、DPO微调流程
- 四、说一下DPO是如何简化RLHF的
- 五、DPO的第0步loss是固定的么?如果固定的话,值是多少
- 六、DPO是一个on-policy还是off-policy的算法,以及这样的算法有什么优劣
- 七、DPO公式是由PPO的objective公式推导过来的,为什么DPO是off-policy算法,而PPO是on-policy算法,到底哪一步推导出了问题
- 八、DPO为什么会在学习过程中training positive的概率和training negative的概率都同时下降
- 九、在什么情况下DPO exactly数学上等同于PPO
- 十、DPO的变体有哪些,主要解决DPO的什么问题
- 十一、DPO训练后的模型为什么会输出越来越长
- 代码解释
一、DPO vs RLHF?
![图表描述:图片对比了RLHF和DPO的流程。左侧是RLHF(Reinforcement Learning from Human Feedback),流程为:preference data经过maximum likelihood训练reward model,reward model生成label rewards,结合sample completions输入LM policy,最后通过reinforcement learning优化。右侧是DPO(Direct Preference Optimization),流程简化为:preference data经过maximum likelihood直接得到final LM。]
上图左边是RLHF算法,右边为DPO算法,两图的差异对比即可体现出DPO的改进之处。
RLHF算法:包含奖励模型(reward model)和策略模型(policy model,也称为演员模型,actor model),基于偏好数据以及强化学习不断迭代优化策略模型的过程。
DPO算法:不包含奖励模型和强化学习过程,直接通过偏好数据进行微调,将强化学习过程直接转换为SFT过程,因此整个训练过程简单、高效,主要的改进之处体现于损失函数。
📝通俗解释:想象一下训练一只会玩游戏的宠物。RLHF就像是你先训练一个"裁判"来告诉宠物哪些动作做得好,然后用奖励和惩罚的方式让宠物学习。而DPO更直接——你直接告诉宠物"这个动作比那个动作好",让它直接模仿学习,省掉了训练"裁判"的步骤。这样训练更快、更简单。
补充说明:
偏好数据,可以表示为三元组(提示语prompt,良好回答chosen,一般回答rejected)。论文中的chosen表示为下标w(即win),rejected表示为下标l(即lose)。
RLHF常使用PPO作为基础算法,整体流程包含了4个模型,且通常训练过程中需要针对训练的actor model进行采样,因此训练起来,稳定性、效率、效果不易控制。
- actor model / policy model:待训练的模型,通常是SFT训练后的模型作为初始化
- reference model:参考模型,也是经SFT训练后的模型进行初始化,且通常与actor model是同一个模型,且模型冻结,不参与训练,其作用是在强化学习过程中,保障actor model与reference model的分布差异不宜过大
- reward model:奖励模型,用于提供每个状态或状态动作对的即时奖励信号
- critic model:作用是估计状态或状态动作对的长期价值,也称为状态值函数或动作值函数
DPO算法仅包含RLHF中的两个模型,即演员模型(actor model)以及参考模型(reference model),且训练过程中不需要进行数据采样。
二、介绍一下DPO的损失函数?
DPO Loss Function Diagram diagram_description
图表内容描述: 图片展示了DPO损失函数的公式及其各部分的含义标注。
公式如下: $$ \mathcal{L}{\text{DPO}}(\pi\theta; \pi_{\text{ref}}) = -\mathbb{E}{(x, y_w, y_l) \sim \mathcal{D}} \left[ \log \sigma \left( \beta \log \frac{\pi\theta(y_w \mid x)}{\pi_{\text{ref}}(y_w \mid x)} - \beta \log \frac{\pi_\theta(y_l \mid x)}{\pi_{\text{ref}}(y_l \mid x)} \right) \right] $$
标注说明:
- Policy to optimize:指向$\pi_\theta$(待优化的策略)
- Reference policy (used to control behavior of LLMs):指向$\pi_{\text{ref}}$(参考策略,用于控制大语言模型行为)
- Aggregation over preference data:指向期望符号$\mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}}$(偏好数据的聚合)
- Logistic function:指向$\log \sigma$(Logistic函数)
- Shift in preferred completion:指向公式中的第一项$\beta \log \frac{\pi_\theta(y_w \mid x)}{\pi_{\text{ref}}(y_w \mid x)}$(优选完成的偏移)
- Shift in dispreferred completion:指向公式中的第二项$\beta \log \frac{\pi_\theta(y_l \mid x)}{\pi_{\text{ref}}(y_l \mid x)}$(非优选完成的偏移)
DPO损失函数
如何将RLHF的Reward model过程简化为上式,作者花了大量篇幅进行了推导,感兴趣的读者可以参考附件DPO的论文。
DPO算法的目的是最大化奖励模型(此处的奖励模型即为训练的策略),使得奖励模型对chosen和rejected数据的差值最大,进而学到人类偏好。
上式的后半部分通过对数函数运算规则,可以进行如下转化:
$$ \begin{aligned} &\left(\beta \log \left(\frac{\pi_{\theta}\left(y_{w} \mid x\right)}{\pi_{ref}\left(y_{w} \mid x\right)}\right)-\beta \log \left(\frac{\pi_{\theta}\left(y_{l} \mid x\right)}{\pi_{ref}\left(y_{l} \mid x\right)}\right)\right) \ =&\beta\left(\log \pi_{\theta}\left(y_{w} \mid x\right)-\log \pi_{ref}\left(y_{w} \mid x\right)\right)-\beta\left(\log \pi_{\theta}\left(y_{l} \mid x\right)-\log \pi_{ref}\left(y_{l} \mid x\right)\right) \ =&\beta\left(\log \pi_{\theta}\left(y_{w} \mid x\right)-\log \pi_{\theta}\left(y_{l} \mid x\right)\right)-\beta\left(\log \pi_{ref}\left(y_{w} \mid x\right)-\log \pi_{ref}\left(y_{l} \mid x\right)\right) \ =&\beta\left(\log \pi_{\theta}\left(y_{w} \mid x\right)-\log \pi_{\theta}\left(y_{l} \mid x\right)-\left(\log \pi_{ref}\left(y_{w} \mid x\right)-\log \pi_{ref}\left(y_{l} \mid x\right)\right)\right) \end{aligned} $$
📝通俗解释:这个公式的核心思想是:让模型更倾向于选择"好"的答案(chosen),同时远离"差"的答案(rejected)。公式中的β是一个控制参数,决定了模型学习的"激进程度"。左半部分是训练模型的选择偏好,右半部分是参考模型的选择偏好,二者的差值类似于KL散度,确保训练后的模型不会偏离原始模型太多。
Loss公式转换
转化后的公式和源代码中的计算函数中的公式是一致的。
其中左半部分是训练的policy模型选择chosen优先于rejected,右半部分是冻结的reference模型选择chosen优先于rejected,二者的差值可类似于KL散度,保障actor模型的分布与reference模型的分布不会有较大的差异。
三、DPO微调流程?
图表描述: 该图展示了DPO的微调流程架构:
- 输入端:左侧有两个输入,分别是"prompt + chosen"和"prompt + rejected"。
- 模型处理:
- 输入同时送入上方的Trained LM(带皇冠图标,表示正在训练的策略模型)和下方的Frozen LM(灰色背景,表示冻结的参考模型)
- Trained LM输出"chosen score"和"rejected score",计算得到$R_{policy} = \frac{\text{chosen score}}{\text{rejected score}}$
- Frozen LM输出"chosen score"和"rejected score",计算得到$R_{reference} = \frac{\text{chosen score}}{\text{rejected score}}$
- Loss计算:
- $R_{policy}$和$R_{reference}$被送入右侧的Loss计算模块
- 计算公式为:$loss = -\log \left( \sigma \left( \beta \cdot \log \left( \frac{R_{policy}}{R_{reference}} \right) \right) \right)$
- 更新:计算出的loss通过"Update weights"箭头反馈回Trained LM进行权重更新
DPO微调流程
上图展示了DPO微调的大致流程,其中Trained LM即为策略模型,Frozen LM即为参考模型,二者均是先进行SFT微调得到的模型进行初始化,其中Trained LM需要进行训练,Frozen LM不参与训练。
两个模型分别针对chosen和rejected进行预测获取对应的得分,再通过DPO的损失函数进行损失计算,进而不断的迭代优化。
📝通俗解释:整个流程就像是比较两篇作文。Trained LM是我们正在学习的"学生",Frozen LM是参考的"标准答案"。学生需要对两篇作文(chosen和rejected)给出评分,同时标准答案也给评分。然后我们比较两个评分的差距,用这个差距来调整学生的学习方向,让他越来越懂得区分好作文和差作文。
四、说一下DPO是如何简化RLHF的?
RLHF是如何训练?
RLHF一般会分2步:
- 第一步是训练reward model。训练数据是同一个prompt的2个回答,让人或GPT-4标注哪个回答更好,reward model会去优化如下的loss:
$$ \max_{r_\phi} \left{ \mathbb{E}{(x, y{\text{win}}, y_{\text{lose}}) \sim \mathcal{D}} [\log \sigma (r_\phi(x, y_{\text{win}}) - r_\phi(x, y_{\text{lose}}))] \right} $$
其中r就是reward model用来给回答打分。D是训练数据集,x是prompt,$y_{win}$和$y_{lose}$分别是好的回答和不好的回答。也就是说,要尽可能让好的回答的得分比不好的回答高,拉大他们之间的差别。
- 第二步是用RL算法来提升模型的得分。使用的loss是:
$$ \max_{\pi_\theta} \left{ \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(y|x)} [r_\phi(x, y)] - \beta \mathbb{D}{\text{KL}} [\pi\theta(y|x) || \pi_{\text{ref}}(y|x)] \right} $$
其中$\pi_\theta$是我们在训练的LLM,$\pi_{\text{ref}}$是训练的初始值。这个loss意思是希望LLM输出的回答的评分能尽可能高,同时$\pi_\theta$不要偏离$\pi_{\text{ref}}$太多,保证它还能正常做回答,不要训成一个评分很高但是回答乱码的东西。
📝通俗解释:RLHF的两步就像是这样:第一步是训练一个"评分老师",让他学会判断哪个回答更好;第二步是让"学生"根据这个评分老师的反馈来改进自己的回答,同时要保证不要偏离原来的正确方向太远。DPO则更直接——它跳过了训练"评分老师"的步骤,直接让学生学习"哪个更好"这个概念。
DPO优化策略?
DPO的作者们意识到,后面的这个式子是有显式解的。因为:
$$ \begin{aligned} &\max_{\pi_\theta} { \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(y|x)} [r_\phi(x, y)] - \beta \mathbb{D}{\text{KL}}[\pi\theta(y|x) || \pi_{\text{ref}}(y|x)] } \ =&\max_{\pi_\theta} \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(y|x)} [r_\phi(x, y) - \beta \log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)}] \ =&\min_{\pi_\theta} \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(y|x)} [\log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x)} - \frac{1}{\beta} r_\phi(x, y)] \ =&\min_{\pi_\theta} \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(y|x)} [\log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x) e^{r_\phi(x, y)/\beta}}] \end{aligned} $$
如果我们归一化一下分母,即取
$$ Z(x) = \sum_y \pi_{\text{ref}}(y|x) e^{r_\phi(x, y)/\beta} $$
也就可以构造出一个新的概率分布:
$$ \pi^*(y|x) = \pi_{\text{ref}}(y|x) e^{r_\phi(x, y)/\beta} / Z(x) $$
那么上式变成了:
$$ \begin{aligned} &\min_{\pi_\theta} \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(y|x)} [\log \frac{\pi_\theta(y|x)}{\pi_{\text{ref}}(y|x) e^{r_\phi(x, y)/\beta}}] \ =&\min_{\pi_\theta} \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(y|x)} [\log \frac{\pi_\theta(y|x)}{\pi^(y|x)} - \log Z(x)] \ =&\min_{\pi_\theta} \mathbb{E}{x \sim \mathcal{D}, y \sim \pi\theta(y|x)} [\log \frac{\pi_\theta(y|x)}{\pi^(y|x)}] \ =&\min_{\pi_\theta} \mathbb{E}{x \sim \mathcal{D}} \mathbb{D}{\text{KL}}(\pi_\theta(y|x) || \pi^*(y|x)) \end{aligned} $$
由于KL散度在2个分布相等时取最小值,我们得到了这样的结论:RLHF训练希望得到的最优的概率分布就是$\pi^*$。
另一个角度来说,由$\pi^$的公式,我们相当于是得到了r和$\pi^$的关系,那么是否我们可以把训练r转化成直接去训练$\pi^*$呢?
简单转换一下$\pi^*$的定义式,可以得到:
$$ r_\phi(x, y) = \beta \log \frac{\pi^*(y|x)}{\pi_{\text{ref}}(y|x)} + \beta \log Z(x) $$
带入最上面优化r的loss,也就有了:
$$ \max_{\pi^} \left{ \mathbb{E}{(x, y{\text{win}}, y_{\text{lose}}) \sim \mathcal{D}} [\log \sigma (\beta \log \frac{\pi^(y_{\text{win}}|x)}{\pi_{\text{ref}}(y_{\text{win}}|x)} - \beta \log \frac{\pi^*(y_{\text{lose}}|x)}{\pi_{\text{ref}}(y_{\text{lose}}|x)})] \right} $$
或者说,我们可以直接用这个loss去求$\pi_\theta$:
$$ \max_{\pi_\theta} \left{ \mathbb{E}{(x, y{\text{win}}, y_{\text{lose}}) \sim \mathcal{D}} [\log \sigma (\beta \log \frac{\pi_\theta(y_{\text{win}}|x)}{\pi_{\text{ref}}(y_{\text{win}}|x)} - \beta \log \frac{\pi_\theta(y_{\text{lose}}|x)}{\pi_{\text{ref}}(y_{\text{lose}}|x)})] \right} $$
这就是DPO的loss。DPO通过以上的公式转换把RLHF无损地转化为了SFT,在训练的时候不再需要同步跑4个模型(reward model, ref model, critic, actor),而是只用跑actor和ref 2个模型,甚至由于不再在线采数据,ref model的输出可以预先存下来,训练的时候重复使用。
📝通俗解释:DPO的数学推导非常巧妙!它证明了RLHF的最优解可以用一个特定的概率分布来表示,然后直接用这个分布来训练模型,而不需要中间的reward model。这就像是你发现了一条捷径——本来要绕很多路才能到达目的地,但现在有一条直接的路可以走。DPO就是这样把复杂的4步训练简化为2步。
五、DPO的第0步loss是固定的么?如果固定的话,值是多少?
是固定的,因为DPO loss为:
$$ loss = -\log \text{sigmoid}\left(\beta \log \frac{p_\theta(y_w|x)}{p_{ref}(y_w|x)} - \beta \log \frac{p_\theta(y_l|x)}{p_{ref}(y_l|x)}\right) $$
其中$y_w$是positive的y,而$y_l$是negative的y。那么开始的时候由于优化的网络参数等于reference的网络参数,因此
$$ p_\theta(y_w|x) = p_{ref}(y_w|x) $$
同理可得
$$ p_\theta(y_l|x) = p_{ref}(y_l|x) $$
故
$$ loss = -\log \text{sigmoid}(\beta \cdot 0 - \beta \cdot 0) = -\log \text{sigmoid}(0) = -\log(0.5) \approx 0.693 $$
这个数应该≈0.693(即log(2)的值)。
📝通俗解释:在训练开始时,模型还没有学习任何东西,所以它对"好"答案和"坏"答案的判断是完全一样的。这时候loss是一个固定值,大约等于0.693。这个值就像是一个"起跑线",之后模型会通过学习让loss逐渐下降,变得越来越会区分好答案和坏答案。
六、DPO是一个on-policy还是off-policy的算法,以及这样的算法有什么优劣?
DPO是一个off-policy的算法,因为训练DPO的pair数据不一定来自ref policy或者sft policy。
优势是:不需要对模型进行采样,然后标注,直接可以拿已有的数据集进行训练,这样的情况下包括采样的成本和标注的成本都可以节约。
劣势是:效果很难保证,尤其是你的模型本身能力和发布的pair数据不匹配的时候。相比而言,PPO是一个on-policy的算法,整体效果会比DPO要好。
📝通俗解释:On-policy就像是你自己亲身去尝试各种选择,然后从中学到什么好什么不好;off-policy就像是你看着别人做选择的结果来学习。DPO是off-policy,意味着它是用已有的、别人标注好的数据来训练的,省去了自己尝试的成本,但可能不如PPO学得精准。PPO就像是一个亲身实践的学习者,虽然成本高但学得更扎实。
七、DPO公式是由PPO的objective公式推导过来的,为什么DPO是off-policy算法,而PPO是on-policy算法,到底哪一步推导出了问题?
在DPO公式推导中,由目标公式:
$$ \max_{\pi_{\theta}} E_{x \sim D, y \sim \pi_{\theta}} [r(x, y)] - \beta D_{KL} [\pi_{\theta}(y|x) || \pi_{ref}(y|x)] $$
推导出optimal policy
$$ \pi_{r}(y|x) = \frac{1}{Z(x)} \pi_{ref}(y|x) \exp\left(\frac{1}{\beta} r(x, y)\right) $$
在公式中其实$\pi_{ref}$应该是随着模型更新而一直改变的,但是真正实现的时候一般使用$\pi_{SFT}$(SFT后的模型)代替。那么就导致了DPO从on-policy变成了off-policy的方法。
DPO面临着RL领域经典的state distribution shift的问题,从而效果会不如PPO。除此之外,由于DPO中$\pi_{ref}$和$\pi_{SFT}$有KL散度的限制,所以state distribution shift的问题不会像传统RL中那么大,所以整体上还是work的。
📝通俗解释:问题出在公式推导中,我们假设$\pi_{ref}$是固定的,但实际上在真正的RL训练中,参考模型应该随着训练不断更新。DPO用一个固定的SFT模型代替了原本应该动态更新的参考模型,这就导致了从"边学边做"的on-policy变成了"只学不做"的off-policy。虽然这样效率更高,但学习效果会打折扣。
补充说明:
经修正,相比于不加KL散度或者传统bandit算法算是分布差异小,但整体分布差异仍然很大(约25左右),如图所示:
图表描述: 图片包含两个并排的折线图,展示了RLHF训练过程中的数据。
左图:标题为"RLHF Train PM Size = 52B"。Y轴为"Test PM Score (52B)",范围从-2.5到0.0。X轴为$\sqrt{D_{KL}(policy||policy_0)}$,范围从0到5。右侧有一个颜色条表示"Policy Parameters",范围从$10^8$到$10^{10}$。图中有多条不同颜色的曲线,随着X轴数值增加,Y轴数值(Test PM Score)总体呈上升趋势。
右图:标题为"RLHF Train PM Size = Policy Size"。坐标轴含义与左图类似,Y轴为"Test PM Score (52B)",X轴为$\sqrt{D_{KL}(policy||policy_0)}$。右侧同样有表示"Policy Parameters"的颜色条。曲线趋势与左图相似,随着KL散度增加,Test PM Score上升。
八、DPO为什么会在学习过程中training positive的概率和training negative的概率都同时下降?
因为DPO的loss是BT loss,是maximize training set中positive和negative的gap。那从公式上它就无法保证training positive的概率是一直上升的。
继续探究它背后的原因,主要和采样的方式以及DPO loss组成相关。
首先还是把DPO loss列出来:
$$ loss = -\log \text{sigmoid}\left(\beta \log \frac{p_\theta(y_w|x)}{p_{ref}(y_w|x)} - \beta \log \frac{p_\theta(y_l|x)}{p_{ref}(y_l|x)}\right) $$
采样方式:一般DPO的数据采样来自模型内部生成(也就是所谓的on-policy采样),那么两个response的token level的重合度比较高,那么两个response的编辑距离会比较低。
数学原因:在同一模型中,我们有个假设当response的编辑距离$D(y_w, y_l) < T$,我们就有$|p(y_w|x) - p(y_l|x)| < S$,因此$|\beta \log \frac{p_\theta(y_w|x)}{p_{ref}(y_w|x)} - \beta \log \frac{p_\theta(y_l|x)}{p_{ref}(y_l|x)}| < H$,由于logloss的性质,这个公式只能在$p_\theta(y_w|x)$和$p_\theta(y_l|x)$越来越小的时候loss持续变小。
📝通俗解释:这个问题很有意思。就像是你在训练一只狗区分"坐下"和"站起来"两个命令。DPO的训练目标是让两个动作的差距越大越好,但如果两个动作太相似(比如只是稍微不同的站立姿势),狗可能会选择"都不做"来最小化错误——因为不做就不会错。模型也是类似的,当两个答案太相似时,模型发现降低两者的概率比提高正确答案的概率更容易减少loss。
举例说明:
- $p_{ref}(y_w|x) = 0.4$,$p_{ref}(y_l|x) = 0.3$,假设$S = 0.1$
- 那么如果正例概率升高$p_\theta(y_w|x) = 0.5$,由于$|p(y_w|x) - p(y_l|x)| < S$,所以$p_\theta(y_l|x) > 0.4$,假设$\beta = 0.1$,所以loss一定大于0.697
- 如果$p_\theta(y_w|x) = 0.21$,那么$p_\theta(y_l|x) > 0.11$,那么loss一定大于0.685
- 如果$p_\theta(y_w|x) = 0.11$,那么$p_\theta(y_l|x) > 0.01$,那么loss一定大于0.681
附计算代码:
import numpy as np
x = 0.00001
x_ref = 0.4
y = 0.0
y_ref = 0.3
beta = 0.1
gap = beta * (x / x_ref - y / y_ref)
print(gap)
log_sigmoid_value_pos = -np.log(1 / (1 + np.exp(-gap)))
print(log_sigmoid_value_pos)九、在什么情况下DPO exactly数学上等同于PPO?
- $p(y > y') = \delta(r(y) - r(y'))$
- $y'$通过蒙特卡洛采样
- DPO loss需要改成IPO loss形式,也就是 $$ \max_\pi \mathbb{E}{x \sim \rho, y \sim \pi(.|x), y' \sim \mu(.|x)} [\phi(p^*(y > y'|x))] - \tau D(\pi || \pi_{ref}) $$
- $\phi(q) = \frac{q}{1-q}$
📝通俗解释:这是一种很理想的情况。简单来说,当DPO使用特定的IPO loss形式,并且满足严格的数学假设(perfect reward function、完美采样等)时,DPO在数学上就等于PPO。但在实际应用中,这些假设很难完全满足,所以DPO只是PPO的近似。
参考论文:IPO(A General Theoretical Paradigm to Understand Learning from Human Preferences),细节证明可以看IPO论文。
十、DPO的变体有哪些,主要解决DPO的什么问题?
| 变体 | 解决问题 |
|---|---|
| RSO [1] | 由于DPO的蒙特卡洛采样很难达到,所以其实DPO几乎是off-policy的采样方式,RSO主要从DPO的采样方式来解决DPO的问题 |
| Iterative DPO [2] | 同样由于DPO的蒙特卡洛采样很难达到,所以通过on-policy的方式采样来替代off-policy的采样 |
| IPO [3] | 由于BT model的目标是最大化正负response的reward gap,但其实其中忽略了真实情况下我们的pair可能会有噪音,那么无限去扩大reward gap其实是不准确的,也就是overfit了preference的pair数据,那么解决方案是需要限制这个gap的范围 |
| DPOP [4] | 由于LLM model很难区分编辑距离较小的pair,那么当持续去区分这批case的时候,模型效果会崩塌,现象是正例子和负例子的概率都往下掉。那么DPOP用了一个新项来惩罚正例往下掉的pair,使得正例概率继续提升 |
📝通俗解释:DPO虽然好,但有一些"小毛病",所以研究者们发明了各种"升级版"来修补:
- RSO:改进采样方式,让训练数据更准确
- Iterative DPO:反复迭代训练,一步步逼近最优
- IPO:加个"紧箍咒",防止过度拟合噪音数据
- DPOP:专门解决"正负例子概率一起下降"的崩溃问题
参考文献:
- [1] Liu T, Zhao Y, Joshi R, et al. Statistical rejection sampling improves preference optimization[J]. arXiv preprint arXiv:2309.06657, 2023.
- [2] Yuan W, Pang R Y, Cho K, et al. Self-rewarding language models[J]. arXiv preprint arXiv:2401.10020, 2024.
- [3] Azar M G, Rowland M, Piot B, et al. A general theoretical paradigm to understand learning from human preferences[J]. arXiv preprint arXiv:2310.12036, 2023.
- [4] Pal A, Karkhanis D, Dooley S, et al. Smaug: Fixing failure modes of preference optimisation with DPO-Positive[J]. arXiv preprint arXiv:2402.13228, 2024.
十一、DPO训练后的模型为什么会输出越来越长?
并不是一定会越来越长。如果你尝试用所有正例子的response都比负例子的短,那么也会输出越来越短。
原因是由于数据构造原因导致的DPO后训练后的模型输出越来越长。 因为,在短的response中一句话结束后的概率会很大,但是在长的response中,"但是"、"而且"等细节描述词会接在一句话后,那么这些词语的概率会由DPO过程逐渐变大。
📝通俗解释:这就像是你在训练一个学生写文章。如果你给的好例子都是长文章,学生就会倾向于写长文章。因为长文章里有更多的"然后"、"而且"、"但是"这样的连接词,模型学到这些词出现的概率越来越高,自然就越写越长。这是数据分布造成的"偏见",不是DPO本身的bug。
代码解释
1. DPO损失函数代码实现
def preference_loss(policy_chosen_logps: torch.FloatTensor,
policy_rejected_logps: torch.FloatTensor,
reference_chosen_logps: torch.FloatTensor,
reference_rejected_logps: torch.FloatTensor,
beta: float,
label_smoothing: float = 0.0,
ipo: bool = False,
reference_free: bool = False) -> Tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]:
"""
参数说明:
- policy_chosen_logps: 训练模型对于chosen经过log后的logits
- policy_rejected_logps: 训练模型对于rejected经过log后的logits
- reference_chosen_logps: 参考模型对于chosen经过log后的logits
- reference_rejected_logps: 参考模型对于rejected经过log后的logits
- beta: policy和reference的差异性控制参数
"""
# actor模型选择chosen优先于rejected
pi_logratios = policy_chosen_logps - policy_rejected_logps
# reference模型选择chosen优先于rejected
ref_logratios = reference_chosen_logps - reference_rejected_logps
if reference_free:
ref_logratios = 0
# 差值可类似于KL散度,保障actor模型的分布与reference模型的分布不会有较大的差异
logits = pi_logratios - ref_logratios # also known as $h_{\pi_{\theta}}^{y_w, y_l}$
if ipo:
losses = (logits - 1/(2 * beta)) ** 2 # IPO loss
else:
# 标准的DPO loss (使用sigmoid)
# label_smoothing为0时,对应原始DPO论文的算法
losses = -F.logsigmoid(beta * logits) * (1 - label_smoothing) - F.logsigmoid(-beta * logits) * label_smoothing
# chosen和rejected的奖励
chosen_rewards = beta * (policy_chosen_logps - reference_chosen_logps).detach()
rejected_rewards = beta * (policy_rejected_logps - reference_rejected_logps).detach()
return losses, chosen_rewards, rejected_rewards📝通俗解释:这段代码实现了DPO的核心损失函数。简单来说:
- 先计算训练模型对"好答案"和"差答案"的评分差(pi_logratios)
- 再计算参考模型的评分差(ref_logratios)
- 两者相减得到最终的logits,然后用sigmoid函数计算loss
- 最后返回loss和两个奖励值,用于监控训练效果
2. DPO批次训练过程代码实现
def get_batch_metrics(self, batch: Dict[str, Union[List, torch.LongTensor]], loss_config: DictConfig, train=True):
"""Compute the SFT or DPO loss and other metrics for the given batch of inputs."""
if loss_config.name in ('dpo', 'ipo'):
# policy模型针对chosen和rejected进行预测
policy_chosen_logps, policy_rejected_logps = self.concatenated_forward(self.policy, batch)
with torch.no_grad():
# reference模型针对chosen和rejected进行预测(不参与梯度计算)
reference_chosen_logps, reference_rejected_logps = self.concatenated_forward(self.reference_model, batch)
if loss_config.name == 'dpo':
loss_kwargs = {'beta': loss_config.beta, 'reference_free': loss_config.reference_free,
'label_smoothing': loss_config.label_smoothing, 'ipo': False}
elif loss_config.name == 'ipo':
loss_kwargs = {'beta': loss_config.beta, 'ipo': True}
else:
raise ValueError(f'unknown loss {loss_config.name}')
# 损失计算
losses, chosen_rewards, rejected_rewards = preference_loss(
policy_chosen_logps, policy_rejected_logps, reference_chosen_logps,
reference_rejected_logps, **loss_kwargs)
reward_accuracies = (chosen_rewards > rejected_rewards).float()
elif loss_config.name == 'sft':
policy_chosen_logits = self.policy(batch['chosen_input_ids'],
attention_mask=batch['chosen_attention_mask']).logits.to(torch.float32)
policy_chosen_logps = _get_batch_logps(policy_chosen_logits, batch['chosen_labels'],
average_log_prob=False)
losses = -policy_chosen_logps
return losses.mean()📝通俗解释:这个函数展示了DPO训练的完整流程:
- 首先让训练中的模型(policy)对"好"和"差"答案进行预测
- 然后让冻结的参考模型(reference)也做同样的预测
- 用两者的预测结果计算DPO loss
- 最后返回平均loss用于反向传播
3. LM的交叉熵计算代码实现
def _get_batch_logps(logits: torch.FloatTensor, labels: torch.LongTensor, average_log_prob: bool = False) -> torch.FloatTensor:
"""经模型后的logits进行批量计算logps"""
assert logits.shape[:-1] == labels.shape
# 基于先前的token预测下一个token
labels = labels[:, 1:].clone() # 移位,对下一个token进行预测
logits = logits[:, :-1, :] # 去掉最后一个logits
loss_mask = (labels != -100) # 忽略padding部分
# dummy token; we'll ignore the losses on these tokens later
labels[labels == -100] = 0
# 交叉熵计算:gather找到每个label对应的logit,然后计算log概率
per_token_logps = torch.gather(logits.log_softmax(-1), dim=2,
index=labels.unsqueeze(2)).squeeze(2)
if average_log_prob:
return (per_token_logps * loss_mask).sum(-1) / loss_mask.sum(-1)
else:
return (per_token_logps * loss_mask).sum(-1)📝通俗解释:这是语言模型计算交叉熵损失的标准方法。核心思想是:
- 模型输出的是每个位置可能是哪个token的" logits"
- 我们用log_softmax把这些logits转换成概率
- 用gather找到每个实际token位置对应的预测概率
- 最后求和(或平均)得到整体的log概率 这个log概率越大,说明模型预测得越准,loss就越小。
整理完成
笔记来源:AiGC面试宝典 整理日期:2024年8月11日