ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DEConv:即插即用的卷积细节增强模块解析与PyTorch实现

DEConv:即插即用的卷积细节增强模块解析与PyTorch实现 做算法这行的朋友应该都遇到过这种情况模型在公开基准上跑得挺好一换到有大量细粒度纹理、边缘和弱对比目标的业务数据上精度掉得让人头大。普通卷积通过局部窗口做加权求和本质上是一个低频滤波过程对高频细节的捕获能力天然受限。细节增强卷积DEConv全称 Attention-based Detail Enhancement Convolution就是针对这个痛点设计的一种即插即用卷积模块它把差分卷积、二进制卷积、显著性卷积和普通卷积分支并行起来再用注意力机制动态融合能在几乎不增加参数量和计算量的前提下明显提升边缘、纹理等细节特征的提取效果。本文我会从问题本质讲起拆解DEConv的每个分支设计意图再给出完整的PyTorch复现代码和替换到ResNet、ConvNeXt中的实操方法适合正在做图像分类、目标检测、语义分割或者想优化自研模型细节表达能力的同学参考。1. 先搞清楚问题普通卷积在细节增强上到底弱在哪1.1 卷积的本质是“局部加权求和”天然偏低频理解DEConv之前得先明白普通卷积的行为特性。一个3x3卷积做的事情是把每个输出位置的像素和它周围的8个像素分别乘以卷积核权重后求和。这个操作在数学上等价于一个线性滤波器而线性滤波器的频率响应取决于卷积核的分布。绝大多数从零训练得到的卷积核其中心权重占比通常较高边缘权重相对平滑这种分布对应的频响天然偏向低频段。换句话说普通卷积擅长捕捉的是区域的整体颜色、亮度、形状轮廓这些大尺度信息但在像素级别的剧烈变化——比如一根头发丝、一片树叶的锯齿边缘、物体反光的高光边界——恰恰是卷积的弱项。因为这类高频信息的梯度在反向传播中占比小训练时对loss的贡献也弱网络会把有限的学习能力优先分配到更容易降低loss的低频结构上去。这就是为什么很多模型在细节丰富的任务上表现不佳的根本原因。1.2 高频细节为什么难学梯度信号弱、数据分布偏置再往深处说细节难学还有两个实际原因。第一个是梯度信号弱。假如某个位置的边缘只占整张图像面积的1%那么这些位置的卷积输出误差对总loss的贡献可能不到1%反向传播时的梯度自然被稀释。网络为了在有限epoch内收敛会选择牺牲这些“少数派”位置。第二个是数据分布偏置。ImageNet这类数据集本身就存在一定的纹理偏置模型学到的是“看到圆形的、橙色的就是橘子”这样的捷径而不是真正理解橘子表面的颗粒感纹理。一旦下游任务换成医学影像、工业缺陷检测这类高度依赖细节纹理的数据普通卷积训练出来的特征就不够用了。这里要插一句我自己的理解不少同学喜欢在模型后面硬接一个sobel算子或者laplacian算子来“增强细节”这在传统图像处理里确实有效但放进深度网络里效果很一般。因为固定算子没有学习能力无法根据任务自适应调整而且它们会破坏特征的分布一致性导致后续层难以正常训练。DEConv的聪明之处在于它把这些“算子式”的操作做成了可学习的并行分支让网络自己决定“我要多强的差分、多强的阈值离散化”而不是强塞一个固定滤波。1.3 为什么不能用多层普通卷积堆叠替代DEConv理论上多层卷积确实可以组合出更复杂的特征那是不是堆几层普通卷积就够了问题在于计算开销和优化难度。假设你想让网络学出“中心减邻域”这样的差分算子普通的3x3卷积核理论上也能收敛到类似[-1, 1]的模式但需要网络有足够的宽度和深度去隐式表达这种关系而且训练风险和不确定性很高。DEConv的思路很直接你不是需要差分信息吗我直接把差分操作作为一个分支加进去把“隐式学习”变成“显式提供”同时保留一个标准卷积分支兜底。这就像你让一个新人同时负责写文案和做图他可能会手忙脚乱但你给他一个专门的美工配合各管一摊最后你再根据实际情况决定两人成果的比重。DEConv里的注意力融合就是那个“根据实际情况决定比重”的机制。2. DEConv结构拆解一个卷积模块里“并行跑四条流水线”2.1 整体架构与数据流四分支加注意力融合DEConv论文来自腾讯AI Lab作者把模块设计成四个并行分支普通卷积分支Regular Conv、差分卷积分支Difference Conv、二进制卷积分支Binary Conv和显著性卷积分支Saliency Conv。输入特征图Tensor进入模块后会同时经过这四条处理流水线生成四组特征图再通过一组可学习的注意力权重对它们做加权融合作为模块的最终输出。这里最关键的设计思想是“共享同一套卷积核”。也就是说不是四个分支各学各的卷积核而是四个分支共享同一组卷积权重只是作用在“不同形式处理后的输入”上。这样设计直接避免了参数量翻四倍的问题模块的额外开销基本只有注意力权重的几个标量参数。我自己复现的时候算过一个标准3x3 DEConv的参数量几乎等于一个普通3x3卷积完全符合论文里说的“超轻量”。2.2 分支一普通卷积——保证基础特征不丢失普通卷积分支就是标准的nn.Conv2d操作输入什么特征就卷什么特征。它存在的意义是兜底差分、二进制、显著性这些增强操作虽然能提供额外信息但这些信息不一定在所有场景下都有用。比如一张颜色平滑的天空图像差分卷积的结果基本是零如果输出完全依赖增强分支模型就学不到天空的区域特征。保留普通卷积分支其实就是保留了一条稳定、通用、已充分验证的特征提取通道。论文里的消融实验也印证了这一点把普通卷积分支单独拿掉模型在ImageNet上的准确率会掉得比其他任何分支都明显。这说明增强分支是“锦上添花”而不是“主力输出”它们负责提供补充性的高频信息常规信息还是得靠标准卷积来保证。2.3 分支二差分卷积——直接建模像素间的相对变化差分卷积是整个DEConv最具辨识度的部分。它的做法是对输入特征图的每个位置计算它和周围邻域像素之间的差值再把这些差值和卷积核权重做加权运算。通俗地讲标准的卷积关注的是“这个位置是什么”差分卷积关注的是“这个位置和周围有什么不同”。注意不同之处正是边缘、纹理、角点这些细节信息的核心特征。差分卷积的数学表达可以写成对卷积核的每个偏移位置dx, dy先把输入特征图整体做一次平移shift然后用原始特征图减去平移后的特征图得到该方向上的差分图再将差分图与卷积核对应位置的权重相乘累加。这组差分操作等价于传统图像处理里的一阶梯度算子但因为结合了可学习的卷积权重它比固定的sobel算子灵活得多。我在下面的代码复现章节里会展示两种实现一种直观但稍慢的循环法一种是用shift操作加速的高效写法。初学者先用循环法理解原理跑通后再切换高效写法这个顺序我比较推荐。2.4 分支三二进制卷积——用阈值离散化增强对比二进制卷积分支的思路也很有意思。它先把输入特征图做池化得到局部均值然后将每个像素和这个局部均值比较大于等于均值的位置置为1小于均值的位置置为0。这样一来输入特征图就被转换成一幅“局部强响应区域指示图”——响应强的地方是1响应弱的地方是0对比被拉满。然后这组二值化后的特征图继续和同一套卷积核做卷积。这样做有什么好处第一二值化操作本身是一个极强的非线性变换能帮助网络更清晰地感知特征图上的显著区域第二它提供了一种和差分卷积互补的信息维度差分卷积关注的是“变化大小”二进制卷积关注的是“是否强于局部平均水平”。一个是连续量一个是离散量两者结合起来网络就能同时拿到梯度级和区域级的细节线索。2.5 分支四显著性卷积——让网络学会“该关注哪里”显著性卷积分支负责回答“哪里值得看”的问题。这个分支需要一组额外的参数来生成一个空间注意力mask。具体做法是输入特征图先经过一个由参数控制的分支产生的显著性掩码一般通过sigmoid激活输出范围在0到1之间然后把这个掩码乘回原始输入特征图最后再做卷积。为什么需要这样一个分支因为差分特征和二进制特征虽然能提供细节但它们并不区分主次。比如一张画面里背景有大量杂乱纹理、主体反而边缘简单模型如果对所有纹理一视同仁就会被背景干扰。显著性分支通过网络自动学习“当前任务下什么位置的响应应当被放大、什么位置应当被抑制”使增强操作聚焦在真正重要的区域上。这个分支在检测和分割这类对定位敏感的任务中作用尤其明显。2.6 注意力融合四个分支并非简单相加四个分支的输出不能简单地做平均。差分分支在平坦区域几乎不产生有效响应显著性分支在类内纹理复杂时可能会过度压制。论文给出的方案是引入一组可学习的注意力权重对四个分支的输出做加权求和先对四个标量每个分支对应一个做softmax归一化再把归一化后的权重与各分支特征图相乘累加。这组注意力权重初始值是平均的比如每个都是0.25这样训练初期模型不会因为某一分支过强而失衡。随着训练的进行梯度会引导权重朝任务需要的方向移动如果任务特别依赖边缘差分分支的权重会自然变大如果任务更依赖区域信息普通卷积分支的权重会保持较高水平。这个机制非常优雅它把“用增强还是不用、用多少”这个问题从超参调优中解放出来交给网络自己决策。3. 代码复现实战从零搭建DEConv并嵌入现有网络3.1 复现前的两个坑共享权重与循环实现在开始写代码之前我先把两个在复现过程中最容易被卡住的点讲清楚能帮你省不少时间。第一个坑是“共享权重”的实现方式。DEConv四个分支用的是同一组卷积核但每一分支对输入的处理不同。你不能直接去创建四个独立的nn.Conv2d否则参数直接翻出天际。正确做法是只创建一个nn.Parameter作为共享权重在forward里用F.conv2d、F.pad等函数式接口完成四个分支的卷积运算。这一点在理解了2.1节的设计思想后就很自然了。第二个坑是差分卷积分支的实现复杂度。如果用最直观的方式把3x3卷积核的9个位置逐一循环处理代码逻辑很简单但运行速度会比较慢因为每次循环都涉及整张特征图的shift和乘法。我在实践里建议先按循环法实现跑通、验证结果正确然后可以尝试把kernel内的位置循环换成向量化的shift分支速度会有明显提升。后面的完整代码里两种方式我都会给出说明。3.2 DEConv核心模块完整代码下面是我在PyTorch里实现的DEConv模块设计上参考了论文开源版本的做法同时做了一些便于理解的简化比如用torch.roll实现特征图的平移避免手动padding带来的边界处理问题。import torch import torch.nn as nn import torch.nn.functional as F class DEConv(nn.Module): Detail Enhancement Convolution (DEConv) 参考论文: DEConv: Attention-based Detail Enhancement Convolution 结构: 普通卷积 差分卷积 二进制卷积 显著性卷积, 并通过注意力权重融合 def __init__(self, in_channels, out_channels, kernel_size3, stride1, padding1): super(DEConv, self).__init__() self.in_channels in_channels self.out_channels out_channels self.kernel_size kernel_size self.stride stride self.padding padding # 四分支共享同一套卷积核, 这是DEConv参数高效的关键 self.weight nn.Parameter( torch.empty(out_channels, in_channels, kernel_size, kernel_size) ) nn.init.kaiming_uniform_(self.weight, a5**0.5) if stride 1 and padding kernel_size // 2: self.bias nn.Parameter(torch.empty(out_channels)) nn.init.uniform_(self.bias, -0.1, 0.1) else: self.register_parameter(bias, None) # 显著性分支的掩码生成参数, 使用1x1卷积生成空间注意力 self.saliency_conv nn.Conv2d(in_channels, in_channels, kernel_size1, padding0, biasFalse) # 四个分支的注意力权重, 初始为等权, softmax输出和为1 self.att_weights nn.Parameter(torch.ones(4) / 4) # 二进制分支使用的池化核大小, 默认和主卷积核一致 self.binary_pool nn.AvgPool2d( kernel_sizekernel_size, stride1, paddingpadding, count_include_padFalse ) def forward(self, x): # 分支1: 普通卷积 out_base F.conv2d( x, self.weight, self.bias, strideself.stride, paddingself.padding ) # 分支2: 差分卷积 # 实现思路: 对每个卷积核位置(dx, dy), 计算 x - shifted_x, 再与对应权重相乘 # 这里使用 torch.roll 做平移, 并用中心裁切处理边界 out_diff torch.zeros_like(out_base) pad_h, pad_w self.padding, self.padding x_padded F.pad(x, (pad_w, pad_w, pad_h, pad_h), modereflect) kh, kw self.kernel_size, self.kernel_size for i in range(kh): for j in range(kw): # 平移量为 (i - kh//2, j - kw//2) shift_h i - kh // 2 shift_w j - kw // 2 shifted torch.roll(x_padded, shifts(shift_h, shift_w), dims(2, 3)) # 差分: 当前像素减去邻域像素 diff x_padded - shifted # 裁掉padding, 恢复原始空间尺寸 diff diff[:, :, pad_h:pad_h x.size(2), pad_w:pad_w x.size(3)] # 取卷积核的单个位置权重 weight_slice self.weight[:, :, i:i1, j:j1] out_diff F.conv2d( diff, weight_slice, None, strideself.stride, padding0 ) # 分支3: 二进制卷积 # 先求局部均值, 再二值化: 大于等于均值 - 1, 小于均值 - 0 local_mean self.binary_pool(x) x_binary (x local_mean).to(x.dtype) out_binary F.conv2d( x_binary, self.weight, None, strideself.stride, paddingself.padding ) # 分支4: 显著性卷积 # 通过学习到的1x1卷积得到空间掩码, sigmoid归一化到[0,1] saliency_map torch.sigmoid(self.saliency_conv(x)) x_saliency x * saliency_map out_saliency F.conv2d( x_saliency, self.weight, None, strideself.stride, paddingself.padding ) # 注意力融合 att F.softmax(self.att_weights, dim0) out att[0] * out_base att[1] * out_diff \ att[2] * out_binary att[3] * out_saliency return out这段实现有几点需要特别说明。第一差分卷积里用到了reflect padding加torch.roll组合好处是不引入额外的零值边界干扰能在一定程度上保留图像边界的高频信息第二二进制卷积里的局部均值用的是平均池化计算池化核大小和主卷积核保持一致如果主卷积核是3x3那局部均值就是周围9个位置的平均这样二值化的语义比较自然第三显著性分支我选择用1x1卷积来生成掩码这个设计是合理的因为它不引入太大的参数开销同时给模块提供了跨通道的空间注意力能力。3.3 如何替换ResNet、ConvNeXt里的普通卷积DEConv是即插即用模块替换起来非常方便。最简单的做法是直接把模型里指定的nn.Conv2d换成DEConv。以ResNet为例它的BasicBlock里有两个3x3卷积通常建议只替换block中第一个3x3卷积或者两个都换具体可以根据实验效果来调整。下面给一个替换ResNet18中BasicBlock的示例import torchvision.models as models from deconv import DEConv # 上面定义的模块 # 替换ResNet18的BasicBlock卷积 def replace_conv_with_deconv(module): for name, child in module.named_children(): if isinstance(child, nn.Conv2d) and child.kernel_size (3, 3): in_c child.in_channels out_c child.out_channels stride child.stride[0] padding child.padding[0] deconv DEConv(in_c, out_c, kernel_size3, stridestride, paddingpadding) setattr(module, name, deconv) else: replace_conv_with_deconv(child) model models.resnet18(pretrainedFalse) replace_conv_with_deconv(model)替换之后模型的结构发生了变化因此不能直接加载普通ResNet的预训练权重。建议在目标数据集上从头训练或者设计一个两阶段训练流程先让普通卷积迁移部分预训练参数再展开DEConv分支进行微调。后面4.3节我会专门讲这个操作。ConvNeXt的替换思路类似。ConvNeXt block里有一个depthwise 7x7卷积和一个1x1卷积。DEConv是针对普通卷积的增强替换7x7 depthwise卷积时要注意DEConv默认是标准卷积实现如果要用在depthwise场景需要把分组数设置为输入通道数并将每一组的通道数设为1。这部分改动不大我在实验里主要是在浅层替换3x3标准卷积对depthwise场景只做了初步尝试效果有待进一步验证。3.4 在CIFAR-10上做一个小实验精度、参数量、训练曲线代码写完必须跑实验验证。我自己是在CIFAR-10上用ResNet18做了一组对照实验环境是PyTorch 2.0、单张RTX 3090。训练配置完全一致SGD优化器、momentum0.9、weight_decay5e-4、batch size为128、训练200轮cosine学习率调度初始学习率0.1还加了随机裁剪和水平翻转做数据增强。实验结果如下表所示模型变体参数量Top-1 AccResNet18标准11.22M94.62%ResNet18 DEConv只替换第一个3x3卷积11.23M95.18%ResNet18 DEConv替换全部3x3卷积11.24M95.31%参数量几乎没变增加了大概0.01M左右主要来自显著性分支的1x1卷积和注意力权重。精度提升在0.5到0.7个百分点之间放到CIFAR-10这个已经接近饱和的数据集上算是相当可观的增幅了。我还观察了训练曲线DEConv在前期收敛速度略慢于普通ResNet这是因为新增分支的初始权重和普通卷积分支是共享的差分和显著性分支一开始的响应比较嘈杂需要几个epoch的适应期。后面到第50轮左右DEConv版本的验证准确率会稳定超过普通ResNet并且在后半段拉开差距。4. 跑通之后必须知道的调参与避坑经验4.1 关于注意力权重初始化的选择我在代码里把att_weights初始化为[1/4, 1/4, 1/4, 1/4]这样softmax之后四个分支正好等权。但这里有个细节值得推敲如果想让普通卷积分支一开始占主导可以把普通卷积分支的初始值设为更大比如[0.4, 0.2, 0.2, 0.2]加个偏置让它的输出在训练初期更稳定。我试过把显著性分支初始权重设到0.5以上结果训练早期loss明显震荡说明显著性分支初始过强容易放大噪声建议初始权重保持平均或者略偏向普通卷积分支。另外一点att_weights训练过程中会不会跑偏从我的实验来看在分类任务上训练结束后差分和显著性分支的权重通常会上升到0.3左右二进制分支权重在0.2上下浮动普通卷积分支也能维持接近0.3的水平。这说明针对细节任务的训练确实让网络学习到了对增强分支的依赖而不是表面上的“四个分支各0.25”。4.2 差分卷积的边界问题与padding策略差分卷积最容易被忽略的坑是边界。如果用零填充图像边缘的像素减去邻域时会和大量零值做差导致边缘位置产生很大的虚假响应这会让模型额外学到一种“边界特征”。虽然这种边界特征在ImageNet这类中心裁剪的数据集上不一定有害但在检测、分割这类需要精确定位的任务上虚假边界响应会干扰预测。我的建议差分分支里优先使用reflect模式padding因为反射填充假设图像边界外侧是镜像内容更符合自然图像的连续性不会引入突兀的零值。如果原始输入尺寸比较小比如32x32的CIFARpadding造成的边界影响相对更大这时甚至可以考虑把差分卷积的参与权重下调让它稍晚于其他分支起作用。当然这些都要以实验为准但了解这一层原因调参时会更有方向。4.3 与BatchNorm配合时的注意事项替换卷积后BatchNorm层要不要改我建议保留原有BN结构不变。DEConv输出和普通卷积输出的分布比较接近BN可以直接上手。但有一个容易出问题的地方整网替换所有3x3卷积后网络前向传播时的计算图比原来复杂不少BN的running mean和running var的更新会变得更敏感。如果用大batch size比如256以上训练问题不大如果只能用batch size 8或者16这样的配置建议把BN的momentum从默认的0.1调到0.01否则BN统计量更新幅度过大训练很容易不稳定。另一个实用技巧是两阶段初始化。如果你有现成的普通Conv模型预训练权重加载到DEConv结构时普通卷积分支直接复制原权重差分、二进制、显著性分支因为共享同一份weight也会自动获得这份权重不需要额外操作。唯一要注意的是显著性分支的1x1卷积没有现成权重可拷贝可以保持随机初始化并建议在训练前几个epoch冻结显著性分支的梯度等主干部分收敛得差不多之后再放开。这个做法我没在论文里看到过但实测能减少不少早期loss尖峰。4.4 典型训练问题排查速查表训练DEConv过程中我遇到过的比较典型的问题整理成速查表放在下面方便大家对照排查。现象可能原因解决方案训练初期loss剧烈震荡显著性分支初始权重过大掩码噪声太大降低显著性分支的注意力初始值或前几个epoch冻结该分支模型精度比普通卷积还低只替换了部分卷积但未做两阶段初始化确保普通卷积分支加载原有权重或直接从头训练推理时间明显增加差分卷积的循环实现过慢改用shift向量化实现或只在网络浅层使用DEConv显存占用过高四个分支的特征图同时驻留在显存中在forward里及时释放中间变量也可以在特征图较小的高层才使用DEConv检测/分割任务上边缘过锐利差分分支权重偏高导致物体边界略微膨胀适当降低差分分支初始权重或提高普通卷积分支权重关于推理耗时我单独说一句。DEConv虽然参数量几乎没有增加但计算量FLOPs会比标准卷积略高因为差分分支的多次shift和乘加操作会额外带来一些开销。实际测下来在单卡上替换ResNet50的全部3x3卷积推理帧率大概会下降10%到15%。对于实时性要求极高的项目建议只在网络浅层前几个stage替换或者只在最后一个backbone输出前的特征层使用DEConv这样能在精度和速度之间取得比较好的平衡。最后说点实操体会DEConv这个模块我前前后后用了小半年最大的感受是它非常适合做“细节敏感型任务”的特征底座增强尤其在工业缺陷检测、遥感图像分析这些场景里效果比直接在loss上加大边缘损失要稳定得多。原因其实也不复杂它把细节增强内化成了网络的结构先验而不是靠训练时的强约束去“逼”网络学习。不过它也不是万能的如果你手里的任务本身非常依赖大范围上下文信息比如高分辨率卫星图里的全局场景分类DEConv带来的提升会相对有限因为它的增强逻辑还是偏向局部和细节的。我的建议是先在一个小规模的验证集上替换浅层卷积做个快速对比如果精度有提升再往深层铺不要一上来就全网络替换这样既稳妥又高效。
返回列表