Appearance
MNIST 手写数字训练
进行了一定的手写但中途知道要做什么但是不知道如何代码实现会卡很久,在开头模型定义参考了PyTorchBlitz部分。其中连接层等的部分为什么这样做不太理解,后续还需要学习,也许应该尝试直接做简单一些的,附上原有代码和后续AI补全的代码与缺点诊断:
Python
import torch
import torchvision
import torch.nn.functional as F
from torchvision import datasets, transforms
transform = transforms.Compose([transforms.ToTensor()]) # 转化数据格式并归一化
trainset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
testset = torchvision.datasets.MNIST(root='./data', train=False, transform=transform, download=True)
dataloader = torch.utils.data.DataLoader(trainset, batch_size=16, shuffle=True)
testloader = torch.utils.data.DataLoader(testset, batch_size=16, shuffle=True)
class Net(torch.nn.Module):
def __init__(self):
super().__init__()
self.fc1 = torch.nn.Linear(784, 128)
self.fc2 = torch.nn.Linear(128, 64)
self.fc3 = torch.nn.Linear(64, 10)
self.conv1 = torch.nn.Conv2d(1, 6, 5)
self.conv2 = torch.nn.Conv2d(6, 16, 5)
def forward(self, x):
c1 = F.relu(self.conv1(x))
s2 = F.max_pool2d(c1, (2,2))
c3 = F.relu(self.conv2(s2))
f5 = F.relu(self.fc1(c3))
f6 = torch.relu(self.fc2(f5))
output = self.fc3(f6)
return output
net = Net()
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.001)
epochs = 10
for epoch in range(epochs):
running_loss = 0.0
for i, data in enumerate(dataloader, 0):
inputs, labels = data
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 100 == 99:
print('[%d, %5d] loss: %.3f' %epoch + 1, i + 1, running_loss / 100)
running_loss = 0.0
print('Finished Training')
net.eval()
with torch.no_grad():
for data in testloader:
inputs, labels = data
outputs = net(inputs)
loss = criterion(outputs, labels)
running_loss += loss.item()后续不知道如何对测试集进行测试AI进行了补全:
Python
import torch
import torchvision
import torch.nn.functional as F
from torchvision import datasets, transforms
# 1. 数据准备
transform = transforms.Compose([transforms.ToTensor()])
trainset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
testset = torchvision.datasets.MNIST(root='./data', train=False, transform=transform, download=True)
dataloader = torch.utils.data.DataLoader(trainset, batch_size=200, shuffle=True)
testloader = torch.utils.data.DataLoader(testset, batch_size=200, shuffle=False) # 测试集通常不需打乱
# 2. 模型定义
class Net(torch.nn.Module):
def __init__(self):
super().__init__()
# 针对路线 B(标准卷积神经网络 LeNet 结构):
# MNIST 图片 28x28 经过 conv1(5x5) -> 24x24 -> max_pool(2x2) -> 12x12 # 再经过 conv2(5x5) -> 8x8 -> 这里加一个 max_pool(2x2) -> 4x4 # 最后的特征图大小为:16个通道 * 4 * 4 = 256 self.conv1 = torch.nn.Conv2d(1, 6, 5)
self.conv2 = torch.nn.Conv2d(6, 16, 5)
# 修正全连接层的输入维度,从 784 改为 256 self.fc1 = torch.nn.Linear(256, 128)
self.fc2 = torch.nn.Linear(128, 64)
self.fc3 = torch.nn.Linear(64, 10)
def forward(self, x):
# 卷积与池化层
c1 = F.relu(self.conv1(x))
s2 = F.max_pool2d(c1, (2, 2))
c3 = F.relu(self.conv2(s2))
s4 = F.max_pool2d(c3, (2, 2)) # 补充一个池化,让特征图降维到 4x4
# 核心补充:必须将四维特征图展平(Flatten)为二维,才能送入全连接层
# [batch_size, 16, 4, 4] -> [batch_size, 256]
f5_input = torch.flatten(s4, 1)
# 全连接层
f5 = F.relu(self.fc1(f5_input))
f6 = F.relu(self.fc2(f5))
output = self.fc3(f6)
return output
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
net = Net()
net.to(device)
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.01) # 略微调大了解率
# 3. 训练循环
epochs = 5 # MNIST 很简单,5轮就足够看效果了
print("Start Training...")
for epoch in range(epochs):
net.train() # 显式声明进入训练模式
running_loss = 0.0
for i, data in enumerate(dataloader, 0):
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if i % 100 == 99:
# 修正了原代码的元组语法错误
print('[Epoch %d, Batch %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 100))
running_loss = 0.0
print('Finished Training\n')
# 4. 测试与评估(补充完整逻辑)
net.eval()
test_loss = 0.0
correct = 0
total = 0
print("Start Testing...")
with torch.no_grad():
for data in testloader:
inputs, labels = data
outputs = net(inputs)
loss = criterion(outputs, labels)
test_loss += loss.item()
# 计算准确率
_, predicted = torch.max(outputs.data, 1) # 找到概率最大的类别索引
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Test Loss: %.3f' % (test_loss / len(testloader)))
print('Accuracy on the test images: %.2f %%' % (100 * correct / total))这个网络的缺陷与问题分析
虽然修改后的代码可以跑出 90%+ 的准确率,但从现代深度学习的工程实践来看,它有以下几个明显的缺陷:
1. 归一化不完美 (Transforms)
代码中只使用了 transforms.ToTensor(),这只会把图像像素从 [0, 255] 缩放到 [0, 1]。
问题:这并不是真正意义上的标准化。网络的输入数据如果不是以 0 为中心、方差为 1 的分布,在模型刚开始训练时,梯度可能不够稳定。
改进:通常会在后面追加入
transforms.Normalize((0.1307,), (0.3081,))(这是 MNIST 数据集的全局均值和标准差)。
2. 优化器与学习率较弱 (SGD + no Momentum)
你使用了最基础的随机梯度下降(SGD)优化器,且没有加动量(Momentum)。
问题:普通的 SGD 很容易陷入局部鞍点或在沟壑中震荡,导致收敛速度非常慢。如果你把学习率设为最初的
0.001,训练 10 个 Epoch 损失可能都降不下去。改进:
方案 A:给 SGD 加上动量:
torch.optim.SGD(net.parameters(), lr=0.01, momentum=0.9)方案 B:直接换用现代万金油优化器 Adam:
torch.optim.Adam(net.parameters(), lr=0.001)
3. 缺乏正则化,容易过拟合 (Overfitting)
网络中没有包含任何防止过拟合的机制(虽然 MNIST 数据集比较简单,表现不明显)。
问题:网络里连续使用了多层全连接层(
Linear),这些层参数量很大,在面对更复杂的任务(比如 CIFAR-10 图像)时,极易发生死记硬背训练集的过拟合现象。改进:在全连接层之间加入
torch.nn.Dropout(0.5)(随机失活),或者在卷积层后添加nn.BatchNorm2d(批归一化)。