做模型轻量化的人绕不开一个结构——深度可分离卷积。我第一次认真研究它是在某次把实验模型从标准卷积骨干换成轻量骨干时显存占用降了一半多推理延迟从一帧300多毫秒压到了80毫秒以内准确率只掉了一个点左右。那之后就养成了一个习惯凡是往移动端或嵌入式设备上放模型第一件事就是看标准卷积能不能换成深度可分离卷积。这篇文章没有打算只讲定义而是把结构原理、数学账、代码实现、训练踩坑和推理优化整个流程串一遍我尽量用实际经验把这些内容讲透。不管你是刚接触轻量网络的学生还是已经在做模型部署的工程师又或者只是好奇为什么某些模型能跑得这么快的开发者这篇文章都能给你一套可以直接落地的参考方案。下面从最基础的问题讲起深度可分离卷积到底是怎么工作的。1. 深度可分离卷积到底是什么1.1 从标准卷积的冗余讲起先回忆一下标准卷积。一个常规的 3×3 卷积层输入通道数记作 Cᵢₙ输出通道数记作 Cₒᵤₜ那么这层卷积的权重维度是 3×3×Cᵢₙ×Cₒᵤₜ。说白了就是每个输出通道都有一组完整的 3×3×Cᵢₙ 权重用它去扫描输入特征图的所有通道把空间信息和通道信息一次性混合。这个设计在效果上没有任何问题但在计算上存在大量冗余。输出的每一个像素都要把 Cᵢₙ 个通道上的 3×3 邻域全部乘一遍再加起来。假设输入是 112×112 的 64 通道特征图输出也是 128 通道这一层的乘加次数就是 3×3×64×128×112×112差不多 92 亿次。这个数字对服务器显卡来说不算什么对手机芯片来说就很可观了。深度可分离卷积的核心想法非常简单既然标准卷积同时做了空间滤波和通道融合那为什么不把它拆成两步第一步每个通道单独做空间滤波第二步用 1×1 卷积把通道信息重新混合起来。前者叫深度卷积Depthwise Convolution后者叫逐点卷积Pointwise Convolution合起来就是深度可分离卷积Depthwise Separable Convolution。1.2 深度卷积和逐点卷积的分工深度卷积这一步很特殊它把输入特征图按通道分组每组只有一个通道然后对这个通道单独做 3×3 的空间卷积。换句话说64 个输入通道就有 64 组独立的 3×3 卷积核组和组之间完全不共享数据。这一步只改变了每个通道内部的空间信息不改变通道数量也不做跨通道的信息交流。问题来了通道之间如果完全不交流模型就无法根据多个通道的联合信息做判断。所以紧接着需要逐点卷积。逐点卷积就是 1×1 的普通卷积输入有多少通道它就用对应的权重把每个空间位置上的所有通道线性组合一次输出指定的通道数。这样拆开之后参数量从 3×3×Cᵢₙ×Cₒᵤₜ 变成了 3×3×Cᵢₙ 1×1×Cᵢₙ×Cₒᵤₜ。在 Cᵢₙ 和 Cₒᵤₜ 都是大数值时这个差距接近 8 到 9 倍。为什么是 8 到 9 倍下面第三部分会详细推导这里先有一个直观感受就够了。把这个结构和一个工厂流水线类比会很好理解标准卷积像一个全才员工一个人既要处理每块局部区域的纹理又要协调所有部门的信息深度可分离卷积则像两个专业工位第一个工位负责把每个房间的局部整理做好第二个工位再把整理好的信息汇总派发。拆开之后每个工位的工作简单了整体效率反而高了。2. 核心原理标准卷积的“分解式”工作方式2.1 权重张量分解的思想如果你学过矩阵分解比如 SVD那么深度可分离卷积的思想可以一句话讲完把一个四维权重张量近似分解成两个低维张量的组合。标准卷积的权重形状是 K×K×Cᵢₙ×Cₒᵤₜ这是一个不可分离的四维结构深度可分离卷积把它强制分解成一个 K×K×Cᵢₙ 的深度卷积核加上一个 1×1×Cᵢₙ×Cₒᵤₜ 的逐点卷积核。为什么要这样分解从表达能力的角度来讲我们其实是在做一个假设空间滤波和跨通道组合是可以解耦的。图像上相邻像素之间的相关性主要由局部纹理结构决定而特征通道之间的相关性主要由语义组合关系决定。把这两个任务分开处理让每一层只专注做一件事模型可以用更少的参数逼近原来的表达能力。这个假设在实际任务中是基本成立的。尤其是当通道数很多的时候标准卷积里大量权重本身就在做类似的事情一部分权重主要负责提取某种空间模式另一部分权重负责将这些模式重新组合。深度可分离卷积把这个隐式的分工显式化了。不过它也不是没有代价。权重张量分解等于给模型加了一个结构先验自由参数变少了每个参数承载的信息量变大了。如果网络宽度不够或者任务本身需要很强的通道交互建模能力模型容量就会成为瓶颈。这也是为什么深度可分离卷积网络通常会把通道数调得比标准卷积网络更大一些根源就在这里。2.2 感受野不变表达方式变了很多人会问一个问题把标准卷积换成深度可分离卷积感受野会不会变小答案是不会。3×3 深度卷积和 3×3 标准卷积有着完全相同的核尺寸和滑动方式所以每个输出像素对应的感受野范围完全一致。从这个角度说深度可分离卷积替换标准卷积在结构上是非常安全的。但感受野一样不等于表达方式的组合逻辑一样。标准卷积在每一步都同时做空间与通道的混合通道混合发生在空间滤波之前或者之后并无本质区别反正信息是在同一个算子内部完成交换的。深度可分离卷积则把这两件事放在两个串行算子中中间隔着激活函数和归一化层。这意味着通道信息重组是建立在空间滤波结果之上的是一种逐层递进的结构。这些差异会影响模型的优化难度。想在实践中取得好的效果就不能简单地“替换层”就完事而需要配合结构上的调整。比如把通道数放大一些或者在深度卷积之后保留足够的非线性又或者在后续层里加强通道信息流动。这些细节我在后面实操部分会逐一展开。3. 参数量和计算量的数学账3.1 公式推导省的不只是参数这一节是全文最硬核的部分但我会把每一步推导写清楚。先设卷积核尺寸为 K×K输入特征图空间尺寸为 H×W输入通道数 Cᵢₙ输出通道数 Cₒᵤₜ。忽略 padding 和 stride 带来的输出尺寸变化假设输出空间尺寸也是 H×W方便对比。标准卷积参数量K² × Cᵢₙ × Cₒᵤₜ计算量K² × Cᵢₙ × Cₒᵤₜ × H × W深度可分离卷积分两部分。深度卷积部分每个通道独立做空间滤波深度卷积参数量K² × Cᵢₙ深度卷积计算量K² × Cᵢₙ × H × W逐点卷积部分对空间每个位置做 1×1 全通道线性组合逐点卷积参数量1×1 × Cᵢₙ × Cₒᵤₜ逐点卷积计算量Cᵢₙ × Cₒᵤₜ × H × W合起来深度可分离卷积参数量K² × Cᵢₙ Cᵢₙ × Cₒᵤₜ深度可分离卷积计算量(K² Cₒᵤₜ) × Cᵢₙ × H × W两者相比计算量比值 (K² Cₒᵤₜ) / (K² × Cₒᵤₜ) 1/Cₒᵤₜ 1/K²当 K3 时这个比值约等于 0.111也就是标准卷积的九分之一。当 Cₒᵤₜ 是 128 或 256 这样的大数字时1/Cₒᵤₜ 项小到可以忽略。我们通常说深度可分离卷积能把计算量降到标准卷积的八分之一到九分之一来源就在这里。3.2 一个具体的数字例子设某层输入 64 通道输出 128 通道特征图 56×56卷积核 3×3。标准卷积的计算量 3×3×64×128×56×56 9 × 64 × 128 × 3136 ≈ 2.31 × 10⁹深度可分离卷积的计算量 深度卷积部分 3×3×64×56×56 ≈ 1.81 × 10⁶ 逐点卷积部分 64×128×56×56 ≈ 2.57 × 10⁷ 合计 ≈ 2.75 × 10⁷两者相除节省约 8.4 倍。参数量同理标准卷积参数量 9×64×128 73728深度可分离卷积参数量 9×64 64×128 576 8192 8768节省也是约 8.4 倍。这种计算量上的优势在大通道数网络上体现得尤其明显。3.3 和分组卷积、1×1卷积的关系深度可分离卷积不是凭空冒出来的结构它是分组卷积的一个极端特例。分组卷积把输入通道分成 G 组每组内部做标准卷积。当 G1 时就是标准卷积当 GCᵢₙ 时每组只有一个通道这就是深度卷积。逐点卷积呢它在数学上就是一个 1×1 的普通卷积也可以看成分组 G1 的分组卷积。所以整个深度可分离卷积实际上就是“分组卷积的两个极端情况先后登场”。这个视角很有用。做模型设计时你可以在分组数 G 上做插值G 越大越接近深度可分离卷积参数和计算量越小G 越小越接近标准卷积表达能力越强。很多网络结构比如分组卷积和通道混洗的组合本质上就是在“通道分组”和“分组间信息流动”之间做权衡。理解这条谱系比死记某个特定结构更有价值。4. 在经典网络中的设计与应用细节4.1 轻量骨干网络里的设计演进深度可分离卷积真正走入大众视野是从轻量骨干网络开始的。早期轻量网络的做法很简单把标准卷积模块换成深度卷积加逐点卷积前面接归一化中间和末尾接 ReLU。这样一个基本单元在主干网络里反复堆叠网络宽度用两个超参数来控制宽度倍率缩放通道数分辨率倍率缩放输入分辨率。这里的实现有一个容易被忽略的点网络的第一层通常仍然保留标准卷积因为输入通道只有 3 或 4通道数太少拆分成深度卷积和逐点卷积带来的计算收益非常有限直接用一个标准卷积反而更划算。在轻量骨干网络的第二版里设计思路发生了变化。作者发现 ReLU 在低维空间会大量破坏信息导致深度可分离卷积块输出表达能力不足。于是做出了一个看起来反直觉的设计在逐点卷积扩张到高维空间时用一个较窄的瓶颈结构并且把最后一层的 ReLU 去掉换成线性激活。这就是线性瓶颈加上反向残差结构。为了验证这个设计我还专门做过对比实验同一个数据集上带线性瓶颈的网络比不带线性瓶颈的版本准确率高出一个百分点左右参数量几乎没变。这类结构之所以有效和深度可分离卷积的特性是匹配的。深度卷积本身通道信息隔离逐点卷积负责通道重组如果中间的非线性太强高维空间的信息投影回低维时会损失严重线性瓶颈正好避免了这个损失。4.2 深度可分离卷积与 Inception 结构的渊源深度可分离卷积的另一个来源可以追溯到 Inception 系列。Inception 模块的思路是在每个分支上先用 1×1 卷积降维然后让不同尺寸的空间卷积并行提取特征最后把各分支的输出在通道维度上拼接起来。这个设计本质上也是在空间滤波和通道组合之间做解耦。更进一步的做法是把 Inception 模块中所有分支共享一个深度卷积再在最终输出前用一个统一的逐点卷积来做通道混合这就非常接近深度可分离卷积的完整形态了。后来有工作沿着这个方向走得更彻底整个网络基本由深度可分离卷积堆叠而成把通道混合和空间滤波彻底分开。随着网络加深每个模块的通道数不断增加但计算量保持在一个相对较低的水平。这类网络在图像分类任务上的表现证明了深度可分离卷积在深层网络中的扩展能力。4.3 语义分割、检测任务中的使用模式深度可分离卷积在检测和分割任务里同样常见。举个例子在 U-Net 这类编解码结构里编码器特征图分辨率高计算量主要集中在早期的高分辨率层。把这些层的 3×3 标准卷积替换成深度可分离卷积可以显著降低编码器计算开销而解码器部分通常保持标准卷积以保证上采样细节的质量。我在某个分割任务里做过一次替换把模型中间两层换成深度可分离卷积之后参数量减少了约 35%单张 512×512 图像的推理耗时降低了约 40%分割精度只下降了约 1 个百分点。如果对精度有更高要求可以同时在解码器对应层增加一点通道数基本就能追平。这种“在主干中替换、在关键位置保留”的用法比全局一刀切替换要稳妥得多。因为任务不同不同层对通道交互的敏感度也不同一味追求计算缩减容易捡了芝麻丢了西瓜。5. 代码复现从 PyTorch 到部署优化5.1 按结构推导写出第一个版本直接上代码。一个最直观的深度可分离卷积模块可以用 PyTorch 的groups参数实现。深度卷积的本质是每个通道独立卷积groupsin_channels正好实现这一点。import torch import torch.nn as nn class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, stride1, padding1, use_pointwise_actTrue): super().__init__() self.depthwise nn.Conv2d( in_channels, in_channels, kernel_sizekernel_size, stridestride, paddingpadding, groupsin_channels, biasFalse ) self.bn1 nn.BatchNorm2d(in_channels) self.act1 nn.ReLU(inplaceTrue) self.pointwise nn.Conv2d( in_channels, out_channels, kernel_size1, stride1, padding0, biasFalse ) self.bn2 nn.BatchNorm2d(out_channels) self.act2 nn.ReLU(inplaceTrue) if use_pointwise_act else nn.Identity() def forward(self, x): x self.depthwise(x) x self.bn1(x) x self.act1(x) x self.pointwise(x) x self.bn2(x) x self.act2(x) return x这里有几个细节值得说明。深度卷积我设了biasFalse因为后面紧跟 BatchNormbias 会被 BN 的均值归一化抵消留着没有意义还会多出无用参数。逐点卷积同样不设 bias后面的 BN 会接管偏置项。5.2 验证计算量与参数量写完了模块建议先做个快速验证确保自己的实现真的省了参数和计算量。用一个小脚本统计一下def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) in_ch, out_ch 64, 128 ds_conv DepthwiseSeparableConv(in_ch, out_ch, kernel_size3, padding1) # 标准卷积对照 std_conv nn.Conv2d(in_ch, out_ch, kernel_size3, padding1) print(f标准卷积参数量: {count_parameters(std_conv)}) print(f深度可分离卷积参数量: {count_parameters(ds_conv)}) print(f比值: {count_parameters(ds_conv) / count_parameters(std_conv):.4f})输出结果会显示深度可分离卷积的参数量大约是标准卷积的八到九分之一。需要注意的是这个统计包含了 BN 层的参数BN 每层有两个可学习参数缩放和偏移所以实际压缩比会比纯卷积核参数略低这在对比时很容易让人困惑。去掉 BN 之后再对比会更接近理论值。5.3 训练时该怎么调整超参数虽然深度可分离卷积结构简单但训练时和标准卷积还是有些不同。根据我的经验以下几点尤其重要学习率宜小不宜大。深度可分离卷积的可学习参数大量集中在逐点卷积上逐点卷积的梯度相对稠密用太激进的学习率容易震荡。落地时通常把标准卷积的学习率设置直接拿来用会有一点风险建议先降低到原来的 70% 左右观察几个 epoch 的损失曲线。权重初始化用常见的 Kaiming 初始化就够了但要注意深度卷积层的 fan_in 计算会比较特殊因为每个通道只有 K² 个权重所以在实测中如果发现某些通道输出恒为常数多半是初始化出了极端值配合 BN 的前几个 batch 统计数据不准导致。可以把 BN 的 momentum 稍微调小一点比如 0.01 改成 0.001让均值方差统计更稳定。ReLU 置换为 LeakyReLU 有时能换来稳定收益。深度可分离卷积网络通常更瘦通道少ReLU 大量置零后信息容量进一步缩小。我在某个分割模型上试过把激活换成 LeakyReLU负斜率 0.1mIoU 提升了约 0.8 个百分点几乎不增加耗时。如果做迁移学习或从头训练尽量保证每个深度卷积后面有 BN。深度卷积各通道独立计算不同通道的数据分布差异很大没有 BN 很容易让某些通道响应量级失控出现输出溢出或梯度消失。5.4 部署阶段算子融合与内存布局理论计算量降低了八倍但真实设备上的推理耗时能不能也降低八倍答案往往是不能。我在移动端的实践中把普通 3×3 卷积换成深度可分离卷积后实测提速通常只有两到四倍原因主要有两个。第一个原因是算子启动和调度的开销。深度卷积本质上是很多个小矩阵乘法每个矩阵都很小无论是 GPU 还是 CPU一次 kernel 调用只能处理很少的数据调度成本占比很高。第二个原因是内存访问密度。深度卷积每个通道的输入输出都很小计算和访存的比例很低内存带宽反而成了瓶颈。解决这两个问题的方法主要有两种。第一种是算子融合把“深度卷积 BN 激活 逐点卷积”融合成单一算子中间不经过全局内存数据在寄存器或片上缓存里直接流转。很多推理引擎都支持这种融合只是默认开关可能没打开。我在实践中开启融合开关后端到端延迟进一步降低了 12% 左右。第二种是使用通道在内存中连续的布局也就是通常说的 NHWC 数据排布。深度卷积按通道独立处理如果数据是 NCHW 排布每个通道的数据是连续的一大块深度卷积反而要频繁跨内存访问不同通道的片段改成 NHWC 后每个空间位置的各通道值相邻对深度卷积和逐点卷积都更友好。如果这些优化还不够就需要考虑手写自定义算子或者依托推理框架的高性能算子实现了。深度卷积的处理在高性能计算库里有专门优化和标准卷积的实现路径差别很大。这一点是很多只在 PyTorch 里做实验的人容易忽略的模型在服务器上训练时用的是高精度框架部署到设备上时走的是推理引擎两者对深度可分离卷积的支持程度差异极大。选推理引擎时一定要先确认它的深度卷积算子是不是原生优化的版本而不是简单用通用卷积算子去模拟。6. 常见问题与实战排查参考6.1 核心问题速查表我把自己和身边人踩过的坑整理成了一张速查表遇到问题可以先对照排查常见问题可能原因解决方案替换后准确率掉得多通道数不足表达容量下降适当加宽通道数或只在信息冗余度较高的层做替换训练时损失震荡不收敛学习率偏高逐点卷积梯度较密降低学习率至原来的 70% 左右观察 3~5 个 epoch推理速度提升远低于预期深度卷积算子未融合内存访问受限开启算子融合切换通道内存布局到 NHWC某些输出通道恒为常数深度卷积权重初始化为极端值BN 统计不稳定降低 BN 的 momentum改用 LeakyReLUGPU 上速度反而更慢小算子并行度不足kernel 启动开销大优先在 CPU/移动端使用GPU 场景需自定义融合 kernel逐点卷积部分开销太大通道数较大时逐点卷积占大头考虑用分组逐点卷积或通道混洗等替代方案6.2 深度卷积的“通道孤立”陷阱深度卷积完全不做通道间信息交换这是一个结构上的硬约束。如果整个网络都只堆叠深度可分离卷积而没有任何机制来促进通道混合模型就退化成一个逐通道独立处理的空间滤波器集合表达能力会大打折扣。实践中有两个常用的补救方法。一个是在逐点卷积之外显式地增加通道混洗操作让通道间的信息在下一层之前发生交互。另一个是保留少量标准卷积层尤其是主干网络的前几层和后几层让这些层承担强通道交互的任务。我在网络设计时通常是在每个大阶段的最后一层保留一个标准卷积作为通道信息的汇合点效果比纯深度可分离卷积堆叠稳定很多。6.3 和普通卷积混合使用的案例参考最后分享一个我实际做过的小实验。某次在把标准卷积骨干改造成轻量骨干时我最初尝试把所有 3×3 卷积全部替换成深度可分离卷积结果在某个验证集上准确率掉了约 2 个百分点参数量倒是降了很多。随后我做了两个调整一是把网络中部两层的通道数各乘以 1.5二是保留最后一个阶段的标准卷积不替换。同样的训练配置下准确率差距缩小到 0.5 个百分点以内而计算量仍然只有原模型的六成左右。这个案例说明深度可分离卷积不是简单的“免费午餐”它需要和网络宽度、阶段位置、任务特性配合才能发挥价值。没有一种结构是万能的深度可分离卷积的优势在于它把空间与通道处理解耦给了设计者一个更精细的调节旋钮能不能用好这个旋钮就看你对任务的理解了。我的个人体会是凡是涉及轻量模型的场景先别急着堆各种花哨结构把深度可分离卷积这一课吃透理解它的计算账和表达能力边界就已经能解决大部分实际问题了。后续再遇到更多轻量化结构比如通道混洗、动态卷积你会发现它们都是在这条思路上做文章一通百通。
阅读完成 · 觉得有帮助?