尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深度可分离卷积详解:原理、计算量推导与MobileNet实战应用

深度可分离卷积详解:原理、计算量推导与MobileNet实战应用 1. 先搞清楚它解决什么问题深度学习圈子这几年每隔一阵子就会冒出一个刷屏的概念深度可分离卷积Depthwise Separable Convolution绝对算得上是常青树之一。从 MobileNet 到 Xception从边缘设备上的实时推理到 Transformer 里的 Patch Embedding 变体到处都有它的影子。很多人第一次看到这个名字会觉得很高深其实拆开来看就两个词depthwise 和 separable核心就一句话——把标准卷积拆成两步来做从而大幅降低参数量和计算量。我最早接触这个东西是在做移动端图像分类模型压缩的时候。当时要把一个 ResNet 50 大小的模型塞进手机里跑实时推理直接量化蒸馏折腾了半天效果始终不理想。后来换了 MobileNet 的 backbone用深度可分离卷积替换掉标准卷积的堆叠模型小了将近十倍速度上去了精度损失也完全在可接受范围内。那一刻我才真正意识到这种结构上的改进比事后压缩要优雅得多。这篇东西适合谁看如果你是刚入门深度学习的同学可以用它建立起对卷积操作本质的理解如果你已经在用 MobileNet、EfficientNet 这类模型做项目那这篇文章能帮你搞清楚这些网络到底为什么快、为什么小以及真正使用时有哪些坑。我会从原理、计算量推导、手写实现、实际应用和踩坑记录五个维度完整聊一遍尽量做到不废话、可落地。2. 深度可分离卷积的核心思路把标准卷积“拆开”2.1 标准卷积到底在做什么要理解深度可分离卷积第一步是先重新审视一下标准卷积的空间维度。假设输入特征图的尺寸是 H×W×C也就是高 H、宽 W、通道数 C我们想输出一个 H′×W′×N 的特征图标准卷积层会用 N 个尺寸为 k×k×C 的卷积核去滑动计算。这里有个很容易被忽略的细节标准卷积的每个卷积核是同时处理“空间维度”和“通道维度”的。卷积核在空间上扫过 k×k 的邻域在通道上覆盖全部 C 个输入通道然后把 k×k×C 个数加权求和得到一个输出值。也就是说一次卷积操作同时完成了两件事提取局部空间特征和融合跨通道信息。打个比方标准卷积好比一个全能型员工既要做区域调研又要做跨部门的信息汇总所有事情一次性干完。效率听起来很高但代价是参数和计算量都很大。对于一个 k×k 卷积核单个输出通道需要 k×k×C 个参数N 个输出通道就是 k×k×C×N。当 C 和 N 都是 256、512 这种量级时这个数字是非常夸张的。2.2 第一步深度卷积Depthwise Convolution深度可分离卷积的第一步叫做深度卷积它的思路很极端每个输入通道单独配一个 k×k 的卷积核只在空间上做卷积完全不做通道间的信息交互。也就是说输入是 H×W×C我们准备 C 个卷积核每个卷积核负责一个通道输出的尺寸还是 H×W×C只是空间信息被重新编码了一遍。继续用刚才的比喻深度卷积相当于派 C 个专员分头去调研 C 个区域各管各的互不沟通。这样做的直接好处是参数少得可怜只有 k×k×C 个参数相比标准卷积少了 N 这个倍数。但问题也很明显各通道之间完全没有信息流动。如果就这样堆叠多层每个通道始终只看到自己的信息特征表达能力会大打折扣。这也是深度卷积极少单独使用的原因它必须搭配第二步来补上通道间的交互。2.3 第二步逐点卷积Pointwise Convolution第二步叫做逐点卷积本质上就是一个 1×1 的标准卷积。它把深度卷积的输出作为输入用 N 个 1×1×C 的卷积核在通道维度上做线性组合输出尺寸变为 H×W×N。逐点卷积的角色就是“跨通道信息融合专员”。深度卷积负责在空间维度上提取特征逐点卷积负责把各个通道的特征按权重组合起来两者各司其职合在一起正好完成了标准卷积的全部功能。这里有个非常关键的认知标准卷积把“空间特征提取”和“通道特征融合”捆绑在一次操作里完成深度可分离卷积则把这两件事彻底解耦分两步来做。这种解耦带来的直接收益就是参数量和计算量的数量级下降。3. 用数字说话参数量和计算量到底省了多少3.1 复杂度的标准公式先明确一下符号。设输入特征图尺寸为 H×W×C输出通道数为 N卷积核大小为 k×k输出特征图空间尺寸为 H′×W′。忽略偏置的情况下标准卷积的参数量P_std k × k × C × N标准卷积的计算量乘法次数F_std H′ × W′ × k × k × C × N深度可分离卷积的参数量由两部分组成。深度卷积部分每个输入通道一个 k×k 卷积核参数量是 k×k×C逐点卷积部分输出 N 个 1×1×C 卷积核参数量是 1×1×C×N C×N。合计P_dsc k×k×C C×N计算量同样分两部分。深度卷积部分每个通道在 H′×W′ 的空间上做 k×k 卷积共 C 个通道即 H′×W′×k×k×C逐点卷积部分在 H′×W′×C 的特征图上做 1×1 卷积输出 N 个通道即 H′×W′×C×N。合计F_dsc H′×W′×k×k×C H′×W′×C×N3.2 压缩比的推导拿深度可分离卷积和标准卷积做比值计算量压缩比F_dsc / F_std (H′×W′×k×k×C H′×W′×C×N) / (H′×W′×k×k×C×N) 1/N 1/k²这个公式是理解深度可分离卷积价值的核心。当输出通道数 N 比较大时1/N 这一项可以忽略压缩比约等于 1/k²。用 3×3 卷积核时计算量大约降到标准卷积的九分之一用 5×5 就是二十五分之一。参数量压缩比也类似P_dsc / P_std (k×k×C C×N) / (k×k×C×N) 1/N 1/k²3.3 一个具体的实例计算光看公式不够直观我拿一个实际问题来算一遍。假设输入特征图是 112×112×64我们希望输出 128 个通道使用 3×3 卷积核输出空间尺寸保持不变。标准卷积参数量3×3×64×128 73728计算量112×112×3×3×64×128 ≈ 9.25 亿深度可分离卷积深度卷积参数3×3×64 576逐点卷积参数1×1×64×128 8192总参数量8768深度卷积计算量112×112×3×3×64 ≈ 723 万逐点卷积计算量112×112×1×1×64×128 ≈ 1.03 亿总计算量约 1.10 亿算下来参数量大约是标准卷积的 11.9%计算量大约是 11.9%。跟公式预测的 1/64 1/9 ≈ 0.126 基本吻合。对比项标准卷积 3×3深度可分离卷积 3×3比例输入尺寸112×112×64112×112×64—输出通道128128—参数量737288768约 11.9%计算量约 9.25 亿约 1.10 亿约 11.9%注意这里的压缩比是在理想情况下算出来的。实际网络中还有 BN、激活函数、shortcut 等其他结构的开销所以端到端的模型压缩幅度不会正好是九分之一但数量级的优势是实打实的。4. 手写实现从公式到代码一次跑通4.1 PyTorch 实现PyTorch 里实现深度可分离卷积非常直接用 torch.nn.Conv2d 配合 groups 参数就可以完成。先建一个标准模块import torch import torch.nn as nn class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, stride1, padding1): super().__init__() # 第一步深度卷积每个输入通道独立卷积 self.depthwise nn.Conv2d( in_channels, in_channels, kernel_sizekernel_size, stridestride, paddingpadding, groupsin_channels ) # 第二步逐点卷积融合通道信息 self.pointwise nn.Conv2d( in_channels, out_channels, kernel_size1, stride1, padding0 ) def forward(self, x): x self.depthwise(x) x self.pointwise(x) return x这里最关键的是groupsin_channels这个参数它让每个输入通道单独对应一个卷积核实现了 depthwise 的效果。如果不设置 groups默认是 1那这个模块就退化成普通卷积了。实际使用的时候我建议把 BN 和激活函数加进去而不是单独在外面套这样模块的可复用性更高class DepthwiseSeparableConvBlock(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, stride1, padding1): super().__init__() self.depthwise nn.Conv2d( in_channels, in_channels, kernel_sizekernel_size, stridestride, paddingpadding, groupsin_channels ) self.pointwise nn.Conv2d( in_channels, out_channels, kernel_size1, stride1, padding0 ) self.bn1 nn.BatchNorm2d(in_channels) self.bn2 nn.BatchNorm2d(out_channels) self.act nn.ReLU(inplaceTrue) def forward(self, x): x self.act(self.bn1(self.depthwise(x))) x self.act(self.bn2(self.pointwise(x))) return x4.2 TensorFlow / Keras 实现Keras 里的实现思路相同同样利用 DepthwiseConv2D 显式声明深度卷积再用 Conv2D 1×1 做逐点卷积import tensorflow as tf from tensorflow.keras import layers, models def depthwise_separable_conv_block(inputs, out_channels, kernel_size3, stride1, paddingsame): x layers.DepthwiseConv2D( kernel_sizekernel_size, stridesstride, paddingpadding, depth_multiplier1 )(inputs) x layers.BatchNormalization()(x) x layers.ReLU()(x) x layers.Conv2D(out_channels, kernel_size1, strides1, paddingvalid)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) return x注意depth_multiplier1这个参数。它表示每个输入通道生成几个输出通道如果设为 2参数量会相应翻倍但一般轻量网络里都保持 1。4.3 实现过程中的几个关键细节第一点深度卷积的 padding 和 stride 要和标准卷积保持一致否则输出特征图尺寸会变化导致后续逐点卷积的输入维度对不上。第二点MobileNet 原论文里有一个容易被忽略的细节第一个标准卷积层和最后的全连接层不参与深度可分离卷积替换因为输入通道数太少RGB 三通道替换的意义不大直接用一个普通 3×3 卷积反而更高效。第三点如果你的网络要部署到移动端务必确认推理框架对 groups 卷积做了优化。很多老版本框架对深度卷积的底层实现还停留在通用分组卷积的逻辑上速度提升没有理论上那么明显选型时一定要实测。5. 实际应用MobileNet 和 Xception 的不同玩法5.1 MobileNet为移动端而生的轻量网络Google 在 2017 年提出 MobileNet V1 时核心思路就是用深度可分离卷积堆叠整个网络。V1 的网络结构非常规整除了第一层是标准 3×3 卷积中间全部由深度可分离卷积块组成。整网 28 层里绝大多数计算量都集中在 1×1 卷积上因为逐点卷积的 FLOPs 占了大约 74%而深度卷积只占很小的比例。MobileNet V1 还有一个额外的超参数叫 width multiplier宽度乘子通过对每层的通道数统一乘以一个系数来进一步控制模型大小。配合深度可分离卷积你可以把基准模型从 100% 一路缩到 25% 甚至更小这就给不同算力级别的设备提供了灵活的选择。实测下来在 ImageNet 上 MobileNet V1 的 top-1 精度只比 VGG16 低了不到一个百分点但参数量和计算量少了两到三个数量级这在当年的移动端场景完全是颠覆性的。5.2 Xception把解耦做到极致Xception 的出发点不同。它不是奔着轻量去的而是想验证一个假设跨通道相关性和空间相关性是否可以完全解耦。Xception 的做法是把深度可分离卷积里的逐点卷积放在深度卷积之前并且在每个深度卷积后面都接 BN 和 ReLU结构上更接近 Inception 模块的极端版本。Xception 的实验结果很有意思在 ImageNet 上和 Inception V3 对比参数量差不多的情况下精度更高训练收敛也更快。这说明通道间和空间间的信息解耦不仅省资源在一定程度上还能提升特征表达效率。不过要注意Xception 的参数量和 MobileNet 完全不是一个量级它属于高质量高开销的路线不适合直接搬到移动端。5.3 后续演进和其他应用深度可分离卷积的影响远不止 MobileNet 和 Xception。EfficientNet 的 MBConv 模块在深度可分离卷积的基础上加入了 SESqueeze-and-Excitation注意力机制在倒残差结构里先升维再做深度卷积再降维效果进一步提升。MobileNet V2 则引入了 Linear Bottleneck 和 Inverted Residual专门解决深度卷积在低维空间表达能力弱的问题。还有一个容易被忽视的应用场景是图像分割和目标检测的轻量化主干。像 DeepLabV3 的 encoder 部分大量使用了深度可分离卷积因为高分辨率输入本身计算量就大只有用这种结构才能在保证精度的前提下跑得动。6. 踩坑记录真实使用中的常见问题与排查技巧6.1 深度卷积之后到底要不要跟激活函数这个问题我前后纠结过很久也反复查证过。MobileNet V1 的深度卷积后面是跟 ReLU 的但 MobileNet V2 的作者发现在低维空间里 ReLU 会把很多信息映射到零造成信息丢失所以在逐点卷积降维之后不接激活函数直接进残差连接。这就是 Linear Bottleneck 的由来。实操中的建议是如果你在网络的高维特征阶段使用深度可分离卷积接 ReLU 没问题但如果通道数已经压得很低比如 32 甚至 16激活函数可能会成为瓶颈需要靠实验对比来定。6.2 深度卷积没有 BN 会怎样深度卷积的每个卷积核只处理一个通道输出通道的分布高度依赖输入通道。如果不加 BN训练很容易出现梯度不稳定尤其是网络比较深的时候收敛速度会变得很慢。我最初在自定义数据集上训练轻量分类模型时为了图省事跳过了 depthwise 后面的 BN结果 loss 振荡得很厉害加上之后就稳定了。所以深度卷积和逐点卷积之间、以及逐点卷积之后都要保留 BN这个位置很关键。6.3 部署到移动端时的真实速度问题纸面上的 FLOPs 降低不等于实际的推理提速。我踩过一个很典型的坑模型在 GPU 上测试比同等精度的标准卷积模型快了很多但交叉编译到 ARM CPU 上之后速度优势没有想象中明显。原因是很多推理库对深度卷积的底层实现没有单独优化而是走了通用的 im2col 或分组卷积路径导致内存访问不连续缓存命中率下降。解决思路有几个方向。第一优先选择对深度卷积有专门优化算子的推理框架比如较新版本的 TFLite、NCNN、MNN第二把卷积核限定为 3×3因为多数框架对 3×3 深度卷积做了汇编级优化第三如果条件允许考虑通道剪枝或者量化进一步减少逐点卷积的开销因为逐点卷积才是真正的耗时大头。问题现象可能原因排查思路模型训练 loss 不收敛深度卷积后缺少 BN检查每个 DW 和 PW 层后是否都加了 BN精度比预期低低维通道处使用 ReLU 导致信息丢失改用 Linear Bottleneck 或调整通道数GPU 快但手机端慢推理库对深度卷积优化不足换用支持 DW 专用算子的框架或将卷积核统一为 3×3输出尺寸对不上padding 或 stride 设置不一致在模型里打印每层 tensor shape 逐步核对6.4 参数初始化的小技巧深度卷积的每个通道只有 k×k 个参数数据量少初始化不当很容易导致某些通道激活值一直很小变成“死通道”。我的习惯是给深度卷积的权重设置稍大一点的标准差或者用 He 初始化后手动乘以一个大于 1 的缩放系数让每个通道在初始阶段有足够的激活幅度。这个操作在论文里很少提但实测对训练初期的稳定性帮助很大。7. 最后说点个人经验深度可分离卷积这个东西表面上看是一个结构技巧本质上反映的是一种建模思路把复杂操作拆解成语义单一、可独立优化的子操作。空间特征提取和通道特征融合本来就是两个维度的事硬绑在一起反而增加了不必要的冗余。后续的很多工作比如 Ghost Module、FasterNet 里的 Partial Convolution都是在沿着这条思路做更极致的探索。我在实际项目里最常说的一句话是不要只看 FLOPs一定要跑真机。结构优化带来的理论收益和工程收益之间隔着一个算子实现的鸿沟。选型的时候先确认目标推理框架对深度卷积的支持程度再决定主线方案。同时训练策略也要跟着结构走学习率、weight decay、BN 的 momentum 这些超参在轻量网络上的敏感度往往比大网络更高需要多一些耐心去调。如果你正准备在项目里引入深度可分离卷积我的建议是先把 MobileNet V2 的完整代码读一遍尤其是倒残差结构里的维度变化和 stride 处理再动手改自己的网络。前期多花点时间把基础打牢后面工程化的时候会省掉很多不必要的折腾。
返回列表