【Debug日志 | 模型loss不降】
模型“就是不学”:loss 不降、梯度全是 None
在我们进行深度学习网络训练的过程中,经常会遇到损失不降、训练完全不收敛的情况,并且在训练期间, acc 接近随机、学习率/优化器怎么调都无效。为了更系统的剖析其中的原因,本章节将从实际例子出发,记录debug的过程以及最终的可能问题定位。
❓ Bug 现象
- 训练 3–5 个 epoch:
loss ≈ 0.693±0.001
(二分类随机水平),acc ≈ 50%。 - 调大学习率、换 Adam/SGD、关 AMP、换 batch size,均无改善。
- 打印
grad_norm
发现经常是 0 或 非常小;很多参数p.grad is None
。
📽️ 场景复现
为了“便于日志和可视化”,我在前向里对特征做了
.detach()
,同时在正则里用了.data
原地裁剪权重,顺手还做了个原地归一化。
import torch, torch.nn as nn, torch.nn.functional as Fclass Net(nn.Module):def __init__(self): super().__init__()self.feat = nn.Sequential(nn.Conv2d(3, 16, 3, padding=1), nn.ReLU(inplace=True),nn.AdaptiveAvgPool2d(1), nn.Flatten(),)self.fc = nn.Linear(16, 1)def forward(self, x):f = self.feat(x) # [B, 16]f_log = f.detach() # ❌ 为了可视化,提前 detach# …日志里用到了 f_log …# 原地归一化(in-place)f /= (f.norm(dim=1, keepdim=True) + 1e-6) # ❌ in-place 可能破坏版本计数return self.fc(f).squeeze(-1)net = Net().cuda()
opt = torch.optim.AdamW(net.parameters(), lr=1e-3)
scaler = torch.cuda.amp.GradScaler()def l2_sp_regularizer(m: nn.Linear, tau=1e-4):# 为了稀疏化,做了个软阈值with torch.no_grad():m.weight.data = torch.clamp(m.weight.data, -1.0, 1.0) # ❌ .data + 原地return tau * (m.weight.abs().mean()) # 看似没问题for step in range(200):x = torch.randn(64, 3, 224, 224, device="cuda")y = (torch.rand(64, device="cuda") > 0.5).float()opt.zero_grad(set_to_none=True)with torch.cuda.amp.autocast(True):logits = net(x)base = F.binary_cross_entropy_with_logits(logits, y)reg = l2_sp_regularizer(net.fc, 1e-4)loss = base + regscaler.scale(loss).backward() # ⬅️ 梯度经常是 None 或极小scaler.step(opt); scaler.update()
可能存在的问题
- 为了记录前向传播的过程,同时保证模型训练不变影响,我们常常通过detech来使特征脱离梯度计算图的计算,但
f.detach()
之后对f
的原地写入(f /= …
)可能触发 version counter 冲突或让 Autograd 选择不追踪某些路径; - 正则里对
weight.data
的原地操作绕过 autograd,破坏优化器状态(如 Adam 的动量/二阶矩),出现“学一下又被硬改回去”的震荡;
Debug过程
1️⃣ Step 1:确认梯度有没有走到最后
- 在关键层注册backward hook或对非叶子张量调用
retain_grad()
,观察梯度。
def tap_grad(t, name):t.retain_grad()def _hook(grad): print(f"[{name}] grad_norm={grad.norm().item():.4e}")t.register_hook(_hook)return twith torch.cuda.amp.autocast(True):f = net.feat(x)tap_grad(f, "feat") # ✅ 非叶子张量需要 retain_grad 才能看到 .gradlogits = net.fc(f)loss = F.binary_cross_entropy_with_logits(logits, y)loss.backward()
# 观察是否有打印;若无,则在更前面打点,直到发现哪一段“消失”
现象:feat
的梯度没有打印,说明链路到这里已断。
2️⃣ Step 2:搜索 .detach()
/ .data
/ 原地操作
- 全局搜关键字:
.detach(
、.data
、inplace=True
、+=
/-=
/*=
//=
。 - 暂时改掉所有原地写法(使用
out = f / norm
之类非原地的重写),看看是否恢复。 - 把正则里对权重的
.data
改成正常的损失或优化器钩子。
3️⃣ Step 3:开启异常检测确认链路
torch.autograd.set_detect_anomaly(True)
- 在有些 in-place 修改场景下,能报出
one of the variables needed for gradient computation has been modified by an inplace operation
,快速定位。
解决方案
1️⃣ 移除错误的 .detach()
,日志/可视化用副本
# ✅ 用 clone().detach() 生成只用于日志的副本,不参与计算
f = self.feat(x) # 参与反传
f_for_log = f.detach().clone() # 仅用于可视化,别再写回去
# …用 f_for_log 画图/记录…
2️⃣ 避免原地归一化
# ❌ f /= norm
# ✅
norm = (f.norm(dim=1, keepdim=True) + 1e-6)
f = f / norm
3️⃣ 正则/约束不要用 .data
,改为显式 loss 或 optimizer hook
# ✅ 显式正则,进入计算图,由优化器“看得见”
def l2_sp_regularizer(m, tau=1e-4):return tau * m.weight.abs().mean()# ✅ 如果要“硬裁剪”,用 optimizer hook 或 step 之后统一 clamp
@torch.no_grad()
def clamp_weights_(m: nn.Module, lo=-1.0, hi=1.0):for p in m.parameters():p.clamp_(lo, hi)# 训练循环中:
scaler.scale(loss).backward()
scaler.step(opt); scaler.update()
clamp_weights_(net.fc) # ✅ 在优化器更新后、no_grad 下原地裁剪
总结
“模型不学”的绝大多数原因,不在“学习率宇宙之谜”,而在计算图被不经意地剪断了。把 .detach()
/ .data
/ 原地操作这三件事盯住,防止计算图被切断。