从零实现LeNet5卷积神经网络:MNIST手写数字识别实战

在深度学习领域,卷积神经网络(CNN)已成为图像处理的核心技术。本文将带您实现经典的LeNet5网络架构,并在MNIST手写数字数据集上进行训练与评估。我们将深入探讨数据预处理、网络设计、模型训练和性能分析等关键环节,帮助您全面理解卷积神经网络的工作原理。

1. MNIST数据集简介

MNIST是机器学习领域最著名的基准数据集之一,包含60,000张训练图像和10,000张测试图像,每张图像是28×28像素的灰度手写数字(0-9)。

让我们首先加载数据集并进行探索:

import torch

import torchvision

import torchvision.transforms as transforms

import matplotlib.pyplot as plt

import numpy as np

# 加载MNIST数据集

def load_data():

    # 数据预处理

    transform = transforms.Compose([

        transforms.ToTensor(),

        transforms.Normalize((0.1307,), (0.3081,))  # MNIST均值和标准差

    ])

    

    # 加载训练集

    train_dataset = torchvision.datasets.MNIST(

        root='./data', 

        train=True, 

        download=True, 

        transform=transform

    )

    train_loader = torch.utils.data.DataLoader(

        train_dataset, 

        batch_size=128, 

        shuffle=True

    )

    

    # 加载测试集

    test_dataset = torchvision.datasets.MNIST(

        root='./data', 

        train=False, 

        download=True, 

        transform=transform

    )

    test_loader = torch.utils.data.DataLoader(

        test_dataset, 

        batch_size=1000, 

        shuffle=False

    )

    

    return train_loader, test_loader

2. 数据预处理的重要性

深度学习模型对输入数据的分布非常敏感。通过适当的预处理,我们可以加速模型收敛并提高性能。对于MNIST数据集,我们进行两步预处理:

  1. 将像素值从[0, 255]归一化到[0, 1]范围
  1. 使用MNIST数据集的均值(0.1307)和标准差(0.3081)进行标准化

让我们可视化预处理的效果:

def show_preprocessing_comparison():

    # 准备两种不同的transform

    transform_original = transforms.ToTensor()  # 仅转换为tensor,归一化到[0,1]

    

    transform_processed = transforms.Compose([

        transforms.ToTensor(),

        transforms.Normalize((0.1307,), (0.3081,))  # 标准化处理

    ])

    

    # 加载一张图像

    mnist_data = torchvision.datasets.MNIST(

        root='./data', train=True, download=True, transform=transform_original

    )

    dataloader = torch.utils.data.DataLoader(mnist_data, batch_size=1, shuffle=True)

    images, labels = next(iter(dataloader))

    

    # 显示对比

    plt.figure(figsize=(10, 5))

    

    # 原始图像

    img = images[0].squeeze().numpy()

    plt.subplot(1, 2, 1)

    plt.imshow(img, cmap='gray')

    plt.title('原始图像')

    plt.axis('off')

    

    # 预处理后的图像

    processed_img = transforms.Normalize((0.1307,), (0.3081,))(images[0])

    processed_img = processed_img.squeeze().numpy()

    plt.subplot(1, 2, 2)

    plt.imshow(processed_img, cmap='gray')

    plt.title('预处理后图像')

    plt.axis('off')

    

    plt.suptitle(f'MNIST数字: {labels[0].item()}')

    plt.savefig('mnist_preprocessing_comparison.png')

    plt.show()

    

    print(f"原始图像像素值范围: [{img.min():.4f}, {img.max():.4f}]")

    print(f"预处理后图像像素值范围: [{processed_img.min():.4f}, {processed_img.max():.4f}]")

预处理后的图像对比度更高,背景更加纯净,突出了数字特征,有助于模型更好地学习和识别。

3. LeNet5网络架构设计

LeNet5是由Yann LeCun在1998年提出的经典CNN模型,虽然简单,但包含了现代卷积神经网络的核心组件。让我们用PyTorch实现它:

import torch.nn as nn

class LeNet5(nn.Module):

    def __init__(self):

        super(LeNet5, self).__init__()

        # 第一个卷积块

        self.conv1 = nn.Sequential(

            nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=0),  # 输入1通道,输出6通道

            nn.ReLU(),

            nn.MaxPool2d(kernel_size=2, stride=2)  # 最大池化层

        )

        # 第二个卷积块

        self.conv2 = nn.Sequential(

            nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0),  # 输入6通道,输出16通道

            nn.ReLU(),

            nn.MaxPool2d(kernel_size=2, stride=2)

        )

        # 全连接层

        self.fc = nn.Sequential(

            nn.Linear(16 * 4 * 4, 120),  # 第一个全连接层

            nn.ReLU(),

            nn.Linear(120, 84),  # 第二个全连接层

            nn.ReLU(),

            nn.Linear(84, 10)  # 输出层,10个类别

        )

    def forward(self, x):

        x = self.conv1(x)  # 第一个卷积块

        x = self.conv2(x)  # 第二个卷积块

        x = x.view(x.size(0), -1)  # 扁平化

        x = self.fc(x)  # 全连接层

        return x

LeNet5的设计精妙之处在于:

  1. 层次化特征提取:从低级特征(边缘、纹理)到高级特征(形状、部件)
  1. 局部感受野:每个神经元只关注输入的局部区域,减少参数量
  1. 权值共享:卷积核在整个图像上共享,进一步减少参数
  1. 下采样:通过池化操作减少特征图尺寸,降低计算复杂度
  1. 非线性激活:ReLU函数引入非线性变换,增强网络表达能力

4. 模型训练与评估

现在让我们训练模型并评估性能:

import time

def train_model(train_loader, test_loader, device='cpu'):

    # 创建模型

    model = LeNet5().to(device)

    criterion = nn.CrossEntropyLoss()

    optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9)

    

    # 训练参数

    epochs = 10

    train_losses = []

    train_accs = []

    val_losses = []

    val_accs = []

    

    # 记录开始训练时间

    start = time.time()

    

    # 训练循环

    for epoch in range(epochs):

        model.train()  # 设置为训练模式

        running_loss = 0.0

        correct = 0

        total = 0

        

        for inputs, labels in train_loader:

            inputs, labels = inputs.to(device), labels.to(device)

            

            # 梯度清零

            optimizer.zero_grad()

            

            # 前向传播

            outputs = model(inputs)

            loss = criterion(outputs, labels)

            

            # 反向传播和优化

            loss.backward()

            optimizer.step()

            

            # 统计

            running_loss += loss.item()

            _, predicted = outputs.max(1)

            total += labels.size(0)

            correct += predicted.eq(labels).sum().item()

        

        # 计算训练集准确率和损失

        train_loss = running_loss / len(train_loader)

        train_acc = 100. * correct / total

        train_losses.append(train_loss)

        train_accs.append(train_acc)

        

        # 验证集评估

        model.eval()  # 设置为评估模式

        val_loss = 0

        val_correct = 0

        val_total = 0

        with torch.no_grad():  # 不计算梯度

            for inputs, labels in test_loader:

                inputs, labels = inputs.to(device), labels.to(device)

                outputs = model(inputs)

                loss = criterion(outputs, labels)

                

                val_loss += loss.item()

                _, predicted = outputs.max(1)

                val_total += labels.size(0)

                val_correct += predicted.eq(labels).sum().item()

        

        # 计算验证集准确率和损失

import torch.optim as optim

        val_loss = val_loss / len(test_loader)

        val_acc = 100. * val_correct / val_total

        val_losses.append(val_loss)

        val_accs.append(val_acc)

        

        print(f'Epoch {epoch+1}/{epochs}: Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}%, '

              f'Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%')

    

    # 记录结束训练时间

    end = time.time()

    print(f"总训练时间: {end - start:.2f} 秒")

    

    return model, train_losses, train_accs, val_losses, val_accs

训练过程中的关键环节包括:

  1. 优化器选择:使用带动量的SGD(随机梯度下降)
  1. 损失函数:多分类问题使用交叉熵损失
  1. 批处理:每批128个样本,平衡计算效率和内存使用
  1. 模式切换:训练时启用model.train(),评估时启用model.eval()
  1. 梯度清零:每批次前调用optimizer.zero_grad()避免梯度累积

5. 结果可视化与分析

训练完成后,让我们可视化训练过程并分析结果:

def plot_results(train_losses, train_accs, val_losses, val_accs):

    plt.figure(figsize=(12, 5))

    

    # 绘制损失曲线

    plt.subplot(1, 2, 1)

    plt.plot(train_losses, label='训练损失', color='blue')

    plt.plot(val_losses, label='验证损失', color='red')

    plt.xlabel('训练轮次')

    plt.ylabel('损失值')

    plt.legend()

    plt.title('训练和验证损失曲线')

    plt.grid(True)

    

    # 绘制准确率曲线

    plt.subplot(1, 2, 2)

    plt.plot(train_accs, label='训练准确率', color='blue')

    plt.plot(val_accs, label='验证准确率', color='red')

    plt.xlabel('训练轮次')

    plt.ylabel('准确率 (%)')

    plt.legend()

    plt.title('训练和验证准确率曲线')

    plt.grid(True)

    

    plt.tight_layout()

    plt.savefig('lenet5_results.png')

    plt.show()

经过10轮训练,我们的LeNet5模型在MNIST测试集上达到了约99%的准确率。从损失和准确率曲线可以观察到:

  1. 训练损失持续下降,验证损失在前几轮快速下降后趋于平稳
  1. 训练准确率和验证准确率都呈上升趋势,并在后期趋于稳定
  1. 训练集和验证集性能接近,说明模型没有明显过拟合

6. 模型优化与实际应用思考

尽管LeNet5在MNIST上已经取得了出色的性能,但在实际应用中,我们还可以进一步优化:

  1. 数据增强:通过旋转、缩放、平移等变换增加训练样本多样性
  1. 正则化:添加Dropout或BatchNorm层减少过拟合
  1. 学习率调度:实现学习率衰减,帮助模型收敛到更优解
  1. 更深的网络:尝试ResNet等更现代的架构进一步提高性能
  1. 迁移学习:利用在大数据集上预训练的模型进行微调

7. 完整代码整合

将上述所有组件整合,我们可以得到一个完整的LeNet5实现与训练流程:

import torch

import torch.nn as nn

import torch.optim as optim

import torchvision

import torchvision.transforms as transforms

import matplotlib.pyplot as plt

import numpy as np

import time

# 模型定义

class LeNet5(nn.Module):

    def __init__(self):

        super(LeNet5, self).__init__()

        self.conv1 = nn.Sequential(

            nn.Conv2d(1, 6, 5, 1, 0),

            nn.ReLU(),

            nn.MaxPool2d(2, 2)

        )

        self.conv2 = nn.Sequential(

            nn.Conv2d(6, 16, 5, 1, 0),

            nn.ReLU(),

            nn.MaxPool2d(2, 2)

        )

        self.fc = nn.Sequential(

            nn.Linear(16 * 4 * 4, 120),

            nn.ReLU(),

            nn.Linear(120, 84),

            nn.ReLU(),

            nn.Linear(84, 10)

        )

    def forward(self, x):

        x = self.conv1(x)

        x = self.conv2(x)

        x = x.view(x.size(0), -1)

        x = self.fc(x)

        return x

# 主函数

def main():

    # 检查是否有GPU可用

    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    print(f"使用设备: {device}")

    

    # 加载数据

    train_loader, test_loader = load_data()

    

    # 训练模型

    model, train_losses, train_accs, val_losses, val_accs = train_model(

        train_loader, test_loader, device

    )

    

    # 可视化结果

    plot_results(train_losses, train_accs, val_losses, val_accs)

    

    # 保存模型

    torch.save(model.state_dict(), 'lenet5_mnist.pth')

    print("模型已保存为: lenet5_mnist.pth")

if __name__ == "__main__":

    main()

8. 总结与展望

本文从零开始实现了LeNet5卷积神经网络,并在MNIST数据集上进行了训练和评估。通过这个过程,我们深入理解了卷积神经网络的基本原理、数据预处理的重要性、模型训练的核心步骤以及结果分析的方法。

尽管LeNet5是一个相对简单的网络,但它包含了现代CNN的核心组件,是深入学习更复杂模型的理想起点。在实际应用中,我们可以基于这一基础,探索更深层次的网络架构和更先进的训练技术,进一步提升模型性能。

深度学习是一个不断发展的领域,希望这篇文章能为您的学习之旅提供有益的见解和实践经验。


这个简单而完整的实现展示了卷积神经网络的魅力 - 短短几百行代码,就能构建一个在手写数字识别任务上表现优异的模型。我希望这篇博客能帮助您更好地理解深度学习的基础知识,并鼓励您在此基础上探索更多可能性!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值