👉声明

  • 本文为 365 天深度学习训练营 内部参考文章的个人学习记录,代码与实验在本地完成。
  • 数据集来源训练营提供的 PotatoPlants.zip;数据集请勿对外公开传播。
  • 原作者:K同学啊
  • 本篇为个人在 P7 关卡上的学习与实践记录

第 P7 周:马铃薯病害识别(VGG-16 复现)

🏡 我的环境(参考)

  • 语言:Python 3.11.13(教程要求 3.8,思路一致)
  • 编译器:Jupyter Lab
  • 深度学习:PyTorch 2.5.1+cu121,Torchvision 0.20.1+cu121
  • GPU:NVIDIA GeForce RTX 4060 Ti
  • 数据:p7-data/PotatoPlants.zip(自动解压到 p7-data/PotatoPlants

🍺 本周要求

  • 手动搭建 VGG-16 网络框架;
  • 调用官方 VGG-16 网络框架;
  • 查看模型参数量与相关指标;
  • 完成训练、可视化、单图预测和模型评估;
  • 训练中保存验证集最佳权重。

一、本周学习内容概览

本周核心是把“会调用模型”升级为“会复现结构 + 会评估 + 会复盘”:

  • 数据处理:实现压缩包自动解压、ImageFolder 读取、8:2 分层划分训练/验证;
  • 模型双线并行:一条线是 ManualVGG16(手写版),另一条线是 torchvision.models.vgg16(官方版);
  • 训练策略CrossEntropyLoss + Adam(支持切换 SGD)+ LambdaLR
  • 结果闭环:训练曲线、单图预测、最佳模型加载复评;
  • 指标意识:不仅看 Acc,还补充 Precision / Recall / F1 与参数量统计。

二、必要代码与关键点说明

1) 数据自动解压与加载

  • 路径统一用 Path("p7-data/PotatoPlants.zip"),避免手工路径写错;
  • p7-data/PotatoPlants 不存在则自动解压,首次运行更稳;
  • 类别自动识别为 ['Early_blight', 'Late_blight', 'healthy']

核心流程代码:

zip_path = Path("p7-data/PotatoPlants.zip")
extract_root = Path("p7-data")
data_root = extract_root / "PotatoPlants"

assert zip_path.exists(), f"未找到数据压缩包: {zip_path.resolve()}"
if not data_root.exists():
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(extract_root)

total_data = datasets.ImageFolder(str(data_root), transform=None)
classNames = total_data.classes
num_classes = len(classNames)
print(classNames, total_data.class_to_idx)

2) 分层划分与变换

  • 使用 train_test_split(..., stratify=targets)(若环境缺 sklearn 则回退随机划分);
  • 训练变换:Resize(256) + RandomCrop(224) + RandomHorizontalFlip + Normalize
  • 验证变换:Resize(224) + Normalize

核心流程代码:

train_transforms = transforms.Compose([
    transforms.Resize((256, 256)),
    transforms.RandomCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
test_transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

idx_all = np.arange(len(total_data))
targets = np.array(total_data.targets)
train_idx, test_idx = train_test_split(
    idx_all, test_size=0.2, stratify=targets, random_state=42
)

3) 手写 VGG-16(ManualVGG16)

  • 13 conv + 5 pool + 3 fc 结构实现;
  • 用随机张量做前向形状检查,先保证结构正确再训练。

核心流程代码(节选):

class ManualVGG16(nn.Module):
    def __init__(self, num_classes):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(True),
            nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(True),
            nn.MaxPool2d(2, 2),
            # ... 中间 Block 省略 ...
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(True),
            nn.MaxPool2d(2, 2),
        )
        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096), nn.ReLU(True), nn.Dropout(0.5),
            nn.Linear(4096, 4096), nn.ReLU(True), nn.Dropout(0.5),
            nn.Linear(4096, num_classes)
        )

4) 官方 VGG-16(可切换)

  • 封装 build_official_vgg16(num_classes, pretrained=True)
  • 自动替换最后分类层到当前类别数;
  • 通过 MODEL_TYPE = "official" / "manual" 切换训练对象。

核心流程代码:

def build_official_vgg16(num_classes, pretrained=True):
    if pretrained:
        from torchvision.models import vgg16, VGG16_Weights
        model = vgg16(weights=VGG16_Weights.IMAGENET1K_V1)
    else:
        model = torchvision.models.vgg16(weights=None)
    model.classifier[-1] = nn.Linear(model.classifier[-1].in_features, num_classes)
    return model

official_model = build_official_vgg16(num_classes, pretrained=True)
MODEL_TYPE = "official"
model = official_model if MODEL_TYPE == "official" else manual_model
model = model.to(device)

5) 参数量与相关指标查看

  • 自定义函数统计 Total / Trainable / Non-trainable
  • 可选 torchinfo.summary 看层级结构;
  • 可选 thop.profileMACs/FLOPs

核心流程代码:

def model_parameter_stats(m):
    total = sum(p.numel() for p in m.parameters())
    trainable = sum(p.numel() for p in m.parameters() if p.requires_grad)
    non_trainable = total - trainable
    return total, trainable, non_trainable

manual_total, _, _ = model_parameter_stats(manual_model)
off_total, _, _ = model_parameter_stats(official_model)
print("ManualVGG16 Total params:", manual_total)
print("OfficialVGG16 Total params:", off_total)

6) 训练与最佳模型保存

  • 每轮流程:train -> test -> scheduler.step()
  • 验证集准确率刷新时保存 best_model_p7.pth
  • 训练日志记录 Train_acc/Train_loss/Val_acc/Val_loss/Lr

核心流程代码:

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
lambda1 = lambda epoch: 0.92 ** (epoch // 4)
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda1)

best_acc = 0.0
best_path = "best_model_p7.pth"
for epoch in range(epochs):
    epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, optimizer)
    epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
    scheduler.step()
    if epoch_test_acc > best_acc:
        best_acc = epoch_test_acc
        torch.save(model.state_dict(), best_path)

请添加图片描述请添加图片描述


三、结果可视化与模型验证

  • 绘制 Accuracy/Loss 双曲线,观察是否出现过拟合;
  • 提供 predict_one_image(),支持传入任意图片路径并输出预测类别;
  • 训练结束后加载最佳权重再次评估,验证最优点是否真实可复现;
  • 补充宏平均 Precision / Recall / F1,避免只盯准确率。

核心流程代码(预测 + 评估):

def predict_one_image(image_path, model, transform, classes):
    test_img = Image.open(image_path).convert("RGB")
    x = transform(test_img).to(device).unsqueeze(0)
    model.eval()
    with torch.no_grad():
        output = model(x)
    _, pred = torch.max(output, 1)
    print(f"预测结果是:{classes[pred.item()]}")

best_model.load_state_dict(torch.load(best_path, map_location=device))
epoch_test_acc, epoch_test_loss = test(test_dl, best_model, loss_fn)
print("验证集指标:", (epoch_test_acc, epoch_test_loss))

请添加图片描述

四、我的探索过程(重点备忘)

  • 探索 1:官方 VGG16 vs 手写 VGG16

    • 目的:确认“结构理解”与“工程训练效果”是否一致;
    • 结论:官方版在稳定性与收敛速度上通常更好,手写版更适合理解网络细节。
  • 探索 2:Adam vs SGD

    • 目的:复现教程里“换优化器后现象变化”;
    • 结论:Adam 前期收敛快,SGD 对学习率和动量更敏感;若参数不调好,可能出现验证集提升慢或波动大。
  • 探索 3:指标不只看 Acc

    • 目的:防止某一类学得差但总准确率看起来还行;
    • 结论:加入 Macro F1 后更容易发现类别不均衡带来的盲点。
  • 探索 4:轻量化可行性

    • 目的:对比 VGG16 与轻量模型参数规模;
    • 结论:轻量模型参数显著更少,后续可做“精度-速度-显存”三目标权衡。

五、遇到的问题与解决记录

  • 问题 1:数据路径易错

    • 现象:压缩包解压路径和代码读取路径不一致;
    • 处理:统一 zip_path/extract_root/data_root 并加断言。
  • 问题 2:notebook 历史输出干扰判断

    • 现象:旧输出和新代码不一致,容易误判训练效果;
    • 处理:从头重跑所有单元,用新日志覆盖旧结果。
  • 问题 3:评价指标不完整

    • 现象:只有 Acc 时难判断类别层面的表现;
    • 处理:补充 classification_report 和宏平均指标。
  • 问题 4:大模型显存压力

    • 现象:VGG16 参数多、显存占用高;
    • 处理:降低 batch_size(如 16->8),必要时冻结部分层。

六、最终总结

本周最大收获不是“把模型跑通”,而是建立了可复用的训练模板

  1. 先把输入管线做稳(自动解压、自动识别类别、分层划分);
  2. 再做模型双轨验证(手写结构理解 + 官方模型性能);
  3. 然后把指标补全(Acc + F1 + 报告);
  4. 最后做可复现闭环(保存 best、加载复评、写下问题与结论)。

对我后续最有价值的备忘是:
每次新任务优先保证“路径稳定、标签正确、指标完整、结果可复现”,再谈调参冲榜。
这套流程比单次高分更重要,因为它能持续复用到 P8、P9 以及以后项目中。


七、下次训练前检查清单

  • Restart Kernel + Run All,清掉历史状态;
  • 检查 classNamesnum_classes 是否一致;
  • 确认 best_model_p7.pth 是本次训练新生成;
  • 同时记录 Best Val AccMacro F1
  • 每次只改一个变量(优化器/学习率/增强)做对照实验。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐