简介基于卷积神经网络的柑橘成熟度识别项目为学习PyTorch图像分类的开发者提供完整可运行的代码与真实标注数据。压缩包共120个文件包括113张JPG与1张JPEG柑橘图片、3个Python脚本、3个TXT说明文件整体约10.8MB。代码实现从数据集预处理短边补灰边转为正方形、随机旋转增强、生成图片路径与标签文本到训练卷积模型并保存权重还带有PyQt交互界面便于直观查看识别效果。附带的requirement.txt可指导环境搭建适合作为图像分类入门或农业目标识别的小型完整项目参考。目前已有235人学习浏览有助于快速掌握数据增强、训练流程与简单界面部署的综合实践。1. 柑橘成熟度识别一个带数据集的 PyTorch 卷积神经网络实战包养过柑橘的人都知道成熟度判断是个靠老师傅眼力的活而移植到计算机视觉里本质就是一个图像二分类问题Healthy 还是 Greening。这次拆的资源是一份完整的 PyTorch 分类项目压缩包里面带了三个可运行脚本和一批真实柑橘图片打通了「图片路径生成 → CNN 模型训练 → PyQt 界面识别」全流程。学完卷积神经网络基础但没跑过完整项目的初学者或者想快速在毕设里落地一个识别系统的同学用它比从零搭框架省不少事。关键是数据集、预处理和界面都齐了解压后照着跑就能看到训练曲线和识别结果后面再谈调参和部署。2. 目录与数据三个脚本一条线先看清数据集里的增强残留训练再玄学也得先搞清楚手里有什么。这份资源的核心线索是三个 Python 文件加一个requirement.txt数据流方向是「读图片 → 生成文本标注 → 训练出模型 → 界面加载模型做推理」。先别急着跑把目录结构和数据分布摸一遍后面能少踩一半坑。2.1 三个脚本、三个阶段的数据流解压后大致结构如下具体名字以实际为准我这里按资源描述整理柑橘成熟度识别/ ├── 01数据集文本生成制作.py ├── 02深度学习模型训练.py ├── 03pyqt_ui界面.py ├── requirement.txt └── 数据集/ ├── Healthy/ │ ├── Healthy (9).jpg │ ├── Healthy (9)_rotated45.jpg │ └── Healthy (9)_flip.jpg └── Greening/ ├── Greening (1).jpeg └── ...三个脚本各管一段01数据集文本生成制作.py负责扫描数据集目录把每张图片的路径和类别标签写进 txt02深度学习模型训练.py读取 txt 划分训练集和验证集开始训练并保存模型03pyqt_ui界面.py加载训练产物把模型包装成一个能选图、点按钮、出结果的界面程序。这设计的思路是数据与模型解耦——想换数据集只改 01 脚本的扫描目录想换网络结构只动 02 脚本中间模型定义的部分界面和训练互不干扰。对新手来说这种分步式结构比一个大而全的 main.py 好调试得多哪一步出错能直接定位到文件。2.2 Healthy 与 Greening从文件名读类别数据集的类别目录很直白Healthy 和 Greening 两个文件夹类别名即标签。文件里的一些命名规律值得注意文件名片段含义备注Healthy (9).jpg原始健康果图片常规 jpgHealthy (9)_rotated45.jpg原图旋转 45 度后的增强样本已带增强痕迹Healthy (9)_flip.jpg原图水平翻转副本已带增强痕迹Greening (1).jpeg黄化/病征果原始图注意是 jpeg 后缀这里有个容易被忽略的信号数据目录里已经混入了_rotated45、_flip这类增强副本。后面跑 01 脚本时如果你不做过滤这些副本会被当作独立图片再次扫描甚至再次增强造成训练集和验证集之间有重复样本指标虚高。稍后避坑章节我会专门展开这个问题。另一个实际问题是后缀不统一有.jpg也有.jpeg。如果脚本里 glob 只写了*.jpgGreening 文件夹里的 jpeg 会被漏读你训练集的样本数会莫名其妙少一截。2.3 环境安装requirement.txt 和「先建环境再装包」环境是这类项目卡住新手的头号原因。资源里带了requirement.txt说明作者已经帮你把依赖列表整理好了安装的核心就一句话conda create -n citrus python3.8 -y conda activate citrus pip install -r requirement.txt第一行创建一个干净的 conda 环境避免和系统 Python 里已有的包互相污染第二行激活环境第三行按依赖清单一次性装完。装好后建议立刻验证关键包能不能正常导入python -c import torch, cv2, numpy; print(torch.__version__, cv2.__version__)如果 cv2 导入报错或 torch 版本输出异常优先检查 pip 源是否可用或者把 requirement.txt 里的 torch 版本和当前显卡驱动对照一下。CPU 机器也能跑这个项目只是训练慢一些不影响推理演示。原资源里如果附带免安装环境包那是给实在装不上依赖的人准备的我更推荐自己配环境因为后面你改代码、加包迟早要学会管理依赖靠别人打包好的环境终归是黑匣子。3. 预处理短边补灰边与旋转扩增01 脚本的边界条件预处理决定了模型能学到什么也决定了天花板。这个项目在预处理上做了两件典型的事把非正方形图片通过补灰边变成正方形再用旋转和翻转扩增数据集。这两步看起来简单里面的取舍和坑都不少。3.1 为什么补灰边而不是拉伸成正方形卷积神经网络的输入通常是固定尺寸比如 224×224 或 256×256。最粗暴的做法是把任意尺寸图片直接resize(224, 224)但这样会破坏长宽比——本来椭圆的柑橘被压成圆形或者细长叶片被拉宽模型很容易把「畸变后的形状」当成类别特征而不是真正学到果实本身的纹理和颜色。常见做法是先在短边对称补边让图变成正方形再整体 resize 到网络输入尺寸。灰边是中性值既不像黑边那样引入大片零值区域也不像白边那样可能拉高整体亮度统计。补边时用 PIL 的ImageOps.expand非常方便from PIL import Image, ImageOps def pad_to_square(img): 把非正方形图片按短边补灰边返回正方形图 w, h img.size if w h: return img # 计算四个方向的补边宽度灰边值用 128 if w h: top bottom (w - h) // 2 left right 0 else: left right (h - w) // 2 top bottom 0 return ImageOps.expand(img, border(left, top, right, bottom), fill128)border参数顺序是左、上、右、下很多人第一次会写反。fill128是中性灰既非纯黑也非纯白对归一化后的分布影响最小。如果图片原本就是正方形函数直接返回不做处理这段逻辑和资源描述完全一致。3.2 旋转扩增的度数与重复样本问题数据扩增是为了让模型见更多变体缓解小数据集的过拟合。旋转 45 度、水平翻转是图像分类里最常规的扩增手段对柑橘这种目标居中的图片45 度旋转不会把目标旋出画面翻转也不会引入语义歧义——健康果翻过来还是健康果。扩增的倍数大概是这样的关系1 张原始图可以衍生出旋转 45 度副本、翻转副本再加上可能的组合变换最后训练集规模能扩大到原来的 3 到 5 倍。但扩增不是越猛越好旋转 90 度以上时果实的朝向已经完全改变如果真实场景里摄像头很少拍倒置图这种样本反而会干扰模型。这份资源比较特别的地方在于数据目录里已经带了_rotated45和_flip后缀的成品也就是说增强副本已经在文件层面存在了。这时候运行 01 脚本要格外小心如果脚本会把目录里所有图片都读一遍并再次做旋转/翻转等于把增强样本又增强一遍数据分布会严重偏向某些源图验证集还容易泄漏。拿到代码后先打开 01 脚本看它的扫描逻辑确认它是否按文件名后缀排除了已有增强副本。3.3 01 脚本解析读路径、补边、写标签这类「数据集文本生成」脚本的逻辑大同小异核心就是遍历类别目录、整理路径和标签、按比例切分训练验证集、写入 txt。它的框架大概是这样的import os import random from PIL import Image, ImageOps data_root 数据集 output_train train.txt output_val val.txt class_names [d for d in os.listdir(data_root) if os.path.isdir(os.path.join(data_root, d))] class_to_id {name: idx for idx, name in enumerate(class_names)} # 比如 {Greening: 0, Healthy: 1} all_lines [] for cls in class_names: cls_dir os.path.join(data_root, cls) for fn in os.listdir(cls_dir): # 注意这里要同时匹配 jpg/jpeg过滤 _rotated45/_flip 增强副本 if not (fn.endswith(.jpg) or fn.endswith(.jpeg)): continue if _rotated45 in fn or _flip in fn: continue all_lines.append(f{os.path.join(cls_dir, fn)} {class_to_id[cls]}\n) random.shuffle(all_lines) split_idx int(len(all_lines) * 0.8) # 常见 8:2 划分 with open(output_train, w) as f: f.writelines(all_lines[:split_idx]) with open(output_val, w) as f: f.writelines(all_lines[split_idx:])标签用数字编码而不是直接写类别名是因为 PyTorch 的损失函数只认整数索引你需要在训练时用一个class_names列表把索引映射回名字。8:2 是常见的划分比例图片总量少的项目可以考虑 7:3但验证集最少也得留出几十张不然准确率波动太大没法判断模型好坏。这一步的产出是 txt 文件每行左边是图片绝对路径或相对路径右边是标签数字。后面 02 脚本会逐行读这个文件。建议生成后顺手打开看几行确认路径存在、标签和类别对应别等训练时报了文件不存在才回头查。4. 训练02 脚本的读图、参数与模型落盘位置训练脚本是这整套项目的核心。它要把 txt 里的路径变成张量把张量喂进卷积神经网络算损失、回传梯度最后把学到的权重存成本地文件。这一段我们拆开看数据读取、网络选型和参数设置再说模型保存路径的细节。4.1 DataLoader 与训练/验证划分02 脚本拿到 01 生成的 txt 后第一步是解析文本第二步是用 PyTorch 的Dataset和DataLoader封装数据from torch.utils.data import Dataset, DataLoader from PIL import Image from torchvision import transforms class CitrusDataset(Dataset): def __init__(self, txt_path, transformNone): self.samples [] with open(txt_path, r, encodingutf-8) as f: for line in f: path, label line.strip().split( ) self.samples.append((path, int(label))) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_loader DataLoader(CitrusDataset(train.txt, transform), batch_size16, shuffleTrue, num_workers2) val_loader DataLoader(CitrusDataset(val.txt, transform), batch_size16, shuffleFalse, num_workers2)Resize((224, 224))因为前面已经补成正方形了所以这里 resize 不会再造成畸变。Normalize的参数是 ImageNet 统计出来的均值和标准差用了预训练权重就得配这套归一化参数否则特征分布对不上。shuffleTrue只用在训练集验证集不打乱方便每次评估看到一致的顺序。这个自定义 Dataset 的写法是 PyTorch 标准模板核心就两个方法__len__告诉迭代器总共有多少样本__getitem__根据索引返回一个(图片张量, 标签)对。读 txt 时用strip().split( )是按「路径 空格 标签」的结构解析如果 01 脚本写的是\t分隔这里要相应改成split(\t)。4.2 网络、损失与训练参数怎么设资源标题写的是「基于卷积神经网络」具体网络结构以代码里为准——常见的选择是 ResNet18、ResNet34 或一个自定义的小型 CNN。用预训练 ResNet 做迁移学习在小数据集上效果最好因为柑橘图片纹理和 ImageNet 里大量自然图像的特征分布接近模型不需要从头学边缘和纹理基元。参数建议值说明输入尺寸224×224ResNet 等结构的标准输入batch_size16 或 32显存不够就降到 8epoch50 到 100小数据集 50 轮基本收敛学习率1e-3 起Adam 配 1e-3SGD 配 1e-2优化器Adam 或 SGDAdam 收敛快SGD 上限高损失函数CrossEntropyLoss二分类多分类通用二分类可以用CrossEntropyLoss它内部自带 softmax网络最后一层直接输出两个类别的 logits 就行不要在损失函数前手动接 softmax否则梯度会叠加出问题。学习率的经验值是用预训练权重时特征提取部分设小一点比如 1e-4新加的分类头设大一点1e-3可以用分组学习率的写法但新手阶段统一 1e-3 也没问题。训练循环里除了算 loss至少要打印每一轮的train_loss和val_acc。如果 val_acc 在 70 轮后还在涨说明没收敛完继续加轮数如果 train_loss 一直降但 val_acc 不动甚至下降典型的过拟合这时候应该加扩增或把模型换小。4.3 模型保存与加载pth 文件别乱动训练完成后保存模型是最后一个关键动作。PyTorch 有两种保存方式整个模型或仅状态字典常见做法是保存state_dict# 训练完后保存 torch.save(model.state_dict(), citrus_model.pth) # 推理时加载 model ResNet18(num_classes2) model.load_state_dict(torch.load(citrus_model.pth, map_locationcpu)) model.eval()map_locationcpu这行至关重要。很多人训练用 GPU加载时机器没 GPU 或 CUDA 版本不对直接torch.load会报类似No such operator的错。显式指定map_location能把权重先搬到 CPU再按需转回 GPU。加载后记得调用model.eval()把 dropout 和 BN 层切成推理模式否则同一张图每次预测结果可能不一样。模型文件建议放在项目根目录或者单独的weights/文件夹里别塞到数据集目录里。03 界面脚本默认会按相对路径找模型文件你挪了位置界面就加载失败。这份资源训练好的模型是保存在本地的也就是说你可以随时训练出自己的版本替换它。5. 避坑五个让指标虚高的数据与部署陷阱这类带数据集的 PyTorch 项目跑通本身不难难的是跑出来的指标是真实的。下面五条踩坑记录有的是这个数据目录结构直接导致的有的是同类项目里反复出现的通病每条按「现象 → 原因 → 解决」展开。5.1 增强副本泄漏到验证集指标虚高训练完看到 val_acc 99.8%心里刚美一下拿几张开微博上随便找的柑橘图一测识别效果一塌糊涂。这通常是数据泄漏。这个数据集里已经存在大量_rotated45、_flip后缀的增强副本如果 01 脚本扫描时把它们也算作独立样本同一个源图的旋转版和翻转版很可能同时出现在 train.txt 和 val.txt 里。模型等于提前见过答案验证集准确率自然高得离谱。解决方法是扫描时按文件名后缀过滤只把不含增强标记的原始图纳入划分。更稳妥的做法是按「源图」去重把同一源图衍生的所有样本放进同一个集合要么全在训练集要么全在验证集。拿到代码后先确认作者有没有做这一步没有就自己加一行判断。5.2 灰边被模型当成特征旋转越多越糟模型训练时 loss 降得很快但可视化预测结果发现模型判断依据不是果实颜色和纹理而是图片边缘有没有灰边——带灰边的图被归到某一类。原因在于补灰边操作对类别分布产生了系统性偏差如果 Greening 类图片普遍是横构图、补边量大Healthy 类多是正方形近景图网络会偷懒学习「有灰边 Greening」这种低成本特征。解决方法是让补边和扩增顺序配合好先做旋转裁切再补边或者补边时随机用 115 到 140 之间的灰度值填充让灰边不是一个固定常量。还有一个思路是干脆用中心裁剪配合 resize虽然会损失一点边缘信息但至少不会引入固定伪影。5.3 jpg 和 jpeg 混放训练集悄悄变少训练日志里显示的样本总数比数据集里的图片数量少或者训练时报FileNotFoundError仔细一看路径指向的是.jpeg文件而扫描逻辑只匹配了.jpg。这份资源里 Healthy 类是 jpgGreening 类里出现了 jpeg后缀不统一是打包数据的常态。扫描时用多后缀匹配直接避开这个坑if not (fn.lower().endswith(.jpg) or fn.lower().endswith(.jpeg) or fn.lower().endswith(.png)): continue另外 Windows 和 Linux 对文件名大小写敏感度不同Healthy.JPG在 Linux 下用*.jpg匹配不到统一转小写再判断最保险。5.4 显存溢出或训练中断训练跑到一半进程被杀或者直接报CUDA out of memory。常见原因有三个batch_size 太大、图片resize后分辨率还是偏高、num_workers设置过多导致内存读写压力大。这个数据集图片本身尺寸不会太大但如果你自己加图片有些手机拍的原图是 4000×3000不进 resize 直接进网络一张图就能吃掉几百兆显存。解决顺序是先把图片尺寸压到 224×224 或 256×256再降 batch_size 到 8最后把num_workers降到 0 或 2。如果还是崩检查是不是 PyTorch 版本和显卡驱动不匹配这个属于环境问题重装对应版本的 torch 比硬调代码效率高。5.5 PyQt 界面加载不出图或颜色不对03 脚本跑起来窗口正常但点选图片后界面黑屏、或者柑橘颜色变得发青发蓝。这是经典的通道顺序问题OpenCV 读图是 BGR 顺序而 QImage 和 PIL 都是 RGB直接互传就会颜色错乱。如果界面脚本用了 cv2 读图再转 QImage必须加一步转换rgb_img cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB)另外一个隐蔽问题是路径含中文或空格。资源解压路径如果带中文文件夹名PyQt 的文件对话框和 PIL 在部分 Windows 环境下会读取失败。把项目放在纯英文路径下运行能省掉一堆莫名其妙的文件访问异常。6. 验证与进阶离线盲测、批量推理与一个收尾习惯模型训练完、界面能出结果项目只能算「跑通」还不能算「可信」。接下来要做的验证有三个动作。第一盲测。从网上找或者用自己的手机拍 10 到 20 张这个项目没见过的柑橘图不要是数据集里的原图和增强副本逐张喂给模型看结果。这一步能直接暴露指标虚高的问题。第二看混淆矩阵统计 Healthy 被错判成 Greening 多少张、反向多少张如果某一类错判特别多说明那一类的特征没学到。第三对同一张图做亮度扰动和轻微旋转看预测结果是否稳定不稳定说明模型对光照和角度太敏感部署到真实环境会翻车。如果想绕开界面直接快速验证我习惯写一个十行左右的批量推理脚本import torch from PIL import Image from torchvision import transforms model load_model() # 复用训练时的网络结构和权重 transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) for img_path in test_images: img transform(Image.open(img_path).convert(RGB)).unsqueeze(0) pred model(img).argmax(dim1).item() print(img_path, class_names[pred])这套脚本可以做批量回放测试也能用来统计误判案例。从那以后我每次拿到这类打包资源第一件事不是双击跑 01 脚本而是先扫一遍文件后缀、看脚本里的 glob 规则、确认增强副本有没有泄漏到验证集再开始训练。这套检查流程帮我避开了很多次「指标看着很美、实际没法用」的尴尬。希望帮到你。本文还有配套的精品资源点击获取