news 2026/6/4 19:43:37

012、卷积神经网络核心回顾:Conv、BN、SiLU、Pooling 的前向传播公式与梯度反向传播

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
012、卷积神经网络核心回顾:Conv、BN、SiLU、Pooling 的前向传播公式与梯度反向传播

012、卷积神经网络核心回顾:Conv、BN、SiLU、Pooling 的前向传播公式与梯度反向传播

上周帮组里新人调一个YOLOv8的魔改模型,训练到第50个epoch突然loss炸了,NaN满天飞。我盯着log看了半天,发现是BN层的running_var在某个分支上变成了负数——这玩意儿理论上不可能,除非有人手贱改了eps或者把梯度传错了。最后定位到问题:一个自定义的C2f模块里,有人把SiLU换成了ReLU,但没改BN的初始化参数,导致某些通道的激活值全为0,BN的方差估计直接崩了。

这种坑我踩过不下十次。今天干脆把Conv、BN、SiLU、Pooling这四个YOLO里最基础的算子,从公式到反向传播,掰开揉碎讲清楚。别指望教科书式的“首先、其次”,咱们直接上干货,代码里该注意的地方我会用口语标出来。

1. Conv2d:别把im2col和矩阵乘法搞混

前向传播公式很简单:
output = input * weight + bias
但实际实现里,卷积是通过im2col把输入展开成矩阵,然后做一次大矩阵乘法。这一步是显存杀手,也是梯度传播的关键。

前向传播
假设输入形状为(N, C_in, H, W),卷积核(C_out, C_in, K, K),步长s,填充p。
im2col把每个滑动窗口拉成一行,得到一个(N * H_out * W_out, C_in * K * K)的矩阵。
然后和权重矩阵(C_out, C_in * K * K)做乘法,再加bias。
这里有个细节:bias的梯度是直接对output求和,但权重梯度需要把im2col后的输入矩阵转置再乘output梯度。

反向传播
梯度公式:

  • d_weight = im2col(input).T @ d_output
  • d_bias = sum(d_output, dim=(0,2,3))
  • d_input = col2im(d_output @ weight.T)

注意col2im这一步,它要把矩阵还原回图像形状,而且同一个像素位置可能被多个卷积窗口覆盖,所以需要累加。PyTorch里用F.conv2d_backward,但如果你手写,记得用torch.nn.functional.unfoldfold

踩坑经验

  • 别用torch.nn.Conv2dgroups参数时忘了检查in_channels % groups == 0,否则报错很隐蔽。
  • 自定义卷积时,如果输入是float16,im2col后的矩阵乘法容易溢出,建议先转float32再算。
  • 梯度检查时,用torch.autograd.gradcheck,但注意它默认用双精度,你模型是单精度的话会报错,记得设eps=1e-3

2. BatchNorm:running_mean和running_var的坑

BN的前向分训练和推理两个模式,很多人只记得公式,忘了running统计量的更新逻辑。

训练模式
对输入x,形状(N, C, H, W),先算每个通道的均值和方差:
mu = mean(x, dim=(0,2,3))
var = var(x, dim=(0,2,3))
然后归一化:
x_hat = (x - mu) / sqrt(var + eps)
最后缩放平移:
y = gamma * x_hat + beta

推理模式
用训练时累积的running_meanrunning_var
y = gamma * (x - running_mean) / sqrt(running_var + eps) + beta

反向传播
梯度公式比较复杂,因为mu和var本身也是x的函数。
d_y是上层传来的梯度,则:

  • d_gamma = sum(d_y * x_hat, dim=(0,2,3))
  • d_beta = sum(d_y, dim=(0,2,3))
  • d_x_hat = d_y * gamma
  • d_var = sum(d_x_hat * (x - mu) * (-0.5) * (var + eps)^(-1.5), dim=(0,2,3))
  • d_mu = sum(d_x_hat * (-1/sqrt(var+eps)), dim=(0,2,3)) + d_var * mean(-2*(x-mu), dim=(0,2,3))
  • d_x = d_x_hat / sqrt(var+eps) + d_var * 2*(x-mu)/N + d_mu/N

这里N是N*H*W。别自己手写这个,PyTorch的torch.nn.BatchNorm2d已经优化好了,但如果你要魔改(比如YOLOv8里有人把BN换成GroupNorm),一定要理解这个梯度流。

踩坑经验

  • running_meanrunning_var的更新是momentum控制的,默认0.1。如果训练时loss震荡,可以试试调大momentum到0.5,让统计量更快适应。
  • 推理时如果running_var出现负数,检查是不是eps设得太小(比如1e-5以下),或者某个通道的激活值全相同导致方差为0。
  • 多卡训练时,BN的统计量是每个卡独立算的,SyncBN需要额外实现。YOLOv8默认用SyncBN,但如果你自己写分布式训练,别忘了同步。

3. SiLU:比ReLU平滑,但梯度计算容易忘

SiLU也叫Swish,公式是f(x) = x * sigmoid(x)。YOLOv8的backbone和neck里大量用它,因为比ReLU更平滑,梯度流更稳定。

前向传播
直接算:y = x * torch.sigmoid(x)
注意sigmoid的数值范围是(0,1),所以SiLU的输出范围是负无穷到正无穷,但梯度不会像ReLU那样在负数区域为0。

反向传播
s = sigmoid(x),则:
dy/dx = s + x * s * (1 - s)
化简一下:dy/dx = s * (1 + x * (1 - s))
或者写成:dy/dx = s + x * s - x * s^2

实际代码里,你可以在前向时缓存s,反向时直接用。PyTorch的torch.nn.SiLU已经实现了,但如果你要自定义梯度(比如做量化训练),记住这个公式。

踩坑经验

  • SiLU在x很大时(比如>10),sigmoid接近1,梯度接近1 + x * 0 = 1,不会饱和。但x很负时(比如<-10),sigmoid接近0,梯度接近0,所以梯度消失比ReLU轻,但依然存在。
  • 混合精度训练时,SiLU的sigmoid计算在float16下容易溢出,建议用torch.cuda.amp自动混合精度,或者手动把输入转float32。
  • 别把SiLU和GELU搞混,GELU是x * Phi(x),Phi是正态分布CDF,计算更复杂,但YOLOv8不用它。

4. Pooling:MaxPool和AvgPool的梯度差异

YOLO里Pooling用得不多,但SPPF(Spatial Pyramid Pooling Fast)里用了MaxPool。Pooling没有可学习参数,但梯度传播有陷阱。

MaxPool前向
取每个窗口的最大值,同时记录最大值的位置索引(用于反向传播)。
输出形状:(N, C, H_out, W_out),其中H_out = floor((H + 2*p - K) / s + 1)

MaxPool反向
梯度只传给最大值所在的像素,其他位置梯度为0。
公式:d_input[位置索引] = d_output,其他位置填0。
PyTorch里用torch.nn.functional.max_unpool2d,但注意它需要前向时保存的indices。

AvgPool前向
取每个窗口的平均值,公式:output = sum(input) / (K*K)

AvgPool反向
梯度均匀分配给窗口内的每个像素:
d_input = d_output / (K*K),然后通过col2im累加。

踩坑经验

  • MaxPool的indices在GPU上存储的是int64,如果你要自定义反向,记得用torch.empty分配相同类型。
  • AvgPool的反向如果窗口重叠,同一个像素会被多个窗口的梯度累加,这是正确的,但如果你手写,别忘记累加。
  • SPPF里用了三个连续的MaxPool,每个的kernel_size=5,stride=1,padding=2,这样输出尺寸不变。但注意,多个MaxPool串联会导致梯度稀疏,因为每个池化只保留一个最大值,梯度流会越来越窄。YOLOv8的设计者可能考虑到了这一点,所以SPPF后面接了卷积来恢复梯度密度。

个人经验性建议

  1. 调试梯度爆炸/消失时,先检查BN。我见过太多人花几天调学习率,结果发现是BN的running_var初始化成0或者eps设错了。用torch.norm打印每层梯度的L2范数,如果某层梯度突然变大,大概率是BN或激活函数的问题。

  2. 自定义算子时,用torch.autograd.Function实现前向和反向。别偷懒用torch.no_grad或者直接改data,那样梯度链会断。写完后用gradcheck验证,但注意设atol=1e-3,因为单精度误差大。

  3. SiLU和BN的顺序有讲究。YOLOv8里是Conv -> BN -> SiLU,这是标准做法。如果你改成Conv -> SiLU -> BN,梯度会不稳定,因为SiLU的输出范围大,BN的归一化效果会变差。别问我怎么知道的,我试过。

  4. Pooling的梯度稀疏性会影响后续卷积的梯度。如果你在某个分支里用了多个MaxPool,后面接的卷积层梯度会非常稀疏,导致训练缓慢。可以考虑用AvgPool或者Strided Conv替代。

  5. 写代码时,把每个算子的前向和反向公式贴在注释里。比如:

    # SiLU forward: y = x * sigmoid(x)# SiLU backward: dy/dx = sigmoid(x) * (1 + x * (1 - sigmoid(x)))

    这样半年后你回来看代码,不用重新推导。

最后,别迷信“深度学习框架自动求导,不用管梯度”。当你需要魔改YOLO结构时,理解这些算子的梯度流能帮你省下至少一周的调试时间。下次遇到loss炸了,先检查BN,再检查激活函数,最后看Pooling的梯度稀疏性——按这个顺序排查,90%的问题都能定位。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/4 19:39:42

终极指南:用html-to-docx实现HTML到Word文档的完美转换

终极指南&#xff1a;用html-to-docx实现HTML到Word文档的完美转换 【免费下载链接】html-to-docx HTML to DOCX converter 项目地址: https://gitcode.com/gh_mirrors/ht/html-to-docx 还在为HTML内容转换成Word文档后格式全乱而烦恼吗&#xff1f;html-to-docx这个Jav…

作者头像 李华
网站建设 2026/6/4 19:38:49

思源宋体TTF字体完全指南:7种字重免费商用,5分钟快速上手

思源宋体TTF字体完全指南&#xff1a;7种字重免费商用&#xff0c;5分钟快速上手 【免费下载链接】source-han-serif-ttf Source Han Serif TTF 项目地址: https://gitcode.com/gh_mirrors/so/source-han-serif-ttf 还在为寻找一款既专业又免费的中文字体而烦恼吗&#…

作者头像 李华
网站建设 2026/6/4 19:37:40

论文太单薄?博导推荐这几个AI论文网站

想写论文又快又好&#xff0c;关键是用对 AI 工具、走对流程——资深教授普遍推荐&#xff1a;千笔AI&#xff08;中文全流程首选&#xff09; 豆包学术版&#xff08;轻量高效&#xff09; DeepSeek 学术版&#xff08;理工 / 长文本&#xff09; Grammarly Academic&#xff…

作者头像 李华
网站建设 2026/6/4 19:34:04

Windows平台Python机器学习:Dlib预编译包完整指南

Windows平台Python机器学习&#xff1a;Dlib预编译包完整指南 【免费下载链接】Dlib_Windows_Python3.x Dlib compiled binaries (.whl) for Python 3.7-3.14 and Windows x64 项目地址: https://gitcode.com/gh_mirrors/dl/Dlib_Windows_Python3.x 还在为Windows上安装…

作者头像 李华