读 ncnn 源码(XXIII):fuse_deconvolution_batchnorm——反卷积层的 BN 融合
在之前的篇章中,我们已经深入探讨了标准卷积 (Convolution) 和深度可分离卷积 (ConvolutionDepthWise) 如何与后续的 BatchNorm 层进行融合。Deconvolution(也称为转置卷积或分数步长卷积)作为上采样操作中常用的层,其后同样可能跟随 BatchNorm 层。为了保持优化策略的一致性和覆盖度,ncnn 提供了 fuse_deconvolution_batchnorm Pass 来处理 Deconvolution -> BatchNorm 这一特定模式。
本篇,我们将剖析该函数的源码,理解其如何将 BN 融合的通用原理应用于反卷积层。
TL;DR
- 目标: 将
Deconvolution 层后紧随的 BatchNorm 层进行融合,消除 BatchNorm 在推理时的计算开销。
- 模式匹配: 遍历网络
layers,查找 Deconvolution -> BatchNorm 的直接连接模式。
- 数学原理: 与
fuse_convolution_batchnorm 完全一致。Deconvolution 本质上也是一种线性变换(可以表示为与特定稀疏矩阵的乘法),后接 BatchNorm 仍然构成连续的线性计算链。融合公式不变:
- 令 BN 参数导出系数
b = slope / sqrt(var + eps) 和 a = bias - slope * mean / sqrt(var + eps)。
- 新权重 Wdeconv′=Wdeconv⋅b (逐输出通道相乘)。
- 新偏置 bdeconv′=bdeconv⋅b+a (逐输出通道计算)。
- 代码实现: 几乎与
fuse_convolution_batchnorm 完全相同。
- 计算中间系数
a 和 b。
- 遍历
Deconvolution 层的每个输出通道 i。
- 将该通道对应的所有权重 (
weight_per_outch 个元素) 乘以 b[i]。
- 更新该通道的偏置
bias[i] = bias[i] * b[i] + a[i] (如果原层无偏置,则先创建)。
- 图结构修改: 将
Deconvolution 层的 top 指向原 BatchNorm 的 top,更新 blob 的 producer,并将 BatchNorm 标记为 "ncnnfused"。
- 效果: 消除了
BatchNorm 层的计算和内存访问,对于使用 Deconvolution 进行上采样的网络(如某些分割模型、生成模型)具有优化效果。

1. 融合动机:线性变换合并的普适性
Deconvolution 虽然常用于上采样,但其数学本质仍然是一种线性变换。它可以被看作是卷积操作的一种“逆运算”形式(尽管并非严格意义上的逆),并且同样可以表示为输入向量与一个(通常是更大的、结构化的)权重矩阵的乘积,再加上一个偏置项。
y=Wdeconv∗x+bdeconv
由于 Deconvolution 和 BatchNorm 都是线性操作,将它们合并为一个等效的 Deconvolution 操作在数学上是可行的,并且能够带来性能上的收益(减少计算步骤和访存)。
2. 原理
融合反卷积(Deconvolution / Transposed Convolution)和批量归一化(Batch Normalization, BN)的数学原理,与融合标准卷积和 BN 的原理是完全一致的。
二者融合的核心思想是:反卷积和 BN(在推理时)都是线性变换。我们可以将两个连续的线性变换在数学上合并(absorb)成一个单独的、等效的线性变换,从而减少计算量。
这个过程只在模型推理(Inference)时进行,因为在训练时,BN 层的均值(mean)和方差(variance)是动态变化的。在推理时,我们会使用在整个训练集上统计得到的固定的均值 μ 和方差 σ2。
1. 符号定义
我们先定义两个层的操作:
-
输入: x
-
反卷积层(Deconvolution):
-
权重: Wdeconv
-
偏置: bdeconv
-
操作: Y=Wdeconv⊛x+bdeconv
(⊛ 代表反卷积/转置卷积操作)
-
批量归一化层(Batch Norm):
- 缩放因子(可学习): γ (gamma)
- 平移因子(可学习): β (beta)
- 训练时统计的均值: μ (running_mean)
- 训练时统计的方差: σ2 (running_var)
- 数值稳定常量: ϵ (epsilon)
- 操作: z=γ⊙σ2+ϵY−μ+β
重要说明:bdeconv, γ, β, μ, σ2 都是向量,其维度等于反卷积层的输出通道数 Cout。在计算时,它们会广播(broadcast)到 Y 张量的 (N,Cout,Hout,Wout) 维度上。
2. 数学推导
我们的目标是找到一组新的反卷积权重 Wdeconv′ 和偏置 bdeconv′,使得单独一个反卷积层就能产生与原先两个层序列完全相同的结果 z。
z=Wdeconv′⊛x+bdeconv′=?γ⊙σ2+ϵ(Wdeconv⊛x+bdeconv)−μ+β
我们从等式右侧开始,将其展开并重新组合成 A⋅(Wdeconv⊛x)+B 的形式。
步骤 1:展开 BN 公式
z=(σ2+ϵγ)⊙(Wdeconv⊛x+bdeconv−μ)+β
步骤 2:应用乘法分配律
我们将 (σ2+ϵγ) 乘入括号内的每一项:
z=(σ2+ϵγ)⊙(Wdeconv⊛x)+(σ2+ϵγ)⊙(bdeconv−μ)+β
步骤 3:重新组合
现在我们有两个部分:
- 与 x 相关(卷积)的部分
- 与 x 无关(偏置)的部分
对于第 1 部分(权重部分):
(σ2+ϵγ)⊙(Wdeconv⊛x)
由于 γ 和 σ2 都是按通道 o 作用的,这个缩放因子可以直接吸收到 Wdeconv 的输出通道维度上。
=((σ2+ϵγ)⊙Wdeconv)⊛x
因此,我们定义新的权重 Wdeconv′:
Wdeconv′=(σ2+ϵγ)⊙Wdeconv
- 代码实现:对于 Wdeconv(维度 Cin,Cout,Kh,Kw)[注1],我们将向量 σo2+ϵγo 乘到第 o 个输出通道对应的所有权重上。
对于第 2 部分(偏置部分):
(σ2+ϵγ)⊙(bdeconv−μ)+β
这整一项都不依赖于输入 x,因此它们共同构成了新的偏置 bdeconv′:
bdeconv′=(σ2+ϵγ)⊙(bdeconv−μ)+β
- 代码实现:这完全是向量运算。γ,β,μ,σ2,bdeconv 都是 (Cout) 维的向量。
3. 融合公式总结
融合反卷积和 BN 层,就是用一个具有新参数 Wdeconv′ 和 bdeconv′ 的新反卷积层来替换它们。新参数的计算公式如下:
令 A=σ2+ϵγ (这是一个 (Cout) 维的向量)
则:
-
新权重 Wdeconv′:
Wdeconv′=A⊙Wdeconv
(将 A 广播到 Wdeconv 的输出通道维度上相乘)
-
新偏置 bdeconv′:
bdeconv′=A⊙(bdeconv−μ)+β
注意:如果在反卷积层中没有使用偏置(即 bdeconv=0),公式依然成立,此时:
bdeconv′=−(A⊙μ)+β=β−σ2+ϵγ⊙μ
[注1]:PyTorch 中反卷积权重的维度是 (Cin,Cout,...),而标准卷积是 (Cout,Cin,...)。但无论哪种情况,γ,β,μ,σ2 都是作用在输出通道 Cout 维度上的。
3. 代码实现:复用成熟的融合逻辑
fuse_deconvolution_batchnorm 的实现代码再次证明了 BN 融合逻辑的高度可复用性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| int NetOptimize::fuse_deconvolution_batchnorm() { const size_t layer_count = layers.size(); for (size_t i = 0; i < layer_count; i++) { if (layers[i]->type != "Deconvolution") continue; int top_blob_index = layers[i]->tops[0];
size_t j = i + 1; if (j == layer_count) continue;
ncnn::Deconvolution* deconvolution = (ncnn::Deconvolution*)layers[i]; ncnn::BatchNorm* batchnorm = (ncnn::BatchNorm*)layers[j];
fprintf(stderr, "fuse_deconvolution_batchnorm %s %s\n", deconvolution->name.c_str(), batchnorm->name.c_str());
{ int channels = batchnorm->channels; float eps = batchnorm->eps;
std::vector<float> a(channels); std::vector<float> b(channels); for (int ch = 0; ch < channels; ch++) { float sqrt_var = static_cast<float>(sqrt(batchnorm->var_data[ch] + eps)); a[ch] = batchnorm->bias_data[ch] - batchnorm->slope_data[ch] * batchnorm->mean_data[ch] / sqrt_var; b[ch] = batchnorm->slope_data[ch] / sqrt_var; }
if (deconvolution->bias_term == 0) { deconvolution->bias_term = 1; deconvolution->bias_data = ncnn::Mat(channels); deconvolution->bias_data.fill(0.f); }
const int weight_per_outch = deconvolution->weight_data_size / channels;
float* weight = deconvolution->weight_data; float* bias = deconvolution->bias_data;
for (int ch = 0; ch < channels; ch++) { float* conv_weight_outch = weight + weight_per_outch * ch; for (int k = 0; k < weight_per_outch; k++) { conv_weight_outch[k] *= b[ch]; } bias[ch] = bias[ch] * b[ch] + a[ch]; } }
int top_blob_index_final = batchnorm->tops[0]; deconvolution->tops[0] = top_blob_index_final; blobs[top_blob_index_final].producer = i; batchnorm->type = "ncnnfused"; } return 0; }
|
关键点:
- 模式匹配: 查找
Deconvolution -> BatchNorm 的直接连接。
- 参数计算: 计算
a, b 系数的逻辑不变。
- 权重更新: 将
Deconvolution 层每个输出通道 ch 对应的所有权重(weight_per_outch 个)乘以该通道的 b[ch]。
- 偏置更新: 将偏置
bias[ch] 更新为 bias[ch] * b[ch] + a[ch]。
- 图修改: 标准的重定向连接 + 标记融合操作。
4. 意义:完善优化覆盖面
虽然 Deconvolution 在 CNN 中可能不如标准卷积或深度卷积那么常见,但在需要上采样的任务(如图像分割、图像生成、超分辨率)中仍然扮演着重要角色。提供 fuse_deconvolution_batchnorm Pass 确保了 ncnn 对这类网络结构也能应用关键的 BN 融合优化,体现了其优化策略的全面性。
5. 结语
fuse_deconvolution_batchnorm 再次印证了 ncnn 图优化中合并连续线性变换这一核心思想的强大威力与通用性。通过复用与标准卷积融合 BN 完全相同的参数变换逻辑,ncnn 能够有效地消除 Deconvolution 层后的 BatchNorm 计算,为使用反卷积进行上采样的网络结构带来了实实在在的性能提升。这一系列针对不同卷积变体(标准、深度、反卷积)的 BN 融合 Pass,共同构成了 ncnnoptimize 优化流水线中的关键环节。
该封面图片由Frauke Riether在Pixabay上发布