深度学习: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:64个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或ImageNet等更复杂的数据集。该数据集包含0-9共10个阿拉伯数字的手写灰度图像;训练集60,000张图像;测试集10,000张图像,该数据集的特点:

 MNIST数据集的优点:

【1】 数据干净、标注准确

【2】 尺寸小(约12MB),易于快速实验

【3】 良好的类别平衡(每个数字约6,000训练样本)

 

MNIST数据集的局限性:

【1】 过于简单,现代算法易达到饱和性能

【2】 缺乏真实场景的复杂性(如背景噪声、倾斜变形等)

5、用到的技术和工具

开发工具:Anaconda Jupyter Notebook

深度学习框架:PyTorch

Python工具包:Numpy、matplotlib

二、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 matplotlib.pyplot as plt
import numpy as np
import warnings

# 忽略警告信息
warnings.filterwarnings(“ignore”)

# 设置随机种子保证结果可复现
torch.manual_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.Normalize((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 = 64  # 每批加载的图像数量
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_images(images, labels, nrows=3, ncols=5):
    plt.figure(figsize=(10, 6))
    for i in range(nrows * ncols):
        plt.subplot(nrows, ncols, i + 1)
        plt.imshow(images[i].squeeze(), cmap='gray')  # 显示灰度图像
        plt.title(f”Label: {labels[i]}”)
        plt.axis('off')
    plt.tight_layout()
    plt.show()

# 获取一批训练数据
data_iter = iter(train_loader)
images, labels = next(data_iter)

# 显示图像
plot_images(images, 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(灰度图), 输出通道64, 3×3卷积核, padding=1
            nn.Conv2d(1, 64, 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: 输入通道64, 输出通道128, 3×3卷积核, padding=1
            nn.Conv2d(64, 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, 64, 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(64, 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 images, labels in train_loader:
        # 将数据移动到设备(GPU或CPU)
        images = images.to(device)
        labels = labels.to(device)
        
        # 前向传播
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        # 反向传播和优化
        optimizer.zero_grad()  # 清空梯度
        loss.backward()  # 反向传播计算梯度
        optimizer.step()  # 更新参数
        
        # 统计训练损失和准确率
        running_loss += loss.item()
        _, predicted = torch.max(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 images, labels in test_loader:
            images = images.to(device)
            labels = labels.to(device)
            
            outputs = model(images)
            loss = criterion(outputs, labels)
            
            val_loss += loss.item()
            _, predicted = torch.max(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 images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        
        outputs = model(images)
        loss = criterion(outputs, labels)
        
        test_loss += loss.item()
        _, predicted = torch.max(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_matrix
import seaborn as sns

cm = confusion_matrix(all_labels, all_preds)

# 绘制混淆矩阵
plt.figure(figsize=(10, 8))
sns.heatmap(cm, annot=True, fmt='d', cmap='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)
images, labels = next(data_iter)
images = images.to(device)
labels = labels.to(device)

# 进行预测
outputs = model(images)
_, predicted = torch.max(outputs, 1)

# 将数据移回CPU以便可视化
images = images.cpu()
labels = labels.cpu()
predicted = predicted.cpu()

# 可视化预测结果
def plot_predictions(images, 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(images[i].squeeze(), cmap='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(images, labels, predicted)

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容