简介:这份资源面向深度学习入门者与计算机视觉方向的在校学生,提供一套可直接上手的花卉图像五分类实战资料,帮助解决从数据集准备到模型训练全流程的落地问题。压缩包共约2000个文件、596.75MB,其中1972张jpg花卉图片构成五类样本主体,14个py脚本承载TensorFlow模型搭建与训练逻辑,另有txt说明、xml标注、md笔记与pdf教程辅助理解数据组织方式。资源配套作者录制的B站讲解视频,已有14841人学习下载,热度较高。读者可借助脚本与教程完成数据读取、模型构建、训练评估的完整闭环,并通过标注文件与说明文档理解类别划分与目录结构,适合作为课程设计、入门练手或迁移学习实验的基础素材。
1. 花卉识别数据集5类:从拿到压缩包到跑出第一张预测图
你手上有一个叫「花卉识别数据集5类-提供代码和教程.zip」的压缩包,解压之后大概率是五个文件夹,每个文件夹里塞满同一种花的照片,外加一份训练脚本和一份说明文档。这件事的核心价值不在于数据集本身有多大,而在于它是一条完整的、可以端到端跑通的图像分类流水线——从读图、切分训练验证集、搭建卷积网络、训练、评估,到最后拿一张新照片做推理。适合谁?适合刚学完 Python 基础、想找一个真实可复现的小项目练手的人,也适合需要快速验证某个 backbone 或数据增强策略是否有效的工程师。五类花卉意味着类别数少、数据量可控,单卡甚至 CPU 都能在可接受时间内跑完一轮,这是它最大的优势。但「能跑」和「跑得好」之间隔着数据清洗、类别不均衡、过拟合这几道坎,后面几章会把这些坑一个个拆开讲。
2. 五类花卉数据集的目录结构与加载方式
2.1 先看清压缩包里的目录长什么样
拿到压缩包后不要急着写模型代码,先把目录结构摸清楚。常见的组织方式有两种:一种是flower_photos/下面直接放五个类别文件夹,每个文件夹名就是类别标签;另一种是train/和val/已经帮你切好,各自下面再分五个类别文件夹。这两种结构决定了你后面用ImageFolder还是自己写Dataset。
先执行一条命令看结构:
# 查看解压后的目录树,只看两层,避免输出太长 find ./flower_dataset -maxdepth 2 -type d | sort如果输出类似下面这样,说明是第一种结构:
./flower_dataset ./flower_dataset/daisy ./flower_dataset/dandelion ./flower_dataset/rose ./flower_dataset/sunflower ./flower_dataset/tulip每个类别文件夹里的图片数量往往不一样,这是第一个需要记录的信息。用一条命令统计:
# 统计每个类别文件夹下的图片数量 for dir in ./flower_dataset/*/; do echo -n "$dir: " ls "$dir" | wc -l done逻辑说明:for dir in ./flower_dataset/*/遍历所有子目录,ls | wc -l统计文件数。参数上注意,如果图片格式混杂(jpg、png、jpeg),这条命令统计的是所有文件,包括可能存在的隐藏文件或说明文件。更严谨的做法是只统计图片后缀:
# 只统计常见图片格式的数量 for dir in ./flower_dataset/*/; do count=$(find "$dir" -maxdepth 1 -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) | wc -l) echo "$dir: $count" done这一步的意义在于:如果五个类别数量差距超过 3 倍,后面训练时就要考虑类别权重或者重采样,否则模型会偏向样本多的类。我一般会把这个统计结果记下来,作为后面调整WeightedRandomSampler的依据。
2.2 用 ImageFolder 还是自定义 Dataset
如果目录结构是「一个大文件夹下面五个类别文件夹」,PyTorch 的torchvision.datasets.ImageFolder可以直接用,它会自动把文件夹名映射成 0 到 4 的标签。这是最省事的做法,代码量最少。
import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader, random_split # 定义基础变换:统一尺寸、转张量、归一化 data_transform = transforms.Compose([ transforms.Resize((224, 224)), # 统一缩放到 224x224 transforms.ToTensor(), # 转成 [0,1] 的张量 transforms.Normalize( # 按 ImageNet 统计量归一化 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ]) # 加载整个数据集,ImageFolder 会自动按文件夹名排序生成标签 full_dataset = datasets.ImageFolder( root='./flower_dataset', transform=data_transform ) # 打印类别到索引的映射,确认顺序 print(full_dataset.class_to_idx) # 典型输出:{'daisy': 0, 'dandelion': 1, 'rose': 2, 'sunflower': 3, 'tulip': 4} # 按 8:2 切分训练集和验证集 train_size = int(0.8 * len(full_dataset)) val_size = len(full_dataset) - train_size train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size]) # 构建 DataLoader train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=2)逻辑说明:ImageFolder要求根目录下每个子文件夹是一个类别,子文件夹里直接放图片。class_to_idx的顺序是按文件夹名字母序排列的,这一点必须确认,因为后面推理时输出的索引要能对应回类别名。random_split是随机切分,如果数据集本身已经分好 train/val,就不要再用它,直接对两个ImageFolder分别加载即可。
参数说明:Resize((224, 224))是为了适配大多数预训练模型的标准输入,如果你打算从头训练一个小网络,可以改成 128 或 96 以加快速度。Normalize的均值和方差用的是 ImageNet 的统计量,这是迁移学习场景下的常规做法;如果完全从头训练,可以改成自己数据集的均值和方差,但影响通常不大。num_workers在 Windows 上如果报错就改成 0,这是血泪经验。
如果目录结构是已经切好的train/和val/,那就写两个ImageFolder:
train_dataset = datasets.ImageFolder(root='./flower_dataset/train', transform=train_transform) val_dataset = datasets.ImageFolder(root='./flower_dataset/val', transform=val_transform)注意训练集和验证集的变换要分开定义:训练集加随机翻转、随机裁剪等增强,验证集只做 Resize 和归一化。这是最容易被忽略的一点,很多人直接复用同一个 transform,导致验证结果波动很大。
2.3 数据增强到底加在哪一步
数据增强是提升小数据集泛化能力最便宜的手段。五类花卉数据集通常每类几百张,总量几千张,这个量级下不做增强很容易过拟合。常见的增强操作包括随机水平翻转、随机旋转、颜色抖动、随机裁剪。
# 训练集专用的增强变换 train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), # 随机裁剪并缩放 transforms.RandomHorizontalFlip(p=0.5), # 一半概率水平翻转 transforms.RandomRotation(15), # 随机旋转 ±15 度 transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), # 颜色抖动 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 验证集只做确定性变换 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]) ])逻辑说明:RandomResizedCrop的scale=(0.7, 1.0)表示随机裁剪出原图 70% 到 100% 的区域再缩放到 224,这个参数不要设得太激进,否则花蕊等关键特征可能被裁掉。RandomRotation(15)的 15 度是经验值,花卉图像旋转太多会引入不自然的背景。ColorJitter的三个参数都设 0.2 是保守做法,再大可能让颜色成为噪声而不是特征。
参数说明:增强的强度需要根据验证集准确率来调。如果训练准确率远高于验证准确率,说明过拟合严重,可以加大增强;如果两者都低,说明欠拟合,应该先检查学习率和模型容量,而不是继续加增强。
3. 用迁移学习在五类花卉上跑通训练与评估
3.1 选 ResNet18 还是自己搭一个小卷积网络
五类花卉、几千张图片,这个规模下最稳妥的方案是迁移学习。ResNet18 在 ImageNet 上预训练过的权重,拿来微调五分类,通常十几轮就能到 90% 以上的验证准确率。自己从头搭一个三四层的卷积网络也能跑,但需要更多轮次和更仔细的超参调整,最终精度往往还不如迁移学习。
import torch.nn as nn from torchvision import models # 加载预训练 ResNet18 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 替换最后的全连接层,输出改为 5 类 num_features = model.fc.in_features model.fc = nn.Linear(num_features, 5) # 冻结前面的卷积层,只训练最后的分类层(第一阶段) for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True # 把模型放到 GPU(如果有) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device)逻辑说明:models.resnet18(weights=...)加载预训练权重,这一步需要联网下载,如果网络不通可以提前把权重文件放到~/.cache/torch/hub/checkpoints/目录下。替换fc层是因为原模型输出 1000 类,我们要改成 5 类。冻结卷积层是迁移学习的第一阶段策略,先让随机初始化的分类层收敛,避免一开始就把预训练好的特征破坏掉。
参数说明:ResNet18_Weights.IMAGENET1K_V1是 torchvision 新版的权重枚举写法,旧版写pretrained=True也可以,但会有弃用警告。如果显存不够,可以把resnet18换成resnet18但减小 batch size,或者换成更小的mobilenet_v3_small。
3.2 两阶段训练:先冻结再解冻
第一阶段只训练分类层,通常 5 轮左右验证准确率就趋于稳定。然后解冻所有层,用更小的学习率做微调,这是提升精度的关键一步。
import torch.optim as optim # 第一阶段:只训练 fc 层,学习率可以大一点 optimizer_stage1 = optim.Adam(model.fc.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() # 训练循环(第一阶段,5 轮) for epoch in range(5): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer_stage1.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer_stage1.step() running_loss += loss.item() print(f"Stage1 Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}") # 第二阶段:解冻所有层,用更小的学习率微调 for param in model.parameters(): param.requires_grad = True optimizer_stage2 = optim.Adam(model.parameters(), lr=1e-4) # 学习率降一个量级 for epoch in range(10): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer_stage2.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer_stage2.step() running_loss += loss.item() print(f"Stage2 Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}")逻辑说明:第一阶段学习率 1e-3 是因为分类层是随机初始化的,需要快速下降。第二阶段学习率降到 1e-4,是因为预训练权重已经很好,大学习率会破坏它们。这个两阶段策略比直接端到端微调更稳,尤其在小数据集上。
参数说明:optim.Adam的lr是最关键的参数。如果第二阶段 loss 震荡不降,把lr再降到 5e-5。如果 loss 下降太慢,可以试试optim.SGD加动量,但 Adam 在大多数情况下够用。batch_size设 32 是显存和梯度的折中,显存够可以加到 64。
3.3 评估不能只看准确率
五类花卉如果类别不均衡,准确率会被多数类主导。必须同时看混淆矩阵和每类的精确率、召回率。
from sklearn.metrics import classification_report, confusion_matrix import numpy as np model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) # 打印分类报告 print(classification_report(all_labels, all_preds, target_names=full_dataset.classes)) # 打印混淆矩阵 print(confusion_matrix(all_labels, all_preds))逻辑说明:model.eval()会关闭 dropout 和 batch norm 的训练模式,这是评估时必须做的。torch.no_grad()关闭梯度计算,节省显存。classification_report输出每类的 precision、recall、f1-score,能直接看出哪个类被混淆了。
参数说明:target_names=full_dataset.classes把数字索引映射回类别名,输出更可读。混淆矩阵的对角线是正确预测数,非对角线是错误预测,如果某两类之间互相混淆严重,说明它们的特征在模型看来太接近,需要考虑加数据或换更强的 backbone。
4. 推理部署:拿一张新照片验证模型到底学到了什么
4.1 单张图片推理的完整代码
训练完之后,最直接的验证方式是拿一张模型没见过的花卉照片跑一遍推理。
from PIL import Image def predict_image(image_path, model, transform, class_names, device): """对单张图片进行预测,返回类别名和置信度""" model.eval() image = Image.open(image_path).convert('RGB') # 确保三通道 input_tensor = transform(image).unsqueeze(0).to(device) # 增加 batch 维度 with torch.no_grad(): outputs = model(input_tensor) probabilities = torch.softmax(outputs, dim=1) # 转成概率 confidence, predicted = torch.max(probabilities, 1) class_name = class_names[predicted.item()] conf = confidence.item() return class_name, conf # 使用示例 class_names = full_dataset.classes # ['daisy', 'dandelion', 'rose', 'sunflower', 'tulip'] name, conf = predict_image('./test_flower.jpg', model, val_transform, class_names, device) print(f"预测类别: {name}, 置信度: {conf:.4f}")逻辑说明:Image.open().convert('RGB')是为了处理灰度图或带 alpha 通道的 PNG,统一转成三通道。unsqueeze(0)在第一个维度增加 batch 大小,因为模型期望输入是[batch, channel, height, width]。torch.softmax把 logits 转成概率分布,torch.max同时返回最大值和对应索引。
参数说明:推理时必须用验证集的 transform,不能用训练集的增强 transform,否则结果会不稳定。confidence低于 0.6 时建议人工复核,这说明模型对这个样本不太确定。
4.2 批量推理和结果导出
如果有一批测试图片,逐张调用效率太低,可以写一个批量推理脚本,把结果导出成 CSV。
import os import pandas as pd def batch_predict(image_dir, model, transform, class_names, device): """对目录下所有图片批量预测,返回 DataFrame""" results = [] model.eval() for filename in os.listdir(image_dir): if not filename.lower().endswith(('.jpg', '.jpeg', '.png')): continue image_path = os.path.join(image_dir, filename) try: name, conf = predict_image(image_path, model, transform, class_names, device) results.append({'filename': filename, 'predicted': name, 'confidence': round(conf, 4)}) except Exception as e: results.append({'filename': filename, 'predicted': 'ERROR', 'confidence': 0.0}) print(f"处理 {filename} 失败: {e}") return pd.DataFrame(results) # 批量预测并保存 df = batch_predict('./test_images', model, val_transform, class_names, device) df.to_csv('./predictions.csv', index=False, encoding='utf-8-sig') print(df.head())逻辑说明:os.listdir遍历目录,用后缀过滤图片文件。try-except捕获单张图片读取失败的情况,避免整个批次中断。encoding='utf-8-sig'是为了 Excel 打开 CSV 时不乱码,这是实际交付时经常被忽略的细节。
参数说明:round(conf, 4)保留四位小数,够用且不冗长。如果图片量很大,可以把predict_image改成 batch 推理,一次处理多张,但要注意显存限制。
5. 五类花卉识别避坑记录:从数据到部署的五个翻车点
5.1 类别文件夹里混进了非图片文件
现象:ImageFolder加载时报错FileNotFoundError或者某个类别数量异常多。原因:压缩包里可能带了Thumbs.db、.DS_Store或者说明文档。解决:加载前先清理,或者在ImageFolder的is_valid_file参数里过滤。
# 清理非图片文件的命令(Linux/macOS) find ./flower_dataset -type f ! \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" \) -delete5.2 验证集准确率远高于训练准确率
现象:训练时 loss 不降,但验证准确率很高。原因:验证集的 transform 用了训练集的增强,导致验证结果不可信;或者验证集太小,恰好都是简单样本。解决:确认验证集只用 Resize 和 Normalize,并且验证集比例不低于 15%。
5.3 显存溢出但 batch size 已经很小
现象:CUDA out of memory,但 batch size 已经降到 8。原因:图片分辨率太高,或者num_workers太多导致内存泄漏。解决:先把Resize降到 128 试试,再把num_workers设为 0 排除多进程问题。
5.4 推理时置信度全部接近 1.0 或全部接近 0.2
现象:所有预测的置信度要么极高要么极低。原因:模型过拟合,或者归一化参数和训练时不一致。解决:检查推理时的Normalize是否和验证集完全一致,并回看验证集的混淆矩阵,如果某类召回率为 0,说明模型没学到这个类。
5.5 保存的模型加载后预测结果全乱
现象:训练时准确率 90%,保存后重新加载预测全错。原因:保存的是state_dict但加载时模型结构不一致,或者忘了调用model.eval()。解决:保存时同时存class_to_idx,加载后先eval()再推理。
# 正确的保存和加载方式 torch.save({ 'model_state_dict': model.state_dict(), 'class_to_idx': full_dataset.class_to_idx }, 'flower_model.pth') # 加载 checkpoint = torch.load('flower_model.pth', map_location=device) model.load_state_dict(checkpoint['model_state_dict']) model.eval()6. 把五类花卉模型推到 95% 以上的三个微调技巧
第一个技巧是分层学习率。解冻之后不要所有层用同一个学习率,前面的卷积层学习率设小一点,后面的层设大一点。ResNet18 可以按layer1到layer4分组,学习率依次递增。
# 分层学习率:前面的层学习率小,后面的层学习率大 params = [ {'params': model.conv1.parameters(), 'lr': 1e-5}, {'params': model.layer1.parameters(), 'lr': 1e-5}, {'params': model.layer2.parameters(), 'lr': 5e-5}, {'params': model.layer3.parameters(), 'lr': 1e-4}, {'params': model.layer4.parameters(), 'lr': 5e-4}, {'params': model.fc.parameters(), 'lr': 1e-3}, ] optimizer = optim.Adam(params)第二个技巧是余弦退火学习率调度。固定学习率在后期容易在最优解附近震荡,余弦退火让学习率平滑下降,通常能再涨 1 到 2 个百分点。
from torch.optim.lr_scheduler import CosineAnnealingLR scheduler = CosineAnnealingLR(optimizer, T_max=20, eta_min=1e-6) # 在每个 epoch 结束后调用 for epoch in range(20): # ... 训练代码 ... scheduler.step()第三个技巧是测试时增强(TTA)。推理时对同一张图片做多次变换(原图、水平翻转、不同裁剪),把多次预测的概率平均,能显著降低单次预测的方差。
def predict_with_tta(image_path, model, class_names, device): """测试时增强:原图 + 水平翻转 + 中心裁剪""" model.eval() image = Image.open(image_path).convert('RGB') tta_transforms = [ transforms.Compose([transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]), transforms.Compose([transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=1.0), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]), transforms.Compose([transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])]), ] probs_list = [] with torch.no_grad(): for t in tta_transforms: tensor = t(image).unsqueeze(0).to(device) outputs = model(tensor) probs = torch.softmax(outputs, dim=1) probs_list.append(probs) avg_probs = torch.mean(torch.stack(probs_list), dim=0) confidence, predicted = torch.max(avg_probs, 1) return class_names[predicted.item()], confidence.item()逻辑说明:TTA 的核心思想是「同一个样本的多个视角应该得到一致的预测」。水平翻转和中心裁剪是最常用的两种视角,计算开销小,收益稳定。torch.stack把三次预测的概率堆叠成[3, 1, 5],torch.mean(dim=0)在第一个维度平均,得到[1, 5]的平均概率。
参数说明:TTA 的变换数量不要超过 5 个,否则推理时间线性增长但收益递减。CenterCrop(224)配合Resize(256)是标准做法,先放大再裁剪,保留更多上下文。
这三个技巧我一般按顺序上:先分层学习率,再余弦退火,最后 TTA。每加一个都跑一次验证集,确认有提升再保留。如果加了之后验证准确率反而降了,说明当前模型还没到需要这些技巧的阶段,先回去检查数据质量和增强策略。希望帮到你。
本文还有配套的精品资源,点击获取