读 ncnn 源码(XXVII):fuse_innerproduct_batchnorm——全连接层的 BN 融合
在本系列的前几篇中,我们已经详细探讨了 ncnnoptimize 如何将 BatchNorm 层融合到各种卷积变体(Convolution, ConvolutionDepthWise, Deconvolution, DeconvolutionDepthWise)中。InnerProduct 层,即全连接层(Fully Connected Layer),作为神经网络(尤其是分类头部)的另一个关键组件,同样可能后接 BatchNorm 层。为了实现彻底的优化,ncnn 提供了 fuse_innerproduct_batchnorm Pass 来处理 InnerProduct -> BatchNorm 这一模式。
本篇,我们将剖析该函数的源码,理解其如何将 BN 融合的通用原理应用于全连接层。
TL;DR
- 目标: 将
InnerProduct (全连接) 层后紧随的 BatchNorm 层进行融合,消除 BatchNorm 在推理时的计算开销。
- 模式匹配: 遍历网络
layers,查找 InnerProduct -> BatchNorm 的直接连接模式。
- 数学原理: 与
fuse_convolution_batchnorm 完全一致。InnerProduct 也是一种线性变换 (y=Wx+bfc),后接 BatchNorm 仍构成连续线性链。融合公式不变:
- 令 BN 参数导出系数
b = slope / sqrt(var + eps) 和 a = bias - slope * mean / sqrt(var + eps)。
- 新权重 Wfc′=Wfc⋅b (注意这里的乘法是按输出维度进行的)。
- 新偏置 bfc′=bfc⋅b+a (逐输出维度计算)。
- 代码实现: 几乎与
fuse_convolution_batchnorm 完全相同。
- 计算中间系数
a 和 b。
- 遍历
InnerProduct 层的每个输出单元 i (对应 BN 的 channels)。
- 将连接到该输出单元的所有权重 (
weight_per_outch 个元素) 乘以 b[i]。
- 更新该输出单元的偏置
bias[i] = bias[i] * b[i] + a[i] (如果原层无偏置,则先创建)。
- 图结构修改: 将
InnerProduct 层的 top 指向原 BatchNorm 的 top,更新 blob 的 producer,并将 BatchNorm 标记为 "ncnnfused"。
- 效果: 消除了
BatchNorm 层的计算和内存访问,对于使用了 InnerProduct + BatchNorm 结构的网络(例如某些模型的分类头)有优化效果。

1. 融合动机:线性本质的统一
InnerProduct 层执行的是标准的矩阵乘法加偏置操作:y=Wx+bfc。这显然是一个线性变换。因此,当其后跟随另一个线性变换 BatchNorm 时,这两个操作在数学上可以合并。
将 BatchNorm 的缩放和平移参数“烘焙”进 InnerProduct 层的权重矩阵 W 和偏置向量 b_{fc} 中,可以在推理时省去 BatchNorm 的计算,从而提升性能。
2. 数学原理
核心数学原理是:两个连续的线性变换(Linear Transformation)可以合并为一个单一的线性变换。
“Inner Product”(内积)层在深度学习框架中(如 Caffe)通常指的是“Fully Connected”(FC,全连接)层或“Dense”层。其数学形式是一个线性变换:y=Wx+b。
“Batch Normalization”(BN,批量归一化)层在推理时也是一个线性变换。
下面是详细的数学推导过程:
1. 两个独立层的数学表示
假设我们有一个全连接层(Inner Product),紧跟着一个批量归一化层(Batch Norm)。
a) 全连接层 (FC)
全连接层的计算非常简单,它对输入 x 进行一次线性变换:
yfc=Wfc⋅x+bfc
- x:输入向量。
- Wfc:全连接层的权重矩阵。
- bfc:全连接层的偏置(bias)向量。
- yfc:全连接层的输出。
b) 批量归一化层 (BN)
BN 层在训练(training)和推理(inference)时的行为是不同的。
- 训练时:BN 层会计算当前 mini-batch 的均值 μbatch 和方差 σbatch2,并用它们来归一化。
- 推理时:BN 层使用在整个训练过程中累积的“滑动平均均值”(running mean)μrun 和“滑动平均方差”(running variance)σrun2。这些值是固定的常量。
BN 层的推理计算公式如下:
ybn=γ(σrun2+ϵyfc−μrun)+β
- yfc:BN 层的输入(即上一层 FC 的输出)。
- μrun:训练时保存的运行均值。
- σrun2:训练时保存的运行方差。
- γ:BN 层的可学习缩放(scale)参数。
- β:BN 层的可学习平移(shift)参数。
- ϵ:一个很小的常数,用于防止除以零。
2. 融合 (Fusion) 的数学推导
我们的目标是找到一组新的权重 Wfused 和偏置 bfused,使得单一的全连接层 yfused=Wfused⋅x+bfused 能够产生与 FC→BN 序列完全相同的结果 ybn。
推导过程如下:
-
将 FC 层的公式 yfc=Wfc⋅x+bfc 代入 BN 层的公式中:
ybn=γ(σrun2+ϵ(Wfc⋅x+bfc)−μrun)+β
-
展开并重新排列上式,使其变为 A⋅x+B 的形式。我们先把分母定义为一个常量 Cbn(因为 σrun2 和 ϵ 都是常量):
Cbn=σrun2+ϵ
-
代入 Cbn 并展开:
ybn=Cbnγ(Wfc⋅x+bfc−μrun)+β
-
继续展开,将 Cbnγ 乘进去:
ybn=(Cbnγ⋅Wfc)x+(Cbnγ⋅(bfc−μrun))+β
-
现在,我们将所有作用于 x 的项组合为新的权重 Wfused,并将所有常量项组合为新的偏置 bfused:
ybn=Wfused(CbnγWfc)x+bfused(Cbnγ(bfc−μrun)+β)
3. 融合结果:新的 W′ 和 b′
通过上述推导,我们得到了融合后“新”全连接层的权重和偏置:
融合后的权重 Wfused:
Wfused=σrun2+ϵγWfc
融合后的偏置 bfused:
bfused=σrun2+ϵγ(bfc−μrun)+β
注意:这里的 γ, β, μrun, 和 σrun2 都是向量(维度与 yfc 的通道数/特征数相同)。因此,上述乘法和除法是逐元素(element-wise)进行的。例如,计算 Wfused 时, Wfc 的每一行(对应一个输出神经元)都乘以 γ 和 1/σrun2+ϵ 向量中的相应元素。
特殊情况:如果 FC 层没有偏置
在某些网络设计中,FC 层后面如果(明确知道)要跟一个 BN 层,会设置 bfc=0 来节省参数。在这种情况下,融合后的偏置 bfused 简化为:
bfused (if bfc=0)=σrun2+ϵ−γ⋅μrun+β
3. 代码实现:复用 BN 融合核心逻辑
fuse_innerproduct_batchnorm 的代码实现与卷积类 BN 融合 Pass 展现了高度的一致性。
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 74
| int NetOptimize::fuse_innerproduct_batchnorm() { const size_t layer_count = layers.size(); for (size_t i = 0; i < layer_count; i++) { if (layers[i]->type != "InnerProduct") continue; int top_blob_index = layers[i]->tops[0];
size_t j = i + 1; if (j == layer_count) continue;
ncnn::InnerProduct* innerproduct = (ncnn::InnerProduct*)layers[i]; ncnn::BatchNorm* batchnorm = (ncnn::BatchNorm*)layers[j];
fprintf(stderr, "fuse_innerproduct_batchnorm %s %s\n", innerproduct->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 (innerproduct->bias_term == 0) { innerproduct->bias_term = 1; innerproduct->bias_data = ncnn::Mat(channels); innerproduct->bias_data.fill(0.f); }
const int weight_per_outch = innerproduct->weight_data_size / channels;
float* weight = innerproduct->weight_data; float* bias = innerproduct->bias_data;
for (int ch = 0; ch < channels; ch++) { float* fc_weight_outch = weight + weight_per_outch * ch; for (int k = 0; k < weight_per_outch; k++) { fc_weight_outch[k] *= b[ch]; } bias[ch] = bias[ch] * b[ch] + a[ch]; } }
int top_blob_index_final = batchnorm->tops[0]; innerproduct->tops[0] = top_blob_index_final; blobs[top_blob_index_final].producer = i; batchnorm->type = "ncnnfused"; } return 0; }
|
关键点:
- 模式匹配: 查找
InnerProduct -> BatchNorm 的直接连接。
- 参数计算: 计算
a, b 系数的逻辑不变。
- 权重更新:
InnerProduct 的权重 W 通常是 [num_output, num_input] 的形状。weight_per_outch 在这里代表 num_input。代码正确地将连接到第 ch 个输出单元的所有输入权重(即权重矩阵的第 ch 行)乘以该输出通道对应的 b[ch]。
- 偏置更新: 将偏置
bias[ch] 更新为 bias[ch] * b[ch] + a[ch]。
- 图修改: 标准的重定向连接 + 标记融合操作。
4. 意义:优化全连接部分
虽然现代 CNN 架构越来越倾向于使用全局平均池化(Global Average Pooling)后直接接一个 InnerProduct 层进行分类,减少了全连接层的比重,但在某些网络或特定任务中,仍然可能存在 InnerProduct 后接 BatchNorm 的结构(例如,某些旧的网络设计,或者在需要对全连接层输出进行归一化的场景)。
fuse_innerproduct_batchnorm Pass 确保了 ncnn 对这种情况也能进行优化,消除了不必要的 BN 计算,对于包含此类结构的模型可以带来性能提升。
5. 结语
fuse_innerproduct_batchnorm 将 ncnn 中成熟的 BatchNorm 融合策略扩展到了全连接层 (InnerProduct)。其实现与卷积层 BN 融合高度一致,再次证明了合并连续线性变换是图优化中一条普遍适用的核心原则。通过系统性地覆盖各种层类型(卷积、反卷积、深度变体、全连接)与 BatchNorm 的融合,ncnnoptimize 工具能够有效地“压缩”网络计算图,减少冗余操作,为模型在资源受限环境下的高效推理提供有力保障。
该封面图片由Cristian Oltean在Pixabay上发布