一、实验目标
1.1 核心目的
本实验旨在帮助初级学者掌握深度学习的基础知识和PyTorch框架的使用,通过实现一个完整的手写数字分类任务,建立对深度学习核心概念的理解。
1.2 预期成果
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 Softmax2.2 模块划分
2.3 实现步骤
三、完整代码
#!/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.0014.2 数据集信息
4.3 模型结构
输入层: 784 神经元 (28x28图像)
隐藏层1: 512 神经元 + ReLU + Dropout(0.2)
隐藏层2: 256 神经元 + ReLU + Dropout(0.2)
输出层: 10 神经元 (数字0-9)
总参数数量: 535,818
可训练参数: 535,8184.4 训练过程
每批次训练进度(每100批次记录):
4.5 训练结果汇总
总训练耗时: 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 预测结果演示
4.8 输出文件
五、总结与建议
5.1 实验总结
本实验成功实现了基于PyTorch的MNIST手写数字分类器,主要成果:
-
准确率: 最终测试集准确率达到 97.70%
-
效率: 使用RTX 5090 GPU加速,5个epoch仅需52.68秒
-
代码质量: 模块化设计,完整中文注释,适合初学者
5.2 后续学习建议
-
超参数调优: 尝试调整学习率、批次大小、训练轮数
-
模型改进: 尝试更复杂的模型结构(如CNN,可达99%+准确率)
-
正则化: 尝试不同的Dropout比例或添加BatchNorm
-
实践应用: 使用模型进行自己的手写数字识别
报告生成时间: 2026-03-26