用 AlexNet 风格网络分类 CIFAR-10#
学习目标与记号#
准确解释 AlexNet 结构、输入分辨率适配与 ReLU;
根据公式和张量维度分析 Dropout,并识别常见实现错误;
用
Python实现或验证训练流程;在其他条件相同的情况下,只改变一个需要研究的因素进行比较,并根据实验结果分析该因素可能带来的影响。
本节沿用统一记号:普通小写字母表示标量,粗体小写字母表示向量,粗体大写字母表示矩阵或高阶张量;层编号写作上标 \([l]\),样本或时间编号写作下标;转置写作 \(\trans\)。第 \(l\) 层特征图的通道数、高和宽分别记为 \(d_C^{[l]}\)、\(d_H^{[l]}\) 和 \(d_W^{[l]}\)。除非另有说明,程序中的一批图像按照“样本数、通道数、高、宽”的顺序存放,维度写作 \(m\times d_C^{[l]}\times d_H^{[l]}\times d_W^{[l]}\)。正文与练习中的程序都应同时检查数值结果和数组维度。本节练习的参考答案见 AlexNet 与 CIFAR-10 实现答案。
实现前建议回顾 AlexNet 结构、ReLU、Dropout 和 Adam;数据划分原则见 训练集、验证集与测试集。
现在,我们将使用 AlexNet 教学实现对 CIFAR-10 数据集进行分类。CIFAR-10 (Canadian Institute for Advanced Research, 10 classes)是一个广泛使用的小型图像分类数据集,由加拿大多伦多大学的 Alex Krizhevsky、Vinod Nair 和 Geoffrey Hinton 开发。该数据集包含 60,000 张 \(32\times32\) 像素的彩色图像,分为 10 个类别,每个类别有 6,000 张图像。CIFAR-10 数据集的图像涵盖了现实生活中的 10 种常见物体类别:飞机、汽车、鸟类、猫、鹿、狗、青蛙、马、船和卡车。CIFAR-10 数据集通常用于计算机视觉领域的图像分类任务,因为其图像尺寸较小且类别数量适中,非常适合作为模型训练和评估的标准基准。数据集被分为 50,000 张训练图像和 10,000 张测试图像,用于评估模型在新图像上的表现。
AlexNet 的模型规模和输入分辨率都明显大于前面的 LeNet 风格网络,本例同时演示设备选择。GPU 可以加速训练,但代码仍应在没有 CUDA 的环境中正确退回 CPU。
图形处理单元(Graphics Processing Unit, GPU)
图形处理器(Graphics Processing Unit, GPU)是一种专门用于图形处理的处理器,由于其高并行性和强大的计算能力,GPU 也被广泛应用于深度学习领域。在深度学习中,GPU 可以加速神经网络的训练和推理过程,提高计算效率。PyTorch 支持使用 GPU 进行计算,可以通过 torch.cuda 模块来实现。CUDA 是 NVIDIA 公司推出的并行计算平台和编程模型,它允许开发者使用 C/C++、Python 等语言直接调用 GPU 的计算能力。PyTorch 通过 torch.cuda 模块封装了 CUDA 的接口,提供了一系列的函数和类,方便用户在 PyTorch 中使用 GPU 进行计算。
首先导入数组、绘图、PyTorch 模型与 CIFAR-10 数据加载所需的库,并把 NumPy、PyTorch 以及可用 CUDA 设备的随机种子设为 42。随后选择 CUDA 或 CPU 作为统一计算设备;这段准备代码不接收图像,也不会打印训练结果,后面的模型与每批数据都要移动到这里得到的 device。
1import numpy as np
2import matplotlib.pyplot as plt
3import torch
4import torch.nn as nn
5import torch.optim as optim
6import torch.nn.functional as F
7from torch.utils.data import DataLoader
8from torchvision import datasets, transforms
9
10# 设置随机数种子
11np.random.seed(42)
12torch.manual_seed(42)
13if torch.cuda.is_available():
14 torch.cuda.manual_seed_all(42)
15
16# 设置 GPU
17device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
torch.cuda.is_available() 判断当前环境是否支持 NVIDIA CUDA;若支持,device 为 cuda,否则为 cpu。固定种子只能减少随机初始化等因素造成的差异,不能保证跨硬件和软件版本逐位复现;这段选择逻辑也不会自动使用 Apple MPS 等其他加速后端。
AlexNet 教学版模型搭建#
接下来搭建 AlexNet 教学版模型。在上一小节中,我们实现了卷积层和池化层,这里改用 torch.nn 模块中的 Conv2d 和 MaxPool2d 类。forward 接收 NCHW 排列的三通道图像批量;本例随后提供 \(m\times3\times227\times227\) 输入,预期得到 \(m\times10\) 的线性类别得分。下面的代码保留五个卷积层、LRN、三个全连接层和 Dropout 等经典结构,但把历史原版分布在两个 GPU 上的通道合并到一个设备,并加入自适应平均池化,将卷积层输出的特征图调整为固定大小,从而确定最后一个全连接分类层的输入维度。因此,它适合用于理解 AlexNet 的主要计算过程,但不应被视为逐项复现历史原版。
1class AlexNet(nn.Module):
2 def __init__(self, num_classes=10):
3 super().__init__()
4 self.features = nn.Sequential(
5 nn.Conv2d(3, 96, kernel_size=11, stride=4),
6 nn.ReLU(inplace=True),
7 nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=2), # LRN层
8 nn.MaxPool2d(kernel_size=3, stride=2),
9 nn.Conv2d(96, 256, kernel_size=5, padding=2),
10 nn.ReLU(inplace=True),
11 nn.LocalResponseNorm(size=5, alpha=0.0001, beta=0.75, k=2), # LRN层
12 nn.MaxPool2d(kernel_size=3, stride=2),
13 nn.Conv2d(256, 384, kernel_size=3, padding=1),
14 nn.ReLU(inplace=True),
15 nn.Conv2d(384, 384, kernel_size=3, padding=1),
16 nn.ReLU(inplace=True),
17 nn.Conv2d(384, 256, kernel_size=3, padding=1),
18 nn.ReLU(inplace=True),
19 nn.MaxPool2d(kernel_size=3, stride=2),
20 )
21 self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
22 self.classifier = nn.Sequential(
23 nn.Dropout(),
24 nn.Linear(256 * 6 * 6, 4096),
25 nn.ReLU(inplace=True),
26 nn.Dropout(),
27 nn.Linear(4096, 4096),
28 nn.ReLU(inplace=True),
29 nn.Linear(4096, num_classes),
30 )
31
32def forward(self, x):
33 x = self.features(x)
34 x = self.avgpool(x)
35 x = torch.flatten(x, 1)
36 x = self.classifier(x)
37 return x
对输入维度为 \(m\times3\times227\times227\) 的一批图像,features 依次完成五次卷积和三次最大池化,得到 \(m\times256\times6\times6\);自适应平均池化在本例中保持该空间大小,并使分类器接口在其他合法输入尺寸下仍固定为 \(6\times6\)。展平后,三个线性层最终返回 \(m\times\text{num\_classes}\) 的线性输出,默认即每张图像的 10 个类别得分;它们不是概率,交叉熵损失会在内部完成所需的 log-softmax。
LRN 会在相邻通道间归一化响应,是 AlexNet 的历史组件,但不应简单称为防止过拟合的正则化层。现代架构更常使用 BatchNorm、LayerNorm 等方法;这里保留 LRN 只是为了接近经典结构。该模型还保留了两个 4096 维全连接层,参数和内存开销很大,主要适合教学或结构对照,而不是小图像任务的高效方案。
下面定义通用训练和测试函数。两者都接收模型、按批提供 (data, target) 的加载器、交叉熵损失和计算设备;训练函数还接收优化器与轮数,返回每轮每隔 100 批记录一次的损失列表,测试函数则遍历全部测试样本并打印平均损失和准确率。
# 训练模型
def train(model, train_loader, criterion, optimizer, num_epochs, device):
model.train()
loss_list = []
for epoch in range(num_epochs):
loss_list.append([])
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print(f'Epoch {epoch}, Batch {batch_idx}, Loss: {loss.item()}')
loss_list[-1].append(loss.item())
return loss_list
# 测试模型
def test(model, test_loader, criterion, device):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += criterion(output, target).item() * data.size(0)
pred = output.argmax(dim=1, keepdim=True)
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
accuracy = 100. * correct / len(test_loader.dataset)
print(f'Test Loss: {test_loss:.4f}, Accuracy: {accuracy:.2f}%')
train 在每个小批量中依次清空梯度、前向计算、反向传播并更新参数;返回的 loss_list 只包含抽样记录,并不是逐轮平均损失。test 使用评估模式和 torch.no_grad(),因此 Dropout 会关闭且不会保存梯度,再把批损失按样本数加权,得到整个测试集的平均值。若使用 GPU,模型参数与参与运算的数据必须位于同一设备;函数通过 data.to(device) 与 target.to(device) 移动数据,模型则稍后通过 model.to(device) 移动。若 device 为 CPU,同一套代码仍可直接运行。
接下来加载 CIFAR-10 数据集并进行预处理。输入是数据目录与两套变换规则,输出是训练、测试数据集及按批迭代的加载器。每批图像的维度预期为 \(128\times3\times227\times227\);最后一个批次的样本数可能较小。标签是一维类别编号。
1transform_train = transforms.Compose([
2 transforms.Resize((227, 227)),
3 transforms.RandomHorizontalFlip(),
4 transforms.ToTensor(),
5 transforms.Normalize(
6 (0.4914, 0.4822, 0.4465),
7 (0.2470, 0.2435, 0.2616),
8 ),
9])
10transform_test = transforms.Compose([
11 transforms.Resize((227, 227)),
12 transforms.ToTensor(),
13 transforms.Normalize(
14 (0.4914, 0.4822, 0.4465),
15 (0.2470, 0.2435, 0.2616),
16 ),
17])
18train_dataset = datasets.CIFAR10(
19 root='./data', train=True, download=True, transform=transform_train
20)
21trainloader = DataLoader(
22 train_dataset, batch_size=128, shuffle=True, num_workers=2
23)
24
25test_dataset = datasets.CIFAR10(
26 root='./data', train=False, download=True, transform=transform_test
27)
28testloader = DataLoader(
29 test_dataset, batch_size=128, shuffle=False, num_workers=2
30)
训练和测试采用相同的缩放与标准化,只有训练集启用随机翻转。标准化常数来自 CIFAR-10 的通道统计量;如果改用预训练 ImageNet 权重,则应使用与该权重配套的预处理。
-
1课件中的 AlexNet 结构尺寸链从 \(227\times227\) 输入开始;一些文字资料也常把输入概括为约 \(224\times224\)。这一差异通常来自裁剪方式、首层填充和边界处理约定。本节采用 \(227\times227\),是为了与首层 \(11\times11\)、步幅 4、无填充时得到 \(55\times55\) 输出的计算一致。CIFAR-10 的原始图像只有 \(32\times32\);把它放大不会增加新的图像细节,却会明显增加计算量,因此实际应用中更适合调整网络结构以直接处理原始分辨率。
Resize用于将图像大小调整为 \(227\times227\) 1课件中的 AlexNet 结构尺寸链从 \(227\times227\) 输入开始;一些文字资料也常把输入概括为约 \(224\times224\)。这一差异通常来自裁剪方式、首层填充和边界处理约定。本节采用 \(227\times227\),是为了与首层 \(11\times11\)、步幅 4、无填充时得到 \(55\times55\) 输出的计算一致。CIFAR-10 的原始图像只有 \(32\times32\);把它放大不会增加新的图像细节,却会明显增加计算量,因此实际应用中更适合调整网络结构以直接处理原始分辨率。,以复现本节采用的 AlexNet 尺寸链;ToTensor用于将 PIL 图像或者numpy.ndarray转换为torch.Tensor;Normalize按通道执行 \((x-\mu)/\sigma\),其均值和标准差应由训练数据或预训练模型规范确定,不能使用测试集重新拟合。
接着,我们使用 datasets.CIFAR10 类加载 CIFAR-10 数据集,其中 root 参数指定了数据集的存储路径,train 参数指定了是否加载训练集,transform 参数指定了对图像进行的预处理操作,download 参数指定了是否下载数据集。然后,我们使用 DataLoader 类创建了训练集和测试集的数据加载器 trainloader 和 testloader,其中 batch_size 参数指定了每个批次的样本数量,shuffle 参数指定了是否打乱数据集,对于训练集我们打乱数据集,对于测试集我们不打乱数据集。
下面实例化一个 AlexNet 教学版模型,用前面创建的 trainloader 作为训练数据,交叉熵作为目标,并训练 25 轮。调用 train 后模型参数会被更新,终端会打印抽样批损失,返回值 loss_list 保存这些记录。在 Krizhevsky 等(2012) 的原始论文中,AlexNet 使用了 SGD 优化器;这里为了演示另一种训练设置,改用 Adam 优化器,并把学习率设为 \(5\times10^{-4}\)。因此,下面的训练结果属于本节教学设置,不能作为历史原版 AlexNet 的复现实验。
1# 实例化模型
2model = AlexNet().to(device)
3# 定义损失函数
4criterion = nn.CrossEntropyLoss()
5# 定义优化器
6optimizer = optim.Adam(model.parameters(), lr=5e-4)
7# 训练模型
8loss_list = train(model, trainloader, criterion, optimizer, 25, device)
这段代码把模型参数移动到 device,用交叉熵直接接收模型的 10 维线性输出,并让 Adam 以学习率 \(5\times10^{-4}\) 更新全部可训练参数。train 运行 25 轮并把抽样批损失保存为 loss_list,同时在终端打印进度;它没有保存最佳检查点,也没有使用验证集早停,因此不宜根据后面的测试结果反复调整设置。
训练的部分输出如下;省略号表示中间日志未全部列出:
Epoch 0, Batch 0, Loss: 2.3031015396118164
Epoch 0, Batch 100, Loss: 1.8027007579803467
Epoch 0, Batch 200, Loss: 1.5465164184570312
Epoch 0, Batch 300, Loss: 1.406746745109558
Epoch 1, Batch 0, Loss: 1.16340172290802
Epoch 1, Batch 100, Loss: 1.1709628105163574
Epoch 1, Batch 200, Loss: 0.8591127395629883
Epoch 1, Batch 300, Loss: 0.9119205474853516
Epoch 2, Batch 0, Loss: 0.9254726767539978
Epoch 2, Batch 100, Loss: 1.0733277797698975
Epoch 2, Batch 200, Loss: 0.9020097255706787
Epoch 2, Batch 300, Loss: 0.9042439460754395
Epoch 3, Batch 0, Loss: 0.8871455192565918
Epoch 3, Batch 100, Loss: 0.7843882441520691
Epoch 3, Batch 200, Loss: 0.8064925670623779
Epoch 3, Batch 300, Loss: 0.7478532195091248
Epoch 4, Batch 0, Loss: 0.705902636051178
...
Epoch 24, Batch 200, Loss: 0.2242554873228073
Epoch 24, Batch 300, Loss: 0.22900965809822083
每行是某一小批量的交叉熵,不是整轮平均值;总体下降说明模型正在拟合训练数据,但局部波动是正常的,仅凭这些抽样点不能断言已经收敛或没有过拟合。下面把训练后的模型交给 test,预期打印整个测试集的平均交叉熵和准确率。
1test(model, testloader, criterion, device)
一次运行的测试输出如下,其中 Test Loss 是按全部测试样本平均的交叉熵,Accuracy 是预测类别等于真实标签的样本比例:
Test Loss: 0.5253, Accuracy: 83.42%
精确结果会随随机性和训练环境变化。由于把 \(32\times32\) 图像放大到 \(227\times227\) 会显著增加计算量,却不会创造新的图像细节,本例主要用于理解原始 AlexNet 的尺寸流程。针对 CIFAR-10 的实际实验,更合理的做法是缩小首层卷积和分类器,直接在原分辨率上训练,并另外划出验证集选择超参数。
核心推导与实现核验#
核心关系
推导路径。 原始 AlexNet 面向较大输入;用于 \(32\times32\) 图像时,首层大卷积和大步幅会过快压缩空间信息,因此需重新计算每层输出并调整核、步幅或池化。
关键条件
只要某层有效核大于其输入空间尺寸,输出不存在;逐层应用输出尺寸公式可证明直接照搬配置可能在 CIFAR-10 上失效。
数据规模
每次改变输入图像的高或宽后,都要重新计算最后一个卷积层的通道数 \(d_C^{[l]}\)、高 \(d_H^{[l]}\) 和宽 \(d_W^{[l]}\)。分类输出层接收的输入数应为 \(d_C^{[l]}d_H^{[l]}d_W^{[l]}\),不能直接照搬 AlexNet 针对另一种图像尺寸设置的固定数值。
常见误区
测试集只用于最终评价,不能用于反复调整模型。随机裁剪、翻转等数据增强通常只用于训练数据;测试数据应采用固定的处理方式。此外,训练集和测试集应使用相同的通道归一化参数。否则,模型之间的结果差异可能来自数据处理方式,而不是真正来自模型本身。
动手检查
用一批随机生成的 \(32\times32\) 图像逐层记录 \(m\times d_C^{[l]}\times d_H^{[l]}\times d_W^{[l]}\) 维度与参数量,确认空间尺寸始终为正、分类输出层输入维度正确,并完成一次前向传播、损失计算和后向传播。
数值稳定性与规模
训练损失直接接收输出层的线性运算结果。若把本节的局部响应归一化替换为 Batch Normalization,应使用足够的精度计算其批量统计量。采用混合精度训练时,可使用损失缩放,并检查每层输出、梯度和学习率是否为有限数值。
本节小结#
迁移经典架构时必须根据输入分辨率重算维度。
架构名称与具体训练配方应清楚区分。
增强、归一化和数据划分必须在实验间保持一致。
综合练习#
前四题用于推导、证明和具体计算,第 5--7 题用于编程实现与核验,第 8--10 题要求在其他条件相同的情况下,只改变一个需要研究的因素进行比较,并根据实验结果分析该因素可能带来的影响。程序题应固定随机种子、写出维度断言并报告运行环境;比较题还应固定数据划分、随机种子集合和训练预算。全部参考答案见 AlexNet 与 CIFAR-10 实现答案。
小输入失效计算。 把原始 AlexNet 风格的首层 \(11\times11\)、步幅 4 卷积和 \(3\times3\)、步幅 2 池化直接用于 \(32\times32\) 输入,随后使用保持尺寸的 \(5\times5\) 卷积和第二个 \(3\times3\)、步幅 2 池化。逐层计算空间尺寸并指出在哪一步出现非法输出。
经典尺寸链。 对 \(227\times227\) 输入,依次计算“\(11\times11\) 步幅 4 卷积”“\(3\times3\) 步幅 2 池化”“填充 2 的 \(5\times5\) 卷积”“第二次 \(3\times3\) 步幅 2 池化”“三个保持尺寸的 \(3\times3\) 卷积”和最后一次 \(3\times3\) 步幅 2 池化”的空间尺寸。
分类输出层参数量。 经典分类输出层把 \(256\times6\times6\) 展平后依次映射到 4096、4096 和 10 维。计算三个含偏置线性层各自与合计的参数量,并说明为什么该分类输出层可能主导整个模型的存储。
Dropout 计算。 分类输出层使用保留概率 \(q=0.5\) 的倒置 Dropout。对激活向量 \((2,0,-4,6)\) 和掩码 \((1,0,1,0)\),计算训练输出及其条件期望;说明评估模式下应输出什么。
CIFAR-10 适配实现。 实现一个直接接收 \(32\times32\) 图像的 AlexNet 变体:使用较小首层卷积,避免把图像强制放大到 227,并用自适应池化或虚拟前向确定分类输出层维度。
维度与模式核验。 用前向传播监听函数(forward hook)打印第 5 题中每一层的输入、输出维度,并与公式计算结果对照;检查模型和数据在同一设备,验证 Dropout 在训练模式下随机、评估模式下确定,并把参数量与框架统计核对。
测试设计。 测试原始结构处理空间尺寸为 \(32\times32\) 的输入图像时,会在预期的网络层因特征图尺寸过小而失败;同时测试适配结构能处理多个批量大小、输出维度为 \(m\times10\)、训练与评估模式切换正确、CPU/GPU 设备一致,以及一次后向传播所得梯度均为有限数值。
输入适配方案比较。 比较“放大到 \(227\times227\) 的经典风格网络”和“直接处理 \(32\times32\) 的适配网络”。固定 CIFAR-10 划分、随机种子集合、训练预算和增强选择预算;报告任务性能、训练时间、固定批量预测时间、参数量、乘加量及峰值内存。
Dropout 概率比较。 在适配 AlexNet 中比较 Dropout 概率 \(0,0.25,0.5\)。各组使用相同的数据划分、随机种子、初始化方法和训练预算,只改变 Dropout 概率;报告任务性能、训练—验证差距、训练时间、固定批量预测时间、参数量及峰值内存,并根据结果分析 Dropout 概率可能带来的影响。
归一化方法比较。 在相同 AlexNet 骨干上比较不使用归一化、保留经典 LRN 和使用 BatchNorm。固定数据划分、随机种子集合、训练预算和增强;报告任务性能、训练时间、固定批量预测时间、参数量及峰值内存,并区分历史结构与现代训练配方。