从 UNet 到 UCTransNet:一次分割项目中 BCE Loss 失效的踩坑记录
1. 问题背景
最近在做医学图像分割任务,目标是对 CT 图像进行前景/背景的二值分割。
在基线实验里,我使用 UNet + BCE 损失函数,训练和验证都能正常收敛,Dice 系数逐步提升,结果符合预期。
但当我尝试使用一些更复杂的网络(比如 UCTransNet、TransFuse)时,情况完全变了:
训练 loss 一直稳定在 0.693 左右(BCE 初始值);
Dice、IoU 等指标几乎始终为 0;
输出的预测 mask 基本全是背景。
2. 调试过程
Step 1: 检查输入输出形状
在训练循环里打印数据和预测的形状:
print("x:", x.shape, "y:", y.shape, "pred:", outputs.shape)
确认输入图像和标签形状正常,模型输出 (B,1,H,W)
也没问题。
Step 2: 打印输出范围
在 outputs = model(x)
后面加:
print("outputs range:", outputs.min().item(), outputs.max().item())
结果发现:
outputs range: 0.0 ~ 1e-6
也就是说,模型的输出几乎都被限制在 0 附近且非负。
3. 原因分析
这里的核心问题是 模型最后一层处理方式不当:
在 UNet 中,最后一层通常是一个 卷积层(Conv2d)直接输出 logits。
在 UCTransNet / TransFuse 的部分实现里,最后一层被加上了 ReLU 或 Sigmoid。
为什么这会导致训练失败?
BCEWithLogitsLoss 的需求
它需要输入的是 原始 logits(可正可负)。
内部会自动做 Sigmoid,把 logits 转成概率。
被 ReLU 截断的情况
如果最后用了 ReLU,logits 就只能 ≥0。
那么 Sigmoid 的最小值就是 0.5,模型没法表达「这是背景」。
损失长期停在 0.693,指标始终为 0。
重复 Sigmoid 的情况
如果最后手动加了 Sigmoid,然后再喂给
BCEWithLogitsLoss
,就相当于 双重 Sigmoid,数值会被压缩,训练同样不收敛。
4. 解决方案
✅ 正确做法
确认模型最后一层是 线性卷积层 (Conv2d),不要加 ReLU / Sigmoid。
使用 BCEWithLogitsLoss 时,直接传 logits,不要自己再做 Sigmoid。
计算指标(Dice、IoU)时,再手动对 logits 做一次 Sigmoid + 阈值化。