ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python花卉识别与自动归档:用迁移学习微调PyTorch模型

Python花卉识别与自动归档:用迁移学习微调PyTorch模型 简介一份面向Python初中级学习者的图像识别实战资源以“识别花卉并自动整理分类”为任务结合百度AI开放接口实现图像上传、识别与按结果归类覆盖Python调用API、图像预处理、结果解析、文件移动等完整流程也适合作为AI应用开发入门练习。压缩包共21个文件包含1个可直接运行的.py主程序、15张png示意图和5张jpg照片合计10.47MB通过脚本和示例图片可直观理解代码逻辑与识别效果。目前已有1756人浏览学习。资源内附“花卉合集”图像数据可用于测试模型或替换为自己的照片脚本结构涉及数据读取、接口调用、结果处理与文件自动分类通过阅读代码可掌握百度智能云API的鉴权与调用方式同时体会实际项目中异常处理和部署时需注意的问题为后续做更复杂的图像分类项目打下基础。1. 让 python 识别花卉并自动整理分类先想清楚整理比识别更值钱面对手机里、相机里几千张未命名的花卉照片很多人第一反应是“要不要自己训一个神经网络”。实际上在python识别花卉种类并自动整理分类这个需求里识别本身早就被迁移学习解决了真正耗时间的反而是“整理”这两个字——建目录、判置信度、处理漏判、把文件挪到对的位置。整条链路里最容易被低估的是数据集怎么组织、阈值怎么定、归档脚本怎么写。这篇文章按“数据准备 → 微调模型 → 批量推理 → 自动归档 → 排坑”的顺序给出一条能直接复现的路径。适合手里有几百到几万张照片、想按品种自动归类的个人和团队也适合刚接触视觉识别的开发者拿它当第一个能落地的练手项目。2. 选型与数据准备用迁移学习而不是自己训网络2.1 为什么选迁移学习视觉识别里的“预训练 微调”路线花卉种类识别本质是图像分类问题输入一张图片输出一个类别标签。很多人一上来就想从零搭一个卷积神经网络但按个人经验除非你有几百万张标注好的花卉图否则这条路基本走不通。常见做法是“预训练 微调”拿一个在 ImageNet 上训好的模型ResNet、EfficientNet、MobileNet 都行把它的最后一层分类头换掉用自己的花卉数据集做几轮微调就可以了。提示PyTorch 的 torchvision 里直接集成了这些预训练模型下载权重后就能用不需要自己设计网络结构。这么做的好处有三点第一预训练模型已经学会了边缘、纹理、形状这些通用视觉特征花卉数据量小也能训得动第二训练时间从几天缩短到几十分钟普通笔记本电脑的 CPU 也能完成推理只是训练时最好有 GPU第三模型代码量非常少核心逻辑只有“加载权重 替换分类头 训练”三件事。为什么不建议直接用现成的图像分类 API因为花卉类别往往很细比如“粉龙”和“红龙”都是月季通用 API 大概率只认到“玫瑰”这一级。自己微调的优势就是可以定义自己的类别列表想分到品种级别就分到品种级别。2.2 数据集结构训练集/验证集/测试集怎么组织数据集组织方式直接影响后面所有代码的复杂度。这里采用 PyTorch 标准的 ImageFolder 目录结构根目录下每个类别一个文件夹文件夹名就是类别名。假设现在要识别“玫瑰、月季、菊花、向日葵”四类目录结构应该是这样的flower_data/ ├── train/ │ ├── rose/ │ │ ├── rose_001.jpg │ │ ├── rose_002.jpg │ │ └── ... │ ├── chrysanthemum/ │ ├── sunflower/ │ └── carnation/ ├── val/ │ ├── rose/ │ ├── chrysanthemum/ │ ├── sunflower/ │ └── carnation/ └── test/ ├── rose/ ├── chrysanthemum/ ├── sunflower/ └── carnation/我一般会按“训练集:验证集:测试集 7:2:1”的比例来分。训练集用来更新模型参数验证集用来调超参数和判断是否过拟合测试集最后测一次准确率就再也不碰了。如果原始图片全堆在一个文件夹里可以用下面这个脚本自动按比例划分。注意先执行 pip install scikit-learn 安装依赖。import os import shutil import random from sklearn.model_selection import train_test_split source_root raw_photos # 原始图片根目录 target_root flower_data # 划分后的根目录 train_ratio 0.7 val_ratio 0.2 # test_ratio 0.1 剩余部分 categories [d for d in os.listdir(source_root) if os.path.isdir(os.path.join(source_root, d))] for cat in categories: cat_path os.path.join(source_root, cat) images [f for f in os.listdir(cat_path) if f.lower().endswith((.jpg, .jpeg, .png))] train_files, temp_files train_test_split( images, train_sizetrain_ratio, random_state42) val_files, test_files train_test_split( temp_files, test_size0.33, random_state42) # 0.33 * 0.3 0.1正好剩余 10% 做测试集 for subset, file_list in [(train, train_files), (val, val_files), (test, test_files)]: dest_dir os.path.join(target_root, subset, cat) os.makedirs(dest_dir, exist_okTrue) for fname in file_list: src os.path.join(cat_path, fname) dst os.path.join(dest_dir, fname) shutil.copy2(src, dst) print(划分完成目标目录, target_root)train_test_split 的 random_state 参数要固定这样每次运行划分结果一致方便复现。这里用的是 copy2 而不是 move是保留原图作为备份磁盘吃紧的话可以改成 move。划分完成后最好肉眼抽查一下每个类别图片有没有放错文件夹这一步很关键标错数据对模型的影响比少几张图大得多。2.3 统一输入尺寸与标签编码神经网络不支持直接读取不同尺寸的图片需要统一缩放。torchvision 的 transforms 模块里封装好了这些操作不需要手写图像处理逻辑。这里定义的训练集预处理做了四件事随机裁剪、水平翻转、缩放、标准化。前两项是数据增强用来防止过拟合标准化则是把像素值从 [0, 255] 映射到以 0 为中心的范围这是预训练模型的输入要求不能省略。from torchvision import transforms # 训练集带数据增强 train_transform transforms.Compose([ transforms.RandomResizedCrop(224), # 随机裁剪后缩放到 224x224 transforms.RandomHorizontalFlip(), # 随机水平翻转 transforms.ToTensor(), # 转为 Tensor值域 [0, 1] transforms.Normalize( # 标准化ImageNet 的均值方差 mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ]) # 验证集/测试集不做增强只缩放和标准化 val_transform transforms.Compose([ transforms.Resize(256), # 先缩放到 256 transforms.CenterCrop(224), # 再中心裁剪到 224 transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ])图片尺寸为什么选 224因为 torchvision 里大部分预训练模型都默认接受 224x224 的输入ResNet、MobileNet 都是这个规格。这里的 mean 和 std 是 ImageNet 数据集的统计值所有预训练模型都沿用这一套不需要自己重新算。验证集不做随机裁剪和翻转是因为验证集的作用是模拟真实场景的准确率如果用随机增强每次验证的输入图都不一样结果不可比。中心裁剪和缩放的组合是为了保证目标完整进入画面。3. 用 PyTorch 微调花卉分类模型3.1 加载预训练模型的两种方式torchvision 里加载预训练模型的接口非常统一核心就是 weights 参数。建议直接用 weightsmodels.ResNet18_Weights.DEFAULT 这种写法它会自动下载适配的权重文件。import torch import torch.nn as nn import torchvision.models as models # 方式一加载 ResNet18替换分类头 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) num_features model.fc.in_features # 原分类头的输入维度ResNet18 是 512 model.fc nn.Linear(num_features, 4) # 替换成自己的 4 类分类头 # 方式二如果不想下载权重随机初始化不推荐效果差很多 # model models.resnet18(weightsNone)替换分类头是最关键的一步。预训练模型最后是一个 1000 类的全连接层对应 ImageNet 的一千个类别我们要把它换成自己数据集的类别数。model.fc.in_features 会自动读取原模型分类头的输入维度这样即使换了别的网络比如 EfficientNet代码也不用改。替换前原模型的所有参数都已经在 ImageNet 上训练收敛替换后只有最后这一层的权重是随机初始化的。这样设计的好处是前面几层学到的通用视觉特征可以直接复用只需要针对性学习“玫瑰和月季的区别”这种细粒度特征。3.2 数据加载器与训练参数设置数据加载这里有一个容易被忽略的细节ImageFolder 会自动按文件夹名生成类别索引但索引顺序是按照文件夹名字母顺序排的不是人工定义的顺序。后面推理归档时要拿这个映射关系来把索引转回类别名。from torchvision import datasets from torch.utils.data import DataLoader train_dataset datasets.ImageFolder(flower_data/train, transformtrain_transform) val_dataset datasets.ImageFolder(flower_data/val, transformval_transform) # 查看类别到索引的映射 print(train_dataset.class_to_idx) # 输出示例{carnation: 0, chrysanthemum: 1, rose: 2, sunflower: 3} train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)batch_size 设为 32 是折中方案。显存不够就降到 16 或 8太大容易 OOM训练集小的话也不要太大否则一个 epoch 没几步就过完了模型参数更新次数不够。shuffleTrue 只用于训练集验证集必须保持顺序因为后续可能要根据索引定位具体图片。num_workers 是数据加载的并行进程数Windows 下如果报错就把它改成 0Linux 下可以调到 4 甚至 8。3.3 训练循环与早停策略训练循环本身没什么神秘就是一个“前向传播 → 计算损失 → 反向传播 → 更新参数”的循环。这里用交叉熵损失函数因为它是多分类问题的标准选择。优化器选了 Adam因为它对学习率的敏感度比 SGD 低新手更容易一次跑通。import torch.optim as optim criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) best_acc 0.0 num_epochs 20 for epoch in range(num_epochs): # -------- 训练阶段 -------- model.train() running_loss 0.0 for inputs, labels in train_loader: optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.size(0) epoch_loss running_loss / len(train_dataset) # -------- 验证阶段 -------- model.eval() correct 0 total 0 with torch.no_grad(): for inputs, labels in val_loader: outputs model(inputs) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fEpoch {epoch1}/{num_epochs} | Loss: {epoch_loss:.4f} | Val Acc: {val_acc:.4f}) # 保存验证集准确率最高的模型 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), flower_model_best.pth) print(f - 保存最佳模型准确率 {val_acc:.4f})model.train() 和 model.eval() 一定要成对出现。Dropout 和 BatchNorm 在训练和推理时行为不同eval 模式下如果忘了切换推理结果会不稳定这是新手最容易忽略的坑。保存模型时只存 state_dict() 而不是整个 model一是文件更小二是加载时不受 PyTorch 版本变化影响。早停的简单实现就是上面代码里的“保存验证集最高准确率”训练结束后取保存的那份权重而不是最后一个 epoch 的权重。因为最后一个 epoch 往往已经过拟合验证集准确率反而会下降。3.4 没有 GPU 时的降级方案如果是纯 CPU 训练数据量几千张、模型用 ResNet18一个 epoch 可能要十几分钟20 轮下来就是好几个小时。两个解决办法一是换轻量模型把 resnet18 换成 mobilenet_v3_small 或 shufflenet_v2推理和训练都快不少准确率也就低一两个点。二是减小输入尺寸把 224 改成 160虽然要改预处理里的所有参数但速度提升很明显。还有一个更实用的方案把图片数量控制一下。每个类挑 100 张训练四类也就 400 张CPU 训练一轮大概几分钟足够验证整个流程能不能跑通。等流程没问题了再上全量数据。4. 批量识别与自动归档按类别建文件夹并移动图片4.1 单张图片预测与置信度阈值训练完成后模型拿到手。归档的第一步是写一个单张图片预测函数输入图片路径输出预测类别和置信度。置信度是 softmax 后所有类别概率的最大值代表模型对这个预测有多确定。import torch from PIL import Image def predict_single(model, image_path, class_names, device): 预测单张图片返回 (类别名, 置信度) image Image.open(image_path).convert(RGB) tensor val_transform(image).unsqueeze(0) # 加 batch 维度 tensor tensor.to(device) model.eval() with torch.no_grad(): outputs model(tensor) probabilities torch.softmax(outputs, dim1) confidence, pred_idx torch.max(probabilities, 1) class_name class_names[pred_idx.item()] confidence confidence.item() return class_name, confidenceconvert(RGB) 这行不能省。有些照片是 RGBA 格式或者灰度图直接输入模型会报维度错误。flatten 成一维再 softmax 也行但 torch.softmax 直接传 dim1 更简洁。torch.no_grad() 块内不会计算梯度推理速度和内存占用都会好很多。class_names 从哪来就是前面提到的 train_dataset.class_to_idx它是 {类别名: 索引} 的形式需要反转成 {索引: 类别名} 的列表。idx_to_class {v: k for k, v in train_dataset.class_to_idx.items()} class_names [idx_to_class[i] for i in range(len(idx_to_class))] print(class_names) # 例如 [carnation, chrysanthemum, rose, sunflower]这里要注意顺序列表下标就是索引所以一定要按索引从小到大排不能按字典的遍历顺序。4.2 批量归档目录结构设计归档的目录结构直接决定了后续使用体验。推荐下面这个设计目标根目录下按类别建子目录另外单独建一个 unclassified 目录放低置信度的图片还有一个 rejected 目录放压根识别不了的图。宁可多留两个目录也不要敢让低置信度的图混进正式的类别目录里。sorted_flowers/ ├── rose/ ├── chrysanthemum/ ├── sunflower/ ├── carnation/ ├── unclassified/ # 置信度 0.6需要人工确认 └── rejected/ # 置信度 0.3大概率不是花这个结构的好处是整理结果一眼就能看出哪些图是干净的、哪些需要人工处理。后期如果再想按月份、地点二次整理也能在类别目录里继续分。4.3 批量处理脚本移动而非复制批量归档逻辑很简单遍历待整理文件夹里的所有图片 → 逐张预测 → 按预测结果移动到对应子目录。这里用 shutil.move 而不是 copy2是为了避免原图和归档副本重复占用磁盘空间。import os import shutil from tqdm import tqdm def organize_images(model, source_dir, target_root, class_names, device, high_threshold0.6, low_threshold0.3): 批量整理图片到分类目录 model.eval() # 创建所有必需的目录 os.makedirs(target_root, exist_okTrue) for name in class_names: os.makedirs(os.path.join(target_root, name), exist_okTrue) os.makedirs(os.path.join(target_root, unclassified), exist_okTrue) os.makedirs(os.path.join(target_root, rejected), exist_okTrue) image_exts (.jpg, .jpeg, .png, .bmp, .webp) image_files [f for f in os.listdir(source_dir) if f.lower().endswith(image_exts)] moved_count 0 for fname in tqdm(image_files, desc整理中): src_path os.path.join(source_dir, fname) try: class_name, confidence predict_single( model, src_path, class_names, device) if confidence high_threshold: dest_dir os.path.join(target_root, class_name) elif confidence low_threshold: dest_dir os.path.join(target_root, unclassified) else: dest_dir os.path.join(target_root, rejected) shutil.move(src_path, os.path.join(dest_dir, fname)) moved_count 1 except Exception as e: print(f处理失败: {fname}错误: {e}) print(f完成共移动 {moved_count} 张图片到 {target_root})tqdm 需要 pip install tqdm 安装它会显示一个进度条批量处理几千张图时能直观看到跑了多少不然干等很煎熬。异常处理这段很重要有些图片虽然扩展名是 .jpg但文件本身损坏或者格式伪造Image.open 会抛异常不 catch 的话整个脚本会中断。image_exts 里加了 webp因为很多从网上下载的图片是 webp 格式只认 jpg/png 会漏掉一批。4.4 多级归档先按年份再按品种如果照片本身就带拍摄时间可以考虑两级归档先按年份分目录再在年份目录下按品种分。这样整理结果更清晰特别是照片跨度几年的场景。做法是在归档循环里加一层时间解析从 EXIF 信息里读拍摄日期解析失败就放到 unsorted/年份未知 目录。from PIL import Image from PIL.ExifTags import TAGS def get_exif_year(image_path): 从 EXIF 读取拍摄年份失败返回 None try: img Image.open(image_path) exif img._getexif() if exif is None: return None for tag_id, value in exif.items(): tag_name TAGS.get(tag_id, tag_id) if tag_name DateTimeOriginal: return value[:4] # 取年份 except Exception: pass return None有了年份后目标目录就变成 target_root/年份/类别/。这个函数要放在归档循环里调用每张图额外做一次 EXIF 读取速度会慢一些但对几千张图来说影响不大值得换来的整理效果提升。5. 常见问题与排查训练和归档路上的六个坑5.1 训练 loss 不下降或验证准确率忽高忽低现象训练了好几轮loss 一直居高不下或者验证集准确率像坐过山车一样大幅波动。原因分两类。loss 不降最常见的是学习率太大或太小Adam 默认 0.001 一般没问题但有的人会随手设成 0.1直接导致优化过程震荡不收敛准确率波动大往往是因为验证集图片太少每个类只有几张图随机性太大。解决先把学习率统一改成 0.001看两轮 loss 的变化趋势。如果还在震荡就把 batch_size 调大一些梯度更新会更平滑。验证集准确率波动的话把 val_ratio 从 0.2 提到 0.3不要省这几张图。再不行检查一下自己的数据集是不是类别间数量差距特别大比如玫瑰 800 张、菊花只有 20 张模型会倾向把所有图都判成玫瑰。5.2 模型把所有图片都预测成同一个类别现象训练完成后测试发现无论输入什么图输出几乎都是同一个类别。这种情况往往不是模型没训好而是训练数据本身就不平衡。原因假设玫瑰有 1000 张、向日葵只有 30 张模型发现“全猜玫瑰”就能拿到 97% 的训练准确率它根本没必要去学分辨不同类别的特征。解决最简单的办法是收集更多少样本类别的图片。如果实在收集不到可以给每类图片设定权重让模型在计算损失时对数量少的类别给予更大惩罚也就是加权交叉熵。另外也可以用更猛的数据增强把少样本类别的图片多做几次随机旋转、色彩抖动让模型看到更多变体。个人经验是分类任务里类别不平衡是最容易踩的暗坑数据集刚开始建设时就要想好每类图片的保底数量建议每类不少于 50 张。5.3 单张图片推理正常batch 推理却报错现象用 predict_single 单张测试没问题但批量处理时程序中途崩溃报错信息是 tensor 维度不匹配或数据类型错误。原因批量处理时不同图片尺寸不一致或者其中有损坏的图片DataLoader 或者手动拼接 tensor 时会因为维度不一致而抛出异常。还有一种情况是某些图片是灰度图只有一个通道而模型要求三通道输入。解决在 predict_single 里已经做了 convert(RGB)灰度图的问题能兜住。批量阶段的处理顺序应该是先把图片读出来统一转成 RGB → 做 transform → 再进模型。如果用的是自定义 DataLoader把 collate_fn 写严格一点凡是 PIL 打开失败的直接跳过记录到日志里不要让它进 batch。批量处理类的程序宁可慢一点、逐张处理也不要图快把整个流程搞挂。5.4 移动归档后原图丢失没有后悔药现象批量整理跑完后发现模型把一批图全放错了目录想找回原始照片但原目录已经被清空。原因shutil.move 是直接移动文件不带任何撤销机制。模型对某些容易混淆的类别比如珊瑚藤和炮仗花可能连续误判如果没有备份整理完就等于把原来的文件结构破坏了。解决归档脚本里默认用 copy2 而不是 move全部整理完、人工抽检无误后再手动删除原文件。另外移动前给每个文件路径打一份 CSV 日志记录每张图“从哪来、预测成什么、置信度多少、移动到了哪”。出了错通过这份日志就能精确回溯批量跑的时候一定要留这个后路。5.5 CPU 推理太慢一千张图要跑很久现象全部流程都跑通了但每张图推理耗时接近 1 秒一万张图就要小半天。原因这是 CPU 上跑深度学习模型的常态MobileNet 或 ResNet18 在 CPU 上单张推理的耗时基本就是几百毫秒到 1 秒。图片过大也会拖慢速度如果原图是 4000x3000 的分辨率即使模型输入只要 224x224读图和预处理的耗时也会翻倍。解决第一推理前先把图片用 PIL 的 thumbnail 缩到 512 以内再做 transform大图缩小后再进模型速度提升明显。第二换更轻量的模型比如把 ResNet18 换成 MobileNetV3-Small精度损失不多但速度快一倍以上。第三打开 PyTorch 的推理优化开关在推理前加一行 torch.set_num_threads(4)用多线程跑瓶颈不在单张图片的计算量而在批量读取和预处理用多线程能压榨出不少性能。5.6 整理结果里出现 unclassified 目录爆满现象高置信度阈值 0.6 设得太高大量正常图片被丢进 unclassified人工审核工作量暴增。原因这是阈值设置的经典难题。0.6 是个人经验值但它并不适合所有模型和数据场景。模型对某些“长得很像”的类别如不同品种的月季天然就会给出 0.5 左右的低置信度这是正常的。解决调阈值前先用一小批人工标注过的图片做一次批量预测把置信度分布打印出来看落在各区间有多少张。然后根据可接受的人工审核量来定阈值如果时间充裕、追求整理准确率阈值就调高一点如果想减少人工介入、只挑出最不确定的图阈值就调低到 0.4 左右。这一步调参没有万能数值只有适合自己的方案。调阈值时不仅看准确率还要看 unclassified 目录里有多少张本不该出现的图。整个过程走下来最大的一个体会是模型训练只是这个项目的前半场真正决定项目体验的是归档逻辑和阈值设计。识别错了靠阈值和人工检查还能补救归档这一步要是搞砸了几千张图全部打乱重来那是真的折磨。所以我现在的习惯是每次跑批量处理前都会先拿 50 张图做预演确认模型预测和目录结构都符合预期再放心跑全量。希望这一套流程能帮你在自己的照片库上少走些弯路一次把“识别 归档”这条链路稳稳跑通。本文还有配套的精品资源点击获取
返回列表