第 P7 周:马铃薯病害识别(VGG-16 复现)
·
👉声明
- 本文为 365 天深度学习训练营 内部参考文章的个人学习记录,代码与实验在本地完成。
- 数据集来源训练营提供的
PotatoPlants.zip;数据集请勿对外公开传播。 - 原作者:K同学啊
- 本篇为个人在 P7 关卡上的学习与实践记录
第 P7 周:马铃薯病害识别(VGG-16 复现)
🏡 我的环境(参考)
- 语言:Python 3.11.13(教程要求 3.8,思路一致)
- 编译器:Jupyter Lab
- 深度学习:PyTorch
2.5.1+cu121,Torchvision0.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.profile看MACs/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),必要时冻结部分层。
六、最终总结
本周最大收获不是“把模型跑通”,而是建立了可复用的训练模板:
- 先把输入管线做稳(自动解压、自动识别类别、分层划分);
- 再做模型双轨验证(手写结构理解 + 官方模型性能);
- 然后把指标补全(Acc + F1 + 报告);
- 最后做可复现闭环(保存 best、加载复评、写下问题与结论)。
对我后续最有价值的备忘是:
每次新任务优先保证“路径稳定、标签正确、指标完整、结果可复现”,再谈调参冲榜。
这套流程比单次高分更重要,因为它能持续复用到 P8、P9 以及以后项目中。
七、下次训练前检查清单
- 先
Restart Kernel + Run All,清掉历史状态; - 检查
classNames与num_classes是否一致; - 确认
best_model_p7.pth是本次训练新生成; - 同时记录
Best Val Acc和Macro F1; - 每次只改一个变量(优化器/学习率/增强)做对照实验。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)