ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

可分离卷积与深度可分离卷积:从原理到移动端部署实战

可分离卷积与深度可分离卷积:从原理到移动端部署实战

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, stride=1, padding=0): super().__init__() # 第一步:逐通道卷积,groups=in_channels 是关键 self.depthwise = nn.Conv2d( in_channels, in_channels, kernel_size=kernel_size, stride=stride, padding=padding, groups=in_channels, # 每个通道独立卷积 bias=False ) # 第二步:逐点卷积,1x1 卷积做通道融合 self.pointwise = nn.Conv2d( in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False ) self.bn = nn.BatchNorm2d(out_channels) self.relu = nn.ReLU(inplace=True) def forward(self, x): x = self.depthwise(x) x = self.pointwise(x) x = self.bn(x) x = self.relu(x) return x

这里最关键的参数是groups=in_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, padding=1) # 标准卷积 std_conv = nn.Conv2d(256, 256, 3, padding=1, bias=False) 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 要分开设置。空间下采样只发生在逐通道卷积这一步,逐点卷积永远是 stride=1、padding=0。我见过有人把 stride 同时设到两步上,结果特征图尺寸算错,网络直接报错。记住:下采样归逐通道,通道变换归逐点,各管各的。

7. 这个思路还能怎么扩展

深度可分离卷积不是终点,它背后的"分解卷积"思想还在被不断延伸。比如有些工作把逐点卷积进一步拆成更稀疏的结构,或者把逐通道卷积的核做成可变形的方式,让它在空间上更灵活。还有的把深度可分离和注意力机制结合,用注意力来决定通道融合的权重,相当于让逐点卷积变得更"聪明"。

从工程角度看,我觉得最值得关注的方向是硬件感知的卷积设计。同一个深度可分离卷积,在不同芯片上的实际表现差别很大,未来的趋势是根据目标硬件反向设计卷积结构,而不是先设计好再想办法部署。这个思路在近几年的移动端网络设计里已经越来越明显了。

如果你现在手上正好有个模型要往移动端塞,我的建议是先从中间层开始替换,边换边测精度和速度,找到那个平衡点。别一上来就全换,也别指望一次就调到最优。我调这类模型通常要来回折腾好几轮,每轮记录下参数量、MACs、实测耗时和精度,最后选综合最优的那版。这个过程本身比记住几个公式有价值得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进