实验四 卷积神经网络CNN
一、实验学时: 2学时
二、实验目的
- 掌握卷积神经网络CNN的基本结构;
- 掌握数据预处理、模型构建、训练与调参;
- 探索CNN在MNIST数据集中的性能表现;
三、实验内容
实现深度神经网络CNN。
四、主要实验步骤及结果
1.搭建一个CNN网络,使用MNIST手写数字数据集进行训练与测试,并体现模型最终结果,CNN网络的具体框架可参考下图,也可自己设计:
图4-1 CNN架构图
(1)该图表示输入层为28*28*1的尺寸,符合MNIST数据集的标准尺寸。
(2)第一个卷积层,使用5*5卷积核,32个滤波器,填充(Padding)为2。输出尺寸为28*28*32。
(3)第一个池化层,使用2*2池化窗口,步长(stride)为2。输出尺寸为14*14*32。
(4)第二个卷积层,使用5*5卷积核,64个滤波器,填充(Padding)为2。输出尺寸为14*14*64。
(5)第二个池化层,使用2*2池化窗口,步长(stride)为2。输出尺寸为7*7*64。
(6)全连接层包含1024个神经元,输出尺寸为1*1*1024。
(7)Dropout层用于防止过拟合。
(8)输出层包含10个神经元,对应手写数字的0-9。输出尺寸为1*1*10。
模型实现:
以该架构图搭建CNN网络,使用MNIST手写数字数据集进行训练与测试,训练和测试结果如图4-2所示:
图4-2 CNN测试结果
2.尝试使用不同的数据增强方法、优化器、损失函数、学习率、batch size和迭代次数来进行训练,记录训练过程,评估模型性能,保存最佳模型。
编号 | batch size | 训练轮次 | 学习率 | 数据增强方法 | 优化器 | 实验结果 |
1 | 32 | 2 | 1e-4 | 无 | Adam | 98.62% |
2 | 64 | 2 | 1e-4 | 无 | Adam | 98.56% |
3 | 64 | 4 | 1e-4 | 无 | Adam | 99.08% |
4 | 64 | 4 | 3e-4 | 无 | Adam | 99.08% |
5 | 64 | 4 | 3e-4 | 旋转+平移 | Adam | 98.90% |
5 | 64 | 4 | 3e-4 | 无 | Adam(L2正则化) | 99.23% |
6 | 64 | 4 | 1e-4 | 无 | SGD+momentum | 97.30% |
其中数据增强方法采用随机旋转和平移吗,原始代码中包含ToTensor()和Normalize(),给原始代码添加随机旋转10度和随机平移10%,代码如下:
# 数据加载(归一化)
transform = torchvision.transforms.Compose([
torchvision.transforms.RandomRotation(10), # 随机旋转10度
torchvision.transforms.RandomAffine(0, translate=(0.1, 0.1)), # 随机平移10%
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize((0.1307,), (0.3081,))
])
优化器选择方面使用SGD+momentum(0.9)替代原Adam优化器,
# 使用SGD+momentum
optimizer = torch.optim.SGD(model.parameters(), lr=LEARN_RATE, momentum=0.9)
根据训练过程记录的数据,最佳模型尊却绿为99.23%,最佳模型代码如下:
import torch
import torchvision
import torch.nn as nn
from torch.utils.data import DataLoader
BATCH_SIZE = 64
EPOCHS = 4
LEARN_RATE = 3e-4
DROPOUT_RATE = 0.5
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
# 数据加载(归一化)
transform = torchvision.transforms.Compose([
# torchvision.transforms.RandomRotation(10), # 随机旋转10度
# torchvision.transforms.RandomAffine(0, translate=(0.1, 0.1)), # 随机平移10%
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize((0.1307,), (0.3081,))
])
train_data = torchvision.datasets.MNIST(
root='./mnist',
train=True,
download=True,
transform=transform
)
train_loader = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True)
test_data = torchvision.datasets.MNIST(
root='./mnist',
train=False,
transform=transform
)
test_loader = DataLoader(test_data, batch_size=1000, shuffle=False)
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv_layers = nn.Sequential(
# 第一层卷积:5x5 卷积核,32 个过滤器,padding=2
nn.Conv2d(1, 32, kernel_size=5, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2), # 池化后 14x14x32
# 第二层卷积:5x5 卷积核,64 个过滤器,padding=2
nn.Conv2d(32, 64, kernel_size=5, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2) # 池化后 7x7x64
)
self.fc_layers = nn.Sequential(
nn.Linear(64 * 7 * 7, 1024), # 全连接层:7x7x64 → 1024
nn.ReLU(),
nn.Dropout(DROPOUT_RATE), # Dropout层
nn.Linear(1024, 10) # 输出层:1024 → 10
)
self._initialize_weights()
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.conv_layers(x)
x = x.view(x.size(0), -1) # 展平操作
x = self.fc_layers(x)
return x
model = CNN().to(device)
loss_fn = nn.CrossEntropyLoss().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=LEARN_RATE, weight_decay=1e-5)
# optimizer = torch.optim.SGD(model.parameters(), lr=LEARN_RATE, momentum=0.9) # 使用SGD+momentum
# 训练循环
for epoch in range(EPOCHS):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print(f'Epoch {epoch + 1} [{batch_idx * len(data)}/{len(train_loader.dataset)}] Loss: {loss.item():.4f}')
# 测试
model.eval()
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
pred = output.argmax(dim=1)
correct += pred.eq(target).sum().item()
print(f'Test Accuracy: {correct}/{len(test_loader.dataset)} ({100. * correct / len(test_loader.dataset):.2f}%)')
3.使用画图工具将自己的学号逐个写出,使用保存的最佳模型对每个数字进行推理,比较模型对每个数字的准确率预测,也可以尝试实现一个实时识别手写数字的demo。
(1)使用画图工具将自己的学号逐个写出,进行反色处理,并将图片命名为“x_001.png”格式。
图4-3手写数字
(2)在训练代码(CNN.py)中添加模型保存代码。
torch.save(model.state_dict(), 'mnist_cnn.pth')
(3)编写推理代码读取img文件夹中的手写图片并预测,预测代码如下所示:
import torch
import torch.nn as nn
from torchvision import transforms
from PIL import Image
import numpy as np
import os
# 定义模型结构(需与训练代码一致)
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv_layers = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=5, padding=2),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(32, 64, kernel_size=5, padding=2),
nn.ReLU(),
nn.MaxPool2d(2, 2)
)
self.fc_layers = nn.Sequential(
nn.Linear(64 * 7 * 7, 1024),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(1024, 10))
def forward(self, x):
x = self.conv_layers(x)
x = x.view(x.size(0), -1)
x = self.fc_layers(x)
return x
# 加载模型
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = CNN().to(device)
model.load_state_dict(torch.load('mnist_cnn.pth', map_location=device))
model.eval()
# 定义预处理(与训练一致)
transform = transforms.Compose([
transforms.Resize((28, 28)), # 确保输入为28x28
transforms.Grayscale(num_output_channels=1), # 转换为单通道
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
# 遍历img文件夹中的图片并推理
img_dir = 'img'
digit_stats = {str(i): {'correct': 0, 'total': 0} for i in range(10)}
for filename in os.listdir(img_dir):
if filename.lower().endswith(('.png', '.jpg', '.jpeg')):
# 从文件名中提取真实标签(假设文件名为 "label_xxx.png")
try:
true_label = filename.split('_')[0] # 例如文件名 "3_001.png" → 标签为3
true_label = int(true_label)
if true_label < 0 or true_label > 9:
continue
except:
print(f"跳过文件 {filename}(文件名格式错误)")
continue
# 加载并预处理图像
img_path = os.path.join(img_dir, filename)
image = Image.open(img_path)
image = transform(image).unsqueeze(0).to(device) # 添加batch维度
# 推理
with torch.no_grad():
output = model(image)
pred = output.argmax(dim=1).item()
# 统计结果
digit_stats[str(true_label)]['total'] += 1
if pred == true_label:
digit_stats[str(true_label)]['correct'] += 1
print(f"图片 {filename} 真实标签: {true_label}, 预测: {pred} → {'正确' if pred == true_label else '错误'}")
# 计算每个数字的准确率
accuracies = {}
for digit in digit_stats:
if digit_stats[digit]['total'] > 0:
acc = digit_stats[digit]['correct'] / digit_stats[digit]['total']
accuracies[digit] = acc
print(f"数字 {digit} 的准确率: {acc:.2%}")
预测结果如图4-4所示:
图4-4预测结果
预测结果显示“1”和“4”预测结果错误,其他均正确。
五、实验小结(包括问题和解决办法、心得体会、意见与建议等)
1.问题和解决办法:
问题1:RuntimeError: Dataset not found. You can use download=True to download it。
解决方法:添加下载训练集的参数download=True。
问题2:使用SGD+momentum优化器后,准确率反而下降了。
解决方法:因为SGD对学习率比较敏感,学习率没有适配,使用StepLR梯度衰减,另外也可以增加训练轮次。
问题3:预测结果全部错误。
解决方法:图片要像素28*28,且黑色背景,白色笔迹,对Windows画图的图片反色处理即可。
2.心得体会:通过本次CNN手写数字识别实验的完整实践,我深刻体会到深度学习模型性能的提升是一个系统工程,需要从数据、模型、训练策略到结果分析的全流程精细化把控,尝试使用不同的数据增强方法、优化器、损失函数、学习率、batch size和迭代次数来进行训练,迭代出最佳模型,再手写数字进行测试。通过以上的学习和实践,我对神经网络的原理和应用有了更深入的理解。神经网络的发展给人工智能带来了巨大的影响,它在图像识别、自然语言处理等领域发挥着重要的作用。我相信,随着技术的进步,神经网络将会有更广泛的应用。