读 ncnn 源码(XXIX):fuse_innerproduct_dropout——推理时移除 Dropout

在本系列的图优化篇章中,我们已经分析了多种针对线性计算链(如 Conv/InnerProduct 后接 BN/Scale/Add/Mul)的融合优化。Dropout 层是神经网络训练时常用的一种正则化手段,但在推理阶段,它的行为通常是恒等映射(Identity)或者乘以一个固定的缩放因子。fuse_innerproduct_dropout 正是利用了 Dropout 在推理时的这一特性,将其与前驱的 InnerProduct 层进行融合或直接消除。

本篇,我们将剖析 fuse_innerproduct_dropout 的源码,理解其如何处理 Dropout 层以优化推理性能。

TL;DR

  1. 目标: 将 InnerProduct (全连接) 层后紧随的 Dropout 层进行融合或消除,因为 Dropout 在推理时通常是恒等操作或固定缩放。
  2. 模式匹配: 遍历网络 layers,查找 InnerProduct -> Dropout 的直接连接模式。
  3. 数学原理:
    • Dropout 在训练时以概率 p 将部分输出置零,并将其余输出乘以 1/(1-p)(Inverted Dropout)或在推理时乘以 (1-p)
    • 在推理阶段,Dropout 的操作等效于:z=yscalez = y \cdot scale,其中 scale 通常是 1.0 (如果训练时采用 Inverted Dropout) 或 1-p (如果训练时未做缩放)。
    • 这个固定的缩放操作可以像融合 Mul 操作一样,被合并到前驱的 InnerProduct 层中。
    • 如果 scale == 1.0(常见情况),则 Dropout 层在推理时完全是恒等操作,可以直接消除
  4. 代码实现:
    • 获取 Dropout 层的缩放因子 scale = dropout->scale
    • 如果 scale != 1.f:
      • 遍历 InnerProduct 层的每个输出单元 i
      • 将连接到该输出单元的所有权重乘以 scale
      • 如果 InnerProduct 层有偏置,则将偏置也乘以 scale
    • 如果 scale == 1.f: 代码中没有显式处理,但融合逻辑(图结构修改)依然执行,效果等同于直接消除 Dropout 层。
  5. 图结构修改: 将 InnerProduct 层的 top 指向原 Dropout 层的 top,更新 blobproducer,并将 Dropout 层标记为 "ncnnfused"
  6. 效果: 完全消除了 Dropout 层在推理时的计算(无论是恒等映射还是缩放)和内存访问,降低了模型复杂度。


1. 融合动机:Dropout 在推理时的“失效”

Dropout 是一种强大的正则化技术,它在训练过程中随机将一部分神经元的输出置零,以防止过拟合并增强模型的泛化能力。为了在训练和推理时保持期望输出值的一致性,通常采用以下两种策略之一:

  1. Inverted Dropout (常用): 在训练时,将未被置零的输出乘以 1 / (1 - p)p 是置零概率)。这样,在推理时,Dropout 层完全是一个恒等操作,直接将输入传递给下一层即可,scale 因子为 1.0
  2. Standard Dropout: 在训练时只置零,不进行缩放。那么,在推理时,需要将所有输出乘以 (1 - p),以补偿训练时丢失的“能量”,此时 scale 因子为 1-p

无论哪种情况,在推理阶段,Dropout 的行为都是确定的、线性的(要么乘以 1,要么乘以一个固定的 scale)。这个线性操作可以被前驱的线性层(如 InnerProduct)吸收。


2. 代码实现:吸收缩放因子或直接消除

fuse_innerproduct_dropout 的代码逻辑相对简单,核心就是处理这个 scale 因子。

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
int NetOptimize::fuse_innerproduct_dropout()
{
const size_t layer_count = layers.size();
for (size_t i = 0; i < layer_count; i++) // 遍历查找 InnerProduct
{
if (layers[i]->type != "InnerProduct") continue;
int top_blob_index = layers[i]->tops[0];

// 查找后续的 Dropout
size_t j = i + 1;
for (; j < layer_count; j++)
{
if (layers[j]->type != "Dropout") continue;
if (layers[j]->bottoms.size() != 1) continue;
if (layers[j]->bottoms[0] == top_blob_index) break; // 确认连接
}
if (j == layer_count) continue; // 未找到

ncnn::InnerProduct* innerproduct = (ncnn::InnerProduct*)layers[i];
ncnn::Dropout* dropout = (ncnn::Dropout*)layers[j];

fprintf(stderr, "fuse_innerproduct_dropout %s %s\n", innerproduct->name.c_str(), dropout->name.c_str());

// --- 参数变换核心 ---
float scale = dropout->scale; // 获取 Dropout 层的缩放因子

// 只有当 scale 不为 1 时,才需要修改 InnerProduct 的参数
if (scale != 1.f)
{
const int num_output = innerproduct->num_output;
// weight_per_outch = num_input
const int weight_per_outch = innerproduct->weight_data_size / num_output;

float* weight = innerproduct->weight_data;
// 遍历每个输出单元
for (int out_idx = 0; out_idx < num_output; out_idx++)
{
// 获取指向连接到当前输出单元的所有权重的指针
float* fc_weight_outch = weight + weight_per_outch * out_idx;
// 将这些权重乘以 scale
for (int in_idx = 0; in_idx < weight_per_outch; in_idx++)
{
fc_weight_outch[in_idx] *= scale; // W'_fc = W_fc * scale
}
}

// 如果有偏置,偏置也要乘以 scale
if (innerproduct->bias_term)
{
float* bias = innerproduct->bias_data;
for (int out_idx = 0; out_idx < num_output; out_idx++)
{
bias[out_idx] *= scale; // b'_fc = b_fc * scale
}
}
}
// 如果 scale == 1.f,则无需修改 InnerProduct 参数,Dropout 层直接被消除

// --- 图结构修改 (标准融合操作) ---
int top_blob_index_final = dropout->tops[0];
innerproduct->tops[0] = top_blob_index_final;
blobs[top_blob_index_final].producer = i;
dropout->type = "ncnnfused"; // 标记 Dropout 层为无效
// --- 图结构修改结束 ---
}
return 0;
}

关键点:

  • 模式匹配: 查找 InnerProduct -> Dropout 的直接连接。
  • 参数更新: 获取 dropout->scale。仅当 scale != 1.0 时,才将 InnerProduct 的权重矩阵 W 和偏置向量 b(如果存在)的所有元素都乘以 scale
  • 图修改: 无论 scale 是否为 1,都执行标准的图结构修改:重定向 InnerProduct 的输出,并将 Dropout 层标记为 "ncnnfused"。这有效地从计算图中移除了 Dropout 层。

3. 意义:移除推理阶段的冗余操作

Dropout 是一个只在训练阶段有意义的层。在推理时保留它(即使是作为恒等映射)也会带来不必要的开销(至少有一次函数调用和可能的数据拷贝)。fuse_innerproduct_dropout Pass 通过将 Dropout 的(通常为 1 的)缩放效果合并到前驱的 InnerProduct 层中,或者直接消除它,确保了推理计算图中不包含任何冗余的 Dropout 操作。

虽然 ncnnoptimize 工具中还有一个更通用的 eliminate_dropout Pass(我们将在后续篇章分析),但 fuse_innerproduct_dropout 的存在可能是为了更早地处理这种常见的线性组合,或者有历史原因。


4. 结语

fuse_innerproduct_dropout 是 ncnn 图优化中一个相对简单的 Pass,它利用了 Dropout 层在推理阶段行为固定的特性,将其与前驱的 InnerProduct 层进行融合或直接消除。虽然 Dropout 本身计算量不大,但移除冗余层有助于简化计算图,减少调度开销和潜在的内存操作。这一优化与其他针对线性计算链的融合 Pass 一起,共同提升了 ncnn 模型在部署时的最终性能。

该封面图片由Bar ElimelechPixabay上发布