在 TTS 训练流程中,SFT (Speaker Fintuning) 无法直接优化人类对语音质量的主观偏好(如自然度、韵律、情感表达等)。DPO(Direct Preference Optimization) 提供了一种无需训练奖励模型的偏好对齐方法。只需要通过”偏好”数据(chosen vs rejected),即可直接优化策略模型,使得模型更倾向于于人类偏好或者抑制模型因为采样而出现的不稳定错误。
CosyVoice 系列在 LLM 从文本预测语音 token 的阶段引入了 DPO 训练(CosyVoice2 开始),本文从训练流程、模型前向运算、Loss 理论推导及代码实现等层面逐步解读。
核心文件功能:
| 文件 | 功能 |
|---|---|
| cosyvoice/bin/train.py | 训练入口,初始化策略(待更新)模型、参考模型和 DPO Loss |
| cosyvoice/llm/llm.py | forward_dpo 方法,计算 chosen/rejected 的对数概率 |
| cosyvoice/utils/train_utils.py | batch_forward 函数,组装 DPO 训练逻辑 |
| cosyvoice/utils/losses.py | DPOLoss 类,实现 DPO/IPO/cDPO 损失函数 |
训练入口:模型与参考模型初始化
monkey-patch 方法替换
1 | if args.dpo is True: |
通过 monkey-patch,DPO 训练时将模型的 forward 方法替换为 forward_dpo。这样训练循环中调用 model(batch, device) 时,无需修改训练代码即可切换为 DPO 前向逻辑。
参考模型初始化
1 | ref_model = deepcopy(configs[args.model]) |
DPO 需要一个冻结的模型 来作为参考模型(reference model)
deepcopy从当前模型配置中拷贝出一份结构相同的模型副本- 加载 SFT 权重:从
--ref_model指定的路径加载 SFT 阶段的 checkpoint 作为参考模型权重 - 实例化 DPO Loss:设置
beta=0.01,不启用 label smoothing 和 IPO(后文详细说明几种训练方式的细节)
损失函数加入训练流程
1 | executor = Executor(gan=gan, ref_model=ref_model, dpo_loss=dpo_loss) |
将参考模型和 DPO Loss 传入 Executor,在每个 training step 中通过 batch_forward 完成 DPO 计算。
模型前向传播:forward_dpo
源码位置:cosyvoice/llm/llm.py —
forward_dpo方法
forward_dpo 的核心思路是将 chosen 和 rejected 语音 token 拼成一个大 batch 做一次 LM 前向推理,再拆分输出分别计算对数概率。
输入数据提取
1 | text_token = batch['text_token'].to(device) # 文本 token |
每个样本包含同一条文本对应的两种语音输出:speech_token(高质量 / chosen)和 reject_speech_token(低质量 / rejected)。
4.2 Chosen + Rejected 凑新 batch
1 | speech_token = unpad_sequence(speech_token, speech_token_len.cpu(), batch_first=True) |
假设原始 batch size 为 repeat(2, ...),使每条 rejected 语音都和对应的 chosen 语音共享同一条文本输入。
LM 前向推理与输出拆分
1 | lm_output, lm_output_mask = self.llm(lm_input, lm_input_len.to(device)) |
一次前向推理处理
1 | loss = self.criterion_ce(chosen_logits, chosen_lm_target.to(device)) |
对数概率计算
1 | chosen_lm_mask = chosen_lm_target == IGNORE_ID |
逻辑拆解:
- 构造 mask:
IGNORE_ID标记的位置是文本/特殊 token(不参与 Loss 计算) - **
masked_fill(..., 0)**:将IGNORE_ID(-1)替换为 0,避免gather时索引越界 - **
torch.gather**:从 log_softmax 输出中取出每个位置上目标 token 的对数概率 - mask 加权平均:对有效位置的 log 概率求平均,得到序列级别的
最终返回策略模型对 chosen/rejected 的平均对数概率,供后续 DPO Loss 使用:
1 | return {'loss': loss, 'acc': acc, 'chosen_logps': chosen_logps, 'rejected_logps': rejected_logps} |
DPO Loss 组装
源码位置:cosyvoice/utils/train_utils.py —
batch_forward函数
1 | with autocast: |
关键设计:
- 策略模型的
chosen_logps/rejected_logps带梯度,DPO Loss 的梯度通过它们回传 - 参考模型在
torch.no_grad()下运行,权重冻结,仅提供参考的 logps - 最终 loss = DPO preference loss + SFT loss,联合优化既保持原始参考模型的语言建模能力,又引导偏好对齐。这一点在 InstructGPT 论文中就已经提到,最近也有不少论文有提到这个设计。
DPO Loss 核心计算
源码位置:cosyvoice/utils/losses.py —
DPOLoss类
DPO 原理推导
DPO(Rafailov et al., 2023)的核心思想是将 RLHF 中的奖励建模 + PPO 两步优化,压缩为一步直接优化。DPO 的精髓在于采用了 Bradley-Terry 偏好模型,将最优策略的闭式解代入该模型,建模过程可以做极大的简化。
Bradley-Terry 模型 假设人类偏好概率为:
其中
RLHF 的目标是:
这里 KL 是显式的。上面这个带 KL 的目标函数,有解析最优解:
可以推导出,在 KL 约束的 RL 目标下,最优策略和奖励之间存在闭式关系:
DPO 则是将这个关系代回 Bradley-Terry 模型(
对应代码:
1 | pi_logratios = policy_chosen_logps - policy_rejected_logps |
其中 logits 等价于:
直觉上:我们希望策略模型相比参考模型,在 chosen 上的概率提升幅度,大于在 rejected 上的,即
cDPO(Conservative DPO)
当偏好数据的标注存在噪声时(即 chosen 不一定真的优于 rejected),原始 DPO 可能过拟合到错误的偏好标签。cDPO(Mitchell, 2023)通过 label smoothing 来缓解这个问题:
其中 label_smoothing 参数。
对应代码:
1 | losses = ( |
原理:
- 第一项
是标准 DPO,推动模型偏好 chosen - 第二项
是”反向 DPO”,推动模型偏好 rejected - 两项加权混合,相当于告诉模型”有
的概率标签是对的,有 的概率标签是反的”
当 label_smoothing=0.0,即使用原始 DPO。
个人感觉这种思路其实一定程度上加剧了 LLM 训练的不稳定性,一般情况如果能把 DPO 训练的数据可靠性提升上去时,不会有限考虑 cDPO / label smoothing 的这种思想。
IPO(Identity Preference Optimization)
IPO(Azar et al., 2023)是另一种 DPO 变体,用 MSE loss 替代 sigmoid loss:
对应代码:
1 | if self.ipo: |
与 DPO 的区别:
| 特性 | DPO | IPO |
|---|---|---|
| Loss 形式 | ||
| 梯度行为 | logits 很大时梯度趋零(sigmoid 饱和) | logits 偏离目标值时梯度始终存在 |
| 过拟合倾向 | 可能过拟合到极端偏好 | 目标值有限,天然正则化 |
| 适用场景 | 标签较干净 | 标签有噪声或希望更稳健的训练 |
IPO 的直觉是:不追求让 logits 无限大(即无限偏好 chosen),而是让 logits 稳定在 ipo=False,未启用此模式。
隐式奖励(Implicit Reward)
1 | chosen_rewards = self.beta * (policy_chosen_logps - reference_chosen_logps).detach() |
这两个值来自 DPO 的理论推导:
它们不参与梯度计算(.detach()),常常作为训练的监控指标:
dpo_acc=(chosen_rewards > rejected_rewards).float().mean():chosen 奖励高于 rejected 的比例- 合理的训练现象:随着训练推进,
chosen_rewards应上升,rejected_rewards应下降或保持稳定
训练监控指标
在 batch_forward 中,以下指标会被写入 TensorBoard:
| 指标 | 含义 | 理想趋势 |
|---|---|---|
loss |
preference_loss + sft_loss(总 loss) | 下降 |
sft_loss |
chosen 上的交叉熵 loss | 下降 |
dpo_loss |
DPO preference loss | 下降 |
acc |
chosen 上的 token 预测准确率 | 上升 |
dpo_acc |
chosen reward > rejected reward 的比例 | 趋近 1.0 |
chosen_reward |
chosen 的平均隐式奖励 | 上升 |
reject_reward |
rejected 的平均隐式奖励 | 下降或不变 |
总结
CosyVoice 的 DPO 训练方案具有以下特点:
- SFT + DPO 联合优化:最终 loss 是 SFT CE loss 和 DPO preference loss 的直接相加,在偏好对齐的同时保持基本的语言建模能力
- 高效的批处理策略:将 chosen 和 rejected 拼成
的大 batch 做一次前向推理,避免两次独立推理的开销 - 灵活的 Loss 选择:代码中实现了 DPO / cDPO / IPO 三种变体,通过
label_smoothing和ipo参数切换 - 完善的监控体系:通过隐式奖励、DPO accuracy 等指标实时监控训练状态
参考文献
- [DPO] Rafailov et al., “Direct Preference Optimization: Your Language Model is Secretly a Reward Model”, 2023. arXiv:2305.18290
- [IPO] Azar et al., “A General Theoretical Paradigm to Understand Learning from Human Feedback”, 2023. arXiv:2310.12036
- [cDPO] Mitchell, “A Note on DPO with Noisy Preferences & Relationship to IPO”, 2023. Blog
- 本文标题:代码解读 | CosyVoice 代码研读(一):CosyVoice2 LLM + DPO
- 创建时间:2025-11-28
- 本文链接:2025/2025-11-25-cosyvoice2-dpo/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!