深度 进修:PyTorch实现CNN手写字识别 深造 进修
一、项目介绍
1、概述
手写数字识别是模式识别领域的经典 难题,旨在让计算机自动识别手写的数字字符。这个 难题看似简单,但对于计算机来说却具有挑战性, 由于不同人的书写风格、字体 大致和形状差异很大。MNIST 数据集是手写数字识别领域的标准基准数据集,包含 60,000 张训练图像和 10,000 张测试图像,每张图像都是 28×28 像素的灰度图,涵盖了 0-9 共 10 个数字。
卷积神经网络 (CNN) 是一种专门为处理具有网格结构数据(如图像)而设计的深度 进修模型。CNN 通过卷积层自动提取图像特征,减少了对人工特征工程的依赖, 并且在图像识别任务中取得了巨大成功。PyTorch 一个开源的深度 进修框架,提供了动态计算图、自动微分等功能,使得构建和训练神经网络变得更加简单和直观。
MNIST手写字识别 一个经典的机器 进修任务,旨在通过卷积神经网络(CNN)对28×28像素的灰度手写数字图像进行分类。该数据集包含70,000张图片,分为60,000张训练图片和10,000张测试图片,每张图片代表一个0-9的手写数字。
本项目的目标是构建一个高精度的卷积神经网络模型,能够准确地识别手写数字, 并且在测试集上达到较高的准确率。 除了这些之后,项目还将探索 怎样优化模型结构、调整超参数以 进步性能。
2、项目意义
【1】教育 价格
MNIST 手写数字识别是深度 进修入门的经典项目,适合初学者 进修和 领会卷积神经网络的基本原理和实现 技巧。通过这个项目,可以掌握 PyTorch 的基本使用、数据处理、模型构建、训练和评估等技能。
【2】技术验证
CNN 在 MNIST 数据集上的优异表现验证了深度 进修在图像识别领域的有效性,为更复杂的图像识别任务提供了 学说和 操作基础。
【3】实际应用
手写数字识别技术在现实生活中有广泛的应用,如邮政编码识别、银行支票处理、表单数字识别等。虽然 MNIST 一个相对简单的数据集,但它为开发更复杂的手写文字识别 体系提供了基础。
【4】模型评估
MNIST 数据集 小编认为 一个标准基准,可以用于评估不同模型架构和训练 技巧的性能,比较它们的优缺点。
3、项目描述
这个项目使用 PyTorch 实现了一个卷积神经网络,用于识别 MNIST 数据集中的手写数字。项目主要包括 下面内容 几许部分:
【1】输入层
手写字MNIST图片
【2】卷积块1
卷积层1: 个3×3卷积核,步长1,padding=1
ReLU激活函数
最大池化:2×2窗口,步长2
【3】卷积块2
卷积层2:128个3×3卷积核,步长1,padding=1
ReLU激活函数
最大池化:2×2窗口,步长2
【4】 展平层:将三维特征图展平为一维向量
【5】全连接层1:1024个神经元
【6】Dropout层:防止过拟合(通常p=0.5)
【7】全连接层2:输出层(示例为0-9数字的10分类) 【8】 结局可视化
4、MNIST 数据集介绍
MNIST(Modified National Institute of Standards and Technology)数据集是机器 进修领域最经典的图像分类数据集 其中一个,专门用于手写数字识别任务。MNIST数据集因其简单性和规范性,至今仍是教学和研究的重要资源, 虽然其技术挑战性已不如前,但作为:
【1】 模型调试的”试金石”
【2】 算法教学的直观案例
【3】 预处理流程的示范样本
仍然具有不可替代的 价格。当需要更具挑战性的基准时,研究者通常会转向CIFAR-10/100或I geNet等更复杂的数据集。该数据集包含0-9共10个 数字的手写灰度图像;训练集60,000张图像;测试集10,000张图像,该数据集的特点:
MNIST数据集的优点:
【1】 数据干净、标注准确
【2】 尺寸小(约12MB),易于快速实验
【3】 良好的类别平衡(每个数字约6,000训练样本)
MNIST数据集的局限性:
【1】 过于简单,现代算法易达到饱和性能
【2】 缺乏 诚恳场景的复杂性(如背景噪声、倾斜变形等)
5、用到的技术和工具
开发工具:Anaconda Jupyter Notebook
深度 进修框架:PyTorch
Python工具包:Numpy、 tplotlib
二、PyTorch实现手写字识别
1、环境准备和数据处理
【1】导入了所有必要的PyTorch模块
【2】设置了随机种子以确保 结局可复现
【3】检查并设置使用GPU或CPU
# 导入必要的库 import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms import tplotlib.pyplot as plt import numpy as np import warnings
# 忽略警告信息 warnings.filterwarnings(“ignore”)
# 设置随机种子保证 结局可复现 torch. nual_seed(42)
# 检查是否可以使用GPU device = torch.device(“cuda” if torch.cuda.is_available() else “cpu”) print(f”Using device: {device}”)
2、数据加载和预处理
【1】 定义了数据转换,包括将图像转换为Tensor和归一化
【2】 载并加载MNIST数据集
【3】 创建了训练和测试的数据加载器
【4】 可视化了一些训练样本以检查数据是否正确加载
【5】输出:显示15张MNIST手写数字图像,每张图像上方有对应的标签
# 定义数据转换 transform = transforms.Compose([ transforms.ToTensor(), # 将PIL图像转换为Tensor transforms.Nor lize((0.1307,), (0.3081,)) # MNIST数据集的均值和标准差 ])
# 下载并加载训练数据集 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 = # 每批加载的图像数量 train_loader = DataLoader( dataset=train_dataset, batch_size=batch_size, shuffle=True # 打乱训练数据 )
test_loader = DataLoader( dataset=test_dataset, batch_size=batch_size, shuffle=False # 测试数据不需要打乱 )
# 可视化一些训练样本 def plot_i ges(i ges, labels, nrows=3, ncols=5): plt.figure(figsize=(10, 6)) for i in range(nrows * ncols): plt.subplot(nrows, ncols, i + 1) plt.imshow(i ges[i].squeeze(), c p='gray') # 显示灰度图像 plt.title(f”Label: {labels[i]}”) plt.axis('off') plt.tight_layout() plt.show()
# 获取一批训练数据 data_iter = iter(train_loader) i ges, labels = next(data_iter)
# 显示图像 plot_i ges(i ges, labels)
3、构建CNN模型
【1】定义了一个CNN类,继承自nn.Module
【2】模型包含两个卷积块,每个块包含卷积层、ReLU激活和最大池化
【3】 接着 一个展平层将三维特征图转换为一维向量
【4】 接着是两个全连接层,中间有Dropout层防止过拟合
【5】 最后打印出模型结构,可以看到各层的参数设置
# 定义CNN模型 class CNN(nn.Module): def __init__(self): super(CNN, self).__init__() # 卷积块1 self.conv_block1 = nn.Sequential( # 卷积层1: 输入通道1(灰度图), 输出通道 , 3×3卷积核, padding=1 nn.Conv2d(1, , kernel_size=3, stride=1, padding=1), nn.ReLU(), # ReLU激活函数 # 最大池化: 2×2窗口, 步长2 nn.MaxPool2d(kernel_size=2, stride=2) ) # 卷积块2 self.conv_block2 = nn.Sequential( # 卷积层2: 输入通道 , 输出通道128, 3×3卷积核, padding=1 nn.Conv2d( , 128, kernel_size=3, stride=1, padding=1), nn.ReLU(), # ReLU激活函数 # 最大池化: 2×2窗口, 步长2 nn.MaxPool2d(kernel_size=2, stride=2) ) # 展平层 self.flatten = nn.Flatten() # 全连接层1 self.fc1 = nn.Linear(128 * 7 * 7, 1024) # 输入尺寸计算: 28×28经过两次池化变为7×7 self.relu = nn.ReLU() # Dropout层 self.dropout = nn.Dropout(0.5) # 全连接层2 (输出层) self.fc2 = nn.Linear(1024, 10) # 10个输出对应10个数字类别 def forward(self, x): # 前向传播 x = self.conv_block1(x) # 通过第一个卷积块 x = self.conv_block2(x) # 通过第二个卷积块 x = self.flatten(x) # 展平特征图 x = self.fc1(x) # 第一个全连接层 x = self.relu(x) # ReLU激活 x = self.dropout(x) # Dropout x = self.fc2(x) # 输出层 return x
# 创建模型实例并移动到设备(GPU或CPU) model = CNN().to(device) print(model)
输出 结局:
CNN( (conv_block1): Sequential( (0): Conv2d(1, , kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): ReLU() (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) ) (conv_block2): Sequential( (0): Conv2d( , 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (1): ReLU() (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) ) (flatten): Flatten(start_dim=1, end_dim=-1) (fc1): Linear(in_features=6272, out_features=1024, bias=True) (relu): ReLU() (dropout): Dropout(p=0.5, inplace=False) (fc2): Linear(in_features=1024, out_features=10, bias=True) )4、定义损失函数和优化器
【1】使用交叉熵损失函数,适合多分类 难题
【2】使用Adam优化器,它是一种自适应 进修率的优化算法
【3】添加了 进修率调度器,当验证损失不再下降时自动降低 进修率
# 定义损失函数 – 交叉熵损失 criterion = nn.CrossEntropyLoss()
# 定义优化器 – Adam优化器 optimizer = optim.Adam(model.parameters(), lr=0.001) # 进修率设为0.001
# 进修率调度器 – 在验证损失不再下降时降低 进修率 scheduler = optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', # 监控验证损失的最小值 factor=0.1, # 进修率衰减因子 patience=5, # 耐心值 verbose=True # 打印 进修率更新信息 )
5、训练模型
【1】 我们设置了15个训练周期(epoch)
【2】 每个epoch中,模型在训练集上训练, 接着在测试集上验证
【3】 记录并打印每个epoch的训练和验证损失及准确率
【4】 使用 进修率调度器根据验证损失调整 进修率
# 训练参数 num_epochs = 15 # 训练轮数 train_losses = [] # 记录训练损失 train_accuracies = [] # 记录训练准确率 val_losses = [] # 记录验证损失 val_accuracies = [] # 记录验证准确率
# 训练循环 for epoch in range(num_epochs): model.train() # 设置模型为训练模式 running_loss = 0.0 correct = 0 total = 0 # 遍历训练数据加载器 for i ges, labels in train_loader: # 将数据移动到设备(GPU或CPU) i ges = i ges.to(device) labels = labels.to(device) # 前向传播 outputs = model(i ges) loss = criterion(outputs, labels) # 反向传播和优化 optimizer.zero_grad() # 清空梯度 loss.backward() # 反向传播计算梯度 optimizer.step() # 更新参数 # 统计训练损失和准确率 running_loss += loss.item() _, predicted = torch. x(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() # 计算平均训练损失和准确率 train_loss = running_loss / len(train_loader) train_accuracy = 100 * correct / total train_losses.append(train_loss) train_accuracies.append(train_accuracy) # 验证阶段 model.eval() # 设置模型为评估模式 val_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): # 不计算梯度 for i ges, labels in test_loader: i ges = i ges.to(device) labels = labels.to(device) outputs = model(i ges) loss = criterion(outputs, labels) val_loss += loss.item() _, predicted = torch. x(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() # 计算平均验证损失和准确率 val_loss = val_loss / len(test_loader) val_accuracy = 100 * correct / total val_losses.append(val_loss) val_accuracies.append(val_accuracy) # 更新 进修率 scheduler.step(val_loss) # 打印训练和验证信息 print(f”Epoch [{epoch + 1}/{num_epochs}], “ f”Train Loss: {train_loss:.4f}, Train Acc: {train_accuracy:.2f}%, “ f”Val Loss: {val_loss:.4f}, Val Acc: {val_accuracy:.2f}%”)
输出 结局:
Epoch [1/15], Train Loss: 0.1201, Train Acc: 96.24%, Val Loss: 0.0327, Val Acc: 98.81% Epoch [2/15], Train Loss: 0.0463, Train Acc: 98.55%, Val Loss: 0.0297, Val Acc: 99.01% Epoch [3/15], Train Loss: 0.0320, Train Acc: 98.99%, Val Loss: 0.0228, Val Acc: 99.30% Epoch [4/15], Train Loss: 0.0257, Train Acc: 99.19%, Val Loss: 0.0237, Val Acc: 99.21% Epoch [5/15], Train Loss: 0.0207, Train Acc: 99.36%, Val Loss: 0.0410, Val Acc: 98.83% Epoch [6/15], Train Loss: 0.0171, Train Acc: 99.48%, Val Loss: 0.0300, Val Acc: 99.19% Epoch [7/15], Train Loss: 0.0148, Train Acc: 99.53%, Val Loss: 0.0253, Val Acc: 99.29% Epoch [8/15], Train Loss: 0.0128, Train Acc: 99.56%, Val Loss: 0.0319, Val Acc: 99.20% Epoch [9/15], Train Loss: 0.0107, Train Acc: 99.66%, Val Loss: 0.0339, Val Acc: 99.22% Epoch [10/15], Train Loss: 0.0046, Train Acc: 99.84%, Val Loss: 0.0247, Val Acc: 99.44% Epoch [11/15], Train Loss: 0.0022, Train Acc: 99.93%, Val Loss: 0.0239, Val Acc: 99.42% Epoch [12/15], Train Loss: 0.0016, Train Acc: 99.95%, Val Loss: 0.0235, Val Acc: 99.42% Epoch [13/15], Train Loss: 0.0015, Train Acc: 99.95%, Val Loss: 0.0250, Val Acc: 99.39% Epoch [14/15], Train Loss: 0.0008, Train Acc: 99.97%, Val Loss: 0.0245, Val Acc: 99.44% Epoch [15/15], Train Loss: 0.0009, Train Acc: 99.97%, Val Loss: 0.0262, Val Acc: 99.41%6、可视化训练 经过
【1】通过可视化可以直观地看到模型的 进修 经过
【2】理想情况下,训练和验证损失都应该下降,准确率都应该上升
【3】如果验证指标开始变差而训练指标继续改善,可能出现过拟合
【4】输出:显示两张图表:一张是训练和验证损失随epoch的变化,另一张是训练和验证准确率随epoch的变化
# 绘制训练和验证损失曲线 plt.figure(figsize=(12, 5)) plt.subplot(1, 2, 1) plt.plot(train_losses, label='Train Loss') plt.plot(val_losses, label='Validation Loss') plt.title('Training and Validation Loss') plt.xlabel('Epoch') plt.ylabel('Loss') plt.legend()
# 绘制训练和验证准确率曲线 plt.subplot(1, 2, 2) plt.plot(train_accuracies, label='Train Accuracy') plt.plot(val_accuracies, label='Validation Accuracy') plt.title('Training and Validation Accuracy') plt.xlabel('Epoch') plt.ylabel('Accuracy (%)') plt.legend()
plt.tight_layout() plt.show()
7、模型评估
【1】在测试集上评估模型的最终性能
【2】计算并显示混淆矩阵,可以查看模型在哪些数字上容易混淆
【3】高准确率表明模型性能良好
# 在测试集上评估模型 model.eval() # 设置模型为评估模式 test_loss = 0.0 correct = 0 total = 0 all_preds = [] all_labels = []
with torch.no_grad(): # 不计算梯度 for i ges, labels in test_loader: i ges = i ges.to(device) labels = labels.to(device) outputs = model(i ges) loss = criterion(outputs, labels) test_loss += loss.item() _, predicted = torch. x(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() # 保存预测 结局和 诚恳标签用于后续分析 all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.cpu().numpy())
# 计算测试集上的准确率 test_accuracy = 100 * correct / total print(f”Test Accuracy: {test_accuracy:.2f}%”)
# 计算混淆矩阵 from sklearn.metrics import confusion_ trix import seaborn as sns
cm = confusion_ trix(all_labels, all_preds)
# 绘制混淆矩阵 plt.figure(figsize=(10, 8)) sns.heat p(cm, annot=True, fmt='d', c p='Blues', xticklabels=range(10), yticklabels=range(10)) plt.title('Confusion Matrix') plt.xlabel('Predicted Label') plt.ylabel('True Label') plt.show()
8、可视化预测 结局
输出:
【1】 显示15张测试图像,每张图像上方显示预测标签和 诚恳标签
【2】 正确预测用绿色显示,错误预测用红色显示
说明:
【1】 可视化一些测试样本的预测 结局
【2】 可以直观地看到模型在哪些图像上表现良好,在哪些图像上出现错误
【3】 有助于 领会模型的优势和局限性
# 获取一批测试数据 data_iter = iter(test_loader) i ges, labels = next(data_iter) i ges = i ges.to(device) labels = labels.to(device)
# 进行预测 outputs = model(i ges) _, predicted = torch. x(outputs, 1)
# 将数据移回CPU以便可视化 i ges = i ges.cpu() labels = labels.cpu() predicted = predicted.cpu()
# 可视化预测 结局 def plot_predictions(i ges, labels, predicted, nrows=3, ncols=5): plt.figure(figsize=(12, 8)) for i in range(nrows * ncols): plt.subplot(nrows, ncols, i + 1) plt.imshow(i ges[i].squeeze(), c p='gray') # 用绿色表示正确预测,红色表示错误预测 color = 'green' if labels[i] == predicted[i] else 'red' plt.title(f”Pred: {predicted[i]} True: {labels[i]}”, color=color) plt.axis('off') plt.tight_layout() plt.show()
plot_predictions(i ges, labels, predicted)