模型CNN定义
python
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 20, (5, 5))
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(20, 40, (5, 5))
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(40 * 4 * 4, 120)
self.fc2 = nn.Linear(120, 10)
self.dropout = nn.Dropout(0.2)
self.fc2 = nn.Linear(120, 10)
def forward(self, x):
'''
向前传播
参数
----
x :torch.FloatTensor,形状为(B, 1, 28, 28)
'''
B = x.shape[0] # (B, 1, 28, 28)
x = self.pool1(F.relu(self.conv1(x))) # (B, 20, 12, 12)
x = self.pool2(F.relu(self.conv2(x))) # (B, 40, 4, 4)
x = x.view(B, -1) # (B, 40 * 4 * 4)
x = F.relu(self.fc1(x)) # (B, 120)
x = self.dropout(x)
x = self.fc2(x) # (B, 10)
return x
model = CNN()
除模型定义外,与文章7中的代码一致
上述 CNN 模型由两个卷积层、两个池化层和两个全连接层组成,中间加入了 Dropout 正则化。输入为单通道 28×28 的灰度图像,经第一层卷积(1→20 通道,5×5 卷积核)和 2×2 最大池化后得到 20×12×12 的特征图;再经第二层卷积(20→40 通道)和池化后压缩为 40×4×4,随后展平送入全连接层,最终输出 10 个类别的预测分数。
卷积层

卷积层通过一组可学习的卷积核(滤波器)在输入图像上滑动,逐位置计算点积并叠加偏置,从而提取局部特征。本例中 nn.Conv2d(1, 20, (5, 5)) 表示输入通道数为 1(灰度图),输出通道数为 20(即 20 个 5×5 的卷积核),每个卷积核可捕捉一种特征模式(如边缘、纹理等)。卷积后紧跟 ReLU 激活函数引入非线性,再经过 MaxPool2d(2, 2) 以 2×2 窗口取最大值进行下采样,使特征图尺寸减半,同时保留最显著的特征响应。

池化层

池化层通过对局部区域进行聚合操作来降低特征图的空间分辨率,从而减少后续层的参数量和计算量,同时增强模型对微小位移的鲁棒性。本例使用 nn.MaxPool2d(2, 2) 进行最大池化,即以 2×2 的窗口滑动并在每个窗口内取最大值输出。最大池化有助于保留纹理特征和边缘响应,与平均池化相比更偏向凸显显著特征。每经过一次 2×2 池化,特征图的高和宽均缩小为原来的二分之一,有效压缩了信息密度。
添加残差层

残差块
python
class ResidualBlock(nn.Module):
def __init__(self, in_channel, out_channel, stride=1):
'''
定义残差块
参数
----
in_channel :int,输入通道
out_channel :int,输出通道
stride :int,步幅大小
'''
super().__init__()
self.conv1 = nn.Conv2d(
in_channel, out_channel, (3, 3),
stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channel)
self.conv2 = nn.Conv2d(
out_channel, out_channel, (3, 3),
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channel)
self.downsample = None
# 如果stride != 1或者in_channel != out_channel,那么输入的形状和输出的形状不一样
# 使用下面的变换使得两个张量的形状一样
if stride != 1 or in_channel != out_channel:
# 下面两个卷积操作的输出形状是一样的
# Conv2d(in_channel, out_channel, (3, 3), stride, padding=1)
# Conv2d(in_channel, out_channel, (1, 1), stride, padding=0)
self.downsample = nn.Sequential(
nn.Conv2d(in_channel, out_channel, (1, 1), stride=stride, bias=False),
nn.BatchNorm2d(out_channel))
def forward(self, x):
'''
向前传播
参数
----
x :torch.FloatTensor,形状为(B, I, H, W)
'''
inputs = x
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
# 让输入(inputs)的形状和输出(out)的形状一样
if self.downsample is not None:
inputs = self.downsample(inputs)
out += inputs
out = F.relu(out)
return out
m = ResidualBlock(3, 4, 2)
print(m(torch.randn(1, 3, 28, 28)).size())
残差块通过引入恒等映射(identity mapping)缓解深层网络的退化问题,使模型学习残差映射而非直接拟合目标。每个残差块包含两条通路:主路径依次执行 Conv2d → BatchNorm → ReLU → Conv2d → BatchNorm,卷积核均为 3×3、padding=1 以保持空间尺寸;跳跃连接则将输入直接传递到主路径末端,二者按元素相加。若输入与输出通道数不一致或步长不为 1,代码通过 self.downsample(1×1 卷积 + BatchNorm)调整输入维度,使两条路径维度对齐后方可相加。最后经过 ReLU 激活得到输出。这种设计使梯度能够沿跳跃连接直接回传,大幅降低优化难度,使训练数十甚至数百层的网络成为可能。
残差神经网络
python
class ResNet(nn.Module):
def __init__(self):
super().__init__()
self.block1 = ResidualBlock(1, 20)
self.block2 = ResidualBlock(20, 40, stride=2)
self.block3 = ResidualBlock(40, 60, stride=2)
self.block4 = ResidualBlock(60, 80, stride=2)
self.block5 = ResidualBlock(80, 100, stride=2)
self.block6 = ResidualBlock(100, 120, stride=2)
self.lm = nn.Linear(120, 10)
def forward(self, x):
'''
向前传播
参数
----
x :torch.FloatTensor,形状为(B, 1, 28, 28)
'''
x = self.block1(x) # (B, 20, 28, 28)
x = self.block2(x) # (B, 40, 14, 14)
x = self.block3(x) # (B, 60, 7, 7)
x = self.block4(x) # (B, 80, 4, 4)
x = self.block5(x) # (B, 100, 2, 2)
x = self.block6(x) # (B, 120, 1, 1)
out = self.lm(x.view(x.shape[0], -1)) # (B, 10)
return out
model = ResNet()
残差神经网络(ResNet)通过堆叠多个残差块逐步提取高层特征。本实现使用了 6 个 ResidualBlock:通道数从 1 逐步增长到 20、40、60、80、100、120,同时通过将后续块的 stride 设为 2 实现空间下采样------特征图尺寸从 28×28 逐级降至 1×1。每个块内部都遵循残差连接的范式,且通过 downsample 自动处理维度匹配。最后将 120×1×1 的特征图展平,经一个线性层 nn.Linear(120, 10) 输出 10 个类别的预测分数。相比基础 CNN,残差结构使梯度传导更顺畅,有效避免了梯度消失和性能退化,从而能用更深的网络获得更好的泛化能力。
