手写体识别

手写数字识别:基于PyTorch的卷积神经网络实现

一、项目概述

使用PyTorch实现一个基于卷积神经网络(CNN)的手写手写数字识别模型,通过MNIST数据集训练,实现对手写数字(0-9)的分类识别。

二、环境依赖

  • Python 3.x
  • PyTorch
  • torchvision
  • matplotlib

三、代码实现与解析

1. 导入必要库

# 导入PyTorch核心库
import torch
# 导入PyTorch神经网络模块
import torch.nn as nn
# 导入PyTorch优化器模块
import torch.optim as optim
# 导入数据加载工具
from torch.utils.data import DataLoader
# 导入计算机视觉相关的数据集和数据转换工具
from torchvision import datasets, transforms
# 导入matplotlib用于可视化
import matplotlib.pyplot as plt

2. 数据准备与预处理

# 定义数据转换管道:将图像转为Tensor并进行标准化
transform = transforms.Compose([
    transforms.ToTensor(),  # 将PIL图像转为Tensor格式,并将像素值从[0,255]归一化到[0,1]
    transforms.Normalize((0.1307,), (0.3081,))  # 用MNIST数据集的均值(0.1307)和标准差(0.3081)标准化数据
])

# 加载MNIST训练数据集(手写数字0-9)
train_dataset = datasets.MNIST(
    root='./data',  # 数据集存储路径
    train=True,     # 加载训练集
    download=True,  # 如果本地没有数据集则自动下载
    transform=transform  # 应用定义好的数据转换
)
# 加载MNIST测试数据集
test_dataset = datasets.MNIST(
    root='./data',  # 数据集存储路径
    train=False,    # 加载测试集
    download=True,  # 如果本地没有数据集则自动下载
    transform=transform  # 应用定义好的数据转换
)

# 创建训练数据加载器:按批次加载数据,支持打乱顺序
train_loader = DataLoader(
    train_dataset,  # 要加载的数据集
    batch_size=64,  # 每个批次包含64个样本
    shuffle=True    # 训练时打乱数据顺序,增加随机性
)
# 创建测试数据加载器:批次更大,不需要打乱
test_loader = DataLoader(
    test_dataset,   # 要加载的数据集
    batch_size=1000,# 每个批次包含1000个样本(测试时可更大)
    shuffle=False   # 测试时不需要打乱顺序
)

3. 定义神经网络模型

class HandwritingRecognizer(nn.Module):
    def __init__(self):
        # 调用父类构造函数
        super(HandwritingRecognizer, self).__init__()
        # 定义卷积层序列:用于提取图像特征
        self.conv_layers = nn.Sequential(
            # 第一个卷积层:输入1通道(灰度图),输出32通道,卷积核3x3,步长1,填充1
            nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),  # 激活函数:引入非线性,增强模型表达能力
            # 最大池化层:2x2窗口,步长2,输出尺寸变为14x14(原28x28)
            nn.MaxPool2d(kernel_size=2, stride=2),
            # 第二个卷积层:输入32通道,输出64通道,卷积核3x3,步长1,填充1
            nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1),
            nn.ReLU(),  # 激活函数
            # 最大池化层:2x2窗口,步长2,输出尺寸变为7x7(原14x14)
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        # 定义全连接层序列:用于分类决策
        self.fc_layers = nn.Sequential(
            # 第一个全连接层:输入为64通道×7×7特征图展平后的向量,输出128维
            nn.Linear(64 * 7 * 7, 128),
            nn.ReLU(),  # 激活函数
            nn.Dropout(0.5),  # 随机丢弃50%神经元,防止过拟合
            nn.Linear(128, 10)  # 输出层:10个类别(对应数字0-9)
        )

    # 定义前向传播过程
    def forward(self, x):
        x = self.conv_layers(x)  # 输入经过卷积层提取特征
        # 将特征图展平为一维向量:-1表示自动计算批次维度,64*7*7为特征维度
        x = x.view(-1, 64 * 7 * 7)
        x = self.fc_layers(x)    # 展平后的特征经过全连接层得到分类结果
        return x

4. 初始化模型、损失函数和优化器

model = HandwritingRecognizer()  # 创建手写数字识别模型实例
criterion = nn.CrossEntropyLoss()  # 定义损失函数:多分类交叉熵损失(适用于分类任务)
# 定义优化器:Adam优化器,学习率0.001(控制参数更新速度)
optimizer = optim.Adam(model.parameters(), lr=0.001)

5. 模型训练函数

def train(model, train_loader, criterion, optimizer, epochs=1):
    model.train()  # 设置模型为训练模式(启用dropout等训练特定层)
    # 遍历训练轮次
    for epoch in range(epochs):
        running_loss = 0.0  # 累计当前轮次的损失
        # 遍历训练数据的每个批次
        for batch_idx, (data, target) in enumerate(train_loader):
            optimizer.zero_grad()  # 清零优化器的梯度(防止梯度累积)
            output = model(data)   # 前向传播:输入数据经过模型得到预测结果
            loss = criterion(output, target)  # 计算预测结果与真实标签的损失
            loss.backward()        # 反向传播:计算损失对各参数的梯度
            optimizer.step()       # 更新模型参数(基于梯度和优化器规则)

            running_loss += loss.item()  # 累加当前批次的损失值
            # 每300个批次打印一次平均损失(便于监控训练过程)
            if batch_idx % 300 == 299:
                print(f'Epoch {epoch+1}, Batch {batch_idx+1}, Loss: {running_loss/300:.4f}')
                running_loss = 0.0  # 重置累计损失

6. 模型测试函数

def test(model, test_loader):
    model.eval()  # 设置模型为评估模式(关闭dropout等训练特定层)
    correct = 0   # 记录正确预测的样本数
    total = 0     # 记录总样本数
    # 关闭梯度计算(测试时不需要反向传播,节省计算资源)
    with torch.no_grad():
        # 遍历测试数据的每个批次
        for data, target in test_loader:
            output = model(data)  # 前向传播得到预测结果
            # 取预测概率最大的类别作为最终预测(dim=1表示按行取最大值)
            _, predicted = torch.max(output.data, 1)
            total += target.size(0)  # 累加总样本数
            # 累加预测正确的样本数(预测类别与真实标签相等)
            correct += (predicted == target).sum().item()
    # 计算并打印测试集准确率
    print(f'Test Accuracy: {100 * correct / total:.2f}%')

7. 执行训练和测试

# 执行训练和测试:训练1轮,然后在测试集上评估
train(model, train_loader, criterion, optimizer, epochs=1)
test(model, test_loader)

8. 可视化预测结果

def visualize_prediction(model, test_dataset, idx=0):
    model.eval()  # 设置模型为评估模式
    image, label = test_dataset[idx]  # 获取测试集中指定索引的图像和真实标签
    # 关闭梯度计算
    with torch.no_grad():
        # 为图像增加批次维度(模型输入需要[批次, 通道, 高, 宽]格式)
        output = model(image.unsqueeze(0))
        predicted = torch.argmax(output).item()  # 取预测概率最大的类别
    # 显示图像:squeeze()去除多余维度,cmap='gray'设置为灰度图
    plt.imshow(image.squeeze().numpy(), cmap='gray')
    # 设置标题:显示真实标签和预测结果
    plt.title(f'True: {label}, Predicted: {predicted}')
    plt.show()  # 显示图像

# 可视化测试集中索引为42的样本的预测结果
visualize_prediction(model, test_dataset, idx=42)

四、模型结构说明

  1. 卷积层部分

    • 第一层卷积:32个3×3卷积核,提取基础边缘和纹理特征
    • 最大池化:将特征图尺寸从28×28降为14×14
    • 第二层卷积:64个3×3卷积核,提取更复杂的组合特征
    • 最大池化:将特征图尺寸从14×14降为7×7
  2. 全连接层部分

    • 第一个全连接层:将64×7×7的特征展平后映射到128维
    • Dropout层:随机丢弃50%神经元,防止过拟合
    • 输出层:10个神经元,对应0-9十个数字的分类结果

五、训练与评估流程

  1. 训练过程:

    • 前向传播:输入数据通过网络得到预测结果
    • 计算损失:使用交叉熵损失衡量预测与真实标签的差距
    • 反向传播:计算损失对各参数的梯度
    • 参数更新:使用Adam优化器根据梯度更新网络参数
  2. 评估过程:

    • 关闭梯度计算,节省计算资源
    • 计算模型在测试集上的准确率
    • 通过可视化查看具体样本的预测结果

六、扩展方向

  1. 增加训练轮次,观察模型性能变化
  2. 调整网络结构(如增加卷积层、调整通道数)
  3. 尝试不同的优化器和学习率
  4. 增加数据增强操作,提高模型泛化能力
  5. 在GPU上运行以加速训练过程
posted @ 2025-10-21 20:37  学java的阿驴  阅读(18)  评论(0)    收藏  举报