【Python学习打卡-Day40】从“能跑就行”到“工程标准”:PyTorch训练与测试的规范化写法

📋 前言

各位伙伴们,大家好!在过去的几天里,我们成功地定义了模型、加载了数据。今天,我们要解决一个更深层次的问题:如何组织我们的代码,让它从一堆杂乱的脚本,变成一个结构清晰、逻辑分明、易于复用和维护的项目?

Day 40 是一个分水岭。我们学习的重点将从"如何实现某个功能",转向"如何优雅地组织所有功能"。我们将把训练和测试过程封装成独立的函数,并深入探讨监控模型训练的两种"分辨率":Epoch 级和 Iteration 级。更重要的是,通过在彩色图像上的实验,我们将亲身体会到当前模型(MLP)的局限性,从而为迎接更强大的 CNN 架构做好铺垫。


一、编程思想的飞跃:为什么要封装函数?

在我们之前的简单示例中,所有的训练逻辑都"平铺"在主流程里。当项目变复杂时,这种写法会变成一场噩梦。将核心逻辑封装成函数,至少有三大好处:

  1. 逻辑清晰,关注点分离 :train 函数只管训练,test 函数只管测试,main 部分只管调用。各司其职,代码的可读性指数级提升。
  2. 参数与实现分离 :封装后,函数的参数(如 learning_rate, epochs)就是我们的"仪表盘"。我们可以轻松调整这些超参数,而无需深入到复杂的训练循环代码中去修改。
  3. 代码复用,效率倍增 :这是最重要的优点!当我们想测试不同的模型时,我们不需要重写训练和测试代码 。我们只需在主流程中实例化一个新模型,然后把它传入同一个 train 函数即可。

二、训练的两种"分辨率":Epoch Loss vs. Iteration Loss

在监控模型训练时,我们通常会看损失曲线。但这条曲线其实有两种不同的"画法"。

  • Epoch Loss (宏观视角)

    • 是什么 :一个 Epoch(完整过一遍训练集)中,所有 Iteration (batch) 损失的平均值。
    • 特点:平滑,能很好地反映模型整体的收敛趋势。
    • 比喻 :就像一份月度财务报告,告诉你这个月总体的盈利状况。
  • Iteration Loss (微观视角)

    • 是什么 :每一个 Iteration (batch) 计算出的瞬时损失值。
    • 特点:非常"抖动"或"嘈杂",因为它受到当前小批量数据随机性的影响。但它能揭示训练过程中的不稳定性。
    • 比喻 :就像实时更新的股票价格,虽然波动剧烈,但能让你看到市场最即时的反应。

观察 Iteration Loss 可以帮助我们诊断问题,比如学习率过大可能导致损失剧烈震荡。在今天的作业中,我们特意绘制了 Iteration Loss 曲线,来获得对训练过程更精细的洞察。


三、作业一:单通道图像(MNIST)的规范化模板

这是我们构建的第一个"标准"训练框架。它包含了数据加载、模型定义、函数封装和结果可视化的完整流程。

3.1 完整代码框架

python 复制代码
# 导入必要的库
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 warnings

warnings.filterwarnings("ignore")
torch.manual_seed(42)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 1. 数据预处理与加载
transform = transforms.Compose([
    transforms.ToTensor(),
    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, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

# 2. 模型定义
class MLP(nn.Module):
    def __init__(self):
        super(MLP, self).__init__()
        self.flatten = nn.Flatten()
        self.layers = nn.Sequential(
            nn.Linear(784, 128),
            nn.ReLU(),
            nn.Linear(128, 10)
        )
    def forward(self, x):
        return self.layers(self.flatten(x))

# 3. 测试函数
def test(model, test_loader, criterion, device):
    model.eval()
    test_loss = 0
    correct = 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 = output.max(1)
            correct += predicted.eq(target).sum().item()
    avg_loss = test_loss / len(test_loader)
    accuracy = 100. * correct / len(test_loader.dataset)
    return avg_loss, accuracy

# 4. 训练函数
def train(model, train_loader, test_loader, criterion, optimizer, device, epochs):
    model.train()
    all_iter_losses = []
    iter_indices = []
    for epoch in range(epochs):
        for batch_idx, (data, target) in enumerate(train_loader):
            data, target = data.to(device), target.to(device)
            optimizer.zero_grad()
            output = model(data)
            loss = criterion(output, target)
            loss.backward()
            optimizer.step()
            
            # 记录 iteration 级损失
            all_iter_losses.append(loss.item())
            iter_indices.append(epoch * len(train_loader) + batch_idx + 1)
            
            if (batch_idx + 1) % 100 == 0:
                print(f'Epoch: {epoch+1}/{epochs} | Batch: {batch_idx+1}/{len(train_loader)} '
                      f'| 单Batch损失: {loss.item():.4f}')
        
        _, epoch_test_acc = test(model, test_loader, criterion, device)
        print(f'Epoch {epoch+1}/{epochs} 完成 | 测试准确率: {epoch_test_acc:.2f}%')
    
    plot_iter_losses(all_iter_losses, iter_indices)
    return epoch_test_acc

# 5. 绘图函数
def plot_iter_losses(losses, indices):
    plt.figure(figsize=(10, 4))
    plt.plot(indices, losses, alpha=0.7, label='Iteration Loss')
    plt.xlabel('Iteration')
    plt.ylabel('Loss')
    plt.title('Training Loss per Iteration')
    plt.grid(True)
    plt.show()

# 6. 主执行流程
model = MLP().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

print("开始训练MNIST模型...")
final_accuracy = train(model, train_loader, test_loader, criterion, optimizer, device, epochs=2)
print(f"训练完成!最终测试准确率: {final_accuracy:.2f}%")

3.2 结果分析

从结果可以看到,Iteration Loss 曲线虽然有许多毛刺和抖动,但整体趋势是稳步下降的。这表明我们的模型在每个小批量上都在学习,并且训练过程是健康的。最终在测试集上达到了很高的准确率(通常 >95%),证明 MLP 对于 MNIST 这种相对简单的图像分类任务是足够有效的。


四、作业二:彩色图像(CIFAR-10)与MLP的"天花板"

现在,我们把同样的 MLP 架构应用到更复杂的 CIFAR-10 彩色图像数据集上。我们只是修改了数据加载部分和模型的第一层以适应新的输入尺寸(3x32x32=3072)。

4.1 核心代码改动

python 复制代码
# 数据加载 (CIFAR-10)
transform_cifar = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # CIFAR-10 常用均值和标准差
])
train_dataset_cifar = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform_cifar)
# ... (DataLoader 定义类似)

# 模型定义 (适应 CIFAR-10)
class MLP_CIFAR(nn.Module):
    def __init__(self):
        super(MLP_CIFAR, self).__init__()
        self.flatten = nn.Flatten()
        self.layers = nn.Sequential(
            nn.Linear(3 * 32 * 32, 512), # 输入维度变为 3072
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(512, 256),
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(256, 10)
        )
    # ... forward a pass

4.2 令人深思的结果

即使我们增加了模型的深度、引入了 Dropout 并训练了更多轮次 (20 epochs),最终的测试准确率也仅仅在 50-55% 之间徘徊。这比瞎猜(10%)好,但远远达不到实用的标准。

观察损失曲线,虽然它也在下降,但下降得非常缓慢,且最终的损失值仍然很高。这说明模型已经尽力了,但它的结构限制了它无法学得更好。

4.3 为什么MLP在图像任务上失败了?

这次"失败"的实验,恰恰是我们最重要的学习机会。原因主要有两点:

  1. 丢失空间信息 (Spatial Information Loss) :nn.Flatten() 操作是"罪魁祸首"。它将一个 32x32 的像素矩阵粗暴地拉成一个 3072 的长向量。图像中,一个像素和它邻近的像素关系极为重要(比如构成一条边、一个角),这种局部空间结构在展平后被完全破坏了。MLP 无法利用这种像素间的邻里关系。

  2. 参数爆炸与过拟合 (Parameter Explosion):全连接层意味着每个输入神经元都与每个输出神经元相连。对于高分辨率图像,这会导致参数数量急剧增加,模型变得异常庞大,不仅训练缓慢,而且极易在有限的数据上发生过拟ટું合。


五、学习心得

今天是我编程思维和深度学习认知的一次重要升级。

  • 从"脚本小子"到"工程师":我学会了将代码模块化、函数化。这不仅让代码更美观,更重要的是提升了它的可维护性和可复用性。这套训练框架将是我未来项目的宝贵起点。
  • 学会用"显微镜"看训练:通过观察 Iteration 级的损失,我对模型的学习过程有了更细致的了解。我明白了宏观趋势(Epoch Loss)和微观动态(Iteration Loss)各自的价值。
  • "失败"是成功之母 :MLP 在 CIFAR-10 上的不佳表现,让我深刻理解了"没有最好的模型,只有最适合的模型"。它让我从实践上认识到了 MLP 处理图像的根本缺陷,并对即将到来的、专为图像而生的 CNN (卷积神经网络) 充满了期待。

我们正站在一座山的山脚下,MLP 帮助我们爬了上来,但要攀登更高峰,我们需要更强大的登山工具。下一站,CNN!


再次感谢 @浙大疏锦行 老师设计的这条精妙的学习路径,通过一次"失败"的实验,完美地激发了我们对新知识的渴望!

相关推荐
懂点Tech5 小时前
ctfshow misc28:GIF 高度修改解题与脚本实现
开发语言·windows·python
comedate6 小时前
SMLib 的 Examples 导读:PyAPI / SM_API / SMLib 三组示例怎么读
python·cad·smlib·examples·sm_api
云和数据.ChenGuang6 小时前
Python 重写(Override)深度解析:从 MRO 到 super() 的完整机制
java·开发语言·人工智能·python·langchain·1024程序员节
小向是个Der6 小时前
嵌入式进阶——MCU上电后的动作
c语言·单片机·mcu·学习·链接脚本
川石课堂软件测试6 小时前
学习软件测试需掌握的一些知识
数据库·python·sql·功能测试·网络协议·mysql·http
万化冥合7 小时前
PyBox屏幕键盘如何辅助演示键盘操作?
python·计算机外设
车软学堂7 小时前
从测试转开发,到底要补什么
网络·人工智能·python·功能测试·单片机·车载系统·自动化
天空依然蔚蓝7 小时前
分享一个自动检测网站在线状态的python程序
python·运维开发·1024程序员节
岁月宁静7 小时前
一个 AI 项目从本地到上线,要经过哪些步骤?全流程讲清楚
python·docker·agent
codists8 小时前
《crafting interpreters》阅读笔记:p45-p45
python