准备 2026 暑假 LLM 算法实习ing
Transformer#
Architecture#
Transformer 的核心是用 Self-Attention + FFN 替代 RNN/CNN,实现对序列中任意位置的全局建模。结构通常分为:
- Encoder-only 用的是双向自注意力,能看到两侧的 token,所以能捕捉整体信息。
- Decoder-only 用的因果掩码注意力,符合自回归生成,适合 NLP 问题。
- Encoder-Decoder 是交叉注意力,Encoder 理解 Decoder 生成,所以适合 seq2seq。
让 Image-2 根据我的手稿生成了一个手绘风格示意图,感觉还可以。
Attention#
$$ \text{Attention}(Q,K,V)=\text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V $$softmax 数值不稳定
数值稳定的 Softmax,减去最大值防止溢出。
softmax 里面缩放点积的作用
自注意力的时间复杂度
- 计算 $QK^T$ 是 $(n \times d_k) \cdot (d_k \times n)$ 的矩阵乘法,计算 $QK^T$ 的每个元素 $A_{ij}$ 需要 $A_{ij}=Q_i\cdot K_j$ 进行点积,所以 $n^2$ 个元素每个 $d_k$ 次计算,一共 $O(n^2d_k)$
- softmax 操作 $O(n^2)$
- 计算 $AV$ 是 $(n \times n)(n \times d_v)$ 的矩阵乘法,一共 $O(n^2d_v)$
- 合计 $O(n^2 d)$
如何实现并行计算的
有哪些减少 attention 计算量的方法
- FlashAttention:注意力计算的时间复杂度依然是 $O(n^2d)$,但是它通过分块读取和 online softmax 降低了显存访问开销,把显存访问复杂度从 $O(n^2)$ 降到了 $O(nd)$。
- KVCache:在生成下一个 token 时,只需要计算当前 token 的 query、key、value。之前的 key/value 被缓存,直接拼接使用,避免重复计算整个历史序列的 attention,从而提高推理阶段的速度。
- MQA/GQA:减少 K 和 V 矩阵注意力头,减小计算量。
KVCache#
class KVCacheAttention(nn.Module):
def forward(self, x, cache=None, mask=None):
# ...
if cache is not None:
k = torch.concat([cache["k"], k], dim=2) # seq_len 维度 concat
v = torch.concat([cache["v"], v], dim=2)
new_cache = {
"k": k,
"v": v
}
scores = q @ k.transpose(-2, -1) / math.sqrt(self.d_k)
if mask is not None:
scores = scores.masked_fill(mask[:t_q, :t_k]==0, float("-inf"))
attn = torch.softmax(scores, dim=-1)
output = (attn @ v).transpose(1, 2).reshape(B, T, self.d_model)
return self.o_proj(output), new_cache加入 KVCache 之后
seq_k和seq_q的长度就不一样了,所以需要进行截长。
Position Embedding#
- Learnable Position Embedding
像 token embedding 一样,用长度为 dim 的向量表示 token 的位置信息,无需多言。问题在于长度是固定的,假如训练时 max_seq=2048 推理时候 max_seq=4096,就会出现位置不存在,所以 Learnable Position Embedding 的问题就是不能外推。
- Sinusoidal Position Embedding
正余弦位置编码的思想也是给每个位置生成唯一编码,从公式可以看出在低维度数值变化快频率高,在高度为数值变化低频率低:
低维:sin(1) sin(2) sin(3) sin(5)
高维:sin(0.0001) sin(0.0002) sin(0.0003) sin(0.0004) 通过这种方法就可以给每一个 position 一个独一无二的向量,因为多个频率组合起来,几乎不可能出现两个位置完全一样。它的问题在于不好体现位置的相对关系,把 sinusoidal pe 加入 token embedding 之后模型得自己找到 $x_i=e_i+PE_i$ 的相对关系。
- RoPE
假设我们有 pos=m 的词向量 $q_m$ 和 pos=n 的词向量 $k_n$,我们分别用旋转矩阵 $R$ 对两个向量进行旋转,然后计算它们的内积:
可以发现内积的结果只和两个词向量的相对位置有关, 最终结果 $q_m^TR((n-m)\theta)k_n$ 与 $m$ 和 $n$ 的绝对值无关,只与相对位置 $(m−n)$ 有关。扩展到高维之后可以看下图:
Normalization#
早期 BatchNorm 论文认为它通过减少 Internal Covariate Shift 来提升训练效果,但后续研究发现这并不是主要原因。现代观点认为 Normalization 的核心作用是控制激活值和梯度的尺度,使网络各层输入保持在稳定范围内,从而改善优化问题、平滑 Loss Surface、提高训练稳定性,并允许使用更大的学习率。
- Batch/Layer Normalization
BatchNorm 和 LayerNorm 的都是标准的归一化公式,区别在于 BatchNorm 的均值和方差计算都在 batch 维度,而 LayerNorm 在 hidden dim 上计算均值和方差。
- RMS Normalization
一方面 RMSNorm 发现去掉均值 $\mu$ 之后影响不大,减少这一步计算可以大幅度节省时间。另一方面是去掉均值可以减少信息损失,让训练更稳定。
为什么 LLM 不适合用 BatchNorm
- 每条数据的长度不相同,在 batch 维度计算均值和方差不稳定。
- batch 大小不够大,显存有限情况下
micro_batch可能为 1。 - 训练和推理的
batch_size可能不同。
PreNorm 和 PostNorm 区别
对 Post-Norm 求关于 $x_l$ 的梯度,由链式法则:
$$ \frac{\partial \mathcal{L}}{\partial x_l} = \frac{\partial \mathcal{L}}{\partial x_{l+1}} \cdot \frac{\partial \text{LN}(x_l + F(x_l))}{\partial x_l} $$梯度回传时候必须经过 LayerNorm 的缩放,对于深层的注意力网络,梯度容易消失或者爆炸。而对 Pre-Norm 求梯度则有:
$$ \frac{\partial \mathcal{L}}{\partial x_l} = \frac{\partial \mathcal{L}}{\partial x_L} \cdot \frac{\partial x_L}{\partial x_l} = \frac{\partial \mathcal{L}}{\partial x_L} \cdot \left(1 + \frac{\partial \sum F}{\partial x_l}\right) $$可以看到第一项是直连梯度,不经过任何 LayerNorm,梯度不会消失。
残差连接作用
梯度裁剪
LLM 里面的梯度裁剪一般是按照 L2 范数 进行裁剪,从而解决梯度爆炸的问题。假设梯度向量为 $g=(g_1,g_2,\dots,g_n)$,我们计算得到 L2 范数:$\|g\|=\sqrt{\sum_i g_i^2}$。假设 L2 范数超过了设定的阈值 $\|g\| > c$ 那么就对他进行裁剪。
$$ \begin{array}{c} g'= \begin{cases} g, & \|g\|\le c\\ g\frac{c}{\|g\|}, & \|g\|>c \end{cases} \end{array} $$loss.backward()
torch.nn.utils.clip_grad_norm_(
model.parameters(),
max_norm=1.0
)
optimizer.step()学习率调整
学习率调整的核心是前后期稳定,中期探索。
- 初期模型的参数随机,太大学习率可能导致梯度爆炸,所以一般用 warmup 让学习率逐步提高。
- 后期模型已经趋于稳定,所以不希望太大学习率导致先前学习的内容被覆盖,可以采用余弦退火算法。
- 一般 sft 预训练时候的学习率相对较大,设置在 2e-5 这样可以快速收敛。在后训练或者对某个方向微调时候,学习率一般比较小在 1e-6 或者更小,避免灾难性遗忘。
为什么 LLM 里很少用 dropout
- LLM 本身参数量就很大,基本不会出现过拟合的问题。
- dropout 会破坏数据的一致性,影响模型训练的稳定性。
激活函数的选择
- ReLU 是硬阈值,如果某个神经元长时间 $x<0$ 没有得到梯度,那么它基本废掉了。
- GeLU 是平滑的 ReLU,训练更稳定一些,但是计算了比 ReLU 大。
- SwiGLU 是门控版的 GeLU,它可以控制哪些特征保留,哪些特征抑制。SwiGLU 的非线性表达能力最强,同时可以提高表达能力。
LoRA#
$$ W_{update} = W + \Delta W $$LoRA 微调的思路是不在原矩阵 $W$ 上进行参数更新,而是用矩阵加法训练一个新的参数矩阵 $\Delta W$。LoRA 牛逼之处在于 $\Delta{W}$ 不需要和 $W$ 一样的参数量,他可以被拆为两个低秩矩阵的乘积:
$$ \Delta W = A \cdot B $$假设原矩阵 $W \in \mathbb{R}^{d \times d}$,低秩矩阵为 $A \in \mathbb{R}^{d \times r}$ 和 $B \in \mathbb{R}^{r \times d}$,那么训练参数量由 $d\times d$ 变成了 $2 \times d \times r$。
class LoRA(nn.Module):
def __init__(self, in_feature: int, out_feature: int, rank: int, alpha: int):
super(LoRA, self).__init__()
self.rank = rank
self.alpha = alpha
self.A = nn.Linear(in_feature, rank, bias=False)
self.B = nn.Linear(rank, out_feature, bias=False)
def forward(self, x):
return self.B(self.A(x)) * self.alpha / self.rankLoRA 加在哪些模块上
rank 和 alpha 怎么选
- $r$ 越大,增量权重的表示能力越强,可以拟合更多特定任务。
- $\alpha$ 是 LoRA scaling,控制增量权重在 forward 中的幅度。
- 一般小参数模型的 rank 在 8~16,然后控制缩放系数 $\alpha / r$ 在 1~4 的范围,视任务而定。
低秩矩阵矩阵初始化
一般是对 $A$ 矩阵应用 kaiming 初始化,对 $B$ 矩阵置为 0:
- 首先 $A$ 和 $B$ 矩阵需要至少有一个为零矩阵,这样 LoRA 一开始更新时 $\Delta W = B A$ 接近于零矩阵,就不会破坏预训练权重。
- 其次 $A$ 和 $B$ 矩阵不能全部为零矩阵,要不然计算出来它们的梯度都是 0,没法更新参数
- 而在前向传播中,低秩更新实际走的路径是:x → A → (scale) → B,也就是说反向传播时是从矩阵 $B$ 到矩阵 $A$。假如矩阵 $A$ 为零矩阵,那么矩阵 $B$ 的梯度为 0,训练就会先更新矩阵 $A$,$A$ 更新的数值尺度就会收到 $B$ 的初始化分布影响,容易放大早期更新的尺度。如果初始化矩阵 $B$ 为零矩阵,那么会先更新矩阵 $B$,把 $B$ 从零矩阵拉开,再更新 $A$,训练更稳定。
为什么不用 SVD 进行矩阵分解
LoRA在推理阶段会增加延迟吗
LoRA 的局限性
SFT#
how#
sft 就是让模型在收集的 trajectory 上做 off-policy 的 teacher-forcing 训练。我们把 labels[:, :t-1] 喂给模型让它进行 next token prediction,然后用预测的 token 和 labels[:, t] 计算交叉熵损失以此优化模型。
具体来说,我们把
labels[:, :-1]喂给模型,由于因果掩码的存在,模型预测的logits(形状为 [B, T-1]) 里面每个 $\text{token}_t$ 都是基于前 $t-1$ 个 token,也就实现了 teacher-forcing,然后我们计算logits和labels[:, 1:]的交叉熵损失就好了。
def sft_loss(logits, labels, ignore_index=-100):
assert logits.dim() == 3
assert labels.dim() == 2
loss = F.cross_entropy(
logits.view(-1, logits.size(-1)),
labels.view(),
ignore_index=ignore_index,
reduction="mean"
)
return losslogits 和 labels 应该是都已经移位过的,logits[:, :-1] 对应 labels[:, 1:]。写到这里突然想起来一个很重要的问题,sft 和 pretrain 的一个很重要的差异就是:pretrain 是在大量数据上学习整个文本,而 sft 是在学习该如何回复 prompt,所以训练时候需要:
- sft 需要给 prompt+response 应用 chat_template,而 pretrain 只需要把 text encode 就好了。
- sft 需要把非 assistant 的部分 mask 掉,避免模型学 prompt 本身的分布。
def only_assistant(input_ids, max_prompt_len, ignore_index=-100):
labels = input_ids[:, 1:]
labels[:, :max_prompt_len] = ignore_index
return labelstricks#
LLM训练-sft 这篇文章里面提到的一个很重要的技巧就是:对不同 task_type 和 special token 分别观察 channel_loss。
def compute_loss(logits, labels, task_ids):
per_token_loss = F.cross_entropy(
logits.view(-1, vocab_size),
labels.view(-1),
ignore_index=-100,
reduction='none'
).view(B, T) # (B, T)
# 按 task_type 分组
for task in unique_tasks:
task_mask = (task_ids == task).unsqueeze(1) # (B, 1) broadcast to (B, T)
valid_mask = (labels != -100) & task_mask # (B, T)
task_loss = per_token_loss[valid_mask].mean()
log(f"loss/{task}", task_loss)
# 训练用的总 loss(所有 task 合并)
total_loss = per_token_loss[labels != -100].mean()我们通过 batch 内数据的 task_type 生成掩码,就可以计算特定 task_type 数据的 loss。然后根据 loss 我们就知道不同 task_type 数据的拟合情况,如果过拟合了我们就需要删减这个 task_type 的数据,或者增加其他类型数据,如果欠拟合就需要扩大这类数据量。
special_token 同理,我们可以通过类似方法观察 special_token 的 loss 变化,正常来说在 sft 初期 special_token 的 loss 是比较高的,因为这些 token 在 pretrain 时候没有见过。
SPECIAL_TOKEN_IDS = {tokenizer.convert_tokens_to_ids(t)
for t in ["<think>", "</think>"]}
# special token 位置 mask
special_mask = torch.zeros_like(labels, dtype=torch.bool)
for tok_id in SPECIAL_TOKEN_IDS:
special_mask |= (input_ids == tok_id)
# 只在 response 范围内且是 special token
special_valid = special_mask & (labels != -100)
normal_valid = ~special_mask & (labels != -100)
special_token_loss = per_token_loss[special_valid].mean()
normal_loss = per_token_loss[normal_valid].mean()
log("loss/special_tokens", special_token_loss)
log("loss/normal_tokens", normal_loss)各种各样的 mask
- attention mask 负责的只是 PAD token,在计算注意力的时候不注意到 PAD token。
- causal mask 负责的是因果掩码,让每个 token 不注意到之后的 token,应用时候需要和 attention mask 取 & 然后 apply。
- loss mask 控制的是哪些 token 应该参与计算损失,比如 PAD token,sft 中 prompt 部分的 token,或者 agentic rollout 时候检索的部分。
left or right padding
logits 的最后一个 token,也就是 next_token_logits = outputs.logits[:, -1, :]。假如我们进行 right padding,那么模型 generate 的第一个 token 就是取 PAD token 对应的 logits 向量。问题在于:PAD token 的 embedding 是随机初始化的,模型从来没有学过"PAD 位置之后应该生成什么",所以这时候生成的 next token 是随机无意义的,就会导致接下去生成的 token 都出现问题。多轮对话微调
- 只训练最后一轮 assistant 会导致上下文利用变差:模型更依赖最后一轮信息。
- 对每一轮 assistant 都训练会导致回答更顺但更爱跑题,还会把旧回答当输出习惯。
解决方案有两种:
- 把多轮对话拆为多个样本进行训练,然后每个样本只训练最后一轮 assistant,并且要把多个样本分散在不同 batch。
- 对每一轮 assistant 都训练,但是加权。
同一个 batch 里面长度差异很大怎么办
sft loss 低但是对话效果差
packing or padding
在去年的一篇文章里我详细介绍了 packing 和 padding 的区别,简单来说 packing 就是把多条文本拼到一个 sequence 里,padding 就是一条数据一个 sequence,很明显 packing 可以减少 pad token 带来的无意义计算量,那该怎么选择的?
结论是不管是 pt 还是 sft 都可以用 packing 策略来对齐,需要注意的是:
- 不同数据放在同一个 sequence 里面,如果用传统的 causal_mask,seq_2 可以注意到 seq_1 的 token。需要用分块的 causal mask 和用 eos token 分割不同句子。
- 如果 packing 导致一个 sample 被截断,那么在下一个 block 的后半部分 sample 计算 attention 时候就看不到上文信息了。
sft loss 持续上升可能是什么原因
next-token prediction 本质是"背书"——即使数据是乱码,loss 也应持平或缓慢下降,持续上升说明存在以下问题:
- 训练代码 bug:梯度反传、优化器、学习率调度等逻辑错误
- 数据格式错误:labels shift 错位、chat template 有问题、eos 错位、prompt 没有 mask、packing mask 错误
- 学习率过大:loss 震荡或发散,需降低 lr 或检查 warmup
初始 loss 的范围、loss 过高或过低
- 小模型先验知识较少,初始的 loss 相对更大,在 1.8
2.5。大模型一般在 11.8。 - loss 过高说明数据太难没有相关知识,ntp 预测的很随机。loss 过低说明没有新学习的知识,和 pretrain 的分布接近。
- 对于不同领域数据 sft 的 loss 范围也不同,开放性问题相较于检索性问题 loss 就更高。
sft 到什么程度可以 rl
怎么评估 sft 效果
special token 的 loss 行为
<|im_start|> 等 special token 的 loss 应该先高后快速下降。这些 special token 是 sft 后 chat template 带来的,一开始模型并不熟悉所以预测这些 token 的概率非常低,但随着训练模型就知道了 <|im_start|>user 后面跟的是 prompt, <|im_start|>assistant 后面跟的是 response。
假如 special token 的 loss 下降很慢可能是:
- special token 设置有问题,tokenizer 没有把 special token 当成单个 token
- chat template 的问题
sft 训练策略
- 多任务学习:直接混合不同数据源,简单有效,但无法针对专业任务精细控制
- 顺序训练:依次在每个数据集上 SFT,灵活,但容易发生灾难性遗忘
- 混合序列训练:先在专业数据集上多任务学习,再在通用数据集上 SFT,兼顾专业与通用
- 双阶段混合微调 DMT:第一阶段专业数据 SFT,第二阶段混合少量专业数据防止遗忘,综合效果最佳
- 渐进式混合:初期 100% 专业 → 中期 50/50 → 后期 10% 专业+90% 通用,像课程学习逐步过渡
混合加训和 cpt 怎么选择
- 混合加训优点是保留原始能力+均衡的吸收新知识,缺点是混合了通用和专业数据集,训练数据量大。
- cpt 优点是训练速度快,显著偏向新数据,缺点是容易出现灾难性遗忘。
其实都选混合加训。
sft 过拟合怎么办
RLHF#
rl 和 sft 区别
credit assignment
rl 数据和 sft 数据需要有重合吗
Monte Carlo#
TD Error#
GAE#
KL Divergence#
f-散度的提出是为了解决 两个分布到底有多么不同 这样一个问题,由于分布是曲线没法相减,所以需要一种把"两条曲线的差异"压缩成一个数的方法,这就是散度。f-散度的核心思路:在每个点 x 处,看 P 和 Q 的密度之比 $r(x)=\frac{p(x)}{q(x)}$。如果 $r(x) = 1$ 处处成立,两个分布完全一样散度应该是 0。如果 $r$ 偏离 1,说明有差异应该被惩罚。用一个凸函数 $f(r)$ 来做这个惩罚,然后对全空间积分(以 Q 为权重)。选不同的 $f$,就得到不同的散度:
$$ D_f(P \| Q) = \int q(x) f\left( \frac{p(x)}{q(x)} \right) dx $$我们在大模型训练中常见的 KL 散度就是 $f(r)=r\log r$ 形状的 f-散度:
$$ D_{KL}(P \| Q) = \int p(x) \log \frac{p(x)}{q(x)} dx $$KL 散度理论上是用概率分布的积分定义的,但现实中我们只有有限的样本数据,没法精确计算,所以需要用样本来近似估算——这个近似方法就叫"估计器"。
一个优秀的估计器通常需要考量两个核心指标:
- 偏置:估计器的数学期望是否等于真实值?如果相等,就是无偏估计;如果不等,就是有偏估计。
- 方差:不同批次的样本算出来的估计值,上下波动大不大?方差太大会导致强化学习的梯度震荡,训练崩盘。
三种估计器#
在推导之前证明一个大前提:对于任何从 $q(x)$ 中采样的比率 $r = \frac{p(x)}{q(x)}$,它的数学期望永远为 1。
首先,我们要明确数学期望的定义。对于任何从分布 $q(x)$ 中采样出来的随机变量 $f(x)$,它的数学期望就是把所有可能的 $x$ 对应的函数值 $f(x)$,乘以它在 $q(x)$ 中的概率密度,然后全空间积分:
$$ \mathbb{E}_{x \sim q}[f(x)] = \int q(x) \cdot f(x) \, dx $$现在,我们把 $f(x) = r = \frac{p(x)}{q(x)}$ 代入这个定义公式中:
$$ \mathbb{E}_{x \sim q}\left[ \frac{p(x)}{q(x)} \right] = \int q(x) \cdot \frac{p(x)}{q(x)} \, dx $$注意到积分符号内部的 $q(x)$ 了吗?分子和分母上各有一个 $q(x)$,它们可以直接约掉(消去):
$$ = \int p(x) \, dx $$根据概率论的基本公理,任何一个合法的概率密度函数,它在全空间的积分(总概率)必须严格等于 1。因为 $p(x)$ 是一个合法的概率分布,所以:
$$ \int p(x) \, dx = 1 $$k1 估计器是最直接的推导,既然我们要计算 $\mathbb{E}_{x \sim q} [-\log r]$,那么直接脱掉期望符号,用单样本的函数值作为估计:
$$ k_1 = \log \frac{q(x)}{p(x)} = -\log \frac{p(x)}{q(x)} = -\log r $$虽然 $\mathbb{E}[k_1]$ 严格等于真实 KL(绝对无偏),但是它的方差极大。因为当某个样本下 $q(x) < p(x)$ 时,$k_1$ 会变成负数。尽管理论上整体 KL 散度永远 $\ge 0$,但 $k_1$ 单个样本却频繁在正负之间剧烈摆动,这在代码里做强化学习策略裁剪(Clip)或加惩罚项时,会带来巨大的不稳定因素。
在强化学习中,由于我们往往会限制新旧策略不能离得太远,因此可以假设 $p \approx q$,这意味着比率 $r \approx 1$。 我们可以利用泰勒展开,在 $r = 1$ 处对函数进行逼近:
$$ -\log r \approx f(1) + f'(1)(r-1) + \frac{1}{2}f''(1)(r-1)^2 = -(r-1) + \frac{1}{2}(r-1)^2 $$接着,我们在期望意义下看待这个公式。因为上面提到了 $\mathbb{E}_{x \sim q}[r - 1] = 0$,所以线性项 $-(r-1)$ 在求期望时直接归零了。因此:
$$ \mathbb{E}[-\log r] \approx \mathbb{E}\left[ \frac{1}{2}(r-1)^2 \right] $$同时,我们知道当 $r \approx 1$ 时,由一阶展开可知 $\log r \approx r - 1$。我们将这个关系代入上式,用 $(\log r)^2$ 替换掉 $(r-1)^2$,就得到了 $k_2$:
$$ k_2 = \frac{1}{2}(\log r)^2 = \frac{1}{2}\left(\log \frac{p(x)}{q(x)}\right)^2 $$因为带有平方,$k_2 \ge 0$ 恒成立,完美避开了 $k_1$ 产生负数导致的剧烈摆动,方差极小。但它是截断泰勒展开的产物,所以是有偏估计。
k3 估计器的思路是设计一个估计器,既像 $k_1$ 一样严格无偏,又像 $k_2$ 一样恒大于 0 且方差极小? 为此,他引入了统计学中大名鼎鼎的控制变量技术:在无偏估计器 $k_1$ 上,加上一个期望值严格为 0 的项,利用它们之间的负相关性来抵消波动。前面我们已经证明了 $\mathbb{E}_{x \sim q}[r - 1] = 0$。那么我们直接把这一项无条件加到 $k_1$ 里面去:
$$ k_3 = k_1 + (r - 1) = -\log r + r - 1 = r - 1 - \log r $$k3 的方差问题根源在于 $r-1$ 这一项:当 $r=\pi_{\theta}/\pi_{ref}$ 很大时(即训练策略对某个 token 分配远高于参考模型的概率),$r一1$ 按 $r 线性增长,而 $k1=-\log r$ 只是对数增长。结果就是:KL 大时,k3 的方差比 k1 高出几个数量级。
前向/反向 KL 散度#
KL 散度是不对称,$D_{K L} \left(\right. P \parallel Q \left.\right) \neq D_{K L} \left(\right. Q \parallel P \left.\right)$,所以"哪个在前哪个在后"非常重要。$D_{KL}(P \| Q) = \int p(x) \log \frac{p(x)}{q(x)} dx$ 从 KL 散度公式可以观察到,传统 KL 散度也就是 Forward KL 是从分布 $p(x)$ 里面采样,而 $D_{KL}(Q \| P)$ 称作 Reverse KL 反向 KL 散度,他从分布 $q(x)$ 采样。
- 从分布 $p(x)$ 中采样意味着:我们需要能获得 $p(x)$ 的数据。所以我们在做 OPD 时候必须反向 KL 散度,OPD 规定了 trajectory 必须从学生模型 $q(x)$ 采样。
- FKL 偏向于把概率质量"摊开",覆盖两个峰之间的低概率谷地,生成的内容是所有 teacher 模式的模糊平均。
- RKL 中 student 只需要找到 teacher 的某一个高概率模式,集中概率质量进去。
惩罚系数#
DAPO、VAPO、MiniMax CISPO 主张完全去掉 KL 散度项,原因是对于这些关注 reasoning RL 的工作:
- 奖励目标本身就是远离 SFT 分布,模型要学会"反思"、“aha moment”,分布必然大幅漂移,KL会阻碍学习。
- RL有可验证 reward(比如 rule-based、math/code verifier),Reward Hacking 风险小,没有必要用 KL 散度。
- 资源上节约 reference model 显存和 forward 计算,训练效率提升明显。
DeepSeek GRPO/Kimi/GLM 保留 KL 散度项,原因是对于基座模型来说,统一 RL stage 里混了 alignment 和 general task 多种任务,很多都是经典 RLHF 里 reward hacking 的高触发场景,KL 能必要的防护。但是这些工作都在 KL 上进行了精细化:
- DeepSeek V3.2为例子,进行了以下几个调整:
- 不同领域适用不同 KL 系数(per-domain):数学场景(Reasoning主导)系数接近 0,通用对齐保留系数。
- 修正KL估计器。
- Kimi K1.5/K2 也使用了 KL 强度动态调整。
Importance Sampling#
重要性采样 IS 的核心思想是用一个分布 $q \left(\right. x \left.\right)$ 采样的数据去估计另一个分布 $p \left(\right. x \left.\right)$ 下的期望,只需要乘以一个修正比率:
$$ \begin{align*} \mathbb{E}_{x \sim p}[f(x)] = \int f(x) p(x) dx = \int f(x) \frac{p(x)}{q(x)} q(x) dx = \mathbb{E}_{x \sim q}\left[ f(x) \cdot \frac{p(x)}{q(x)} \right] \end{align*} $$- $p \left(\right. x \left.\right)$ 是真正想估计期望的分布(目标策略)
- $q \left(\right. x \left.\right)$ 是实际用来采样的分布(行为策略)
- $\frac{p \left(\right. r \left.\right)}{q \left(\right. x \left.\right)}$ 就是重要性比率
PPO#
PPO 公式是怎么得到的
我们在强化学习里的终极目标,是让动作带来的期望回报最高,也就是说我们希望最大化:
$$ J(\theta) = \mathbb{E}_{\tau\sim\pi_\theta}\left[\sum_t \gamma^t r_t\right] $$策略梯度定理告诉我们,这个目标函数的梯度可以写成:
$$ \nabla_\theta J(\theta) = \mathbb{E}_{(s,a)\sim\pi_\theta}\left[\nabla_\theta\log\pi_\theta(a|s)\cdot Q^{\pi_\theta}(s,a)\right] $$为了减小估计的方差,在实践中我们通常用优势函数 $A^{\pi_\theta}(s,a)$ 代替状态动作价值函数 $Q(s,a)$,这不会改变梯度的期望值。于是梯度写为
$$ \nabla_\theta J(\theta) = \mathbb{E}_{s \sim d^\pi, a \sim \pi_\theta} \left[ \nabla_\theta \log \pi_\theta(a|s) \cdot A^{\pi_\theta}(s,a) \right] $$注意上式的期望是在当前策略 $\pi_\theta$ 下取的,advantage 也是 $\pi_\theta$ 下的 $A^{\pi_\theta}$。但我们实际拥有的 rollout 数据是用 $\pi_{old}$ 采样、用 $\pi_{old}$ 算出来的 $A^{\pi_{old}}$,所以需要做重要性采样把分布换到 $\pi_{old}$:
$$ \nabla_\theta J(\theta) = \mathbb{E}_{a\sim\pi_{old}}\left[\frac{\pi_\theta(a|s)}{\pi_{old}(a|s)}\nabla_\theta \log\pi_\theta(a|s)\cdot A(s,a)\right] $$这里是一个近似而非严格等式——只有在 $\pi_\theta$ 与 $\pi_{old}$ 足够接近时,用 $\pi_{old}$ 下采样的轨迹和 $A^{\pi_{old}}$ 去估计 $\pi_\theta$ 下的真实梯度才是可靠的。这一点很重要,它正是 PPO 后续引入 clip 机制的根本原因:clip 通过限制 $\pi_\theta/\pi_{old}$ 的比值范围,把更新约束在这个近似成立的"信任区域"内,防止单次更新让 $\pi_\theta$ 跑得离 $\pi_{old}$ 太远导致上式近似失效。
那么我们找到一个函数 $L(\theta)$,只要使得 $\nabla_\theta L(\theta)$ 恰好等于上面这个式子,对 $L(\theta)$ 进行梯度上升(或者说对 $-L(\theta)$ 进行梯度下降)就等价于对 $J(\theta)$ 进行梯度上升,让 $J(\theta)$ 变大,也就是我们强化学习的优化目标:
$$ L(\theta) = \mathbb{E}_{a\sim\pi_{old}}\left[\frac{\pi_\theta(a|s)}{\pi_{old}(a|s)} A(s,a)\right] $$这就是 PPO 的 surrogate loss,再加上 clip 操作来约束信任区间,就构成了完整的 PPO 目标函数。
公式里面 min 和 clip 的组合
PPO 和 DPO 对 reward 的要求有什么不同
- PPO 的 reward 要能对每个 token 或每个 response 给出相对精确的分值,用于计算 advantage。对 reward 的绝对值和方差都比较敏感。
- DPO 的 reward 只需要能区分好坏(排序能力),是 point-wise 打分后做比较,不需要特别精确的绝对值,容忍度更高。
critic model 重要吗
- 在 PPO 训练中,critic model 用来估计 baseline,计算advantage = reward - value,减少policy gradient的方差,训练更稳定。没有好的critic,PPO的训练信号噪声很大。
- GRPO 等方法用组内 reward 均值做 baseline,避免了单独训练critic的成本,同时在reasoning任务上效果接近甚至更好。
PPO 训练的指标
- KL 散度:太大可能存在 reward hacking 风险,太小可能没有充分更新
- policy loss/critic loss
- entropy:出现 collapse 多样性消失,reward hacking
Reward Model 训练的指标
- auc:chosen > rejected 的 排序准确率
- chosen 和 rejected 的 reward margin:margin 大说明很自信
- ood:RM 在没见过的分布上的表现
- reward distribution:reward 方差是不是太大或者太小,太大容易训练不稳定,太小缺乏梯度信号。
Reward model 训练时候碰到的问题
- 标注数据不足与偏差:如果偏好数据主要来自单一群体或话题,模型在其他领域的表现会较差,甚至带有该群体的主观偏见。奖励模型可能过度偏好训练集中常见的回答风格(如过度详细或倾向某种语气)。例如医疗助手训练后,reward model 对长回复都评为高分,导致 PPO 之后容易长篇大论。解决方案是:扩充多样化数据、在训练时加入字数正则项。
- 过拟合泛化能力差:过拟合往往由数据匮乏和模型容量过高共同导致。大模型微调出的奖励模型有能力记忆训练集中偏好对比的细节,当标注数据有限或包含噪声时,模型可能学习到伪相关特征(如特定词频、长度等)作为判断依据,削弱了真正偏好信号的泛化。解决方案是:正则化、根据验证集曲线早停、缩小模型参数。
- reward hacking:解决方案是 设计对抗样本加入训练、KL 散度限制模型差异,设计针对性的正则项例如长度正则。
- 正负样本 margin 小:缺乏 hard negative sample。
- 分布偏移:RLHF 模型在奖励模型的打分中表现极佳,但人工质检觉得输出空洞或跑偏,未真正提升体验。解决方案是:加大 KL 损失权重、定期迭代映入新样本修正偏差、拆分多个目标奖励。例如客服模型很礼貌但是专业性很差,把回复正确率和礼物拆为两个 reward model 加权组合。
PPO 是 off-policy 还是 on-policy,有什么区别
ppo_epochs)或者受限于显存需要梯度累计,就会导致变成 off-policy,数据是从前几个版本的模型上 rollout 的,这就需要 PPO 公式里面的 重要性采样 来修正。为什么用 actor-critic 而不是纯 critic
DPO#
DPO 的 chosen 和 reject 的 loss 同时下降是因为什么
DPO 训练为什么会导致输出变长
GRPO#
不同 RL 场景怎么设计 reward
GRPO 的优势为什么要减 baseline,一定要除 std 吗
GRPO 为什么加上 KL 散度,KL 散度怎么计算,为什么 DAPO、GSPO 又去掉了KL散度?
熵崩塌#
分布式训练#
Data Parallel#
Tensor Parallel#
Deepspeed Zero#
训练#
参数量计算#
显存计算#
训练出现 NaN 的原因#
lr,除0,log,clamp
