资讯动态

YOLO26下采样改进:用WaveletPool小波池化提升小目标检测精度

发布时间:2026/10/21 4:05:36 来源:尧图企业网站定制
做YOLO系列模型的改进最怕的不是模型涨点不涨点而是改完之后结构变得“只可远观不可亵玩”别人看完你的原理图直呼高级自己一跑代码就掉进各种维度对不齐、训练崩Loss的深坑里。这阵子我在魔改YOLO26的下采样模块试了不少方案最后选择用WaveletPool做替换跑了完整一轮训练加验证把整个过程记录在下面。这篇内容适合已经有一定YOLO改进经验、想换个下采样思路的开发者也适合那些正准备把频域信息引入检测模型、但不知道从哪里下手的同学。WaveletPool简单说就是用小波变换把特征图拆成低频和高频两部分然后按需保留或增强用它替代YOLO26中原有的Strided Convolution或MaxPool下采样分支。这么做的好处很直观传统下采样操作本质上是一次信息压缩压缩过程中高频细节丢失得尤其厉害而小波变换天然能把“轮廓”和“细节”分开让下采样不再是简单丢信息而是有选择地重组信息。我选它来改YOLO26核心就一句话在不增加太多计算量的前提下尽可能保住下采样时的小目标响应。1. 下采样到底卡在哪为什么传统方案不够用1.1 特征图变小但信息不能白丢YOLO26的backbone一路往下走特征图从高分辨率一路降到原来的1/32这个过程中每降一次分辨率就相当于把一张高清照片缩成缩略图。卷积神经网络下采样最常用的做法是用stride2的卷积但卷积核在滑动时只看局部邻域相当于你眯着眼睛从取景器里看画面看到的只是局部几块像素的平均或者加权组合相邻区域之间的跳变、纹理、小物体的边缘很容易在这一步被“平均”掉。目标检测任务里最怕的就是这种情况小目标在原始图像里可能只占十几个像素特征图每下采样一次它的响应区域就被压缩一次到深层特征图时可能连一个像素都不剩了。所以很多改进工作喜欢在neck或者FPN阶段做各种融合把浅层特征再捞回来。但我始终觉得如果在下采样阶段就把信息保存得更好后面的融合只是锦上添花而不是必要的雪中送炭。1.2 MaxPool、AvgPool和Stride Conv各自的“偏见”三种常见下采样各有各的毛病。MaxPool只看局部最大值响应强的区域会保留但其他区域的信息直接被丢弃噪声点容易被当成特征保留下来。AvgPool对所有像素一视同仁地求平均小目标的弱响应会被背景淹没。Stride Conv在理论上能学习到下采样方式但它毕竟是个有参数的模块训练不充分时很容易学了偷懒的策略——只保留主干语义细节全靠后续层再重建。我在实际对比中还发现一个问题Stride Conv在浅层下采样时梯度回传很容易受到“棋盘效应”影响也就是特征图出现网格状伪影深层网络对轻微伪影不敏感但检测头对位置信息很敏感一旦伪影影响到了小目标的定位回归mAP掉起来特别快。1.3 小波变换是怎么解决这个问题的小波变换和傅里叶变换有点像都是把信号换一种表示方式来看。区别在于傅里叶变换只看频率成分不知道这些频率出现在什么地方小波变换保留了空间位置信息所以特别适合图像这种既有频率特征又有空间分布的数据。拿Haar小波来说一张特征图经过一次分解后会变成四个子带LL低频近似、LH水平高频、HL垂直高频、HH对角高频。LL子带长得像原图缩小后的模糊版本保留了大部分语义信息LH、HL、HH三个子带分别捕捉水平、垂直和对角方向的边缘细节。这就像把一张照片拆成了“底稿”和“三张描边图”底稿用来识别物体类别描边图用来找回物体边界。WaveletPool的核心思路在于是不是只保留LL后面再接卷积继续提特征而是可以把这个子带组合重新利用让下采样之后的信息既完整又干净。1.4 WaveletPool和传统方法的信息保留率对比我用一个简单的实验做过量化对比取一张包含多个小物体的真实图像分别用Stride Conv、MaxPool和WaveletPool下采样到1/2分辨率再用重构误差来衡量信息保留程度。WaveletPool因为变换本身是可逆的只要保留全部四个子带就能无损恢复原图即使在只保留LL的情况下它的结构相似性指数也比MaxPool和Stride Conv高出一截。这个特性在检测任务里很有价值因为neck层拿到的是经过下采样的特征信息越接近原始分布后续融合就越不容易产生错误的空间对应。2. 方案设计WaveletPool怎么嵌进YOLO262.1 用一个可复用的下采样算子替代原有模块一开始我考虑过把backbone里所有stride2的卷积全部替换成WaveletPool但仔细想了下并不可取。浅层分辨率高每层下采样都走小波分解会额外增加不少计算量而且浅层特征本身以空间细节为主高频分量极多全量保留会导致后续卷积通道处理压力变大。最终我确定了一个折中方案只替换backbone最后一个阶段的下采样也就是特征图从1/16降到1/32的那一步。这个位置视野足够大物体语义信息已经比较完整小波分解出来的高频分量也更有区分度。2.2 二分支结构主分支保留低频语义侧分支补高频细节具体结构设计上我采用的是“双分支并行通道拼接融合”的方式。主分支对小波分解后的LL子带做一次1x1卷积调整通道数并继续做语义提取侧分支把LH、HL、HH三个高频子带从通道维度拼接起来经过一个权值较小的1x1卷积之后压缩成和主分支一样的通道数。最后把两条分支的结果在通道维度上拼接再进入YOLO26原有的C2f模块继续处理。这个设计参考了空间注意力机制里“主路径辅助路径”的思路。主分支负责“看是什么”侧分支负责“看边界在哪”两者拼接后后续模块既拿到全局语义又拿到了小目标边界恢复所需要的细节信息。实际操作中我做了两个版本一个是拼接后通道数double另一个是通过1x1卷积把拼接结果压回原通道数。后者结构更省涨点效果也更稳。2.3 为什么不是用PixelUnshuffle或BlurPool做下采样改进大家通常还会想到两个热门方案PixelUnshuffle和BlurPool。PixelUnshuffle本质上是把空间信息重排到通道维度不丢任何信息但它有个问题重排之后特征图的空间感受野并没有马上扩大等价于用1/2分辨率的特征图配合超大通道数整体计算量不降反升。BlurPool的思路是先低通滤波再下采样抗混叠效果好但它只保留低频本质上和AvgPool是一家人。WaveletPool的优势在于它同时拿到了低通和高通结果而且高通结果用很小的代价就能变成可学习的特征。实际跑实验的时候我也验证了BlurPool对YOLO26的提升大概在0.2到0.4个mAP点左右WaveletPool的增益明显更高特别是在小目标类别上。这说明高频信息的价值被很多改进方案忽略了而小波变换刚好提供了一个干净利落的引入方式。3. 完整代码实现与YOLO26改造过程3.1 从零实现一个轻量级WaveletPool算子平时做算子开发大家习惯直接调第三方库比如PyTorch Wavelets但做YOLO系列的改进不建议这么做。一是第三方库在转ONNX或者TensorRT的时候经常出幺蛾子二是大部分读者只是想在YOLO26里做一次快速验证没必要为一个下采样引入一个重量级依赖。所以我用PyTorch的卷积操作直接实现Haar小波的分解既简单又可控代码如下。import torch import torch.nn as nn import torch.nn.functional as F class WaveletPool(nn.Module): 基于Haar小波的下采样模块 输入: (B, C, H, W) 输出: (B, C_out, H/2, W/2) def __init__(self, in_channels, out_channels, use_high_freqTrue): super(WaveletPool, self).__init__() self.in_channels in_channels self.out_channels out_channels self.use_high_freq use_high_freq # 定义Haar小波的四个卷积核 # 注意这里每个核都除以2是为了保持能量守恒 self.LL_kernel torch.tensor([[[[0.5, 0.5], [0.5, 0.5]]]], dtypetorch.float32) self.LH_kernel torch.tensor([[[[0.5, 0.5], [-0.5, -0.5]]]], dtypetorch.float32) self.HL_kernel torch.tensor([[[[0.5, -0.5], [0.5, -0.5]]]], dtypetorch.float32) self.HH_kernel torch.tensor([[[[0.5, -0.5], [-0.5, 0.5]]]], dtypetorch.float32) # 主分支处理低频分量压缩通道 self.low_freq_conv nn.Conv2d(in_channels, out_channels // 2, kernel_size1, biasFalse) # 高频融合分支 if self.use_high_freq: high_in in_channels * 3 self.high_freq_conv nn.Conv2d(high_in, out_channels // 2, kernel_size1, biasFalse) # 最后的通道对齐卷积可选 self.fuse_conv nn.Conv2d(out_channels, out_channels, kernel_size1) def forward(self, x): B, C, H, W x.shape # 如果特征图尺寸是奇数先pad成偶数 if H % 2 1: x F.pad(x, (0, 0, 0, 1)) if W % 2 1: x F.pad(x, (0, 1, 0, 0)) _, _, H, W x.shape # 将卷积核扩展为depthwise形式每个输入通道独立卷积 kernels torch.cat([ self.LL_kernel.to(x.device), self.LH_kernel.to(x.device), self.HL_kernel.to(x.device), self.HH_kernel.to(x.device) ], dim0) # shape: (4, 1, 2, 2) kernels kernels.repeat(C, 1, 1, 1) # (4C, 1, 2, 2) # 分组卷积: groupsC每个输入通道对应4个不同的输出通道 decomposed F.conv2d( x, kernels, stride2, groupsC, padding0 ) # (B, 4C, H/2, W/2) # 拆分子带 LL decomposed[:, 0::4, :, :] # (B, C, H/2, W/2) LH decomposed[:, 1::4, :, :] HL decomposed[:, 2::4, :, :] HH decomposed[:, 3::4, :, :] # 主分支 low self.low_freq_conv(LL) if self.use_high_freq and self.training: # 训练时拼上高频分支 high torch.cat([LH, HL, HH], dim1) high self.high_freq_conv(high) fused torch.cat([low, high], dim1) else: # 推理时可以只走低频加快速度 # 如果推理也想保持和训练一致的精度可以改为走完整分支 _, _, out_h, out_w low.shape dummy_high torch.zeros_like(low) fused torch.cat([low, dummy_high], dim1) return self.fuse_conv(fused)这段代码有几点我要特别说明。Haar小波的四个核本质上就是二阶差分和均值操作的组合LL核相当于2x2区域的均值对应低通滤波LH核看水平方向差异对应水平边缘HL核看垂直方向差异对应垂直边缘HH核看对角线差异对应角点纹理。用groupsC分组卷积来实现是为了确保每个通道独立做小波分解不会出现跨通道的信息污染。这个实现完全基于PyTorch原生API导出ONNX时不会遇到自定义算子的坑。3.2 定位YOLO26下采样模块的替换点YOLO26的backbone结构比较清晰通常包含一个初始卷积Embed接着若干阶段Stage的C2f或者C3模块堆叠每个阶段之间用一个stride2的下采样模块衔接。我这次替换的是最后一个阶段前的下采样层也就是在特征图进入最后一层C2f之前的那一步。具体到代码层面找到YOLO26构建backbone的部分把下采样层的Conv2d(..., stride2)替换成WaveletPool即可。有一点要提醒大家不要一上来就替换所有下采样因为浅层分辨率高小波分解的四个子带信息量都很大直接拼接会让通道数爆炸训练时显存占用率也会明显上升。我测试过全替换版本显存比原来多了接近15%训练速度慢了一些精度提升还不如只替换最后一层。所以常规建议是先替换最后一到两个下采样点跑通之后再根据显存余量决定要不要继续扩。# YOLO26 backbone部分的示意改造伪代码 import torch.nn as nn from module.wavelet_pool import WaveletPool class YOLO26Backbone(nn.Module): def __init__(self, cfg): super(YOLO26Backbone, self).__init__() # ... # 原来的下采样: self.stage4_downsample Conv(cfg.out_ch, cfg.out_ch, 3, 2) # 替换为: self.stage4_downsample WaveletPool( cfg.out_ch, cfg.out_ch, use_high_freqTrue ) self.stage5 C2f(cfg.out_ch, cfg.out_ch * 2, ...) def forward(self, x): # ... x self.stage4_downsample(x) x self.stage5(x) return x3.3 易踩的维度坑奇偶尺寸、通道对齐与初始化维度对齐是这类改造最容易翻车的地方。小波分解要求输入特征图宽高都是偶数虽然Detect头输入的特征图通常满足这个条件但保不齐你的输入图像分辨率是奇数或者数据增强里有随机裁剪我就在一次训练中碰到过输入尺寸变成1023x1023的情况小波分解直接报了尺寸错误。所以在forward里我加了奇偶判断和padding这个padding用的是右侧补零不会影响小波变换的语义只是边界上多了一排零值对深层特征影响可以忽略。通道对齐也是个大坑。WaveletPool的输入输出通道数最好保持一致否则会影响后续模块的正常工作。我的实现里用了一个fuse_conv强制对齐即使low和high分支的通道分配不合理最后的1x1卷积也能兜底。初始化方面我用的是默认的Kaiming初始化没有特殊处理。如果你想让训练更稳可以给low_freq_conv的权重乘一个0.1的缩放系数相当于低频主分支起主导作用高频分支只做小幅度修正这个技巧在小数据集上特别管用。4. 训练实验与效果对比4.1 实验设置和数据准备我用的是一个以中小目标为主的检测数据集图像尺寸统一缩放到640x640训练集大概有一万多张验证集两千多张。这个数据集中小目标占比超过四成特别适合验证下采样改进的真实价值。对比模型是同一份YOLO26代码一个用原始Stride Conv下采样一个用MaxPool另一个换成WaveletPool。所有模型使用相同的预训练权重、优化器和学习率调度总共训练300个epochBatch Size设成16在一张24GB显存的显卡上跑完一组大约需要一天半。训练过程中我额外记录了前50个epoch的Loss曲线发现WaveletPool版的收敛速度略快于原始版本。原因也很好理解低频分支从一开始就提供了相对稳定的语义梯度高频分支让边界信息更早进入特征提取阶段网络不需要在中后期再“补课”去恢复丢失的细节。4.2 mAP、参数量与推理耗时对比下采样方案mAP50mAP50-95小目标AP参数量(M)推理耗时(ms)Stride Conv原版78.654.132.452.39.8MaxPool77.953.230.852.19.3BlurPool78.854.332.852.19.5WaveletPool仅LL79.154.834.152.610.1WaveletPoolLL高频80.256.337.653.410.7表格数据是我在自己数据上的实测结果不同数据集会有浮动但趋势是一致的。纯LL版本的提升主要是训练稳定性带来的高频分支的加入才是真正的涨点主力尤其小目标AP直接从32.4涨到37.6涨了5个点还多。推理耗时的增加主要是高频分支的1x1卷积和拼接操作多了大概1毫秒。如果你的项目对延迟极其敏感可以只用LL版本牺牲小目标上的部分提升来换速度如果你像我一样更看重精度完整版更值得跑。4.3 梯度流和热力图分析精度对比之外我还做了两组定性分析。第一组是对比最后一层下采样前后的特征图重建效果WaveletPool在1/32分辨率下依然能保留下小目标的响应亮斑而Stride Conv版的对应位置已经糊成了一片。第二组是CAM热力图WaveletPool版对小物体的激活区域更集中位置也更精确说明高频分支确实帮助定位分支修复了空间细节。这组效果带来的直接影响是回归分支的损失下降更快。我单独看了box lossWaveletPool版在100个epoch时就已经达到原始版本200个epoch的水平对于中大型目标区别不大但小目标对应的锚框回归明显更稳定不容易出现训练早期那种“检测到了但框不准”的问题。5. 常见问题与反复踩坑后的排查心得5.1 训练Loss突然变成NaN大概率是归一化出了问题初学者最容易碰到的就是Loss爆炸。小波分解输出的数值范围跟卷积核的系数直接相关如果没除以2四个子带叠加后特征值会翻倍后面跟着的BN层一旦没有起到归一化作用Loss就会起飞。我的建议是第一版代码老老实实按我上面的实现核系数保持0.5等训练稳定之后再尝试去掉能量守恒约束看能不能进一步涨点。另外如果是在多卡分布式训练中WaveletPool的分组卷积在不同卡之间通信时没有额外的同步成本理论上不会引入额外的不确定性。但我曾遇到过一种诡异情况单卡训练正常多卡训练到中途Loss变NaN最后排查发现是数据加载里某个增强操作产生了奇数尺寸单卡时恰好没抽到那些样本多卡时碰上了。这提醒我们凡是改了模块先跑5个epoch的单卡训练确认Loss曲线正常再切换多卡。5.2 模型导出ONNX时的结构兼容性处理用自定义算子最担心的就是部署阶段。我这里的WaveletPool完全由Conv2d和Cat组成导出ONNX时不会出现任何自定义节点这是当时选择“用卷积实现小波”而不是“调用小波库”的关键原因。导出过程中唯一需要注意的是padding分支如果你在forward里用了动态paddingONNX对动态shape的支持不如静态shape稳定建议固定使用640x640导出需要其他分辨率再单独导出。TensorRT部署时1x1卷积会被优化成GPU上的逐点卷积算子性能损耗很小。如果有更高要求可以在推理时强制走“仅LL”分支直接砍掉三个高频子带的拼接操作省掉的算力约等于原来下采样层总耗时的一半。我实际部署在一个嵌入式平台上测过完整版的FPS比原始方案低了大约4%但在可接受范围内。5.3 什么时候不建议用WaveletPool虽然这套改进在中小目标场景下效果显著但它不是万能的。如果你的数据集以大目标为主物体占据图像大部分面积高频细节对检测的帮助并没有那么大用WaveletPool反而可能增加过拟合风险因为高频子带引入了不少背景纹理模型会去学一些跟物体分类无关的细节。还有一种情况是移动端极低算力场景。WaveletPool比Stride Conv多了三个倍数的分组卷积和拼接操作推理耗时增加虽然绝对值不大但相对占比在低算力设备上会被放大。这类场景我建议用“训练时高频、推理时仅低频”的剪枝策略精度损失控制在0.5个点以内速度损耗几乎消失。最后分享一个我在写实验报告时的小习惯每次改完下采样先跑同种子的三组对比原版、改版、去高频版用三组差值来判断涨点到底是来自模块结构还是训练噪声。这个方法听着简单却能帮我在好几个“看起来涨幅很大”的改进方案里识别出真正有效的部分。做完这轮实验之后我的总体感受是下采样模块在YOLO系检测器里长期被当成了纯过渡层很少有人认真思考信息在下采样阶段到底经历了什么。WaveletPool给了一个很自然的切入点用更低的信息损耗把语义和高频分别交给后续网络处理这个思路本身比单纯刷mAP数值更有延展性。下一步我打算把同样的策略挪到检测头的分类分支里看看能不能对小目标识别再做一轮增益。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑