简介面向PyTorch初学者的植物生长阶段分类实战代码包基于VGG模型实现图像分类适合深度学习入门、课程设计或毕业设计参考。代码仅3个Python脚本分别负责数据集TXT生成、CNN训练和PyQt界面展示每行均含中文注释配合说明文档.docx可快速理解整体流程。压缩包共9个文件包含4张类别示例JPG、3个脚本、1个requirements.txt依赖清单和1个说明文档整体仅227KB轻量易用。资源不含数据集图片目录下每个类别文件夹均附提示图用户按提示自行放入收集的图片即可扩展分类类别并训练模型完成训练后也可通过PyQt界面加载模型进行图片识别便于演示和验证效果。目前已有128人学习下载说明文档还给出了Anaconda、Python3.7/3.8及PyTorch1.7.1/1.8.1环境安装指引能有效降低环境配置门槛帮助读者从零跑通VGG训练与识别流程。1. VGG 识别植物生长阶段这个资源真正帮你省掉的时间植物生长阶段的识别这事看起来只是把图片分成种子、幼苗、开花、结果四类但真用深度学习跑一遍你会发现卡点全在那些“看似简单”的环节环境装不上、文件夹结构没设计好、数据集标签对不上、训练完的模型不知道怎么拿去做预测。这套基于 PyTorch 的 VGG 分类资源正是冲着这几个卡点来的——三个 py 文件把「生成标注 txt → 训练 CNN → PyQt 界面推理」整条链路串完代码逐行带中文注释而且刻意不打包数据集图片逼你按自己的场景去建立类别文件夹、搜集图片。适合两类人一是刚学完 PyTorch 基础、想完整跑通一个图像分类项目的新手二是农学、园艺背景的研究者手里有植物各阶段的实拍图需要快速出一个可用的分类模型。它不是黑匣子每个环节都能打开看、能改。2. VGG 选型与三个 py 文件的架构为什么不用 ResNet很多人在选模型时会纠结现在 ResNet、EfficientNet 满天飞VGG 是不是太老了这个项目用 VGG 其实是有道理的而且恰恰是这种“老模型”最适合作为学习深度学习和图像分类的起点。2.1 VGG 在小数据集上的选型理由VGG 的核心思路很朴素用连续的小卷积核3×3堆叠出足够深的网络把卷积层从 AlexNet 的 5 层加深到 16 层或 19 层。它没有残差连接没有注意力机制就是老老实实的卷积、池化、全连接。也正是因为结构规整它成了理解 CNN 特征提取过程的最佳教学模型——每一步卷积后特征图怎么变化在代码里一眼就能看明白。在你只有几百张到上千张植物图片、类别只有四个的情况下VGG16 的优势比 ResNet 更明显迁移学习效果好VGG 在 ImageNet 上学到的边缘、纹理、颜色等底层特征非常通用。植物叶片的边缘、果实的形状纹理正好能直接复用这些特征只需要替换最后的全连接层花很少的训练轮次就能达到不错的准确率。显存和训练时间可控虽然 VGG 参数量有 1.38 亿但那是全连接层占大头。在这个项目里会替换掉最后的分类层用冻结主干 训练分类头的方式显存占用并不夸张一张 6G 显存的 GTX 1660 就能跑起来。调试方便结构简单意味着出问题时容易定位。训练 loss 不下降你能很快判断是学习率问题还是数据问题而不是在复杂的模型结构里排查。用 ResNet 当然也可以但新手在第一次跑项目时面对残差块的跳跃连接、BatchNorm 的统计量更新很容易被细节带偏。VGG 的价值在于把练手的难度降下来让注意力集中在数据准备和训练流程上。2.2 三个文件的职责与整体数据流这个资源一共三个 py 文件整个流程是这样的文件职责输入输出01生成txt.py扫描类别文件夹生成训练列表数据集/各分类文件夹train.txt每行图片路径 类别索引02CNN训练数据集.py读取 txt 和图片训练 VGG 分类模型train.txt 图片文件夹训练好的模型权重.pth03pyqt界面.py加载权重提供 PyQt 图形界面做推理待预测图片界面显示类别和置信度这个设计的巧妙之处在于txt 文件是数据层和训练层之间的解耦点。你换自己的数据集只需要保证文件夹命名正确、图片放对位置01 文件会自动扫描生成对应的标签文件02 训练文件完全不用动。如果你要增加类别比如加一个“成熟”阶段只需要在数据集文件夹下新建一个成熟文件夹放好图片脚本会自动把新类别纳入训练。后面我会细讲每个文件的实现逻辑。3. 数据准备与 01生成txt.py类别文件夹与标注文件怎么配合整个项目的命门在数据准备这一步。由于资源本身不含数据集图片你需要自己搜集图片放到对应文件夹。这个环节做得好不好直接决定训练出来的模型能不能用。3.1 建立类别文件夹与图片搜集策略解压后你会看到数据集文件夹下已经建好了种子、幼苗、开花、结果四个子文件夹每个里面有一张提示图示意你该往这个文件夹放什么类型的图片。第一步就是替换掉提示图放入你自己搜集的实拍图。搜集图片时有几个策略值得注意每类图片数量尽量均衡四个文件夹各放 100 到 300 张比一个文件夹 500 张、另一个只有 50 张要可靠得多。类别不平衡时模型会倾向于把模糊样本预测为样本多的那一类。图片分辨率不用太高VGG 的输入固定是 224×224你搜集 1000×1000 的大图在加载时会被强制缩放。反而 300×300 左右的图更合适缩放损失的信息少加载也快。多样性优先于数量尽量让图片覆盖不同角度、光照、背景条件。如果所有图片都是同一个角度、同一个背景拍的模型很快会偷懒——它可能在学背景而不是学植物本身。图片放好后完整目录结构是这样的项目根目录/ ├── 01生成txt.py ├── 02CNN训练数据集.py ├── 03pyqt界面.py ├── requirement.txt ├── 说明文档.docx └── 数据集/ ├── 种子/ # 粘贴种子阶段图片 ├── 幼苗/ # 粘贴幼苗阶段图片 ├── 开花/ # 粘贴开花阶段图片 └── 结果/ # 粘贴结果阶段图片注意文件夹命名不要带空格和中文标点因为后续代码会直接拼接路径空格容易在命令行和文件读取时引入隐患。3.2 01生成txt.py 的逐段逻辑这个文件做的事情很简单遍历数据集文件夹下的每个子文件夹把所有图片的绝对路径和对应的类别索引写进一个 txt 文件。核心代码逻辑如下import os # 数据集根目录改成你实际解压的位置 dataset_path ./数据集 # 输出的 txt 文件名 output_file train.txt # 获取所有类别文件夹并按名称排序 classes os.listdir(dataset_path) classes.sort() print(f发现 {len(classes)} 个类别: {classes}) # 建立一个类别名称到索引的映射字典 class_to_idx {cls_name: idx for idx, cls_name in enumerate(classes)} with open(output_file, w, encodingutf-8) as f: for cls_name in classes: cls_folder os.path.join(dataset_path, cls_name) # 跳过非文件夹比如隐藏文件 if not os.path.isdir(cls_folder): continue # 遍历该类别文件夹下的所有图片 for img_name in os.listdir(cls_folder): # 过滤掉提示图和非图片文件 if img_name.startswith(.): continue if not img_name.lower().endswith((.jpg, .jpeg, .png, .bmp)): continue img_path os.path.join(cls_folder, img_name) # 写入格式图片绝对路径 空格 类别索引 f.write(f{img_path} {class_to_idx[cls_name]}\n) print(f已生成 {output_file}共 {sum(1 for _ in open(output_file))} 条记录)逻辑说明这段代码先扫描数据集根目录下的每个子文件夹每个子文件夹的名字就是一个类别然后为每个类别分配一个从 0 开始的索引种子是 0、幼苗是 1、开花是 2、结果是 3最后遍历每个文件夹里的图片把「图片路径 空格 索引」写到 train.txt 的一行。参数说明dataset_path 是你数据集的实际路径注意 Python 字符串里的反斜杠要转义或者像我这样直接用正斜杠img_name.lower() 用于统一后缀判断避免 .JPG 和 .jpg 被当成两种文件编码声明 encodingutf-8 是为了防止 Windows 下默认 GBK 编码导致的中文路径乱码问题。运行方式是在终端里执行python 01生成txt.py运行完后你应该能在项目根目录看到生成的 train.txt。打开看一眼每行格式类似D:/plant_project/数据集/种子/seed_001.jpg 0 D:/plant_project/数据集/开花/flower_010.jpg 24. 训练核心 02CNN训练数据集.py从 DataLoader 到迁移学习调参02 文件是这个资源的核心承担了数据加载、模型构建、训练和保存的全流程。它加载上一步生成的 train.txt配合图片文件夹完成训练。这一章的代码逻辑理解了你就能自己改参数适配不同的数据集。4.1 数据预处理与 DataLoader 关键参数训练前需要对图片做统一的预处理让它们变成固定尺寸、数值范围合理、分布接近标准正态的张量。VGG 官方训练时用的预处理方式是缩放至 256×256、随机裁剪出 224×224、按 ImageNet 的均值方差做标准化。from torchvision import transforms # 训练集的预处理加随机翻转增强减少过拟合 train_transform transforms.Compose([ transforms.Resize((256, 256)), # 先缩放到 256 transforms.RandomCrop(224), # 随机裁剪到 224相当于数据增强 transforms.RandomHorizontalFlip(), # 随机水平翻转增加样本多样性 transforms.ToTensor(), # 转为 Tensor像素值归一化到 0~1 transforms.Normalize( mean[0.485, 0.456, 0.406], # ImageNet 三个通道的均值 std[0.229, 0.224, 0.225] # ImageNet 三个通道的标准差 ) ]) # 验证集用纯 resize center crop不做随机增强 val_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]) ])参数说明Resize((256, 256)) 和 RandomCrop(224) 的组合是 VGG 官方推荐的训练策略——先放大再随机裁剪等于每轮训练看到的是同一张图的不同局部变相扩大了数据集Normalize 的参数 0.485、0.456、0.406 是 ImageNet 全量数据的统计值不要改成别的数字迁移学习要求输入分布尽量接近预训练时的分布RandomHorizontalFlip 对植物图片是安全的增强操作因为植物的左右翻转不改变类别语义。接着用 PyTorch 的 Dataset 和 DataLoader 把 train.txt 的内容变成可迭代的训练批次from torch.utils.data import Dataset, DataLoader from PIL import Image class PlantDataset(Dataset): 读取 train.txt按行解析图片路径和标签 def __init__(self, txt_path, transformNone): self.samples [] self.transform transform with open(txt_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue img_path, label line.rsplit( , 1) self.samples.append((img_path, int(label))) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] img Image.open(img_path).convert(RGB) # 统一转成 RGB 三通道 if self.transform: img self.transform(img) return img, label train_dataset PlantDataset(train.txt, transformtrain_transform) # batch_size 按显存调整4G 显存建议 168G 显存可以用 32 train_loader DataLoader( train_dataset, batch_size32, shuffleTrue, # 每个 epoch 打乱数据顺序 num_workers2, # Windows 下建议设 0 或 2过高会报错 drop_lastTrue # 最后一批不足 batch_size 时丢弃防止 BN 层统计异常 )逻辑说明PlantDataset 类把 train.txt 每一行的「路径 标签」拆开getitem按索引读取图片并做预处理。DataLoader 负责把这些样本拼成 batch。这里有个细节shuffleTrue 在训练时是必须的否则模型每个 epoch 看到的样本顺序完全一样训练容易震荡。参数说明num_workers 在 Windows 上经常出问题如果报 DataLoader worker 相关的错误直接改成 0——它表示用主进程加载数据慢一点但绝对稳定drop_lastTrue 踩过坑的人才知道为什么要加当最后一个 batch 不足 32 张时BatchNorm 层会因为统计量偏差报错或产生异常训练行为。4.2 迁移学习与训练循环模型的构建采用标准的迁移学习方案加载 ImageNet 预训练的 VGG16 权重把最后一层全连接从 1000 类换成 4 类只训练分类头和分类头之前的部分参数。这里给出训练循环的完整代码import torch import torch.nn as nn import torch.optim as optim from torchvision import models # 检测 GPU没有 GPU 就回退到 CPU device torch.device(cuda if torch.cuda.is_available() else cpu) print(f使用设备: {device}) # 加载预训练 VGG16 模型 model models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1) # 替换最后一层全连接4 对应你的类别数 num_classes 4 model.classifier[6] nn.Linear(4096, num_classes) # 冻结前面所有层只训练替换后的全连接层 for param in model.features.parameters(): param.requires_grad False model model.to(device) # 多分类用交叉熵损失自带 Softmax criterion nn.CrossEntropyLoss() # 只把需要训练的参数的梯度传给优化器 optimizer optim.SGD( filter(lambda p: p.requires_grad, model.parameters()), lr0.001, momentum0.9 ) # 学习率调度每 10 个 epoch 学习率乘 0.1 scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1) epochs 30 for epoch in range(epochs): model.train() running_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total print(fEpoch [{epoch1}/{epochs}] Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}) scheduler.step() # 保存整个模型含结构后续推理直接 load 即可 torch.save(model.state_dict(), plant_vgg16.pth) print(训练完成模型已保存为 plant_vgg16.pth)逻辑说明models.vgg16 会先下载预训练权重到本地缓存然后通过 model.classifier[6] 把原本输出 1000 类的全连接层替换成输出 4 类的线性层。冻结 features 层意味着训练时卷积特征提取部分完全不更新参数只在原有特征基础上训练新的分类器——这样训练速度快且小数据集不容易过拟合。optimizer 只用 filter 筛选出的 requires_gradTrue 的参数就是那一个全连接层。参数说明学习率 0.001 搭配 SGD momentum 0.9 是 VGG 迁移学习的经典组合不要上来就换 Adam。Adam 在这类任务上收敛快但最终精度经常不如调好学习率的 SGDStepLR 每 10 个 epoch 把学习率降一个量级作用是后期用小步长精细收敛epochs30 够用了如果训练集很小每类 100 张以下20 个 epoch 就应该停再往后 loss 下降不明显甚至回升。训练结束后项目根目录会出现一个 plant_vgg16.pth 文件。你可以用下面的命令快速验证训练曲线是否有问题python -c import torch; sd torch.load(plant_vgg16.pth, map_locationcpu); print([(k, v.shape) for k, v in sd.items()])如果列出的键里 classifier.6.weight 的形状是 [4, 4096]说明保存正确。5. 复现避坑五条能让你少熬两个通宵的报错记录这个项目我在自己电脑上完整复现过也帮朋友排查过问题。下面这几条是出现频率最高的坑每一条都是实际遇到过的血泪经验。5.1 界面报“No module named‘PyQt5’”现象运行 03pyqt界面.py 时终端直接报 ModuleNotFoundError: No module named PyQt5程序退出。原因PyQt5 很容易装但很多人在装依赖时只用了一行 pip install torch。requirement.txt 里虽然列出了所有依赖但环境没装全。还有一个隐蔽原因PyQt5 对 Python 版本有要求Python 3.9 之后某些老版本 PyQt5 安装会失败。解决在项目根目录执行pip install -r requirement.txt如果你用的是 Anaconda建议先创建一个 Python 3.8 的环境再安装conda create -n plant python3.8 conda activate plant pip install -r requirement.txt如果 PyQt5 还是装不上单独装一次pip install PyQt5 PyQt5-sip5.2 运行 02 时报 FileNotFoundError路径带“数据集”三个字就出错现象01 生成 train.txt 一切正常但 02 训练时一读图片Python 报错说找不到 D:/…/数据集/种子/seed_001.jpg但你明明看到文件就在那里。原因这几乎是 Windows 中文路径的经典 bug。某些版本的 PyTorch 在 Windows 下处理中文路径时底层 C 的编码转换会出问题导致中文目录无法正常读取。我自己排查时发现换成全英文路径后问题立刻消失。解决把项目根目录改在纯英文路径下比如 D:/plant_project/并且确保文件夹里的图片文件名也不含中文。如果你实在需要在中文路径下跑一个绕行方案是把代码里的中文路径改成短路径别名但最省事的方式永远是英文路径。5.3 训练时 loss 一直是 1.3 左右怎么都不下降现象前几个 epoch loss 在 1.3 附近徘徊四分类随机猜测的理论 loss 是 ln(4)≈1.386几十个 epoch 后还是没明显变化准确率在 25% 附近。原因这是典型的标签错位问题。01 生成 txt 时是遍历顺序给类别编号的如果数据集文件夹下的类别顺序变了或者你手动编辑过 train.txt标签和图片对不上号模型学不到任何有效规律。解决删除 train.txt重新运行 01生成txt.py再确认生成的文件里每一行最后一个数字是不是 0 到 3 之间。同时打开一两个路径肉眼核对一下种子 文件夹下的图片路径后面跟的标签必须是 0。如果类别的顺序被打乱最保险的办法是固定数据集文件夹里子文件夹的排列顺序不要随意重命名。5.4 训练过程中显存溢出CUDA out of memory现象训练在跑到第 10 个 epoch 附近时突然报 RuntimeError: CUDA out of memory前面一切正常。原因VGG16 在 224×224 分辨率下每个 batch 的显存占用确实不低。如果你按 32 的 batch_size 能跑是因为某些高分辨率图片在缩放后内容复杂、激活值大或者后台有其他程序占了显存。还有一个常见原因训练中途验证时忘记关梯度导致显存被验证图占住。解决先降 batch_size 到 16 试试一般就能跑通。如果还不行在训练循环里把验证部分的代码包上 torch.no_grad()。另外养成好习惯每次训练前用 nvidia-smi 看一下显存占用把无关进程清掉。5.5 界面能跑但预测一个都不对所有图片都分成同一类现象03 界面打开正常选择图片也能显示结果但不管选种子还是开花预测结果清一色是“种子”。原因这种情况十有八九是类别映射表的问题。03 文件里有一个类别索引到中文名称的映射列表比如 class_names [种子, 幼苗, 开花, 结果]。如果这个列表的顺序和 01 生成 txt 时的遍历顺序不一致模型输出的索引 0 会被错误地翻译成中文名。也有可能是模型的 fc 输出层和类别数不匹配导致最终输出全部偏向某类。解决把数据集文件夹下的类别按名称排序后对照一下 01 生成 txt 打印出的类别顺序跟 03 里的 class_names 列表逐项比对。正确顺序是种子0、幼苗1、开花2、结果3。改完后重新运行 01再训练一次。# 快速检查类别顺序是否正确 python -c import os classes os.listdir(./数据集) classes.sort() print(classes) 6. 进阶部署把 PyQt 界面改成命令行推理脚本03pyqt界面.py 提供了一个图形界面用于单张图片预测但实际使用中你会发现命令行方式往往更灵活尤其是你要批量验证模型效果或者把它接进自动化流程时。把图形界面里的推理逻辑抽出来不到 30 行就能做成一个独立的推理脚本。核心推理代码import torch from torchvision import models, transforms from PIL import Image # 类别顺序必须和训练时一致 class_names [种子, 幼苗, 开花, 结果] device torch.device(cuda if torch.cuda.is_available() else cpu) # 构建模型并加载权重 model models.vgg16() model.classifier[6] torch.nn.Linear(4096, len(class_names)) model.load_state_dict(torch.load(plant_vgg16.pth, map_locationdevice)) model model.to(device) model.eval() # 切换为评估模式关闭 Dropout 和 BN 的统计更新 # 推理时的预处理固定缩放不做随机增强 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]) ]) def predict(img_path): img Image.open(img_path).convert(RGB) tensor transform(img).unsqueeze(0).to(device) # 推理阶段不计算梯度省显存且速度更快 with torch.no_grad(): outputs model(tensor) probs torch.softmax(outputs, dim1) # 把输出转成概率 conf, pred_idx torch.max(probs, dim1) pred_class class_names[pred_idx.item()] conf_value conf.item() * 100 return pred_class, conf_value if __name__ __main__: result predict(test_seed.jpg) print(f预测类别: {result[0]}置信度: {result[1]:.2f}%)逻辑说明models.vgg16() 不带预训练权重时不会触发下载然后手动替换分类层并加载之前保存的 plant_vgg16.pth。torch.no_grad() 是推理的标准写法它告诉 PyTorch 不需要构建计算图显存占用降低一个量级批量预测时速度提升明显。softmax 的作用是把全连接层输出的 logits 转成加和为 1 的概率分布方便解读置信度。验证效果时有一个值得坚持的习惯不要只测一张图就下结论。从那以后我每次做完分类项目都会强制自己跑一遍全部类别的抽查——从每一个类别文件夹里抽出几张图分别走一遍上面的预测代码把预测结果和真实标签对照。特别是看那些错误的样本到底是因为光照、角度还是类别本身太相似。这一步能帮你快速判断是数据问题还是模型问题也能让你对新数据集上模型的可用边界心里有数。希望帮到你。本文还有配套的精品资源点击获取