CSCI3230 T06 · Part 3/3 CNN Project Practice
课程:CSCI3230 (ESTR3108) Fundamentals of Artificial Intelligence · 2025T1 本讲:T06,第 3 部分 / 共 3 部分 · 原件 T06-CNN-Concepts-and-Practice-2025T1.pdf(本地课件) p.18–27 定位:按原代码读数据、网络、训练和测试;本次不下载数据或运行训练。 前后:上一份 Exercise
本文依据 2025T1 旧版材料重写,供基础学习与预习;不是 2026T1 新增要求或考试范围。补充数值例均另行标注。
任务与输出(p.18–20)
前篇只算尺寸,这篇把同一思想放进程序。任务是给28×28灰度手写数字图分类到0–9,共10类。不是回归图片中数字的数值:把9误认8也不是“只错1分”的连续回归目标,而是分类错误。
单张输入含784个像素;批量输入同时装 张,PyTorch这里按 排列。最后输出 ,每行十个数分别是十个类别得分。前置最好先会 T06四道尺寸练习。p.18分隔,p.19数据定义,p.20讨论输出与网络设计。FashionMNIST虽尺寸相同,识别对象不同,不在本篇任务中。
导入与数据(p.21–22)
p.21导入PyTorch并选择CUDA可用就GPU,否则CPU;原代码没有Apple MPS分支。模型和输入必须在一致设备上才能运算,设备选择不改变分类任务定义。
p.22先ToTensor,再用均值0.1307、标准差0.3081变换像素:
是转换后的原像素数, 是网络收到的数。减均值把典型水平移向0,除标准差调整尺度。补充自取 ,因为差为0.3081,变换后为1; 变为0。输入出现负数正常,不表示图片读取错误。
训练和测试用同一固定尺度;不能分别按单张图片任意换标准而假定等价。batch64表示每步通常同时处理64张;训练shuffle用于改变访问顺序,测试不shuffle便于稳定检查。download=True是代码的下载行为,本次只是阅读,不声称已下载或训练成功。
网络形状与 logits(p.23–24)
先读层定义,再对照forward的使用次序。3×3、padding1、stride1卷积保持高宽,2×2、stride2池化减半。
| 操作 | 单样本输出 | 为什么 |
|---|---|---|
| 输入 | 1×28×28 | 一张灰度图 |
| Conv1 + ReLU | 16×28×28 | 16个核;ReLU不改形状 |
| Pool1 | 16×14×14 | 每通道空间减半 |
| Conv2 + ReLU | 32×14×14 | 32个核,核输入深度16 |
| Pool2 | 32×7×7 | 再减半 |
| Flatten | 1568 | |
| Linear1 + ReLU | 128 | 学习1568到128的映射 |
| Linear2 | 10 | 每类一个logit |
含偏置参数:Conv1为 ;Conv2为 ;Linear1为 ;Linear2为 ,共206922。第一全连接最多,因为每个输出都连接1568个输入。改变批量大小B不改变这些参数。
logit是未归一化类别得分,可为负,也不需和为1。原forward注释把输出叫概率,但代码直接返回Linear2结果,应准确称logits。CrossEntropyLoss接收这些得分并内部执行相应log-softmax计算,所以不应为了迁就注释额外加softmax。
补充三类小例:logits 的最大值在第0类,因此预测0;变概率后约 ,最大位置不变。但求loss时应按API所需传logits,不把两种表示混着用。
五轮训练(p.25)
代码用多类交叉熵、Adam、学习率0.001,默认五轮。读训练循环时每一步问“有没有改变参数”:
zero_grad()清掉此前累积的梯度,不是清掉模型权重。- 前向由当前权重算本批logits,再与真实标签计算loss;此时参数还没更新。
backward()按链式法则算loss对参数的梯度;它主要写梯度,也还不是优化器更新。step()才让Adam利用梯度及其内部状态调整参数。
补充理解更新方向的小例,用最简单SGD而不是冒充Adam公式:若某权重为2、梯度3、学习率0.1,则一步为 。梯度正说明局部增大该权重会增大loss,所以往反方向走。Adam还会使用历史梯度统计,不能把它等同这一条裸SGD式。
一个epoch通常意为遍历训练集一次;本篇与后面T07提前100批退出的演示不同。原代码把batch平均loss再平均;最后小batch会被同等加权。补充例:两批分别4张均损失1、1张均损失3,简单批均值为2,按样本平均为 。报告全体样本平均时,累加loss乘该批样本数再除总数更准确。
补充 · 训练日志不该一直抓着反向计算图(通识讲解;演示数据不属于原题)。
训练一批数据时,中间激活会被计算图记录,供随后反向传播使用。反向完成后,通常不需要让日志继续持有整张图。若把每批带梯度关系的loss张量长期放进列表,可能让不再需要的对象仍可被引用,造成不必要的内存占用;只为记录曲线时,应保存脱离计算图的数值。原篇使用的 loss.item() 就是在取供统计使用的标量,而不是拿日志再反传。
调试资源增长时,先看是否跨批保存输出、loss或中间特征,分清哪些是学习所需状态、哪些只是展示所需。评价阶段关闭梯度记录可以减少这部分开销,但仍要另设评价模式。保存模型参数能保留预测函数;若要精确续训,还需要考虑优化器状态和训练进度,不能只凭一份权重文件宣称恢复了完全相同的训练过程。
测试与结束(p.26–27)
测试问的是当前模型对保留数据表现如何,不应该再拿这些标签更新参数。p.26同时用eval()和no_grad(),它们不重复:前者切换dropout/BN等层的行为,后者不建立反向计算图以节省资源。即使本网络没有这些特殊层,也应保留这个概念区别。
每张图取十个logits最大值位置作为预测类,再与标签比;累计正确数与总数。补充例:第一批8/10正确,第二批1/2正确,总准确率为9/12=75%,不能把80%与50%简单平均成65%。
小检查:测试时只写no_grad()是否就自动关闭dropout?
参考答案
不会。它停止梯度记录,不切模型训练/评价模式;关闭dropout等行为要使用eval()。
p.27仅结束页,没有新增实测结果。以上是代码阅读与纸面形状/参数核验,不是MNIST训练报告。
原代码完整串读(p.21–26)
以下依课件顺序转写,统一缩进;原注释中的“probabilities”已按实际返回值注明logits。先建立数据与设备,再定义模型,最后定义训练与评价函数。p.20关于卷积层太少/太多、通过池化降低空间尺寸的说法是结构选择的经验,不是所有CNN必须使用固定层数的定理。
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_dataset = datasets.MNIST(root='./data', train=True,
download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False,
download=True, transform=transform)
batch_size = 64
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
class MNISTCNN(nn.Module):
def __init__(self):
super(MNISTCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1)
self.fc1 = nn.Linear(32 * 7 * 7, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 32 * 7 * 7)
x = F.relu(self.fc1(x))
x = self.fc2(x) # logits
return x
model = MNISTCNN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
epochs = 5
def train(model, train_loader, criterion, optimizer, epochs):
model.train()
for epoch in range(epochs):
running_loss = 0.0
correct = 0
total = 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
avg_loss = running_loss / len(train_loader)
acc = 100 * correct / total
print(f'Epoch {epoch+1}/{epochs} - Loss: {avg_loss:.4f} - Training Accuracy: {acc:.2f}%')
def evaluate(model, test_loader):
model.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
acc = 100 * correct / total
print(f'Test Accuracy: {acc:.2f}%')
return acc
同一个self.pool被调用两次,因为它没有学习参数,这里无需为两次池化各定义一个对象。view的−1让批量维自动推断;torch.max(..., 1)沿类别轴返回最大值及类别下标,代码用_忽略最大值。原代码.data在此用于预测统计,更明确的补充写法是outputs.detach().argmax(dim=1)。
原页止于函数定义。若要自行运行,下面两行是补充调用,不是课件报告的已完成训练:
train(model, train_loader, criterion, optimizer, epochs)
evaluate(model, test_loader)
补充:最后一个小batch为什么不能和大batch等权
自编评价共10条样本,前一批8条的平均损失0.5,最后一批2条的平均损失2。全体损失总和为 ,总体平均为0.8。若直接平均两批的平均数,得到 ,让每条末批样本得到前批样本四倍的权重。
准确率同理。若前批对7条、后批对1条,总准确率为8/10;平均批准确率 并不等于0.8。记录累计损失总和、正确样本数和实际样本总数,再在循环结束除一次,最容易与定义对应。
若损失定义本身按有效token、有效像素或带权样本平均,累积时的分母也应换成相应有效数量,不能机械总是乘batch大小。这里讨论未加权的每样本分类损失,与本节手写数字任务相配;这项算术核验不表示已经重跑五轮训练。
本讲核心考点
- 追踪卷积、池化、展平、全连接的维度(p.23–24,C;与主课计算题同主题)。
- logits/概率、训练/评价模式、逐批平均/逐样本平均各有不同含义(p.24–26,C)。
附录
本部分没有需要另展的推导或题目演练。
来源与证据
- 原件:T06-CNN-Concepts-and-Practice-2025T1.pdf(本地课件),p.18–27;均为当前 PDF 页号。
- 本次重写核对本篇所列讲义页码;旧版作业/试题题面如保留,属于历史练习,不代表 2026T1 考核要求。未以未公开试题推断考试内容。
- 往年卷仅证明相应年份考过;2024 卷及 2025 节选未公开的选择题不作推断。