本文基于d2l项目内容整理,介绍Network In Network (NiN)架构,包括其创新的MlpConv层设计和全局平均池化的应用。

传统CNN架构的局限性:

LeNet、AlexNet 和 VGG 的网络设计模式类似,都是使用卷积层和池化层进行特征提取,然后使用全连接层逐级抽象实现分类,只不过 AlexNet 和 VGG 在 LeNet 的基础上扩大了模型的深度。但该网络设计模式的缺陷在于,全连接层使特征在空间上被展平,放弃了感受野和共享权重对视觉皮层神经元的模拟,没有了感知局部的能力。


1. NiN 网络架构创新

Network In Network 的创新理念:

2013 年出现的网络的网络 (Network In Network, NiN) 提出了一种简单的方案改进了这一缺陷:

NiN网络架构对比图
NiN网络架构对比图
NiN架构详细示意图
NiN架构详细示意图

1.1 MlpConv 层的设计

MlpConv 层替代传统卷积层

默认的卷积层只做加权求和,即使接入了非线性的激活函数,卷积层本身依然是线性的。NiN 用改进后的 MlpConv 层替代传统卷积层,为局部感受野每个高度和宽度位置(像素位置)对应的通道应用多层感知机(或多个使用了激活函数的 1×1 卷积层),提升特征捕捉能力。

全局平均池化 (GAP) 替代全连接层

在 NiN 的分类阶段,用全局平均池化层 (Global Average Pooling, GAP) 替代全连接层,使各个像素位置的值在通道范围内被均值化为标量,消除深度(通道)信息,保留通道的全局响应。这样,既减少了参数量、降低了过拟合的风险,又提升了模型的可解释性(但引入的非线性仍然增加了计算复杂度)。

全局平均池化层工作原理
全局平均池化层工作原理

2. NiN 块的实现

2.1 NiN 块的构成

NiN 块的结构组成:

NiN 网络中的每个 NiN 块依次由以下层组成:

  1. 可自定义窗口大小的卷积层 + ReLU 激活函数
  2. 1×1 卷积层 + ReLU 激活函数
  3. 1×1 卷积层 + ReLU 激活函数

2.2 PyTorch 实现

NiN 块NiNBlock的 PyTorch 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from torch import Tensor, nn


class NiNBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, strides, padding):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU()
)

def forward(self, x) -> Tensor:
return self.block(x)

3. NiN 网络架构设计

3.1 整体网络设计

NiN 网络的设计特点:

NiN 卷积层窗口形状受 AlexNet 启发,设置为 11×11、5×5 和 3×3 的卷积层,且通道数与 AlexNet 的相同。每个 NiN 块后使用 3×3 步幅为 2 的最大池化层,输出每个类别未经激活的预测得分(可根据需要进行归一化或激活)。

3.2 完整网络实现

复用 NiN 块,NiN 网络的 PyTorch 实现如下(继续使用 Fashion-MNIST 数据集):

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
from torch import Tensor, nn


class NiNBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, strides, padding):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU()
)

def forward(self, x) -> Tensor:
return self.block(x)


class NiN(nn.Module):
def __init__(self, num_classes: int):
super(NiN, self).__init__()
self.extractor = nn.Sequential(
NiNBlock(in_channels=1, out_channels=96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2d(3, stride=2),

NiNBlock(in_channels=96, out_channels=256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2d(3, stride=2),

NiNBlock(in_channels=256, out_channels=384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2d(3, stride=2),

nn.Dropout(0.5)
)
self.classifier = nn.Sequential(
NiNBlock(in_channels=384, out_channels=num_classes, kernel_size=3, strides=1, padding=1),
nn.AdaptiveAvgPool2d(output_size=1), # 等价为:nn.AvgPool2d(kernel_size=5, stride=5, padding=0)
nn.Flatten()
)

self._initialize_weights()

def _initialize_weights(self):
"""
默认使用的 Kaiming 均匀分布初始化,似乎难以训练模型
使用 Kaiming 正态分布初始化
"""
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, nonlinearity='relu')

def forward(self, x: Tensor) -> Tensor:
x = self.extractor(x)
x = self.classifier(x)
return x

3.3 网络结构分析

使用torchinfo库的summary函数执行更便利的输出维度测试:

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
from torchinfo import summary

model = NiN(num_classes=10)
summary(model, input_size=(1, 1, 224, 224))
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
NiN [1, 10] --
├─Sequential: 1-1 [1, 384, 5, 5] --
│ └─NiNBlock: 2-1 [1, 96, 54, 54] --
│ │ └─Sequential: 3-1 [1, 96, 54, 54] 30,336
│ └─MaxPool2d: 2-2 [1, 96, 26, 26] --
│ └─NiNBlock: 2-3 [1, 256, 26, 26] --
│ │ └─Sequential: 3-2 [1, 256, 26, 26] 746,240
│ └─MaxPool2d: 2-4 [1, 256, 12, 12] --
│ └─NiNBlock: 2-5 [1, 384, 12, 12] --
│ │ └─Sequential: 3-3 [1, 384, 12, 12] 1,180,800
│ └─MaxPool2d: 2-6 [1, 384, 5, 5] --
│ └─Dropout: 2-7 [1, 384, 5, 5] --
├─Sequential: 1-2 [1, 10] --
│ └─NiNBlock: 2-8 [1, 10, 5, 5] --
│ │ └─Sequential: 3-4 [1, 10, 5, 5] 34,790
│ └─AdaptiveAvgPool2d: 2-9 [1, 10, 1, 1] --
│ └─Flatten: 2-10 [1, 10] --
==========================================================================================
Total params: 1,992,166
Trainable params: 1,992,166
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 763.82
==========================================================================================
Input size (MB): 0.20
Forward/backward pass size (MB): 12.20
Params size (MB): 7.97
Estimated Total Size (MB): 20.37
==========================================================================================

NiN 网络设计原理分析:

使用连续的三次 NiN 块不断减小空间分辨率并加大网络深度

使用最大池化扩大感受野、提高模型的表达力

在特征提取后进行 Dropout 避免过拟合

用 NiN 块将通道数压缩到目标类别数

用全局平均池化 (GAP) 替代全连接层的作用

自适应池化的技术细节:

PyTorch 中的 AdaptiveAvgPool2d在进行 GAP 时是“自适应”的,可根据输入特征矩阵的大小和输出尺寸自动调整池化窗口和步幅,确保输出始终为指定尺寸。


4. 模型训练与评估

4.1 训练配置与实现

继续使用training_tools.py中的工具训练评估模型:

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
import torch
from torch import Tensor, nn, optim

from training_tools import fashionMNIST_loader, Trainer


class NiNBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, strides, padding):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU()
)

def forward(self, x) -> Tensor:
return self.block(x)


class NiN(nn.Module):
def __init__(self, num_classes: int):
super(NiN, self).__init__()
self.extractor = nn.Sequential(
NiNBlock(in_channels=1, out_channels=96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2d(3, stride=2),

NiNBlock(in_channels=96, out_channels=256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2d(3, stride=2),

NiNBlock(in_channels=256, out_channels=384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2d(3, stride=2),

nn.Dropout(0.5)
)
self.classifier = nn.Sequential(
NiNBlock(in_channels=384, out_channels=num_classes, kernel_size=3, strides=1, padding=1),
nn.AdaptiveAvgPool2d(output_size=1), # 等价为:nn.AvgPool2d(kernel_size=5, stride=5, padding=0)
nn.Flatten()
)

self._initialize_weights()

def _initialize_weights(self):
"""
默认使用的 Kaiming 均匀分布初始化,似乎难以训练模型
使用 Kaiming 正态分布初始化
"""
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, nonlinearity='relu')

def forward(self, x: Tensor) -> Tensor:
x = self.extractor(x)
x = self.classifier(x)
return x


if __name__ == '__main__':
BATCH_SIZE = 128
EPOCHS_NUM = 30
LEARNING_RATE = 0.01

model = NiN(num_classes=10)
train_loader, test_loader = fashionMNIST_loader(BATCH_SIZE, resize=224)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), LEARNING_RATE)
platform = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

with Trainer(model, train_loader, test_loader, criterion, optimizer, platform) as trainer:
trainer.train(EPOCHS_NUM)

4.2 训练结果与分析

查看完整训练过程
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
001/30 轮,训练损失:2.0495,训练精度:28.43%,测试损失:1.5681,测试精度:51.51%
002/30 轮,训练损失:0.9821,训练精度:66.64%,测试损失:0.6777,测试精度:75.06%
003/30 轮,训练损失:0.6354,训练精度:77.04%,测试损失:0.5442,测试精度:79.88%
004/30 轮,训练损失:0.5416,训练精度:80.46%,测试损失:0.5270,测试精度:80.00%
005/30 轮,训练损失:0.4881,训练精度:82.34%,测试损失:0.4588,测试精度:83.32%
006/30 轮,训练损失:0.4484,训练精度:83.86%,测试损失:0.4341,测试精度:84.49%
007/30 轮,训练损失:0.4199,训练精度:84.65%,测试损失:0.4069,测试精度:85.37%
008/30 轮,训练损失:0.3984,训练精度:85.55%,测试损失:0.4134,测试精度:85.44%
009/30 轮,训练损失:0.3778,训练精度:86.39%,测试损失:0.3887,测试精度:85.82%
010/30 轮,训练损失:0.3632,训练精度:86.78%,测试损失:0.4775,测试精度:83.04%
011/30 轮,训练损失:0.3501,训练精度:87.31%,测试损失:0.3427,测试精度:87.49%
012/30 轮,训练损失:0.3375,训练精度:87.63%,测试损失:0.4035,测试精度:85.31%
013/30 轮,训练损失:0.3277,训练精度:88.02%,测试损失:0.3529,测试精度:87.10%
014/30 轮,训练损失:0.3199,训练精度:88.21%,测试损失:0.3193,测试精度:88.24%
015/30 轮,训练损失:0.3096,训练精度:88.72%,测试损失:0.3444,测试精度:87.37%
016/30 轮,训练损失:0.3040,训练精度:88.95%,测试损失:0.3439,测试精度:87.36%
017/30 轮,训练损失:0.2953,训练精度:89.18%,测试损失:0.3265,测试精度:88.04%
018/30 轮,训练损失:0.2890,训练精度:89.62%,测试损失:0.3234,测试精度:88.43%
019/30 轮,训练损失:0.2835,训练精度:89.58%,测试损失:0.3689,测试精度:87.20%
020/30 轮,训练损失:0.2752,训练精度:89.96%,测试损失:0.3417,测试精度:87.38%
021/30 轮,训练损失:0.2711,训练精度:90.17%,测试损失:0.2927,测试精度:89.33%
022/30 轮,训练损失:0.2652,训练精度:90.30%,测试损失:0.3334,测试精度:87.89%
023/30 轮,训练损失:0.2601,训练精度:90.47%,测试损失:0.2848,测试精度:89.71%
024/30 轮,训练损失:0.2559,训练精度:90.63%,测试损失:0.2947,测试精度:89.62%
025/30 轮,训练损失:0.2492,训练精度:90.91%,测试损失:0.2871,测试精度:89.46%
026/30 轮,训练损失:0.2470,训练精度:91.07%,测试损失:0.3058,测试精度:89.04%
027/30 轮,训练损失:0.2440,训练精度:91.13%,测试损失:0.2831,测试精度:89.56%
028/30 轮,训练损失:0.2384,训练精度:91.28%,测试损失:0.2850,测试精度:89.24%
029/30 轮,训练损失:0.2354,训练精度:91.32%,测试损失:0.2835,测试精度:89.74%
030/30 轮,训练损失:0.2309,训练精度:91.53%,测试损失:0.2628,测试精度:90.59%
NiN训练过程可视化
NiN训练过程可视化

训练结果分析:

  • 最终性能:NiN 网络在 Fashion-MNIST 上达到 90.59% 的测试精度
  • 模型参数:仅有 199.2 万个参数,相比 AlexNet 的 6100 万参数大幅减少
  • 训练稳定性:使用 Kaiming 正态分布初始化后,训练过程稳定收敛
  • GAP 的作用:全局平均池化有效减少了参数量,降低过拟合风险

若没有显式地初始化,对于示例所用的简单数据集而言,NiN 作为较深的模型而难以训练。于是,使用nn.init.kaiming_normal_()方法的 Kaiming 正态分布初始化权重。


总结

本文介绍了Network In Network (NiN)的创新架构设计:

  1. MlpConv层:用 1×1 卷积层构建的多层感知机,提升特征捕捉能力
  2. 全局平均池化:替代全连接层,减少参数量和过拟合风险
  3. 简洁架构:通过创新设计实现更高的参数效率
  4. 技术影响:为后续的 GoogLeNet、ResNet 等网络的 1×1 卷积应用奠定了基础

NiN 网络通过在局部引入更复杂的非线性变换,在保持计算效率的同时提高了网络的表达能力,为深度学习领域提供了重要的设计思路。