一、MNIST数据集与PyTorch基础
MNIST是深度学习中非常经典的入门数据集,主要用于手写数字识别。数据集一共有70000张图片,其中60000张用于训练,10000张用于测试。每张图片都是28×28大小的灰度图像,对应的标签是0~9,因此MNIST属于典型的十分类问题。
在PyTorch中,可以使用torchvision直接加载MNIST:
training_data = datasets.MNIST(
root="data",
train=True,
download=True,
transform=ToTensor()
)
test_data = datasets.MNIST(
root="data",
train=False,
download=True,
transform=ToTensor()
)
root表示数据集保存的位置,train=True表示加载训练集,train=False表示加载测试集。download=True表示本地没有数据时自动下载。
ToTensor()的作用是将图片转换成PyTorch中的Tensor,同时将像素值转换到0~1之间。原始图片大小为28×28,转换后形状为:
[1, 28, 28]
其中1表示灰度通道。
PyTorch中的Tensor可以理解为一种多维数组,是深度学习中最基本的数据结构。图片、标签以及模型参数等都可以使用Tensor表示,并且Tensor可以放到GPU上进行计算。
为了观察数据,可以使用Matplotlib显示9张图片:
for i in range(9):
img, label = training_data[i]
figure.add_subplot(3, 3, i + 1)
plt.title(label)
plt.axis("off")
plt.imshow(img.squeeze(), cmap="gray")
这里img.squeeze()用于去掉大小为1的通道维度,使图片更适合使用Matplotlib显示。
二、DataLoader、Batch与Epoch
数据集加载完成后,还需要使用DataLoader组织数据:
train_dataloader = DataLoader(
training_data,
batch_size=128,
shuffle=True
)
test_dataloader = DataLoader(
test_data,
batch_size=128
)
batch_size=128表示模型每次读取128张图片进行训练,而不是一次将60000张图片全部送入模型。
shuffle=True表示每轮训练开始时随机打乱训练数据,可以减少模型对数据排列顺序的依赖。
这里需要理解三个重要概念。
Batch表示一次送入模型的一批数据。本代码每个Batch最多包含128张图片。
Iteration表示模型完成一次Batch的训练,包括前向传播、计算损失、反向传播和参数更新。
Epoch表示模型完整训练一次训练集。
60000张训练图片按照128张一批进行划分:
60000 ÷ 128 ≈ 469
因此一个Epoch大约有469个Batch,也就是大约469次参数更新。代码设置训练15个Epoch,所以模型会多次完整学习整个训练集。
三、CNN卷积神经网络
代码使用卷积神经网络CNN进行手写数字识别:
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
继承nn.Module是PyTorch定义神经网络的标准方式。
这个模型主要由卷积层、BatchNorm、ReLU、池化层和全连接层组成。
整个网络的数据尺寸变化如下:
输入
1 × 28 × 28
第一层卷积
32 × 28 × 28
最大池化
32 × 14 × 14
第二层卷积
64 × 14 × 14
第二层卷积
64 × 14 × 14
最大池化
64 × 7 × 7
第三层卷积
128 × 7 × 7
展平
128 × 7 × 7 = 6272
全连接层
256
输出层
10
Conv2d是CNN的核心,通过卷积核在图片上移动提取特征。网络前面的卷积层通常学习比较简单的特征,例如边缘、线条等,随着网络加深,可以逐渐学习更加复杂的数字结构。
例如:
nn.Conv2d(1, 32, 5, 1, 2)
表示输入1个通道,输出32个特征图,卷积核大小为5×5,步长为1,padding为2。由于padding设置合理,因此卷积后图片的宽高仍然保持28×28。
ReLU是常用的激活函数:
nn.ReLU()
它能够增加神经网络的非线性表达能力,使模型可以学习更加复杂的关系。
MaxPool2d(2)是最大池化层,会将特征图的宽和高缩小一半。例如:
28 × 28 → 14 × 14
14 × 14 → 7 × 7
这样可以减少计算量,同时保留比较重要的特征。
BatchNorm2d用于对特征进行归一化处理,可以使训练过程更加稳定,并帮助模型更快地收敛。
经过卷积层后,特征图需要转换成一维数据,再交给全连接层:
x = x.view(x.size(0), -1)
这里view()可以改变Tensor的形状。-1表示让PyTorch自动计算这一维的大小。
最终:
128 × 7 × 7 = 6272
所以全连接层使用:
nn.Linear(128 * 7 * 7, 256)
最后通过:
nn.Linear(256, 10)
输出10个数字类别的结果。
模型中还使用了:
nn.Dropout(0.3)
Dropout会在训练过程中随机关闭一部分神经元,可以减少模型对训练数据的过度依赖,从而降低过拟合。
四、模型训练、损失函数与优化器
模型训练是整个深度学习过程的核心,基本流程为:
输入数据
↓
前向传播
↓
计算损失
↓
反向传播
↓
计算梯度
↓
更新参数
训练代码:
pred = model(x)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
首先将图片输入模型,通过前向传播得到预测结果pred。
然后使用交叉熵损失函数:
loss_fn = nn.CrossEntropyLoss()
计算预测结果与真实标签之间的差距。MNIST属于多分类问题,因此交叉熵损失非常适合这种任务。
接下来执行:
optimizer.zero_grad()
清除上一次计算得到的梯度。PyTorch默认会累积梯度,所以每次更新参数之前通常都需要清空梯度。
然后:
loss.backward()
执行反向传播,根据损失函数计算模型各个参数的梯度。
最后:
optimizer.step()
根据计算出的梯度更新模型参数,使模型下一次预测更加接近正确结果。
代码使用Adam优化器:
optimizer = torch.optim.Adam(
model.parameters(),
lr=0.001,
weight_decay=1e-4
)
model.parameters()表示需要训练的模型参数。
lr是学习率,用于控制参数每次更新的幅度。学习率过大可能导致模型训练不稳定,过小则可能导致训练速度很慢。
weight_decay属于正则化方法,可以限制模型参数过度增长,在一定程度上减少过拟合。
五、学习率调整、模型测试与设备
为了让模型训练后期更加稳定,代码使用StepLR调整学习率:
scheduler = torch.optim.lr_scheduler.StepLR(
optimizer,
step_size=5,
gamma=0.5
)
step_size=5表示每训练5个Epoch调整一次学习率,gamma=0.5表示学习率变为原来的0.5倍。
如果初始学习率为0.001,那么学习率大致变化为:
开始:0.001
第5轮后:0.0005
第10轮后:0.00025
第15轮后:0.000125
训练过程中使用:
scheduler.step()
更新学习率。
模型还可以根据电脑硬件选择运行设备:
device = 'cuda' if torch.cuda.is_available() \\
else 'mps' if torch.backends.mps.is_available() \\
else 'cpu'
如果存在NVIDIA GPU,可以使用CUDA;Apple芯片可以使用MPS;如果都不可用,则使用CPU。
模型和数据必须放在相同设备上:
model = CNN().to(device)
x, y = x.to(device), y.to(device)
训练完成后,需要使用测试集评价模型:
model.eval()
with torch.no_grad():
…
model.eval()表示切换到测试模式,尤其会影响BatchNorm和Dropout等层。
torch.no_grad()表示测试过程中不计算梯度,可以减少内存占用并提高运行效率。
最后通过:
pred.argmax(1)
找到模型输出结果中最大值所在的位置,这个位置就是模型预测的数字。
例如模型输出10个类别的结果:
[0.01, 0.02, 0.05, 0.90, …]
最大值位于第3个位置,则模型预测结果就是数字3。
准确率的计算方式为:
Accuracy = 正确预测数量 ÷ 测试数据总数量
通过这个MNIST项目,可以完整理解PyTorch深度学习的基本流程:从数据加载开始,经过DataLoader组织Batch,再利用CNN提取图像特征,通过损失函数衡量预测结果,利用反向传播计算梯度,最后使用优化器更新参数。经过多个Epoch训练后,再利用测试集计算准确率,从而判断模型的识别效果。


