在大模型微调的实际工程里,Loss指标是衡量模型是否正在学习的核心信号。当训练启动后观察面板上的损失值始终停留在初始区间,既没有震荡也没有缓降,就意味着反向传播链路中某个环节已经失效。这种问题通常不会自己恢复,必须沿着数据、模型、优化器三条主线逐层拆解,才能定位到阻断梯度更新的真实原因。

数据侧:标签错乱与采样偏差
数据管道是大模型微调中最容易被忽视却最常出错的环节。很多自研训练框架在构造 dataset 时,会把文本编码后的 input_ids 同时复制给 labels,或者将 padding 部分的 token 也计入损失计算却没有正确设置 ignore_index。这样一来,模型被迫去预测无意义的填充符,有效语料的监督信号被稀释,整体 Loss 就会表现为平坦不降。正确的做法是在 collate_fn 中明确区分输入与标签,并对非目标位置打上负索引屏蔽。
另一个隐蔽问题是类别或长度分布极端倾斜。例如用指令微调数据训练时,百分之九十的样本都是极短问答,剩下少量长文本被 batch 采样均匀混合,导致梯度方向被短样本主导。此时即便模型有能力拟合长指令,平均 Loss 也难以下探。可以通过按长度分桶采样,或单独为困难样本提升采样权重来缓解。下面这段代码展示了如何过滤无效标签并屏蔽 padding:
def collate_fn(batch):
input_ids = []
labels = []
for item in batch:
ids = item['input_ids']
# 将问题部分设为-100,只计算回答部分的损失
lab = [-100] * len(ids)
start = item['answer_start']
lab[start:] = ids[start:]
input_ids.append(ids)
labels.append(lab)
# 统一padding,padding位置标签保持-100
max_len = max(len(x) for x in input_ids)
for i in range(len(input_ids)):
pad = max_len - len(input_ids[i])
input_ids[i] = input_ids[i] + [0] * pad
labels[i] = labels[i] + [-100] * pad
return {'input_ids': input_ids, 'labels': labels}
除了代码层面的处理,还要肉眼抽检几个 batch 的原始数据。曾经有团队发现标注平台导出的 JSON 里,正样本字段名从 answer 被误改为 ans,加载脚本取不到值后用空串填充,模型全程在拟合空标签。这类错误不会报异常,但 Loss 曲线会忠诚地停在交叉熵初始值附近。
模型侧:冻结策略与参数精度
微调时为了省显存,常用冻结底座、只训 adapter 或分类头的方案。但若配置文件里 layer_freeze 范围写错,把全部 transformer 块都锁死,仅剩最后一层线性层可学,当新领域数据和预训练分布差距较大时,单层容量根本无法承载映射,Loss 就会卡住。建议先放开前三层或全部底座用较小学习率试跑,确认 Loss 有下降趋势后再逐步收回冻结比例。
混合精度训练中的数值溢出也会让梯度变 NaN 或恒为零。某些旧版深度学习框架在 fp16 下对大模型 layernorm 计算不稳定,反向时梯度直接消失。可改用 bf16,或在优化器上挂梯度裁剪。下面示例演示了用 PyTorch 开启 bf16 并加上 clip_grad_norm:
import torch
from torch.cuda.amp import autocast
scaler = torch.cuda.amp.GradScaler(enabled=False)
for step, batch in enumerate(loader):
with autocast(dtype=torch.bfloat16):
out = model(**batch)
loss = out.loss
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
optimizer.zero_grad()
此外,加载预训练权重时若漏掉 strict=False 导致部分 key 不匹配,模型实际在用随机初始化的顶层跑训练,也会造成 Loss 不降。每次载入 checkpoint 后应打印 missing_keys 与 unexpected_keys,确认底座参数确实就位。参数精度方面,若用 LoRA 微调却把基础模型设成 int8 且未配合合适反量化,梯度同样传不进去。
优化器侧:学习率与调度器误区
学习率是大模型微调里最敏感的旋钮。很多人直接套用预训练时的 1e-3,结果在微调小数据集上参数一步跨过最优点,Loss 剧烈震荡后停滞。通常指令微调建议 1e-5 到 5e-5,LoRA 可稍大。若用了带 warmup 的调度器但 warmup_steps 设得比总步数还长,模型前期一直在极小学习率下缓慢移动,短时间看就像没训一样。
优化器状态异常也值得排查。例如从断点续训时只存了模型没存 optimizer.state_dict,恢复后动量项归零,初期更新节奏被打乱。还有人把 weight_decay 设得过大,把可训参数都正则到接近零,等效于变相冻结。可以参考下表对照常见配置与现象:
| 配置项 | 错误取值 | Loss表现 |
|---|---|---|
| learning_rate | 1e-3 | 震荡后平坦 |
| warmup_ratio | 0.5且总步数少 | 前中期无下降 |
| weight_decay | 0.3 | 缓慢锁死 |
| freeze_layers | 全部底座 | 恒高于预训练损失 |
最后要确认损失函数本身。有些多任务脚本把不同任务损失直接相加,而某个子任务数据量为零,除以零后得到常数被加进总损失,拉高了均值。应在计算前判断任务 batch 非空,或用掩码损失。把上述数据、模型、优化器三个角度的检查项做成启动前 checklist,基本可以覆盖九成以上的 Loss 不下降故障。