
1. 从一次模型瘦身说起为什么卷积方式值得深挖前阵子帮一个做移动端图像分类的朋友看模型他拿着一个在服务器上跑得挺欢的网络想直接塞进手机里结果模型体积和推理耗时都超标得离谱。我扫了一眼他的卷积层配置清一色的标准卷积通道数还堆得挺大。当时我就跟他说你这网络不是不能压是你压根没往卷积方式上想。后来把主干里的标准卷积换成深度可分离卷积参数量直接掉到原来的八分之一左右精度只掉了不到一个点重新训练几个epoch就追回来了。这件事让我觉得可分离卷积和深度可分离卷积这两个概念虽然在网上被讲烂了但真正动手用的时候很多人还是分不清它们到底差在哪、什么时候该用哪个、参数是怎么省下来的。所以这篇就从一个实际从业者的角度把这两种卷积从头拆一遍顺带把参数量计算、实现细节、踩过的坑都摊开讲。不管你是刚入门卷积神经网络的新手还是已经在做模型部署的老手应该都能从里面捞到点能直接用的东西。先说清楚这两个词的关系免得后面绕晕。可分离卷积是一个大范畴它指的是把一次卷积运算拆成两个或多个步骤来完成的思路。而深度可分离卷积是可分离卷积里最出名、也是实际用得最多的一种具体实现它把标准卷积拆成逐通道卷积加逐点卷积两步。你可以理解成可分离卷积是分而治之这个方法论深度可分离卷积是这套方法论在卷积神经网络里最成功的一个落地案例。除此之外还有空间可分离卷积这类玩法后面会一并讲到。这篇文章我会按这样的顺序展开先把标准卷积的参数量和计算量算清楚作为对比基准然后讲空间可分离卷积它是理解可分离思想的最好入口接着重点拆深度可分离卷积的两步结构把参数量、计算量的账一笔笔算给你看再往下是实操层面的实现代码、训练技巧和常见坑最后聊聊这个思路还能怎么扩展。全程用生活化的类比加实际数字保证你看完能自己动手改网络。2. 先把基准打牢标准卷积到底在算什么2.1 标准卷积的运作方式与参数量公式要理解可分离省在哪得先知道不可分离费在哪。标准卷积的操作我习惯用一个类比想象你有一摞彩色透明胶片每张代表一个输入通道然后你拿一个小窗口卷积核同时盖住所有胶片上的同一块区域把窗口里所有像素按权重加权求和得到一个输出值。这个窗口在所有通道上滑动一遍就生成了一个输出通道的特征图。关键点在于标准卷积的一个卷积核是同时跨所有输入通道的。假设输入通道数是 $C_{in}$输出通道数是 $C_{out}$卷积核空间尺寸是 $K \times K$那么一个卷积核的参数量是 $K \times K \times C_{in}$而你要生成 $C_{out}$ 个输出通道就需要 $C_{out}$ 个这样的卷积核。所以总参数量是$$Params_{standard} K \times K \times C_{in} \times C_{out}$$计算量以乘加次数 MAC 计则是参数量再乘上输出特征图的空间尺寸 $H_{out} \times W_{out}$$$MACs_{standard} K \times K \times C_{in} \times C_{out} \times H_{out} \times W_{out}$$拿个具体数字感受一下。假设输入是 $224 \times 224 \times 3$ 的彩色图第一层用 $3 \times 3$ 卷积输出 64 通道那么参数量是 $3 \times 3 \times 3 \times 64 1728$。这个数看着不大但到了深层就吓人了。比如中间某层输入 256 通道、输出 256 通道、$3 \times 3$ 卷积参数量是 $3 \times 3 \times 256 \times 256 589824$接近 59 万。一个网络里几十个这样的层堆起来参数量轻松上千万。2.2 为什么标准卷积在移动端会翻车移动端设备的算力和内存都有限标准卷积的问题就暴露得很明显。我总结下来主要是三个参数量大导致模型体积大上面那个 59 万的例子一个层就占 2MB 多的存储按 float32 算几十层下来模型动辄上百 MB塞进 App 里用户下载都嫌大。计算量大导致推理慢MACs 直接决定了推理耗时标准卷积的 MACs 随通道数平方增长深层网络里这是主要瓶颈。内存访问密集标准卷积要同时读取所有输入通道的数据对缓存不友好在手机这种内存带宽受限的设备上实际耗时往往比理论 MACs 还要差。提示很多人优化模型只盯着参数量其实在移动端计算量和内存访问模式往往比参数量更影响实际速度。一个参数量小但访存模式差的模型可能比参数量大但访存友好的模型还慢。所以业界一直在找既保留卷积表达能力又能大幅降低参数量和计算量的方案可分离卷积就是在这个背景下被反复研究和应用的。它的核心思想很朴素与其让一个卷积核同时处理空间信息和通道信息不如把这两件事拆开各干各的。这个思路一旦想通后面的深度可分离卷积就顺理成章了。3. 空间可分离卷积理解可分离思想的最佳入口3.1 用一维卷积拆解二维卷积空间可分离卷积是可分离思想最直观的体现。它的核心观察是一个 $K \times K$ 的二维卷积核在某些情况下可以拆成一个 $K \times 1$ 的列向量和一个 $1 \times K$ 的行向量做外积。最经典的例子是 Sobel 算子、高斯模糊核这类它们本身就是可分离的。举个具体的。一个 $3 \times 3$ 的高斯核1/16 * [[1, 2, 1], [2, 4, 2], [1, 2, 1]]它可以拆成1/4 * [1, 2, 1] 和 1/4 * [[1], [2], [1]]也就是先做一个 $1 \times 3$ 的行卷积再做一个 $3 \times 1$ 的列卷积两步的结果和直接用 $3 \times 3$ 卷积是一样的。这就是空间可分离卷积。3.2 参数量与计算量的收益账这么拆有什么好处算笔账。原来一个 $3 \times 3$ 卷积核有 9 个参数拆成 $1 \times 3$ 加 $3 \times 1$ 之后参数变成 $3 3 6$ 个。看着省得不多但推广到 $K \times K$原始参数量$K^2$可分离后参数量$2K$当 $K 7$ 时原始是 49可分离后是 14省了 70% 多。计算量同理从 $K^2 \times H \times W$ 降到 $2K \times H \times W$。不过这里有个关键限制空间可分离卷积只对本身就可分离的卷积核成立。也就是说你不能随便拿一个训练出来的 $3 \times 3$ 卷积核就把它拆开因为大多数学习到的卷积核并不是可分离的。所以空间可分离卷积在实际网络里用得并不多它更多是作为一个理解可分离思想的跳板。注意有些教程会把空间可分离卷积说成可以随便拆这是误导。只有满足秩为 1 的卷积核才能精确拆成两个一维卷积一般学习到的核不满足这个条件。强行拆会损失表达能力。3.3 它给我们的启发虽然空间可分离卷积本身用得少但它传递了一个重要信号卷积运算里存在冗余可以通过结构化分解来压缩。这个信号直接启发了深度可分离卷积——既然空间维度可以拆那通道维度是不是也能拆答案是能而且效果更好、适用性更广因为通道维度的拆分不要求卷积核满足什么特殊条件任何标准卷积都能改造成深度可分离卷积。这就是为什么深度可分离卷积成了移动端网络的主力而空间可分离卷积更多停留在教学和特定算子里。4. 深度可分离卷积把通道和空间彻底分开4.1 两步走逐通道卷积加逐点卷积深度可分离卷积把标准卷积拆成两个独立的步骤我习惯用先各自处理再汇总融合来概括。第一步逐通道卷积Depthwise Convolution。这一步对每个输入通道单独用一个卷积核做空间卷积通道之间互不干扰。输入有几个通道就用几个卷积核每个卷积核只在自己的通道上滑动。输出通道数等于输入通道数。这一步负责提取空间特征。第二步逐点卷积Pointwise Convolution。这一步用 $1 \times 1$ 的卷积核在通道维度上做加权组合把上一步的输出通道数从 $C_{in}$ 变换到 $C_{out}$。这一步负责通道间的信息融合。用生活类比标准卷积像是一个大厨同时看着所有食材通道和火候空间来炒一盘菜深度可分离卷积则是先让每个帮厨各自处理一种食材逐通道最后大厨再用一个统一的调味方案把所有处理好的食材拌在一起逐点。分工明确各司其职。4.2 参数量对比一笔一笔算清楚现在来算账这是理解深度可分离卷积价值的关键。逐通道卷积的参数量每个输入通道一个 $K \times K$ 核共 $C_{in}$ 个所以是$$Params_{depthwise} K \times K \times C_{in}$$逐点卷积的参数量$1 \times 1$ 卷积输入 $C_{in}$ 通道输出 $C_{out}$ 通道所以是$$Params_{pointwise} 1 \times 1 \times C_{in} \times C_{out} C_{in} \times C_{out}$$深度可分离卷积总参数量$$Params_{sep} K \times K \times C_{in} C_{in} \times C_{out}$$对比标准卷积的 $K \times K \times C_{in} \times C_{out}$两者的比值是$$\frac{Params_{sep}}{Params_{standard}} \frac{1}{C_{out}} \frac{1}{K^2}$$这个公式很漂亮它告诉我们深度可分离卷积相对标准卷积的参数量压缩比主要取决于输出通道数和卷积核大小。当 $K 3$、$C_{out}$ 较大时第二项 $1/K^2 1/9$ 是主导第一项 $1/C_{out}$ 可以忽略所以参数量大约降到原来的九分之一。拿前面那个例子验证输入 256 通道、输出 256 通道、$3 \times 3$ 卷积。标准卷积$3 \times 3 \times 256 \times 256 589824$深度可分离$3 \times 3 \times 256 256 \times 256 2304 65536 67840$比值约 0.115也就是省了将近 88.5% 的参数。这个数字在深层网络里累积起来非常可观。4.3 计算量对比为什么实际加速比没参数量那么夸张计算量的账类似。逐通道卷积的 MACs 是 $K \times K \times C_{in} \times H \times W$逐点卷积是 $C_{in} \times C_{out} \times H \times W$加起来对比标准卷积的 $K \times K \times C_{in} \times C_{out} \times H \times W$比值同样是$$\frac{1}{C_{out}} \frac{1}{K^2}$$理论上计算量也降到约九分之一。但实际部署时加速比往往达不到九倍我实测下来通常在三到六倍之间。原因有几个逐点卷积的访存开销大$1 \times 1$ 卷积虽然 MACs 低但它是访存密集型操作内存带宽容易成为瓶颈。逐通道卷积的并行度低每个通道独立计算GPU 上不容易打满算力。框架实现差异不同推理引擎对深度可分离卷积的优化程度差别很大。提示评估深度可分离卷积的实际收益别只看理论 MACs一定要在目标设备上实测。我见过理论省 8 倍、实测只快 2 倍的情况也见过优化好的引擎能跑到 6 倍以上。4.4 一个容易混淆的点它和分组卷积的关系经常有人把深度可分离卷积和分组卷积搞混。分组卷积是把输入通道分成若干组每组内部做标准卷积组间不交互。当分组数等于输入通道数时分组卷积就退化成了逐通道卷积。所以逐通道卷积可以看成是分组卷积的一个极端特例。但深度可分离卷积比它多了逐点卷积这一步正是这一步补回了通道间的信息交互让精度不至于掉太多。这个区别在面试和实际选型时经常被问到值得记牢。5. 动手实现从零写一遍深度可分离卷积5.1 用 PyTorch 手写两步结构光看公式不够自己写一遍才踏实。下面用 PyTorch 实现一个深度可分离卷积层把两步拆开写方便你对照理解。import torch import torch.nn as nn class DepthwiseSeparableConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride1, padding0): super().__init__() # 第一步逐通道卷积groupsin_channels 是关键 self.depthwise nn.Conv2d( in_channels, in_channels, kernel_sizekernel_size, stridestride, paddingpadding, groupsin_channels, # 每个通道独立卷积 biasFalse ) # 第二步逐点卷积1x1 卷积做通道融合 self.pointwise nn.Conv2d( in_channels, out_channels, kernel_size1, stride1, padding0, biasFalse ) self.bn nn.BatchNorm2d(out_channels) self.relu nn.ReLU(inplaceTrue) def forward(self, x): x self.depthwise(x) x self.pointwise(x) x self.bn(x) x self.relu(x) return x这里最关键的参数是groupsin_channels它告诉 PyTorch 把输入通道分组每组一个通道这正是逐通道卷积的实现方式。很多人第一次写会忘了这个参数结果写出来还是标准卷积。5.2 参数量验证用代码对账写完别急着用先验证参数量对不对得上公式。跑一段代码def count_params(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) # 深度可分离卷积 sep_conv DepthwiseSeparableConv(256, 256, 3, padding1) # 标准卷积 std_conv nn.Conv2d(256, 256, 3, padding1, biasFalse) print(深度可分离参数量:, count_params(sep_conv)) print(标准卷积参数量:, count_params(std_conv)) print(压缩比:, count_params(sep_conv) / count_params(std_conv))按前面的公式深度可分离应该是 $3 \times 3 \times 256 256 \times 256 67840$标准卷积是 $3 \times 3 \times 256 \times 256 589824$比值约 0.115。跑出来如果对得上说明你的实现没问题。这一步我强烈建议每个人都做一遍比看十遍公式都管用。5.3 训练时的几个实操要点把标准卷积换成深度可分离卷积之后训练策略也得跟着调整不然精度容易掉。我踩过的坑总结成几条学习率适当调小深度可分离卷积的参数少梯度更新的绝对量也小用原来的学习率容易震荡。我一般会把学习率降到原来的 0.5 到 0.7 倍。训练轮数适当增加参数少了收敛需要的迭代次数可能反而变多别急着早停。注意 BatchNorm 的位置逐点卷积之后接 BN 是标配但逐通道卷积之后要不要接 BN 要看具体结构。我试过两种接在逐点之后效果更稳。权重初始化逐通道卷积的核建议用较小的初始化避免初始阶段输出方差过大。注意如果你是从一个预训练的标准卷积网络改造成深度可分离结构不要指望直接加载原权重就能用两者的参数形状完全不同。要么从头训练要么用知识蒸馏的方式让新网络学原网络的输出。6. 常见问题与排查技巧实录6.1 精度掉太多怎么办这是最常见的抱怨。深度可分离卷积确实会损失一部分表达能力因为逐通道卷积阶段通道间没有交互。如果精度掉得离谱先排查这几个方向问题现象可能原因排查与解决精度掉 5 个点以上网络容量不足适当增加输出通道数或减少下采样频率训练不收敛学习率过大降低学习率加 warmup验证集波动大BN 统计不稳增大 batch size或改用 GroupNorm特定类别精度崩通道融合不足在关键层保留标准卷积混合使用我的经验是不要全网络一刀切换成深度可分离卷积。主干网络的前几层和最后几层通常对精度影响大保留标准卷积中间层用深度可分离这样精度和速度的平衡最好。MobileNet 系列就是这么做的。6.2 实际推理没变快理论省九倍实测只快两倍这种情况太常见了。排查思路确认推理引擎是否支持有些老版本引擎对groups参数优化很差升级引擎往往立竿见影。检查是否被其他层拖累如果网络里还有大量全连接层或大尺寸标准卷积深度可分离卷积省下的时间会被吃掉。测端到端耗时而非单层单层测出来快端到端可能因为访存模式变化而没快多少。考虑量化深度可分离卷积对量化很友好配合 int8 量化往往能再提速一大截。6.3 和分组卷积选哪个这个问题没有标准答案取决于你的目标。分组卷积的组数是个可调超参灵活度高深度可分离卷积结构固定实现简单、压缩比明确。我的一般建议是追求极致压缩用深度可分离追求精度和速度的精细平衡用分组卷积调组数。两者也可以混用比如某些层用分组、某些层用深度可分离。6.4 一个容易被忽略的坑padding 和 stride 的处理逐通道卷积和逐点卷积的 stride 和 padding 要分开设置。空间下采样只发生在逐通道卷积这一步逐点卷积永远是 stride1、padding0。我见过有人把 stride 同时设到两步上结果特征图尺寸算错网络直接报错。记住下采样归逐通道通道变换归逐点各管各的。7. 这个思路还能怎么扩展深度可分离卷积不是终点它背后的分解卷积思想还在被不断延伸。比如有些工作把逐点卷积进一步拆成更稀疏的结构或者把逐通道卷积的核做成可变形的方式让它在空间上更灵活。还有的把深度可分离和注意力机制结合用注意力来决定通道融合的权重相当于让逐点卷积变得更聪明。从工程角度看我觉得最值得关注的方向是硬件感知的卷积设计。同一个深度可分离卷积在不同芯片上的实际表现差别很大未来的趋势是根据目标硬件反向设计卷积结构而不是先设计好再想办法部署。这个思路在近几年的移动端网络设计里已经越来越明显了。如果你现在手上正好有个模型要往移动端塞我的建议是先从中间层开始替换边换边测精度和速度找到那个平衡点。别一上来就全换也别指望一次就调到最优。我调这类模型通常要来回折腾好几轮每轮记录下参数量、MACs、实测耗时和精度最后选综合最优的那版。这个过程本身比记住几个公式有价值得多。