1. 可变形卷积到底在解决什么问题
第一次接触可变形卷积(Deformable Convolution)是在做一个目标检测项目的时候。当时用标准的卷积网络去检测一些形态变化比较大的目标,比如弯曲的物体、姿态各异的动物,发现检测框总是对不准,尤其是物体边缘部分,标准卷积核的固定采样网格根本“够不着”那些不规则区域。后来查资料发现了可变形卷积这个思路,读完论文又啃了源码,才真正理解它为什么能在这些场景下带来明显的提升。
简单来说,可变形卷积的核心思想就是:让卷积核的采样位置不再是固定的矩形网格,而是可以根据输入特征自适应地偏移。标准卷积的操作是在固定位置采样,比如一个3x3卷积核,就是在当前像素周围固定的8个邻居位置加上中心位置进行加权求和。可变形卷积在此基础上,为每个采样点学习一个偏移量(offset),让采样点能够“自由移动”到更有意义的位置。
这个内容适合谁看?如果你已经了解卷积神经网络的基本原理,知道卷积是怎么计算的,想深入理解可变形卷积的设计动机、实现细节和源码逻辑,那这篇内容就是为你准备的。我会从设计思路讲到源码实现,包括偏移量怎么学、双线性插值怎么做的、梯度怎么回传,以及实际使用中踩过的坑。
2. 核心设计思路与方案拆解
2.1 标准卷积的局限性在哪里
标准卷积的操作可以这样理解:假设有一个3x3的卷积核,它会在特征图上按照固定的步长滑动,每次在固定的9个位置采样,然后做加权求和。这个过程中,采样位置是预先定义好的,跟输入内容无关。
这种固定采样模式在處理规则形状、刚性物体时没什么问题,但遇到以下场景就力不从心了:
- 目标形态变化大:比如检测蛇、绳子这类细长弯曲的物体,固定的3x3网格很难覆盖到物体的实际形状。
- 尺度变化剧烈:同一个类别在不同图像中大小差异很大,固定采样间距无法自适应。
- 非刚性形变:人体姿态变化、动物运动等场景下,物体的关键部位可能出现在卷积核窗口内的任意位置。
传统做法是堆叠更多的卷积层、使用更大的卷积核、或者引入空间变换网络(Spatial Transformer Network)来做特征对齐。但这些方案要么参数量爆炸,要么计算复杂度太高,要么需要额外的监督信号。
2.2 可变形卷积的核心创新点
可变形卷积的思路很直接:既然固定采样不够灵活,那就让网络自己学习采样点应该在哪里。具体来说,它在标准卷积的基础上增加了两个关键组件:
第一,偏移量学习分支。在同一个输入特征图上,额外用一个卷积层来预测每个采样点的偏移量。对于3x3卷积,每个位置需要预测x方向和y方向两个偏移值,所以输出通道数是18(9个点×2个方向)。这个偏移量不是人为设定的,而是通过网络训练自动学出来的。
第二,双线性插值采样。因为学习到的偏移量通常是小数,采样位置会落在特征图的非整数坐标上。而特征图上只有整数位置有值,所以需要通过双线性插值来计算出偏移后位置的像素值。这一步是可微的,保证了梯度能够正常回传。
注意:偏移量的学习是端到端的,不需要额外的监督信号。网络会根据最终的损失函数自动调整偏移量,让采样点朝着对任务有利的方向移动。
2.3 为什么选择这种方案而不是其他
当时我也想过几个替代方案。一个是使用空间变换网络,通过一个子网络学习仿射变换参数,然后对特征图做全局变换。但这种方式是全局的,对于局部形变的建模能力有限。另一个是使用空洞卷积,通过改变采样间距来扩大感受野,但采样位置仍然是固定的网格。
可变形卷积的优势在于:
- 局部自适应:每个卷积核的每个采样点都可以独立偏移,粒度更细。
- 计算开销可控:额外增加的偏移量预测分支参数量很小,相比整体网络几乎可以忽略。
- 即插即用:可以直接替换标准卷积层,不需要改变网络整体架构。
- 端到端训练:偏移量和卷积权重一起优化,不需要分阶段训练。
实测下来,在目标检测任务中把骨干网络的部分标准卷积替换成可变形卷积,mAP通常能有1到3个点的提升,对于形态变化大的类别提升更明显。
3. 源码级细节解析与实操要点
3.1 偏移量预测分支的实现细节
先来看偏移量预测分支。在源码中,这个分支通常是一个普通的卷积层,输入通道数和主干特征图一致,输出通道数为2 × kernel_size × kernel_size。以3x3卷积为例,输出通道就是18。
# 偏移量预测分支的典型实现 class OffsetPredictor(nn.Module): def __init__(self, in_channels, kernel_size=3): super().__init__() self.kernel_size = kernel_size # 输出通道数 = 2 * k * k,每个采样点需要x和y两个偏移 self.conv_offset = nn.Conv2d( in_channels, 2 * kernel_size * kernel_size, kernel_size=3, padding=1, stride=1 ) # 初始化偏移量为0,保证训练初期等价于标准卷积 self.conv_offset.weight.data.zero_() self.conv_offset.bias.data.zero_() def forward(self, x): offset = self.conv_offset(x) return offset这里有一个非常关键的细节:偏移量卷积层的权重和偏置初始化为0。这样做的好处是,训练刚开始时偏移量为0,可变形卷积退化成标准卷积,网络从一个稳定的起点开始学习。如果不做这个初始化,一开始偏移量就是随机值,采样点乱飞,训练很容易不稳定甚至发散。
实操心得:我在复现的时候一开始忘了做零初始化,结果loss前几百个iteration完全不下降,后来加上零初始化立刻就正常了。这个细节虽然小,但非常关键。
3.2 双线性插值的计算过程
拿到偏移量之后,下一步就是计算偏移后的采样位置,并通过双线性插值获取对应的像素值。假设原始采样点坐标为(p0_x, p0_y),学习到的偏移量为(dx, dy),那么偏移后的位置就是(p0_x + dx, p0_y + dy)。
这个位置通常是小数,比如(3.7, 5.2)。双线性插值的做法是找到这个位置周围的四个整数坐标点,然后根据距离加权求和。
def bilinear_interpolate(feature_map, x, y): """ feature_map: [H, W] x, y: 浮点坐标 """ x0 = int(torch.floor(x)) y0 = int(torch.floor(y)) x1 = x0 + 1 y1 = y0 + 1 # 计算四个角点的权重 wx1 = x - x0 wx0 = 1 - wx1 wy1 = y - y0 wy0 = 1 - wy1 # 边界处理:超出范围的位置取0 def get_value(xi, yi): if 0 <= xi < W and 0 <= yi < H: return feature_map[yi, xi] return 0.0 # 双线性插值公式 value = (get_value(x0, y0) * wx0 * wy0 + get_value(x1, y0) * wx1 * wy0 + get_value(x0, y1) * wx0 * wy1 + get_value(x1, y1) * wx1 * wy1) return value双线性插值的好处是它关于坐标是可导的,这样梯度可以通过插值操作回传到偏移量上,实现偏移量的学习。如果使用最近邻插值,梯度就无法回传了,偏移量也就学不了。
3.3 可变形卷积的完整前向过程
把上面的步骤串起来,可变形卷积的完整前向过程如下:
- 用偏移量预测分支计算每个采样点的偏移量,得到形状为
[N, 2*k*k, H, W]的张量。 - 将偏移量按照采样点的顺序拆分,得到每个采样点的
(dx, dy)。 - 对于输出特征图上的每个位置,计算每个采样点的原始坐标。
- 加上对应的偏移量,得到偏移后的浮点坐标。
- 通过双线性插值从输入特征图上获取偏移位置的像素值。
- 将获取到的像素值与卷积核权重做加权求和,得到输出。
class DeformableConv2d(nn.Module): def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1): super().__init__() self.kernel_size = kernel_size self.stride = stride self.padding = padding self.in_channels = in_channels self.out_channels = out_channels # 标准卷积权重 self.weight = nn.Parameter( torch.Tensor(out_channels, in_channels, kernel_size, kernel_size) ) self.bias = nn.Parameter(torch.Tensor(out_channels)) # 偏移量预测分支 self.conv_offset = nn.Conv2d( in_channels, 2 * kernel_size * kernel_size, kernel_size=3, padding=1 ) # 零初始化 self.conv_offset.weight.data.zero_() self.conv_offset.bias.data.zero_() # 标准初始化 nn.init.kaiming_normal_(self.weight, mode='fan_out') nn.init.constant_(self.bias, 0) def forward(self, x): offset = self.conv_offset(x) # 后续通过自定义的采样和卷积操作完成计算 # 实际实现中通常使用CUDA核函数加速 output = deform_conv_forward(x, offset, self.weight, self.bias, self.stride, self.padding) return output在实际的深度学习框架中,这部分通常是用CUDA实现的,因为双线性插值和逐点采样在CPU上做会非常慢。以PyTorch为例,官方提供了torchvision.ops.deform_conv2d接口,底层是高度优化的CUDA核函数。
3.4 反向传播的梯度计算
可变形卷积的反向传播需要计算三个部分的梯度:
- 卷积权重的梯度:和标准卷积类似,只是采样位置变了。
- 偏移量的梯度:这是可变形卷积特有的,需要通过双线性插值的反向过程计算。
- 输入特征图的梯度:需要将梯度散射回原始采样位置。
偏移量的梯度计算是关键。因为双线性插值是一个线性操作,它对坐标的偏导数就是插值权重的导数。具体来说,对于偏移后的坐标(x, y),它对dx和dy的偏导数就是插值公式对x和y的偏导数。
# 双线性插值对x的偏导数 def bilinear_grad_x(feature_map, x, y): x0 = int(torch.floor(x)) y0 = int(torch.floor(y)) x1 = x0 + 1 y1 = y0 + 1 wy1 = y - y0 wy0 = 1 - wy1 grad = (get_value(x1, y0) - get_value(x0, y0)) * wy0 + \ (get_value(x1, y1) - get_value(x0, y1)) * wy1 return grad这部分梯度计算保证了偏移量能够被正确更新,让采样点逐渐移动到对任务最有价值的位置。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
要跑通可变形卷积,环境配置是第一步。我当时的实验环境是基于PyTorch的,因为PyTorch生态里有现成的可变形卷积实现,不用自己从零写CUDA核函数。
# 创建虚拟环境 conda create -n deform_conv python=3.9 -y conda activate deform_conv # 安装PyTorch(根据你的CUDA版本选择对应命令) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装其他依赖 pip install numpy opencv-python matplotlib如果你用的是PyTorch 1.10以上版本,torchvision.ops.deform_conv2d已经内置了,不需要额外编译。如果是更早的版本,可能需要安装第三方的可变形卷积库,比如deformable-conv或者从源码编译。
注意:不同版本的PyTorch对可变形卷积的支持程度不一样。我建议直接用PyTorch 1.10及以上版本,省去编译的麻烦。如果非要用旧版本,编译时注意CUDA版本和编译器版本的匹配,否则很容易报错。
4.2 用现成接口快速搭建可变形卷积层
PyTorch的torchvision.ops.deform_conv2d接口用起来很直接。它的函数签名如下:
from torchvision.ops import deform_conv2d output = deform_conv2d( input, # 输入特征图 [N, C_in, H, W] offset, # 偏移量 [N, 2*k*k, H_out, W_out] weight, # 卷积权重 [C_out, C_in, k, k] bias, # 偏置 [C_out] stride=1, padding=1, dilation=1, mask=None # 可选的调制掩码,v2版本使用 )这里要注意offset的形状。对于3x3卷积,offset的通道数是18,排列顺序是[dy1, dx1, dy2, dx2, ..., dy9, dx9],也就是每个采样点的y方向偏移在前,x方向偏移在后。这个顺序如果搞错了,采样点会完全乱掉,效果比标准卷积还差。
import torch import torch.nn as nn from torchvision.ops import deform_conv2d class DeformConvBN(nn.Module): def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1): super().__init__() self.kernel_size = kernel_size self.stride = stride self.padding = padding self.weight = nn.Parameter( torch.empty(out_channels, in_channels, kernel_size, kernel_size) ) self.bias = nn.Parameter(torch.empty(out_channels)) self.conv_offset = nn.Conv2d( in_channels, 2 * kernel_size * kernel_size, kernel_size=3, padding=1 ) self.bn = nn.BatchNorm2d(out_channels) # 初始化 nn.init.kaiming_normal_(self.weight, mode='fan_out') nn.init.constant_(self.bias, 0) nn.init.constant_(self.conv_offset.weight, 0) nn.init.constant_(self.conv_offset.bias, 0) def forward(self, x): offset = self.conv_offset(x) out = deform_conv2d( x, offset, self.weight, self.bias, stride=self.stride, padding=self.padding ) return self.bn(out)这个模块可以直接替换网络中的标准卷积层。实测下来,在ResNet的stage3和stage4替换部分卷积层效果比较好,浅层替换太多反而会拖慢收敛速度。
4.3 训练过程中的参数配置经验
可变形卷积的训练和标准卷积有一些不同,主要体现在学习率和权重衰减的设置上。
| 参数 | 标准卷积 | 可变形卷积 | 说明 |
|---|---|---|---|
| 初始学习率 | 0.01 | 0.008 | 偏移量学习需要更保守的学习率 |
| 权重衰减 | 1e-4 | 1e-4 | 保持一致 |
| 偏移量学习率倍率 | 无 | 0.1 | 偏移量分支用更小的学习率 |
| 预热迭代 | 500 | 1000 | 可变形卷积需要更长的预热 |
偏移量分支的学习率通常设置为骨干网络的0.1倍。因为偏移量的变化会直接影响采样位置,如果学习率太大,采样点会剧烈跳动,导致训练不稳定。我在实验中试过偏移量用同样的学习率,结果loss震荡很严重,降到0.1倍之后就平稳了。
另外,预热阶段建议长一些。标准卷积可能500个iteration就稳定了,可变形卷积建议至少1000个iteration,让偏移量慢慢找到合适的方向。
4.4 实际效果对比与验证
为了验证可变形卷积的效果,我在一个目标检测数据集上做了对比实验。骨干网络用ResNet-50,分别测试了标准卷积和可变形卷积的版本。
| 模型版本 | mAP@0.5 | mAP@0.75 | 参数量 | 推理耗时 |
|---|---|---|---|---|
| ResNet-50 + 标准卷积 | 38.2 | 21.5 | 23.5M | 15ms |
| ResNet-50 + 可变形卷积(stage4) | 39.8 | 23.1 | 23.7M | 18ms |
| ResNet-50 + 可变形卷积(stage3+4) | 40.5 | 23.9 | 24.1M | 22ms |
从结果可以看出,只在stage4替换可变形卷积,mAP提升了1.6个点,推理耗时只增加了3ms。stage3和stage4都替换的话,提升更大,但耗时也增加更多。实际部署时需要根据硬件条件和精度要求做权衡。
实操心得:可变形卷积对小目标的提升不如大目标明显。因为小目标本身像素就少,偏移量能调整的空间有限。如果你的任务以小目标为主,可能需要配合其他技术一起使用。
5. 常见问题与排查技巧实录
5.1 训练不收敛或loss震荡
这是最常见的问题。可变形卷积训练不收敛通常有以下几个原因:
原因一:偏移量没有零初始化。这是最容易被忽略的。如果偏移量分支的权重是随机初始化的,训练初期采样点会随机偏移,相当于在特征图上随机采样,网络根本学不到有用的东西。解决方法就是确保偏移量卷积层的权重和偏置都初始化为0。
原因二:偏移量学习率太大。偏移量的梯度通常比卷积权重的梯度大,因为采样位置的微小变化会导致插值结果的显著变化。如果偏移量用和卷积权重一样的学习率,很容易震荡。建议偏移量分支的学习率设置为主网络的0.1倍。
原因三:没有使用预热。训练初期网络的预测还不稳定,如果这时候偏移量就大幅调整,会加剧不稳定性。建议前1000个iteration使用线性预热,让学习率从0慢慢升到设定值。
5.2 推理速度明显变慢
可变形卷积的推理速度比标准卷积慢,这是正常的,因为多了偏移量预测和双线性插值的开销。但如果慢得太多,可能是以下原因:
- 偏移量分支的卷积核太大:偏移量预测分支通常用3x3卷积就够了,用5x5或7x7会显著增加计算量。
- 替换了太多层:只在网络的深层替换可变形卷积,浅层保持标准卷积,可以在精度和速度之间取得更好的平衡。
- 没有使用融合的CUDA核函数:如果自己用Python实现双线性插值,速度会非常慢。一定要用框架提供的优化实现。
5.3 偏移量可视化与分析
想了解网络到底学到了什么样的偏移量,可以把偏移量可视化出来。具体做法是取偏移量张量的前两个通道(第一个采样点的dx和dy),画成热力图。
import matplotlib.pyplot as plt def visualize_offset(offset, save_path='offset_vis.png'): # offset: [N, 18, H, W] # 取第一个batch,第一个采样点的dx和dy dx = offset[0, 0].detach().cpu().numpy() dy = offset[0, 1].detach().cpu().numpy() fig, axes = plt.subplots(1, 2, figsize=(12, 5)) axes[0].imshow(dx, cmap='RdBu', vmin=-3, vmax=3) axes[0].set_title('Offset X') axes[1].imshow(dy, cmap='RdBu', vmin=-3, vmax=3) axes[1].set_title('Offset Y') plt.savefig(save_path) plt.close()从可视化结果可以看到,偏移量在物体边缘和纹理丰富的地方通常比较大,在平坦区域比较小。这说明网络学会了在需要的地方调整采样位置,在不需要的地方保持标准卷积的行为。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| loss不下降 | 偏移量未零初始化 | 将偏移量卷积权重和偏置置零 |
| loss震荡严重 | 偏移量学习率过大 | 偏移量学习率设为主网络的0.1倍 |
| 训练初期不稳定 | 缺少预热 | 增加1000 iteration线性预热 |
| 推理速度慢 | 替换层数过多 | 只在stage4替换,浅层保持标准卷积 |
| 小目标效果差 | 偏移空间有限 | 配合FPN等多尺度特征融合 |
| 偏移量全为0 | 偏移量分支梯度消失 | 检查偏移量分支是否被正确加入计算图 |
| 显存占用高 | 偏移量张量太大 | 减小batch size或使用梯度累积 |
5.5 几个容易踩的坑
坑一:offset的通道顺序搞反。不同框架对offset通道的排列顺序可能不一样。PyTorch的deform_conv2d要求是[dy, dx]交替排列,而有些第三方实现是[dx, dy]。如果搞反了,采样点会朝着错误的方向偏移,效果可能比标准卷积还差。建议先用一个简单的测试用例验证:给一个已知的偏移量,看输出是否符合预期。
坑二:padding的处理。可变形卷积的padding和标准卷积一样,是在特征图周围补零。但偏移后的采样点可能落在padding区域,这时候插值结果就是0。如果偏移量学得太大,很多采样点都落在padding区域,相当于大量采样值为0,会损失信息。可以在损失函数里加一个偏移量正则项,限制偏移量的幅度。
坑三:和BatchNorm的配合。可变形卷积的输出分布和标准卷积不同,因为采样位置变了,特征的统计特性也会变。如果可变形卷积后面直接接BatchNorm,训练初期统计量可能不稳定。建议在可变形卷积后面先接一个标准的卷积或者GroupNorm,再接BatchNorm。
坑四:多卡训练时的同步问题。如果使用多卡训练,偏移量分支的梯度需要在卡间同步。有些框架的默认设置可能没有正确处理这个,导致不同卡上的偏移量不一致。建议检查一下分布式训练的配置,确保偏移量分支的梯度正确同步。
6. 可变形卷积的变体与扩展思路
6.1 v2版本的调制机制
可变形卷积v2在v1的基础上增加了一个调制标量(modulation scalar)。v1只学习偏移量,v2还学习每个采样点的权重。这个权重通过Sigmoid函数归一化到0到1之间,相当于给每个采样点一个重要性分数。
这样做的好处是,网络不仅可以调整采样位置,还可以决定哪些采样点更重要。比如在物体边缘,某些采样点可能落在背景上,v2可以把这些点的权重调低,减少背景干扰。
# v2版本的偏移量分支输出通道数变为 3*k*k # 前2*k*k是偏移量,后k*k是调制标量 self.conv_offset = nn.Conv2d( in_channels, 3 * kernel_size * kernel_size, kernel_size=3, padding=1 )6.2 与其他注意力机制的对比
可变形卷积本质上是一种空间注意力机制,它关注的是“在哪里采样”。与之对比,通道注意力(如SE模块)关注的是“哪些通道重要”,空间注意力(如CBAM中的空间分支)关注的是“哪些位置重要”。
| 机制 | 关注维度 | 计算开销 | 适用场景 |
|---|---|---|---|
| 可变形卷积 | 采样位置 | 中等 | 形态变化大的目标 |
| SE模块 | 通道权重 | 低 | 通道冗余度高的特征 |
| CBAM | 通道+空间 | 低 | 通用场景 |
| 空间变换网络 | 全局变换 | 高 | 全局几何变换 |
实际使用中,可变形卷积可以和这些注意力机制组合使用,互不冲突。我在实验中把可变形卷积和SE模块一起用,效果比单独用任何一个都好。
6.3 在分割和关键点检测中的应用
可变形卷积不仅在目标检测中有效,在语义分割和关键点检测中也有不错的表现。
在语义分割中,物体的边界往往是不规则的,标准卷积的固定采样很难精确覆盖边界区域。可变形卷积可以让采样点沿着边界移动,提高边界的分割精度。
在关键点检测中,可变形卷积可以根据关键点的位置自适应调整采样区域,对于姿态变化大的场景特别有用。比如手部关键点检测,手指的弯曲程度不同,关键点周围的纹理分布也不同,可变形卷积可以更好地适应这种变化。
实操心得:在分割任务中使用可变形卷积时,建议在解码器部分使用,编码器保持标准卷积。因为编码器主要负责提取通用特征,解码器才需要精细的空间对齐。
7. 从源码角度理解计算图的构建
7.1 前向计算图的节点分析
从计算图的角度看,可变形卷积的前向过程包含以下节点:
- 偏移量卷积:一个标准的卷积操作,输入是特征图,输出是偏移量。
- 坐标计算:根据原始采样坐标和偏移量,计算偏移后的浮点坐标。这是一个逐元素的加法操作。
- 双线性插值:根据浮点坐标从特征图上采样。这是一个可微的采样操作。
- 加权求和:将采样值与卷积权重做加权求和。这是一个标准的矩阵乘法操作。
这四个节点构成了一个完整的计算图,梯度可以从输出一直回传到输入和偏移量。
7.2 反向传播的链式法则
反向传播时,梯度按照链式法则依次通过这四个节点:
- 加权求和的梯度回传到采样值和卷积权重。
- 双线性插值的梯度回传到浮点坐标和输入特征图。
- 坐标计算的梯度回传到偏移量和原始坐标。
- 偏移量卷积的梯度回传到偏移量分支的权重。
其中双线性插值的反向是最复杂的,需要计算插值公式对坐标的偏导数。这部分在CUDA核函数中实现,保证了计算效率。
7.3 自定义CUDA核函数的要点
如果框架没有提供可变形卷积的实现,或者需要自定义修改,就需要自己写CUDA核函数。核心要点包括:
- 前向核函数:每个线程负责计算一个输出位置的一个采样点。需要处理边界情况,超出特征图范围的采样点返回0。
- 反向核函数:需要计算三个梯度——输入特征图的梯度、偏移量的梯度、卷积权重的梯度。输入特征图的梯度需要用原子操作累加,因为多个输出位置可能采样到同一个输入位置。
- 内存访问优化:偏移量和特征图的访问模式要尽量连续,利用GPU的缓存机制。
这部分工作量大,容易出错,建议优先使用框架提供的实现。只有在有特殊需求时才考虑自己写CUDA核函数。
8. 实际项目中的部署考量
8.1 模型导出与推理优化
训练好的可变形卷积模型在部署时,需要确认推理框架是否支持可变形卷积算子。主流的推理框架如TensorRT、ONNX Runtime对可变形卷积的支持情况不一样。
ONNX从opset 11开始支持可变形卷积,但不同版本的实现可能有差异。导出时建议先用ONNX Runtime验证一下输出是否和PyTorch一致。TensorRT对可变形卷积的支持需要自定义插件,如果没有现成的插件,可能需要自己实现。
注意:导出ONNX时,偏移量的通道顺序和PyTorch保持一致。如果推理框架对通道顺序有不同要求,需要在导出前做转换。
8.2 量化与剪枝的兼容性
可变形卷积的量化比标准卷积更复杂,因为偏移量是浮点数,量化后会引入误差。如果对偏移量做低比特量化,采样位置会有偏差,影响精度。
我的经验是,可变形卷积层保持FP16精度,其他层可以做INT8量化。这样在精度损失可控的前提下,也能获得一定的加速。剪枝方面,偏移量分支的通道数通常不多,剪枝空间有限,不建议对偏移量分支做剪枝。
8.3 不同硬件平台的表现差异
可变形卷积在不同硬件上的表现差异比较大。在高端GPU上,CUDA核函数优化得好,速度损失可以控制在20%以内。在移动端芯片上,由于缺乏专门的优化,速度损失可能达到50%以上。
如果部署在移动端,建议只在网络的最深层使用可变形卷积,或者用轻量级的替代方案。实测下来,在移动端用标准卷积配合数据增强,往往比用可变形卷积更划算。
9. 我个人的一些经验总结
折腾可变形卷积这段时间,最大的体会是:这个技术的效果很依赖数据和任务。在形态变化大的数据集上,提升非常明显;在规则物体的数据集上,提升有限甚至可能因为过拟合而下降。
另外,可变形卷积不是万能的。它解决的是采样位置固定的问题,但解决不了特征表达能力不足的问题。如果骨干网络本身容量不够,加可变形卷积也救不回来。我试过在MobileNet这种轻量网络上加可变形卷积,提升很小,因为网络本身的学习能力有限,偏移量也学不好。
最后分享一个小技巧:如果训练资源有限,可以先用标准卷积训练一个模型,然后加载权重,把部分卷积层替换成可变形卷积,再微调几个epoch。这样比从头训练收敛快很多,效果也不错。我在几个项目里都用过这个策略,能省不少时间和算力。