4、卷积核优化:深度可分离卷积的原理与在 ISP 中的应用

说到模型轻量化,深度可分离卷积是个绕不开的话题。很多刚入行的同学问我,为什么标准卷积在端侧跑不动?说白了,就是计算量太大了。我最早在手机上做去噪模型时,一个 3x3 的卷积层就把骁龙 865 的 NPU 占满了,帧率惨不忍睹。后来换了深度可分离卷积,计算量直接降了七八成,效果还基本没掉。嗯,今天我们就来拆解一下这个技术。

4.1 标准卷积的计算瓶颈

先看标准卷积。假设输入特征图是 H×W×Cin,输出是 H×W×Cout,卷积核大小是 K×K。那一次卷积的计算量是多少?

标准卷积计算量 = H × W × Cin × Cout × K × K

举个例子。输入 128×128×32,输出 128×128×64,3×3 卷积。计算量就是:

128 × 128 × 32 × 64 × 3 × 3 = 301,989,888 次乘加

3 亿次操作。在端侧 ISP 里,一张图要跑几十层这样的卷积,发热和功耗根本扛不住。为什么会这么高?因为标准卷积同时做了两件事:空间特征提取和通道信息融合。这两件事绑在一起,计算量就爆炸了。

4.2 深度可分离卷积:拆开做

深度可分离卷积的思路很简单——把上面两件事拆开:

  1. Depthwise Conv(逐通道卷积):每个通道单独做空间卷积,不跨通道。
  2. Pointwise Conv(逐点卷积):用 1×1 卷积做通道融合。

计算量对比一下就清楚了:

卷积类型 计算量公式 示例计算量(128×128×32→64)
标准卷积 H×W×Cin×Cout×K×K 301,989,888
深度可分离卷积 H×W×Cin×K×K + H×W×Cin×Cout 4,718,592 + 33,554,432 = 38,273,024

从 3 亿降到 3800 万,差不多是 1/8。这个差距在端侧就是能不能跑起来的区别。

我的经验:在 ISP 任务里,深度可分离卷积的参数量大约是标准卷积的 1/K² + 1/Cout。K=3 时,参数量能降到 1/9 到 1/8。但要注意,通道数越少,压缩比越不明显。我一般建议 Cin 和 Cout 都大于 16 时才用深度可分离。

4.3 深度可分离卷积的结构图

下面这张图展示了标准卷积和深度可分离卷积的流程对比。我习惯用这种图给团队新人讲,一看就明白。

标准卷积 vs 深度可分离卷积 标准卷积 输入 K×K 卷积核 (空间+通道) 输出 计算量: H×W×Cᵢₙ×Cₒᵤₜ×K×K 深度可分离卷积 输入 Depthwise 逐通道空间卷积 Pointwise 1×1 通道融合 输出 计算量: H×W×Cᵢₙ×K×K + H×W×Cᵢₙ×Cₒᵤₜ 深度可分离卷积将空间卷积和通道融合解耦,计算量约为标准卷积的 1/8 适合端侧 ISP 中的去噪、超分、HDR 等任务 关键对比 标准卷积 • 空间+通道同时处理 • 计算量大,参数量多 • 端侧推理延迟高 深度可分离卷积 • 空间+通道分步处理 • 计算量降至 1/8~1/9 • 端侧推理速度快 5~8 倍

4.4 在 ISP 中的具体应用

深度可分离卷积在端侧 ISP 里用得最多的地方,我总结下来有三个:

4.4.1 去噪网络中的特征提取

去噪任务需要大感受野,但端侧算力有限。我习惯在 U-Net 的编码器部分用深度可分离卷积替代标准卷积。比如一个 3×3 的 Depthwise + 1×1 的 Pointwise,感受野不变,计算量却降了 8 倍。我在某款旗舰机的夜景模式里就是这么做的,去噪效果没掉,帧率从 15fps 提到了 30fps。

4.4.2 超分网络的上采样前处理

超分模型通常先做特征提取,再做上采样。特征提取阶段用深度可分离卷积,能省下大量算力给上采样层。我建议在 ESPCN 或 FSRCNN 的浅层特征提取部分全部换成深度可分离卷积,参数量能降 60% 以上。

4.4.3 HDR 融合中的多尺度特征

HDR 融合需要处理多曝光图像,特征图通道数通常很大(64 或 128)。这时候深度可分离卷积的优势就出来了——通道数越大,压缩比越高。我在一个 HDR 项目里,把融合网络的中间层全部换成深度可分离卷积,模型大小从 12MB 降到了 3.5MB,推理时间从 120ms 降到了 35ms。

我曾经踩过的坑:深度可分离卷积在通道数很少(比如 4 或 8)时,压缩效果不明显,甚至可能因为 Depthwise 层缺乏跨通道信息交互而导致精度下降。我的建议是:通道数低于 16 时慎用,或者配合 Channel Shuffle 操作来弥补信息流动不足。

4.5 代码实现示例

下面是我在项目中常用的深度可分离卷积实现,基于 PyTorch。注意这里用了分组卷积来实现 Depthwise 部分:

import torch
import torch.nn as nn

class DepthwiseSeparableConv(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1):
        super().__init__()
        # Depthwise: 分组数 = 输入通道数
        self.depthwise = nn.Conv2d(
            in_channels, in_channels, kernel_size,
            stride=stride, padding=padding,
            groups=in_channels,  # 关键:每个通道独立卷积
            bias=False
        )
        # Pointwise: 1x1 卷积做通道融合
        self.pointwise = nn.Conv2d(
            in_channels, out_channels, 1,
            stride=1, padding=0,
            bias=True
        )
        # 我习惯在 Depthwise 后加 BN 和 ReLU
        self.bn = nn.BatchNorm2d(in_channels)
        self.relu = nn.ReLU(inplace=True)

    def forward(self, x):
        x = self.depthwise(x)
        x = self.bn(x)
        x = self.relu(x)
        x = self.pointwise(x)
        return x

# 使用示例
model = DepthwiseSeparableConv(in_channels=32, out_channels=64, kernel_size=3)
input_tensor = torch.randn(1, 32, 128, 128)
output = model(input_tensor)
print(f"输出形状: {output.shape}")  # [1, 64, 128, 128]

我的习惯:在端侧部署时,我会把 BN 层融合到前面的卷积层里,减少一次内存访问。另外,Depthwise 卷积在有些硬件上优化得不够好,建议先跑一下 profiler 看看实际耗时。如果 Depthwise 比标准卷积还慢,那可能是硬件对分组卷积支持不好,这时候可以考虑用 ShuffleNet 的变体。

4.6 端侧部署的注意事项

把深度可分离卷积部署到端侧,有几个点要特别留意:

  • 硬件适配:高通 Adreno GPU 对 Depthwise 卷积有专门优化,但有些低端 DSP 不支持分组卷积。我建议先查一下目标芯片的文档。
  • 量化影响:Depthwise 卷积的权重分布和标准卷积不同,量化时容易出现精度损失。我一般会单独对 Depthwise 层做 per-channel 量化。
  • 内存带宽:Depthwise 卷积的访存模式是逐通道的,缓存命中率可能不如标准卷积。如果内存带宽是瓶颈,可以考虑把多个 Depthwise 层合并成一个大的分组卷积。
  • 算子融合:在部署时,把 Depthwise + BN + ReLU + Pointwise 融合成一个算子,能减少 kernel launch 开销。NCNN 和 MNN 都支持这种融合。

总结一下:深度可分离卷积是端侧 ISP 模型轻量化的核心手段。它把标准卷积拆成 Depthwise 和 Pointwise 两步,计算量降到 1/8 左右。在去噪、超分、HDR 等任务中,我实测都能在保持精度的前提下大幅降低延迟。但要注意通道数、硬件适配和量化策略,否则可能适得其反。