一、实验目标

1.1 核心目的

本实验旨在帮助初级学者掌握深度学习的基础知识和PyTorch框架的使用,通过实现一个完整的手写数字分类任务,建立对深度学习核心概念的理解。

1.2 预期成果

能力项

目标描述

数据加载

理解PyTorch数据加载机制(DataLoader, Dataset)

模型定义

掌握神经网络层的概念和模型构建方法

训练循环

理解前向传播、损失计算、反向传播、参数更新的完整流程

损失函数

学会使用交叉熵损失(CrossEntropyLoss)处理多分类问题

优化器

掌握Adam等优化器的使用

GPU加速

学会将数据和模型迁移至CUDA设备

日志输出

实现训练过程的双输出(控制台+文件)

1.3 实验环境

  • 框架: PyTorch 2.x

  • 加速: CUDA (NVIDIA GeForce RTX 5090 Laptop GPU)

  • 任务: MNIST手写数字分类(10类,0-9)

  • 数据集规模: 60,000训练样本 / 10,000测试样本


二、实验规划

2.1 技术方案

框架选择

  • PyTorch 2.x: 主流深度学习框架,API简洁易学

  • CUDA: NVIDIA GPU加速,大幅提升训练速度

任务选择

  • MNIST手写数字分类: 经典入门任务,结构简单,效果直观

    • 10类分类(数字0-9)

    • 28×28灰度图像

    • 60,000训练样本,10,000测试样本

网络架构

采用简单全连接网络(MLP),适合初学者理解基础概念:

 输入层(784) → 隐藏层1(512) → 隐藏层2(256) → 输出层(10)
      ↓            ↓              ↓            ↓
   28×28展平    ReLU+Dropout   ReLU+Dropout   Softmax

2.2 模块划分

模块

功能

配置模块 (Config)

超参数设置、设备配置(CUDA/CPU)、路径设置

日志模块 (Logger)

日志配置,双输出(控制台+文件)

数据模块 (Data)

数据集下载加载、预处理、DataLoader创建

模型模块 (Model)

MLP模型类定义、层结构设计

训练模块 (Training)

损失函数、优化器配置、训练循环

评估模块 (Evaluation)

测试集评估、准确率计算

演示模块 (Demo)

预测结果可视化展示

2.3 实现步骤

阶段

内容

阶段1

环境准备:确认PyTorch和CUDA环境

阶段2

基础框架搭建:配置模块、数据加载模块

阶段3

模型构建:MLP模型类定义、前向传播测试

阶段4

训练循环实现:单轮训练、监控、GPU支持

阶段5

评估与保存:测试集评估、模型保存

阶段6

代码优化:详细注释、日志功能


三、完整代码

 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 """
 MNIST手写数字分类器 - 深度学习入门项目
 ========================================
 ​
 本项目旨在帮助初学者掌握深度学习的基础知识和PyTorch框架的使用。
 ​
 学习要点:
 1. 数据加载: 使用torchvision加载MNIST数据集,使用DataLoader批量读取
 2. 模型定义: 使用torch.nn.Module定义多层感知机(MLP)模型
 3. 训练循环: 实现前向传播、损失计算、反向传播、参数更新的完整流程
 4. 损失函数: 使用CrossEntropyLoss处理多分类问题
 5. 优化器: 使用Adam优化器更新模型参数
 6. GPU加速: 自动检测并使用CUDA设备进行加速
 ​
 作者: AI Assistant
 日期: 2026-03-26
 """
 ​
 import torch
 import torch.nn as nn
 import torch.optim as optim
 from torchvision import datasets, transforms
 from torch.utils.data import DataLoader
 import time
 import logging
 from datetime import datetime
 import os
 ​
 # ============================================================
 # 第一部分: 配置模块
 # ============================================================
 # 这里定义模型训练所需的所有超参数,便于集中管理和修改
 ​
 class Config:
     """配置类: 存放所有超参数和设置"""
 ​
     # 数据相关参数
     batch_size = 64        # 每批处理的样本数量,越大训练越快但占用内存越多
     test_batch_size = 1000 # 测试时的批次大小
 ​
     # 训练相关参数
     epochs = 5             # 训练轮数,1个epoch意味着所有样本都被训练过一次
     learning_rate = 0.001 # 学习率,控制参数更新的步长大小
     momentum = 0.9         # 动量,用于SGD优化器(本项目使用Adam,可忽略)
 ​
     # 随机种子
     seed = 42              # 随机种子,保证实验可重复性
 ​
     # 路径设置
     model_save_path = "mnist_mlp_model.pth"  # 模型保存路径
     log_save_path = "training_log.txt"       # 日志保存路径
 ​
     @classmethod
     def get_device(cls):
         """
         获取可用的计算设备
         自动检测系统是否支持CUDA,优先使用GPU加速
         """
         if torch.cuda.is_available():
             device = torch.device("cuda")
             get_logger().info(f"检测到CUDA支持,使用GPU: {torch.cuda.get_device_name(0)}")
         else:
             device = torch.device("cpu")
             get_logger().info("未检测到CUDA,使用CPU进行训练")
         return device
 ​
 ​
 # ============================================================
 # 第二部分: 日志配置模块
 # ============================================================
 # 负责配置日志输出,同时写入控制台和文件
 ​
 def setup_logger(log_file):
     """
     配置日志系统,使输出同时显示在控制台和写入文件
 ​
     参数:
         log_file: 日志文件路径
 ​
     返回:
         logger: 配置好的logger对象
     """
 ​
     # 创建logger对象
     logger = logging.getLogger("MNIST_Training")
     logger.setLevel(logging.INFO)
 ​
     # 清除已有的handlers,避免重复添加
     logger.handlers.clear()
 ​
     # 创建 formatter,控制输出格式
     formatter = logging.Formatter(
         '%(asctime)s - %(levelname)s - %(message)s',
         datefmt='%Y-%m-%d %H:%M:%S'
     )
 ​
     # ---------------------------------------------------------
     # 文件Handler: 将日志写入文件
     # ---------------------------------------------------------
     file_handler = logging.FileHandler(log_file, mode='w', encoding='utf-8')
     file_handler.setLevel(logging.INFO)
     file_handler.setFormatter(formatter)
 ​
     # ---------------------------------------------------------
     # 控制台Handler: 将日志显示在终端
     # ---------------------------------------------------------
     console_handler = logging.StreamHandler()
     console_handler.setLevel(logging.INFO)
     console_handler.setFormatter(formatter)
 ​
     # 添加handlers到logger
     logger.addHandler(file_handler)
     logger.addHandler(console_handler)
 ​
     return logger
 ​
 ​
 def get_logger():
     """获取全局logger对象"""
     return logging.getLogger("MNIST_Training")
 ​
 ​
 # ============================================================
 # 第三部分: 数据加载模块
 # ============================================================
 # 负责下载、预处理和批量加载MNIST数据集
 ​
 def get_mnist_data_loaders(batch_size, test_batch_size, device):
     """
     加载MNIST数据集并返回训练和测试数据加载器
     """
 ​
     # 数据预处理变换
     train_transform = transforms.Compose([
         transforms.ToTensor(),  # 转换为[0,1]范围的Tensor
         transforms.Normalize((0.1307,), (0.3081,))  # 归一化
     ])
 ​
     test_transform = transforms.Compose([
         transforms.ToTensor(),
         transforms.Normalize((0.1307,), (0.3081,))
     ])
 ​
     get_logger().info("正在加载MNIST数据集...")
     get_logger().info("数据将保存在当前目录下")
 ​
     train_dataset = datasets.MNIST(
         root='./data',
         train=True,
         download=True,
         transform=train_transform
     )
 ​
     test_dataset = datasets.MNIST(
         root='./data',
         train=False,
         download=True,
         transform=test_transform
     )
 ​
     train_loader = DataLoader(
         train_dataset,
         batch_size=batch_size,
         shuffle=True,
         num_workers=0,
         pin_memory=True if device.type == "cuda" else False
     )
 ​
     test_loader = DataLoader(
         test_dataset,
         batch_size=test_batch_size,
         shuffle=False,
         num_workers=0,
         pin_memory=True if device.type == "cuda" else False
     )
 ​
     get_logger().info(f"数据加载完成! 训练集: {len(train_dataset)} 样本, {len(train_loader)} 批次 | 测试集: {len(test_dataset)} 样本, {len(test_loader)} 批次")
 ​
     return train_loader, test_loader
 ​
 ​
 # ============================================================
 # 第四部分: 模型定义模块
 # ============================================================
 # 定义多层感知机(MLP)神经网络模型
 ​
 class MLP(nn.Module):
     """
     多层感知机(Multi-Layer Perceptron)模型
 ​
     网络结构:
         输入层(784) -> 隐藏层1(512) -> 隐藏层2(256) -> 输出层(10)
     """
 ​
     def __init__(self):
         super(MLP, self).__init__()
 ​
         # 隐藏层1: 784 -> 512
         self.fc1 = nn.Linear(in_features=784, out_features=512)
 ​
         # 隐藏层2: 512 -> 256
         self.fc2 = nn.Linear(in_features=512, out_features=256)
 ​
         # 输出层: 256 -> 10
         self.fc3 = nn.Linear(in_features=256, out_features=10)
 ​
         # ReLU激活函数
         self.relu = nn.ReLU()
 ​
         # Dropout正则化
         self.dropout = nn.Dropout(p=0.2)
 ​
     def forward(self, x):
         """
         前向传播: 定义数据如何在网络中流动
         """
         # 数据展平: 28x28 -> 784
         x = x.view(-1, 784)
 ​
         # 第一层: 线性变换 + 激活 + Dropout
         x = self.fc1(x)
         x = self.relu(x)
         x = self.dropout(x)
 ​
         # 第二层: 线性变换 + 激活 + Dropout
         x = self.fc2(x)
         x = self.relu(x)
         x = self.dropout(x)
 ​
         # 输出层: 线性变换
         x = self.fc3(x)
 ​
         return x
 ​
 ​
 # ============================================================
 # 第五部分: 训练模块
 # ============================================================
 # 包含训练和测试的核心逻辑
 ​
 def train(model, device, train_loader, optimizer, criterion, epoch):
     """
     训练函数: 在一个epoch内完成所有训练样本的前向传播和反向传播
     """
 ​
     model.train()
     running_loss = 0.0
     correct = 0
     total = 0
 ​
     for batch_idx, (data, target) in enumerate(train_loader):
         # 步骤1: 数据迁移到设备
         data, target = data.to(device), target.to(device)
 ​
         # 步骤2: 梯度清零
         optimizer.zero_grad()
 ​
         # 步骤3: 前向传播
         output = model(data)
 ​
         # 步骤4: 计算损失
         loss = criterion(output, target)
 ​
         # 步骤5: 反向传播
         loss.backward()
 ​
         # 步骤6: 参数更新
         optimizer.step()
 ​
         # 统计信息
         running_loss += loss.item()
         _, predicted = torch.max(output.data, dim=1)
         total += target.size(0)
         correct += (predicted == target).sum().item()
 ​
         # 每100个批次打印进度
         if (batch_idx + 1) % 100 == 0:
             progress = 100.0 * (batch_idx + 1) / len(train_loader)
             batch_loss = loss.item()
             get_logger().info(f"Epoch [{epoch}] Batch [{batch_idx + 1}/{len(train_loader)}] Progress: {progress:.1f}% Loss: {batch_loss:.4f}")
 ​
     avg_loss = running_loss / len(train_loader)
     accuracy = 100.0 * correct / total
 ​
     get_logger().info(f"Epoch {epoch} 完成 - 平均损失: {avg_loss:.4f}, 训练准确率: {accuracy:.2f}%")
 ​
     return avg_loss, accuracy
 ​
 ​
 def test(model, device, test_loader, criterion):
     """
     测试函数: 在测试集上评估模型性能
     """
 ​
     model.eval()
     test_loss = 0.0
     correct = 0
     total = 0
 ​
     with torch.no_grad():
         for data, target in test_loader:
             data, target = data.to(device), target.to(device)
             output = model(data)
             test_loss += criterion(output, target).item()
             _, predicted = torch.max(output.data, dim=1)
             total += target.size(0)
             correct += (predicted == target).sum().item()
 ​
     avg_loss = test_loss / len(test_loader)
     accuracy = 100.0 * correct / total
 ​
     get_logger().info(f"测试集评估 - 损失: {avg_loss:.4f}, 准确率: {accuracy:.2f}%")
 ​
     return avg_loss, accuracy
 ​
 ​
 # ============================================================
 # 第六部分: 预测演示模块
 # ============================================================
 ​
 def demonstrate_predictions(model, device, test_loader, num_samples=5):
     """
     演示模型预测效果
     """
 ​
     get_logger().info("=" * 60)
     get_logger().info("预测结果演示")
     get_logger().info("=" * 60)
 ​
     data_iter = iter(test_loader)
     images, labels = next(data_iter)
     images, labels = images.to(device), labels.to(device)
 ​
     model.eval()
 ​
     with torch.no_grad():
         outputs = model(images)
 ​
         for i in range(min(num_samples, len(labels))):
             _, predicted = torch.max(outputs[i], dim=0)
             true_label = labels[i].item()
             pred_label = predicted.item()
             probs = torch.softmax(outputs[i], dim=0)
             confidence = probs[predicted].item() * 100
             status = "正确" if true_label == pred_label else "错误"
             get_logger().info(f"样本 {i + 1}: 真实标签={true_label}, 预测标签={pred_label}, 置信度={confidence:.1f}% [{status}]")
 ​
 ​
 # ============================================================
 # 第七部分: 主程序入口
 # ============================================================
 ​
 def main():
     """
     主函数: 整合所有模块,完成模型训练和评估
     """
 ​
     # 初始化日志系统
     timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
     log_file = f"training_log_{timestamp}.txt"
 ​
     logger = setup_logger(log_file)
     logger.info("=" * 60)
     logger.info("MNIST手写数字分类器 - 深度学习入门项目")
     logger.info("=" * 60)
     logger.info(f"日志文件: {log_file}")
 ​
     # 步骤1: 配置
     logger.info("[步骤 1] 配置训练参数...")
     device = Config.get_device()
     logger.info(f"批次大小: {Config.batch_size}")
     logger.info(f"训练轮数: {Config.epochs}")
     logger.info(f"学习率: {Config.learning_rate}")
 ​
     torch.manual_seed(Config.seed)
     if device.type == "cuda":
         torch.cuda.manual_seed(Config.seed)
 ​
     # 步骤2: 加载数据
     logger.info("[步骤 2] 加载MNIST数据集...")
     train_loader, test_loader = get_mnist_data_loaders(
         Config.batch_size,
         Config.test_batch_size,
         device
     )
 ​
     # 步骤3: 创建模型
     logger.info("[步骤 3] 创建MLP模型...")
     model = MLP().to(device)
 ​
     logger.info("模型结构:")
     logger.info("-" * 40)
     logger.info("输入层:  784 神经元 (28x28图像)")
     logger.info("隐藏层1: 512 神经元 + ReLU + Dropout(0.2)")
     logger.info("隐藏层2: 256 神经元 + ReLU + Dropout(0.2)")
     logger.info("输出层:  10  神经元 (数字0-9)")
     logger.info("-" * 40)
 ​
     total_params = sum(p.numel() for p in model.parameters())
     trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
     logger.info(f"总参数数量: {total_params:,}")
     logger.info(f"可训练参数: {trainable_params:,}")
 ​
     # 步骤4: 配置损失函数和优化器
     logger.info("[步骤 4] 配置损失函数和优化器...")
     criterion = nn.CrossEntropyLoss()
     logger.info("损失函数: CrossEntropyLoss")
 ​
     optimizer = optim.Adam(
         model.parameters(),
         lr=Config.learning_rate
     )
     logger.info(f"优化器: Adam (lr={Config.learning_rate})")
 ​
     # 步骤5: 训练模型
     logger.info("[步骤 5] 开始训练模型...")
     logger.info("-" * 40)
 ​
     total_start_time = time.time()
 ​
     history = {
         'train_loss': [],
         'train_acc': [],
         'test_loss': [],
         'test_acc': []
     }
 ​
     for epoch in range(1, Config.epochs + 1):
         epoch_start_time = time.time()
 ​
         train_loss, train_acc = train(
             model, device, train_loader, optimizer, criterion, epoch
         )
 ​
         test_loss, test_acc = test(
             model, device, test_loader, criterion
         )
 ​
         history['train_loss'].append(train_loss)
         history['train_acc'].append(train_acc)
         history['test_loss'].append(test_loss)
         history['test_acc'].append(test_acc)
 ​
         epoch_time = time.time() - epoch_start_time
         logger.info(f"Epoch {epoch} 耗时: {epoch_time:.2f}秒")
         logger.info("")
 ​
     total_time = time.time() - total_start_time
     logger.info("-" * 40)
     logger.info(f"训练完成! 总耗时: {total_time:.2f}秒")
 ​
     # 步骤6: 保存模型
     logger.info("[步骤 6] 保存模型...")
     torch.save({
         'model_state_dict': model.state_dict(),
         'optimizer_state_dict': optimizer.state_dict(),
         'history': history,
         'config': {
             'epochs': Config.epochs,
             'learning_rate': Config.learning_rate,
             'batch_size': Config.batch_size
         }
     }, Config.model_save_path)
     logger.info(f"模型已保存至: {Config.model_save_path}")
 ​
     # 步骤7: 显示训练历史
     logger.info("[步骤 7] 训练历史汇总")
     logger.info("-" * 50)
     logger.info("Epoch | Train Loss | Train Acc | Test Loss | Test Acc")
     logger.info("-" * 50)
     for i in range(len(history['train_loss'])):
         logger.info(f"{i + 1:5d}  | {history['train_loss'][i]:10.4f} | "
               f"{history['train_acc'][i]:9.2f}% | {history['test_loss'][i]:9.4f} | "
               f"{history['test_acc'][i]:8.2f}%")
     logger.info("-" * 50)
 ​
     # 步骤8: 预测演示
     demonstrate_predictions(model, device, test_loader)
 ​
     logger.info("=" * 60)
     logger.info("训练完成!")
     logger.info(f"最终测试集准确率: {history['test_acc'][-1]:.2f}%")
     logger.info("=" * 60)
     logger.info(f"日志文件已保存: {log_file}")
 ​
 ​
 if __name__ == "__main__":
     main()

四、实验输出

4.1 训练环境

 检测到CUDA支持,使用GPU: NVIDIA GeForce RTX 5090 Laptop GPU
 批次大小: 64
 训练轮数: 5
 学习率: 0.001

4.2 数据集信息

数据集

样本数

批次数

训练集

60,000

938

测试集

10,000

10

4.3 模型结构

 输入层:  784 神经元 (28x28图像)
 隐藏层1: 512 神经元 + ReLU + Dropout(0.2)
 隐藏层2: 256 神经元 + ReLU + Dropout(0.2)
 输出层:  10  神经元 (数字0-9)
 ​
 总参数数量: 535,818
 可训练参数: 535,818

4.4 训练过程

每批次训练进度(每100批次记录):

Epoch

Batch

进度

损失值

1

100

10.7%

0.2492

1

200

21.3%

0.3255

1

300

32.0%

0.2055

1

400

42.6%

0.1825

1

500

53.3%

0.1850

1

600

64.0%

0.1222

1

700

74.6%

0.3716

1

800

85.3%

0.0770

1

900

95.9%

0.2277

2

100

10.7%

0.0863

...

...

...

...

5

900

95.9%

0.0258

4.5 训练结果汇总

Epoch

Train Loss

Train Acc

Test Loss

Test Acc

耗时(秒)

1

0.2362

92.77%

0.1146

96.38%

10.58

2

0.1099

96.62%

0.0913

97.21%

10.53

3

0.0906

97.21%

0.0787

97.61%

10.53

4

0.0695

97.80%

0.0785

97.61%

10.52

5

0.0607

98.01%

0.0786

97.70%

10.51

总训练耗时: 52.68秒

4.6 训练曲线分析

 训练损失:  0.2362 → 0.0607  (下降74.3%)
 测试损失:  0.1146 → 0.0786  (下降31.4%)
 训练准确率: 92.77% → 98.01% (提升5.24%)
 测试准确率: 96.38% → 97.70% (提升1.32%)

观察结论:

  • 训练损失持续下降,表明模型在训练集上学习效果良好

  • 测试准确率在Epoch 4-5趋于稳定(~97.6%),未出现明显过拟合

  • Dropout正则化有效控制了过拟合风险

4.7 预测结果演示

样本

真实标签

预测标签

置信度

状态

1

7

7

100.0%

正确

2

2

2

100.0%

正确

3

1

1

100.0%

正确

4

0

0

100.0%

正确

5

4

4

100.0%

正确

4.8 输出文件

文件

说明

mnist_mlp_model.pth

训练好的模型权重

training_log_20260326_213654.txt

完整训练日志


五、总结与建议

5.1 实验总结

本实验成功实现了基于PyTorch的MNIST手写数字分类器,主要成果:

  • 准确率: 最终测试集准确率达到 97.70%

  • 效率: 使用RTX 5090 GPU加速,5个epoch仅需52.68秒

  • 代码质量: 模块化设计,完整中文注释,适合初学者

5.2 后续学习建议

  1. 超参数调优: 尝试调整学习率、批次大小、训练轮数

  2. 模型改进: 尝试更复杂的模型结构(如CNN,可达99%+准确率)

  3. 正则化: 尝试不同的Dropout比例或添加BatchNorm

  4. 实践应用: 使用模型进行自己的手写数字识别


报告生成时间: 2026-03-26