MNIST 手写数字分类中的SE模块:

下面以 MNIST 手写数字分类为例,详细讲解在 PyTorch 中手工设计 SE 模块的完整流程,包括模块构建、网络整合、训练测试全流程,代码会附带通俗解释。

一、先明确任务和 SE 模块的定位

  • 任务:MNIST 数据集是手写数字(0-9)的灰度图(28×28 像素),我们需要用带 SE 模块的卷积神经网络实现分类。
  • SE 模块作用:在卷积层提取特征后,给有用的特征通道加权重(比如 “数字边缘” 通道更重要),提升分类精度。

二、手工设计 SE 模块的步骤(核心)

SE 模块的核心是 “压缩→激励→加权” 三步,在 PyTorch 中实现分 4 步:

步骤 1:定义 SE 模块类(关键代码)
import torch
import torch.nn as nn

class SEBlock(nn.Module):
    def __init__(self, channel, reduction=16):  # reduction就是缩放系数r
        super(SEBlock, self).__init__()
        # 1. 压缩(Squeeze):全局平均池化,将每个通道的特征图转为1个数字
        self.avg_pool = nn.AdaptiveAvgPool2d(1)  # 输入任意H×W,输出1×1
        
        # 2. 激励(Excitation):两个全连接层+Sigmoid,计算通道权重
        self.fc = nn.Sequential(
            nn.Linear(channel, channel // reduction, bias=False),  # 压缩到C/r
            nn.ReLU(inplace=True),  # 非线性激活
            nn.Linear(channel // reduction, channel, bias=False),  # 还原到C
            nn.Sigmoid()  # 输出0~1的权重
        )

    def forward(self, x):
        b, c, h, w = x.size()  # 获取输入形状:(批量数, 通道数, 高度, 宽度)
        
        # 压缩:(b,c,h,w) → (b,c,1,1) → 展平为(b,c)
        y = self.avg_pool(x).view(b, c)
        
        # 激励:(b,c) → (b,c),得到每个通道的权重
        y = self.fc(y).view(b, c, 1, 1)  # 调整形状为(b,c,1,1),方便后续广播
        
        # 3. 加权(Scale):将权重与原特征图相乘
        return x * y  # 广播机制:每个通道的特征图都乘以对应的权重y
步骤 2:理解 SE 模块的输入输出
  • 输入:卷积层输出的特征图,形状为(b, c, h, w)(比如(32, 64, 14, 14),32 个样本,64 个通道,14×14 大小)。
  • 输出:和输入形状完全相同的特征图,但每个通道的特征都被乘以了 0~1 的权重(重要通道被放大,不重要的被削弱)。

三、构建带 SE 模块的分类网络

我们设计一个简单的卷积网络,在关键位置插入 SE 模块,用于 MNIST 分类:

class SE_MNIST_Net(nn.Module):
    def __init__(self):
        super(SE_MNIST_Net, self).__init__()
        # 第一个卷积块:卷积+SE模块+池化
        self.conv1 = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, padding=1),  # 输入1通道(灰度图),输出32通道
            nn.BatchNorm2d(32),  # 批归一化,加速训练
            nn.ReLU(inplace=True),
            SEBlock(32),  # 插入SE模块,通道数32
            nn.MaxPool2d(2, 2)  # 池化后尺寸:14×14
        )
        
        # 第二个卷积块:卷积+SE模块+池化
        self.conv2 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=3, padding=1),  # 32→64通道
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            SEBlock(64),  # 插入SE模块,通道数64
            nn.MaxPool2d(2, 2)  # 池化后尺寸:7×7
        )
        
        # 分类头:全连接层
        self.fc = nn.Sequential(
            nn.Linear(64 * 7 * 7, 128),  # 64通道×7×7特征图 → 128维
            nn.ReLU(inplace=True),
            nn.Linear(128, 10)  # 10个类别(0-9)
        )

    def forward(self, x):
        x = self.conv1(x)  # 经过第一个带SE的卷积块
        x = self.conv2(x)  # 经过第二个带SE的卷积块
        x = x.view(x.size(0), -1)  # 展平特征图:(b, 64*7*7)
        x = self.fc(x)  # 分类输出
        return x
网络结构说明
  • 输入:28×28 的灰度图(1 通道)。
  • 卷积块:每个卷积层后都插入 SE 模块(分别处理 32 和 64 个通道)。
  • 输出:10 个类别的预测概率(对应数字 0-9)。

四、完整训练和测试流程

步骤 1:准备数据(MNIST 数据集)
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader

# 数据预处理:转为Tensor并归一化
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))  # MNIST的均值和标准差
])

# 加载训练集和测试集
trainset = torchvision.datasets.MNIST(
    root='./data', train=True, download=True, transform=transform
)
testset = torchvision.datasets.MNIST(
    root='./data', train=False, download=True, transform=transform
)

# 数据加载器
trainloader = DataLoader(trainset, batch_size=64, shuffle=True, num_workers=2)
testloader = DataLoader(testset, batch_size=64, shuffle=False, num_workers=2)
步骤 2:设置训练参数
import torch.optim as optim

# 初始化模型、损失函数、优化器
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 用GPU加速
net = SE_MNIST_Net().to(device)
criterion = nn.CrossEntropyLoss()  # 分类任务用交叉熵损失
optimizer = optim.Adam(net.parameters(), lr=0.001)  # Adam优化器
步骤 3:训练模型
# 训练5个epoch
for epoch in range(5):
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        inputs, labels = data[0].to(device), data[1].to(device)
        
        # 清零梯度
        optimizer.zero_grad()
        
        # 前向传播+计算损失+反向传播+参数更新
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        
        # 打印训练信息
        running_loss += loss.item()
        if i % 100 == 99:  # 每100批打印一次
            print(f'[{epoch + 1}, {i + 1}] loss: {running_loss / 100:.3f}')
            running_loss = 0.0

print('Finished Training')
步骤 4:测试模型准确率
correct = 0
total = 0
with torch.no_grad():  # 测试时不计算梯度
    for data in testloader:
        images, labels = data[0].to(device), data[1].to(device)
        outputs = net(images)
        _, predicted = torch.max(outputs.data, 1)  # 取预测概率最大的类别
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print(f'测试集准确率: {100 * correct / total:.2f}%')

五、关键代码解释和注意事项

  1. SE 模块的参数reduction:
    示例中用了默认值 16(SEBlock(32)即reduction=16),对 MNIST 这种简单任务足够。如果后续效果不好,可尝试调小到 8(增强筛选能力)或调大到 32(减少计算量)。

  2. SE 模块的插入位置:
    示例插在卷积层和池化层之间,这是最常用的位置 —— 卷积提取特征后立即筛选,避免无用特征进入池化。

  3. 为什么在 MNIST 上有效:
    MNIST 的数字特征(如 “0 的圆圈”“1 的竖线”)由不同通道捕捉,SE 模块会给这些关键通道更高权重,减少背景噪声(如纸张污点)的干扰。

  4. 预期效果:
    不带 SE 的网络在 MNIST 上准确率约 98.5%,加入 SE 模块后通常能提升到 99% 以上(简单任务提升明显)。

总结

手工设计 SE 模块的核心流程是:

  1. 定义SEBlock类,实现 “压缩→激励→加权” 三步;
  2. 在现有卷积网络的卷积层后插入 SE 模块;
  3. 正常训练网络,SE 模块会自动学习通道权重。

这种方式在 PyTorch 中实现简单,且能有效提升模型对关键特征的关注度,尤其适合图像分类任务。

SE模块的复用:

下面提供高度模块化的 SE 模块代码,使其可以像 “插件” 一样轻松复用到其他模型中,并详细说明复用流程和注意事项。

一、高度模块化的 SE 模块代码

将 SE 模块单独封装成一个可直接调用的组件,与具体任务解耦:

PyTorch模块化SE模块

import torch
import torch.nn as nn

class SEBlock(nn.Module):
    """
    高度模块化的SE通道注意力模块
    输入: 任意形状的特征图 (batch_size, channels, height, width)
    输出: 与输入形状相同的特征图(已通过通道权重加权)
    """
    def __init__(self, in_channels, reduction=16):
        """
        参数说明:
        - in_channels: 输入特征图的通道数(必须指定,与上一层输出通道数一致)
        - reduction: 缩放系数r,默认16(平衡计算量和效果)
        """
        super(SEBlock, self).__init__()
        # 1. 压缩阶段:全局平均池化 (Squeeze)
        self.global_avg_pool = nn.AdaptiveAvgPool2d(1)  # 输出形状:(batch, channels, 1, 1)
        
        # 2. 激励阶段:全连接层计算通道权重 (Excitation)
        self.fc_layers = nn.Sequential(
            nn.Linear(in_channels, in_channels // reduction, bias=False),  # 压缩通道
            nn.ReLU(inplace=True),  # 非线性激活
            nn.Linear(in_channels // reduction, in_channels, bias=False),  # 恢复通道
            nn.Sigmoid()  # 输出0~1的权重
        )

    def forward(self, x):
        # x形状:(batch_size, channels, height, width)
        batch_size, channels, _, _ = x.size()
        
        # 压缩:(batch, channels, h, w) → (batch, channels, 1, 1) → 展平为(batch, channels)
        channel_weights = self.global_avg_pool(x).view(batch_size, channels)
        
        # 激励:计算每个通道的权重 → 形状调整为(batch, channels, 1, 1)
        channel_weights = self.fc_layers(channel_weights).view(batch_size, channels, 1, 1)
        
        # 3. 加权:将权重与原特征图相乘(广播机制自动适配h和w维度)
        return x * channel_weights

二、复用 SE 模块的完整流程(以不同模型为例)

复用流程非常简单,核心是 “导入模块→在网络中插入→正常训练”,以下用两个例子说明:

例 1:复用到自定义 CNN(如之前的 MNIST 分类网络)
# 1. 导入模块化的SEBlock
from se_module import SEBlock
import torch.nn as nn

# 2. 构建带SE模块的网络
class CustomCNN(nn.Module):
    def __init__(self):
        super(CustomCNN, self).__init__()
        # 第一个卷积块:卷积 + SE模块
        self.block1 = nn.Sequential(
            nn.Conv2d(1, 32, kernel_size=3, padding=1),  # 输出32通道
            nn.BatchNorm2d(32),
            nn.ReLU(),
            SEBlock(in_channels=32),  # 插入SE模块(需与上一层通道数一致)
            nn.MaxPool2d(2)
        )
        
        # 第二个卷积块:卷积 + SE模块
        self.block2 = nn.Sequential(
            nn.Conv2d(32, 64, kernel_size=3, padding=1),  # 输出64通道
            nn.BatchNorm2d(64),
            nn.ReLU(),
            SEBlock(in_channels=64),  # 插入SE模块(通道数64)
            nn.MaxPool2d(2)
        )
        
        # 分类头
        self.classifier = nn.Linear(64*7*7, 10)  # MNIST任务输出10类

    def forward(self, x):
        x = self.block1(x)
        x = self.block2(x)
        x = x.view(x.size(0), -1)  # 展平
        return self.classifier(x)
例 2:复用到 ResNet(修改残差块)
# 以ResNet的基础残差块为例,插入SE模块
class BasicBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super(BasicBlock, self).__init__()
        # 主分支卷积
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU()
        
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_channels)
        
        # 插入SE模块(关键步骤)
        self.se = SEBlock(in_channels=out_channels)  # 对conv2的输出做通道加权
        
        #  shortcut连接(处理通道数/尺寸不匹配)
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        out = self.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out = self.se(out)  # 应用SE模块
        out += self.shortcut(x)  # 残差连接
        out = self.relu(out)
        return out

三、复用 SE 模块的注意事项(避坑指南)

  1. 必须匹配通道数(最关键)

    • SE 模块的in_channels参数必须等于上一层卷积的输出通道数(如 conv 输出 64 通道,SE 就必须是SEBlock(64))。
    • 错误示例:上一层输出 32 通道,却写成SEBlock(64),会直接报 “维度不匹配” 错误。
  2. 合理选择插入位置

    • 推荐位置:卷积层之后、激活函数 / 池化层之前(如Conv→BN→SE→ReLU→Pool)。
    • 不推荐位置:网络的前两层(特征未抽象)或最后一层卷积后(易过拟合)。
  3. 根据任务调整缩放系数reduction

    • 默认reduction=16适用于大多数场景(如 ImageNet、MNIST)。
    • 轻量模型(如 MobileNet):可增大到 24 或 32(减少计算量)。
    • 复杂任务(如细粒度分类):可减小到 8(增强通道区分能力)。
  4. 小数据集需防止过拟合

    • 若数据集较小(如 < 1 万张图),可在 SE 模块的全连接层后加nn.Dropout(0.2),代码修改如下:
      self.fc_layers = nn.Sequential(
          nn.Linear(in_channels, in_channels//reduction),
          nn.ReLU(),
          nn.Dropout(0.2),  # 防止过拟合
          nn.Linear(in_channels//reduction, in_channels),
          nn.Sigmoid()
      )
      
  5. 检查权重是否有效

    • 训练 1-2 个 epoch 后,打印 SE 模块输出的权重(如print(channel_weights)),正常应在 0.2~0.8 之间分布。
    • 若权重多数接近 0 或 1,说明reduction设置不当(太大或太小),需调整。
  6. 兼容不同框架的维度顺序

    • 本代码基于 PyTorch 的 “NCHW” 格式(批量 × 通道 × 高 × 宽),若用 TensorFlow 需调整维度处理(但模块逻辑一致)。

总结

模块化的 SE 模块就像一个 “即插即用” 的注意力插件,复用流程为:

  1. 从se_module.py导入SEBlock;
  2. 在网络的卷积层后插入,确保in_channels与卷积输出通道一致;
  3. 按正常流程训练,必要时调整reduction参数。

遵循这些规则,SE 模块可以轻松复用到 CNN、ResNet、MobileNet 等几乎所有卷积神经网络中,稳定提升模型性能。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐