Python花卉识别与自动归档:用迁移学习微调PyTorch模型
2026/9/24 20:45:46 网站建设 项目流程

简介:一份面向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_size=train_ratio, random_state=42) val_files, test_files = train_test_split( temp_files, test_size=0.33, random_state=42) # 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_ok=True) 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 参数。建议直接用 weights=models.ResNet18_Weights.DEFAULT 这种写法,它会自动下载适配的权重文件。

import torch import torch.nn as nn import torchvision.models as models # 方式一:加载 ResNet18,替换分类头 model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) num_features = model.fc.in_features # 原分类头的输入维度,ResNet18 是 512 model.fc = nn.Linear(num_features, 4) # 替换成自己的 4 类分类头 # 方式二:如果不想下载权重,随机初始化(不推荐,效果差很多) # model = models.resnet18(weights=None)

替换分类头是最关键的一步。预训练模型最后是一个 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", transform=train_transform) val_dataset = datasets.ImageFolder("flower_data/val", transform=val_transform) # 查看类别到索引的映射 print(train_dataset.class_to_idx) # 输出示例:{'carnation': 0, 'chrysanthemum': 1, 'rose': 2, 'sunflower': 3} train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)

batch_size 设为 32 是折中方案。显存不够就降到 16 或 8,太大容易 OOM;训练集小的话也不要太大,否则一个 epoch 没几步就过完了,模型参数更新次数不够。shuffle=True 只用于训练集,验证集必须保持顺序,因为后续可能要根据索引定位具体图片。num_workers 是数据加载的并行进程数,Windows 下如果报错就把它改成 0,Linux 下可以调到 4 甚至 8。

3.3 训练循环与早停策略

训练循环本身没什么神秘,就是一个“前向传播 → 计算损失 → 反向传播 → 更新参数”的循环。这里用交叉熵损失函数,因为它是多分类问题的标准选择。优化器选了 Adam,因为它对学习率的敏感度比 SGD 低,新手更容易一次跑通。

import torch.optim as optim criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.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(f"Epoch {epoch+1}/{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, dim=1) confidence, pred_idx = torch.max(probabilities, 1) class_name = class_names[pred_idx.item()] confidence = confidence.item() return class_name, confidence

convert("RGB") 这行不能省。有些照片是 RGBA 格式或者灰度图,直接输入模型会报维度错误。flatten 成一维再 softmax 也行,但 torch.softmax 直接传 dim=1 更简洁。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_threshold=0.6, low_threshold=0.3): """批量整理图片到分类目录""" model.eval() # 创建所有必需的目录 os.makedirs(target_root, exist_ok=True) for name in class_names: os.makedirs(os.path.join(target_root, name), exist_ok=True) os.makedirs(os.path.join(target_root, "unclassified"), exist_ok=True) os.makedirs(os.path.join(target_root, "rejected"), exist_ok=True) 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 张图做预演,确认模型预测和目录结构都符合预期,再放心跑全量。希望这一套流程能帮你在自己的照片库上少走些弯路,一次把“识别 + 归档”这条链路稳稳跑通。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询