深度学习篇---MNIST 手写数字分类中的SE模块
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}%')
五、关键代码解释和注意事项
-
SE 模块的参数
reduction:
示例中用了默认值 16(SEBlock(32)即reduction=16),对 MNIST 这种简单任务足够。如果后续效果不好,可尝试调小到 8(增强筛选能力)或调大到 32(减少计算量)。 -
SE 模块的插入位置:
示例插在卷积层和池化层之间,这是最常用的位置 —— 卷积提取特征后立即筛选,避免无用特征进入池化。 -
为什么在 MNIST 上有效:
MNIST 的数字特征(如 “0 的圆圈”“1 的竖线”)由不同通道捕捉,SE 模块会给这些关键通道更高权重,减少背景噪声(如纸张污点)的干扰。 -
预期效果:
不带 SE 的网络在 MNIST 上准确率约 98.5%,加入 SE 模块后通常能提升到 99% 以上(简单任务提升明显)。
总结
手工设计 SE 模块的核心流程是:
- 定义
SEBlock类,实现 “压缩→激励→加权” 三步; - 在现有卷积网络的卷积层后插入 SE 模块;
- 正常训练网络,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 模块的注意事项(避坑指南)
-
必须匹配通道数(最关键)
- SE 模块的
in_channels参数必须等于上一层卷积的输出通道数(如 conv 输出 64 通道,SE 就必须是SEBlock(64))。 - 错误示例:上一层输出 32 通道,却写成
SEBlock(64),会直接报 “维度不匹配” 错误。
- SE 模块的
-
合理选择插入位置
- 推荐位置:卷积层之后、激活函数 / 池化层之前(如
Conv→BN→SE→ReLU→Pool)。 - 不推荐位置:网络的前两层(特征未抽象)或最后一层卷积后(易过拟合)。
- 推荐位置:卷积层之后、激活函数 / 池化层之前(如
-
根据任务调整缩放系数
reduction- 默认
reduction=16适用于大多数场景(如 ImageNet、MNIST)。 - 轻量模型(如 MobileNet):可增大到 24 或 32(减少计算量)。
- 复杂任务(如细粒度分类):可减小到 8(增强通道区分能力)。
- 默认
-
小数据集需防止过拟合
- 若数据集较小(如 < 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() )
- 若数据集较小(如 < 1 万张图),可在 SE 模块的全连接层后加
-
检查权重是否有效
- 训练 1-2 个 epoch 后,打印 SE 模块输出的权重(如
print(channel_weights)),正常应在 0.2~0.8 之间分布。 - 若权重多数接近 0 或 1,说明
reduction设置不当(太大或太小),需调整。
- 训练 1-2 个 epoch 后,打印 SE 模块输出的权重(如
-
兼容不同框架的维度顺序
- 本代码基于 PyTorch 的 “NCHW” 格式(批量 × 通道 × 高 × 宽),若用 TensorFlow 需调整维度处理(但模块逻辑一致)。
总结
模块化的 SE 模块就像一个 “即插即用” 的注意力插件,复用流程为:
- 从
se_module.py导入SEBlock; - 在网络的卷积层后插入,确保
in_channels与卷积输出通道一致; - 按正常流程训练,必要时调整
reduction参数。
遵循这些规则,SE 模块可以轻松复用到 CNN、ResNet、MobileNet 等几乎所有卷积神经网络中,稳定提升模型性能。
更多推荐



所有评论(0)