在我们进行深度学习网络训练的过程中,经常会遇到损失不降、训练完全不收敛的情况,并且在训练期间, acc 接近随机、学习率/优化器怎么调都无效。为了更系统的剖析其中的原因,本章节将从实际例子出发,记录debug的过程以及最终的可能问题定位。
loss ≈ 0.693±0.001
(二分类随机水平),acc ≈ 50%。grad_norm
发现经常是 0 或 非常小;很多参数 p.grad is None
。为了“便于日志和可视化”,我在前向里对特征做了
.detach()
,同时在正则里用了.data
原地裁剪权重,顺手还做了个原地归一化。
import torch, torch.nn as nn, torch.nn.functional as F
class 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 + reg
scaler.scale(loss).backward() # ⬅️ 梯度经常是 None 或极小
scaler.step(opt); scaler.update()
f.detach()
之后对 f
的原地写入(f /= …
)可能触发 version counter 冲突或让 Autograd 选择不追踪某些路径;weight.data
的原地操作绕过 autograd,破坏优化器状态(如 Adam 的动量/二阶矩),出现“学一下又被硬改回去”的震荡;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 t
with torch.cuda.amp.autocast(True):
f = net.feat(x)
tap_grad(f, "feat") # ✅ 非叶子张量需要 retain_grad 才能看到 .grad
logits = net.fc(f)
loss = F.binary_cross_entropy_with_logits(logits, y)
loss.backward()
# 观察是否有打印;若无,则在更前面打点,直到发现哪一段“消失”
现象:feat
的梯度没有打印,说明链路到这里已断。
索 .detach
() / .da
ta / 原地操作.detach(
、.data
、inplace=True
、+=
/-=
/*=
//=
。out = f / norm
之类非原地的重写),看看是否恢复。.data
改成正常的损失或优化器钩子。torch.autograd.set_detect_anomaly(True)
one of the variables needed for gradient computation has been modified by an inplace operation
,快速定位。的 .detach
(),日志/可视化用副本# ✅ 用 clone().detach() 生成只用于日志的副本,不参与计算
f = self.feat(x) # 参与反传
f_for_log = f.detach().clone() # 仅用于可视化,别再写回去
# …用 f_for_log 画图/记录…
# ❌ f /= norm
# ✅
norm = (f.norm(dim=1, keepdim=True) + 1e-6)
f = f / norm
用 .da
ta,改为显式 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
/ 原地操作这三件事盯住,防止计算图被切断。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。