【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!


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

相关推荐
Yyuanyuxin7 小时前
保姆级学习开发安卓手机软件(一)--安装软件及配置
学习
闲人编程8 小时前
消息通知系统实现:构建高可用、可扩展的企业级通知服务
java·服务器·网络·python·消息队列·异步处理·分发器
大神君Bob8 小时前
【AI办公自动化】如何使用Pytho让Excel表格处理自动化
python
Heorine8 小时前
数学建模 绘图 图表 可视化(6)
python·数学建模·数据可视化
栈与堆8 小时前
LeetCode-1-两数之和
java·数据结构·后端·python·算法·leetcode·rust
●VON8 小时前
跨模态暗流:多模态安全攻防全景解析
人工智能·学习·安全·von
星火开发设计8 小时前
C++ map 全面解析与实战指南
java·数据结构·c++·学习·算法·map·知识
智航GIS9 小时前
10.7 pyspider 库入门
开发语言·前端·python
副露のmagic9 小时前
更弱智的算法学习 day25
python·学习·算法