[深度学习网络从入门到入土] 网络中的网络NiN

[深度学习网络从入门到入土] 网络中的网络NiN

个人导航

知乎:https://www.zhihu.com/people/byzh_rc

CSDN:https://blog.csdn.net/qq_54636039

注:本文仅对所述内容做了框架性引导,具体细节可查询其余相关资料or源码

参考文章:各方资料

文章目录

  • [[深度学习网络从入门到入土] 网络中的网络NiN](#[深度学习网络从入门到入土] 网络中的网络NiN)
  • 个人导航
  • 参考资料
  • 背景
  • 架构(公式)
        • [1. MLPConv(卷积里的多层感知机)](#1. MLPConv(卷积里的多层感知机))
        • [2. 全局平均池化(GAP)](#2. 全局平均池化(GAP))
  • 创新点
        • [1. 卷积内部引入非线性(MLPConv)](#1. 卷积内部引入非线性(MLPConv))
        • [2. ==去全连接化(GAP)==](#2. ==去全连接化(GAP)==)
        • [3. 端到端的"网络嵌套网络"思想](#3. 端到端的“网络嵌套网络”思想)
  • 代码实现
  • 项目实例

参考资料

Network In Network.

背景

AlexNet(2012) 之后,CNN 的基本范式逐渐固化:卷积层 → 非线性 → 池化 → 全连接

但这种结构有两个明显问题:

  1. 卷积层表达能力有限
    • 标准卷积 = 对局部 patch 做 线性映射
    • 通道间关系只能靠堆深网络"慢慢学"
  2. 全连接层参数量巨大
    • 例如 AlexNet 中 FC 层参数占比超过 90%
    • 易过拟合、显存和算力成本高

-> NiN 的核心问题意识

  • 为什么非线性只能放在层与层之间?
  • 为什么不能让卷积本身更像一个小网络?

架构(公式)

完整:

MLPConv结构:

1. MLPConv(卷积里的多层感知机)

NiN 将一次卷积替换为一个 微型 MLP
y i , j = f ( n ) ( ⋯ f ( 2 ) ( f ( 1 ) ( x i , j ) ) ) \mathbf{y}{i,j} = f^{(n)}( \cdots f^{(2)}( f^{(1)}(\mathbf{x}{i,j}) ) ) yi,j=f(n)(⋯f(2)(f(1)(xi,j)))

其中:

  • x i , j \mathbf{x}_{i,j} xi,j:某个空间位置上的 通道向量
  • f ( l ) f^{(l)} f(l):通常由 1 × 1 1\times1 1×1 卷积 + 非线性 组成

-> 实现形式非常关键:MLPConv = 普通卷积 + 多层 1×1 卷积

2. 全局平均池化(GAP)

NiN 彻底移除了全连接层 ,用 全局平均池化:
y k = 1 H × W ∑ i , j x i , j , k y_k = \frac{1}{H \times W} \sum_{i,j} x_{i,j,k} yk=H×W1i,j∑xi,j,k

作用:

  • 每个通道 → 一个类别得分
  • 无可学习参数
  • 强结构先验,降低过拟合

创新点

1. 卷积内部引入非线性(MLPConv)

1×1 卷积 ≠ 降维工具, 本质是 通道维度上的全连接

-> 提升了局部特征的判别能力

2. 去全连接化(GAP)
  • 用结构设计代替参数堆砌
  • 分类结果与空间位置解耦(无关)
  • 对尺度变化更鲁棒
3. 端到端的"网络嵌套网络"思想

从"堆层数"转向"提升单层表达力"的重要转折

代码实现

py 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F

from byzh.ai.Butils import b_get_params

class MLPConv(nn.Module):
    """
    NiN 的核心模块:MLPConv("卷积里的小网络")
    结构:卷积(k×k) + ReLU  ->  1×1卷积 + ReLU  ->  1×1卷积 + ReLU

    直观理解:
    - 第一个 k×k 卷积负责提取局部空间特征(看邻域)
    - 后面两层 1×1 卷积相当于对"通道维"做 MLP(增强通道间非线性组合能力)
    """
    def __init__(self, in_ch, out_ch, k, s=1, padding=0):
        super().__init__()
        self.net = nn.Sequential(
            # 空间卷积:提取局部空间模式
            nn.Conv2d(in_ch, out_ch, kernel_size=k, stride=s, padding=padding, bias=True),
            nn.ReLU(inplace=True),

            # 1×1 卷积:等价于对每个像素位置的通道向量做一次"全连接"
            nn.Conv2d(out_ch, out_ch, kernel_size=1, stride=1, padding=0, bias=True),
            nn.ReLU(inplace=True),

            # 1×1 卷积:进一步增强非线性表达(模拟更深的 MLP)
            nn.Conv2d(out_ch, out_ch, kernel_size=1, stride=1, padding=0, bias=True),
            nn.ReLU(inplace=True),
        )

    def forward(self, x):
        return self.net(x)


class B_NiN_Paper_CIFAR(nn.Module):
    """
    CIFAR 版本(输入 32×32)的 NiN
    论文风格要点:
    - 多个 MLPConv 堆叠
    - 中间用 pooling 下采样
    - 用 Dropout 抑制过拟合
    - 最后用 1×1 conv 输出类别通道 + 全局平均池化(GAP) 代替全连接

    input shape: (N, 3, 224, 224)
    """
    def __init__(self, num_classes=10, dropout_p=0.5):
        super().__init__()

        # -------------------------
        # Block 1
        # 输入: 32×32
        # conv(5,pad2) 后仍是 32×32
        # maxpool(3, stride=2) -> 15×15(因为 (32-3)/2 + 1 = 15)
        # -------------------------
        self.block1 = nn.Sequential(
            MLPConv(3, 192, k=5, s=1, padding=2),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Dropout(p=dropout_p),
        )

        # -------------------------
        # Block 2
        # 输入: 15×15
        # conv(5,pad2) 后仍是 15×15
        # avgpool(3, stride=2) -> 7×7(因为 (15-3)/2 + 1 = 7)
        # -------------------------
        self.block2 = nn.Sequential(
            MLPConv(192, 192, k=5, s=1, padding=2),
            nn.AvgPool2d(kernel_size=3, stride=2),
            nn.Dropout(p=dropout_p),
        )

        # -------------------------
        # Block 3
        # 输入: 7×7
        # conv(3,pad1) 后仍是 7×7
        # 通常最后一个大块可以不再下采样(直接为分类头服务)
        # -------------------------
        self.block3 = nn.Sequential(
            MLPConv(192, 192, k=3, s=1, padding=1),
            nn.Dropout(p=dropout_p),
        )

        # -------------------------
        # 分类头:
        # 1×1 conv 把通道数映射到 num_classes
        # 然后用 GAP 把 H×W 平均成 1×1
        # 相当于"无参数全连接",显著减少参数并缓解过拟合
        # -------------------------
        self.classifier = nn.Conv2d(192, num_classes, kernel_size=1, stride=1, padding=0, bias=True)

    def forward(self, x):
        x = self.block1(x)
        x = self.block2(x)
        x = self.block3(x)

        # 输出:N, num_classes, H, W
        x = self.classifier(x)

        # 全局平均池化:把每个类别通道在空间维度做平均 -> N, num_classes, 1, 1
        x = F.adaptive_avg_pool2d(x, 1)

        # 拉平为 N, num_classes
        x = x.view(x.size(0), -1)
        return x


class B_NiN_Paper_ImageNet(nn.Module):
    """
    ImageNet 版本(输入 224×224)的 NiN(常见复现形态)
    主要变化:
    - 开头用更大的卷积核 + 更大的 stride,快速扩大感受野并降分辨率
    - 通道数整体更大
    - 同样保留:MLPConv + Dropout + 最终 1×1 conv + GAP

    input shape: (N, 3, 224, 224)
    """
    def __init__(self, num_classes=1000, dropout_p=0.5):
        super().__init__()

        self.features = nn.Sequential(
            # -------------------------
            # Stem:快速降采样
            # 224×224 -> conv(11, stride=4, pad=2) -> 55×55
            # 55×55 -> maxpool(3, stride=2) -> 27×27
            # -------------------------
            MLPConv(3, 96, k=11, s=4, padding=2),
            nn.MaxPool2d(3, stride=2),
            nn.Dropout(p=dropout_p),

            # -------------------------
            # 27×27 -> MLPConv(5,pad2) -> 27×27
            # 27×27 -> maxpool(3, stride=2) -> 13×13
            # -------------------------
            MLPConv(96, 256, k=5, s=1, padding=2),
            nn.MaxPool2d(3, stride=2),
            nn.Dropout(p=dropout_p),

            # -------------------------
            # 13×13 -> MLPConv(3,pad1) -> 13×13
            # 13×13 -> maxpool(3, stride=2) -> 6×6
            # -------------------------
            MLPConv(256, 384, k=3, s=1, padding=1),
            nn.MaxPool2d(3, stride=2),
            nn.Dropout(p=dropout_p),

            # -------------------------
            # 6×6 -> MLPConv(3,pad1) -> 6×6
            # 这里不再 pool,直接交给分类头 + GAP
            # -------------------------
            MLPConv(384, 1024, k=3, s=1, padding=1),
            nn.Dropout(p=dropout_p),
        )

        # 分类头:通道映射到类别数
        self.classifier = nn.Conv2d(
            in_channels=1024, out_channels=num_classes,
            kernel_size=1, stride=1, padding=0, bias=True
        )

    def forward(self, x):
        x = self.features(x)

        # N, num_classes, H, W
        x = self.classifier(x)

        # GAP:N, num_classes, 1, 1
        x = F.adaptive_avg_pool2d(x, 1)

        # N, num_classes
        return x.flatten(1)


if __name__ == "__main__":
    # -------------------------
    # CIFAR 测试:输入 32×32
    # -------------------------
    model = B_NiN_Paper_CIFAR(num_classes=10)
    x = torch.randn(4, 3, 32, 32)
    y = model(x)
    print(y.shape)  # 期望输出: torch.Size([4, 10])
    print(f"参数量: {b_get_params(model)}")  # 1_492_618

    # -------------------------
    # ImageNet 测试:输入 224×224
    # -------------------------
    model = B_NiN_Paper_ImageNet(num_classes=1000)
    x = torch.randn(2, 3, 224, 224)
    y = model(x)
    print(y.shape)  # 期望输出: torch.Size([2, 1000])
    print(f"参数量: {b_get_params(model)}")  # 8_644_776

项目实例

库环境:

复制代码
numpy==1.26.4
torch==2.2.2cu121
byzh-core==0.0.9.21
byzh-ai==0.0.9.54
byzh-extra==0.0.9.12
...

NiN训练MNIST数据集:

py 复制代码
# copy all the codes from here to run

import torch

from byzh.ai.Btrainer import B_Classification_Trainer
from byzh.ai.Bdata import B_Download_CIFAR10, b_get_dataloader_from_tensor
# from uploadToPypi_ai.byzh.ai.Bmodel.study_cnn import B_NiN_Paper_CIFAR
from byzh.ai.Bmodel.study_cnn import B_NiN_Paper_CIFAR
from byzh.ai.Butils import b_get_device

##### hyper params #####
epochs = 10
lr = 1e-3
batch_size = 32
device = b_get_device(use_idle_gpu=True)

##### data #####
downloader = B_Download_CIFAR10(save_dir='D:/study_cnn/datasets/CIFAR10')
data_dict = downloader.get_data()
X_train = data_dict['X_train_standard']
y_train = data_dict['y_train']
X_test = data_dict['X_test_standard']
y_test = data_dict['y_test']
num_classes = data_dict['num_classes']

train_dataloader, val_dataloader = b_get_dataloader_from_tensor(
    X_train, y_train, X_test, y_test,
    batch_size=batch_size
)

##### model #####
model = B_NiN_Paper_CIFAR(num_classes=num_classes)

##### else #####
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = torch.nn.CrossEntropyLoss()

##### trainer #####
trainer = B_Classification_Trainer(
    model=model,
    optimizer=optimizer,
    criterion=criterion,
    train_loader=train_dataloader,
    val_loader=val_dataloader,
    device=device
)
trainer.set_writer1('./runs/nin/log.txt')

##### run #####
trainer.train_eval_s(epochs=epochs)

##### calculate #####
trainer.draw_loss_acc('./runs/nin/loss_acc.png', y_lim=False)
trainer.save_best_checkpoint('./runs/nin/best_checkpoint.pth')
trainer.calculate_model()
相关推荐
AI周红伟2 小时前
周红伟:企业智能体构建实操,《下一代智能体:Claude code+Skills+Gemini+RAG+Agent智能体构建案例实操》
人工智能
向上的车轮2 小时前
Chaterm:AI 智能终端极简手册
人工智能
世优科技虚拟人2 小时前
从教室到校史馆讲解,AI数字人一体机打造全场景智慧校园助手
人工智能·科技·大模型·数字人
edisao2 小时前
《CV认知变异度模型:在系统规训中保留结构自由》
人工智能
睡醒了叭2 小时前
coze-工作流-意图识别、批处理、变量聚合
人工智能·aigc
prince_zxill3 小时前
AionUi:开源本地AI协作平台
人工智能
Bruce_Liuxiaowei3 小时前
渗透测试中的提权漏洞:从低权限到系统控制的全解析
网络·windows·安全
半问3 小时前
Vibecoding:想法行不行,做出来看看
人工智能·程序人生·ai·产品运营·互联网
张3蜂3 小时前
Python pip 命令完全指南:从入门到精通
人工智能·python·pip