网络中的网络 (NiN)

探索Network In Network架构的创新设计,理解1×1卷积和全局平均池化的重要作用

发布于 2025-08-09 更新于 2025-08-091,314 字 3 分钟
网络中的网络 (NiN)的文章封面
展开文章目录
  1. 1. NiN 网络架构创新
  2. 1.1 MlpConv 层的设计
  3. 2. NiN 块的实现
  4. 2.1 NiN 块的构成
  5. 2.2 PyTorch 实现
  6. 3. NiN 网络架构设计
  7. 3.1 整体网络设计
  8. 3.2 完整网络实现
  9. 3.3 网络结构分析
  10. 4. 模型训练与评估
  11. 4.1 训练配置与实现
  12. 4.2 训练结果与分析
  13. 参考资料

1. NiN 网络架构创新

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

1.1 MlpConv 层的设计

NiN创新点

MlpConv 层替代传统卷积层

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

MlpConv层结构示意图 1×1卷积在MlpConv中的作用

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

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

全局平均池化层工作原理


2. NiN 块的实现

2.1 NiN 块的构成

2.2 PyTorch 实现

NiN 块NiNBlock的 PyTorch 实现如下:

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 整体网络设计

3.2 完整网络实现

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

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函数执行更便利的输出维度测试:

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
==========================================================================================

4. 模型训练与评估

4.1 训练配置与实现

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

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 训练结果与分析

查看完整训练过程
第 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训练过程可视化


参考资料

如果这篇记录对你有帮助,可以留下一句回应。

前往留言

DISCUSSION

讨论与回应

欢迎补充细节、指出问题,或分享与这篇文章有关的经验。

昵称与邮箱为必填项,邮箱仅用于头像和回复通知,不会公开。网址可以留空。

正在准备留言区…

演示赞赏界面

谢谢你愿意支持长期写作。

第一阶段不会发起支付或收集任何信息。接入真实赞赏渠道后,这里会展示清楚的金额、渠道和完成状态。

输入关键词开始搜索 · 按 Esc 关闭
打开完整搜索页 →