ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

花卉图片集(01)PyTorch实战:从数据预处理到细粒度图像分类训练

2026/10/1 2:36:56 拓冰建站 浏览量
花卉图片集(01)PyTorch实战:从数据预处理到细粒度图像分类训练 简介这套资源面向花卉识别与图像分类实践场景将16种花卉、共32000张224×224彩色图片的数据集与基于PyTorch搭建的训练源码整合在一起适合正在学习深度学习图像分类、需要真实数据集进行模型训练与效果验证的开发者。压缩包内共110个文件以Python源码、txt说明文件为主并包含启动脚本、数据集文件及少量预编译模块整体大小584.72MB结构完整可直接开展实验。训练源码汇集了23种主流图片分类模型训练时可根据设备与任务需求灵活选择不同网络结构便于对比准确率与收敛速度对理解迁移学习、模型选型及调参方法很有帮助。数据集按批次发布本批涵盖一年蓬、蒲公英、油菜花、诸葛菜等16种花卉若需更多类别可另行下载其他批次。目前已有1165人学习下载适合作为花卉识别入门训练、课程设计或算法验证的实用资源。1. 花卉识别一个看似简单、实则最考验数据功底的 CV 入门项目花卉识别是计算机视觉里最典型的细粒度图像分类任务。和猫狗分类不同花卉之间的差异往往只在花瓣形状、颜色分布、叶片纹理这些局部细节上同一科属的不同品种在深度学习模型眼里可能只差几个像素。这个标题里的“花卉图片集01”点出了最核心的痛点数据不是拿来就能用的你需要自己动手把原始图片整理成训练集、验证集、测试集再配上标签文件模型才能开工。很多初学者在这个项目上翻车不是因为模型选得不好而是数据预处理和训练参数设置出了问题——比如标签和文件名对不上、类别不平衡、训练集和验证集有重复图片、图像尺寸没有统一。本文从数据集构建、训练源码解读、参数调优到推理部署走一遍完整流程让新手能跟着复现出可用的花卉分类模型让熟手能直接拿走一套可靠的数据处理和训练模板。2. 花卉图片集01的整理与标注决定模型上限的第一道工序2.1 数据集的目录结构设计训练、验证、测试三分离在动手写任何训练代码之前第一步是把原始图片集整理成标准的分类数据集目录。PyTorch 的torchvision.datasets.ImageFolder能直接识别按类别分文件夹的数据集这也是最省事、最不容易出错的方案。flowers_dataset/ ├── train/ │ ├── rose/ │ │ ├── rose_001.jpg │ │ ├── rose_002.jpg │ │ └── ... │ ├── daisy/ │ ├── sunflower/ │ └── ... ├── val/ │ ├── rose/ │ ├── daisy/ │ └── ... └── test/ ├── rose/ ├── daisy/ └── ...注意train、val、test 三个目录下的类别子文件夹名称必须完全一致否则ImageFolder生成标签时会出现类别索引错位。常见的做法是先把所有图片集中在all_images/下按类别建好子目录然后用脚本按比例随机分割。我一般用 7:2:1 划分 train:val:test这个比例在中小规模数据集上既能保证训练样本充足又能让验证集和测试集统计意义足够。2.2 批量重命名与格式统一写一个可复用的预处理脚本从标题“图片集01”推断这多半是整理过的第一批发图文件名可能是相机原始编号也可能是下载时的乱序编号。直接用原始文件名训练没有问题但统一重命名后更方便排查问题——尤其在标签错乱时能一眼看出某张图应该属于哪个类别。下面这个脚本可以一键完成重命名、格式转换和尺寸检查。import os import shutil from PIL import Image def normalize_images(src_root, dst_root, target_size(224, 224)): 将 src_root 下的图片统一重命名、转成 RGB、缩放到 target_size 后存入 dst_root 目录结构: src_root/类别名/图片文件 - dst_root/类别名/类别名_编号.jpg for cls_name in os.listdir(src_root): cls_src os.path.join(src_root, cls_name) if not os.path.isdir(cls_src): continue cls_dst os.path.join(dst_root, cls_name) os.makedirs(cls_dst, exist_okTrue) valid_ext {.jpg, .jpeg, .png, .bmp, .webp} img_files [f for f in os.listdir(cls_src) if os.path.splitext(f)[1].lower() in valid_ext] img_files.sort() # 保证编号顺序稳定 for idx, fname in enumerate(img_files): src_path os.path.join(cls_src, fname) try: with Image.open(src_path) as img: img img.convert(RGB) img img.resize(target_size, Image.BILINEAR) new_name f{cls_name}_{idx:04d}.jpg img.save(os.path.join(cls_dst, new_name), JPEG, quality95) except Exception as e: print(f[跳过] {src_path}: {e}) if __name__ __main__: normalize_images(raw_images, processed_images)这段代码里有三个关键设计。第一个是用img.convert(RGB)强制转成三通道这能滤掉灰度图、RGBA 图带透明通道的 PNG导致的训练报错“通道数不匹配”。第二个是统一缩放到 224×224这是 ResNet、MobileNet 这类分类网络的默认输入尺寸提前缩放可以避免训练时每个 batch 都要做 resize能省不少 CPU 时间。第三个是用try-except跳过损坏图片而不是直接中断整个脚本——实际数据集里总有几张截断下载或编码异常的图让脚本停下来等你手动处理最浪费时间。2.3 类别标签映射与样本量统计训练前必须核对的清单整理完图片后写一个统计脚本输出每类图片数量、总样本数、类别到索引的映射并检查是否有类别图片数量过少比如少于 50 张。这一步看似不起眼但能提前暴露“某类只有 3 张图”这种让训练直接崩掉的问题。import os from collections import Counter def inspect_dataset(root): 统计 root 下每个类别的图片数量打印类别索引映射 root 的结构: root/类别名/图片文件 class_names sorted([d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))]) class_to_idx {name: idx for idx, name in enumerate(class_names)} print(f共 {len(class_names)} 个类别) print(类别索引映射:, class_to_idx) counts Counter() for cls_name in class_names: cls_path os.path.join(root, cls_name) counts[cls_name] len([f for f in os.listdir(cls_path) if f.lower().endswith((.jpg, .jpeg, .png))]) for cls_name, cnt in counts.items(): flag -- 注意样本过少 if cnt 50 else print(f{cls_name}: {cnt} 张{flag}) inspect_dataset(processed_images/train)提示如果某个类别的样本量确实很少优先考虑数据增强翻转、裁剪、色彩抖动而不是直接删除该类别。3. 花卉识别训练源码从数据加载到模型训练的最小可运行框架3.1 数据加载与增强策略torchvision 的 transforms 怎么配置才不玄学训练源码的核心并不在模型定义部分而在DataLoader和transforms的配置上。数据增强直接决定模型能不能泛化到真实场景里的花卉照片——大晴天、阴天、手机拍的、单反拍的光照和背景差异远大于花卉本身的品种差异。我常用的增强策略如下。from torchvision import transforms # 训练集随机增强让模型见过更多“变体” train_transforms transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3, hue0.1), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 验证集/测试集只做确定性变换保证结果可复现 eval_transforms transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里有个关键区别训练集的RandomResizedCrop会随机裁剪不同区域再缩放到 224这等于免费生成了大量局部特写样本对花卉识别特别有用——因为很多品种的判别特征就在花蕊或花瓣边缘而验证集必须用固定的Resize CenterCrop否则每次评估结果都会因为随机裁剪而抖动你没法判断模型是真的变好了还是运气好。Normalize用的 mean 和 std 是 ImageNet 数据集的统计值这不是拍脑袋定的而是因为后面要加载在 ImageNet 上预训练过的模型权重输入分布必须和预训练时保持一致。如果你从头训练不加载预训练权重理论上可以自己算数据集的 mean 和 std但效果通常不会比 ImageNet 统计值更好。3.2 模型选型为什么说 ResNet18 是花卉识别性价比最高的起点花卉识别属于细粒度分类理论上需要能捕捉细微差异的模型但实际工程里数据量往往才是瓶颈。如果你的数据集只有几千张图直接上 ResNet50 或 EfficientNet-B4 大概率过拟合——训练集准确率 99%验证集只有 70%。我通常用 ResNet18 作为第一个基线模型理由有三条结构简单容易调试、参数量只有 1100 万左右不容易过拟合、在 ImageNet 上的预训练权重随处可得。import torch import torch.nn as nn from torchvision import models def create_model(num_classes, pretrainedTrue): 创建 ResNet18 分类模型替换最后一层全连接 num_classes: 花卉类别总数 pretrained: True 表示加载 ImageNet 预训练权重强烈推荐 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None) in_features model.fc.in_features # 替换全连接层原输出 1000 类改为花卉类别数 model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model model create_model(num_classes10) print(model)替换最后一层全连接时加了一个Dropout(0.3)这是一个容易被忽略但很实用的细节。迁移学习中预训练模型的前面几层已经学会了通用的边缘、纹理、形状特征我们只训练最后一层分类头但因为训练数据量小最后的线性层很容易过拟合在fc前加 Dropout 等于给分类头加了一层正则化。如果后续发现验证集准确率明显低于训练集可以把这个 dropout 概率从 0.3 调到 0.5或者加一层隐层但不要一开始就加大——会拖慢收敛速度。3.3 训练循环与关键超参学习率、batch size、 epoch 怎么配训练脚本是整个源码的主干。下面这段代码可以作为直接复用的模板它实现了标准的训练循环前向传播、计算损失、反向传播、更新权重、每个 epoch 结束做一次验证。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for inputs, labels in dataloader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * inputs.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 return epoch_loss, epoch_acc # 参数配置 device torch.device(cuda if torch.cuda.is_available() else cpu) batch_size 32 learning_rate 1e-4 num_epochs 30 num_classes 10 # 数据集加载假设已按第 2 节整理好目录结构 train_dataset ImageFolder(processed_images/train, transformtrain_transforms) val_dataset ImageFolder(processed_images/val, transformeval_transforms) train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse, num_workers4, pin_memoryTrue) # 模型、损失函数、优化器 model create_model(num_classesnum_classes).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lrlearning_rate) # 只训练最后一层前几层冻结前期收敛更快 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True for epoch in range(num_epochs): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) print(fEpoch {epoch1}/{num_epochs} | Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f})这段代码里最值得琢磨的是“冻结前几层只训练 fc 层”的策略。迁移学习的标准做法是先用小学习率1e-4 左右只训练新加的分类头几个 epoch等损失降下来后再解冻全部层、用更小的学习率1e-5微调整个网络。上面这段代码只实现了第一阶段因为对大多数中小型花卉数据集来说只训练分类头已经能到 90% 以上的准确率全量微调的收益有限但训练时间会翻好几倍。另外注意num_workers4的作用数据加载和增强是 CPU 操作如果设成 0GPU 每处理完一个 batch 都要干等 CPU 准备下一个 batch训练速度会明显变慢。4 到 8 是常见配置超过 CPU 核数反而会因线程切换导致性能下降。3.4 完整训练流程从冻结训练到全量微调的两阶段法把 3.3 的代码扩成完整的两阶段训练这是我在多个花卉数据集上验证过最稳的路径。第一阶段只训练分类头第二阶段解冻全部层用更低学习率微调。# 第一阶段只训练 fc 层 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True optimizer optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-4) for epoch in range(10): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) print(f[阶段1] Epoch {epoch1} | Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f}) # 第二阶段解冻全部层低学习率微调 for param in model.parameters(): param.requires_grad True optimizer optim.Adam(model.parameters(), lr1e-5) for epoch in range(20): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) print(f[阶段2] Epoch {epoch1} | Train Acc: {train_acc:.4f} | Val Acc: {val_acc:.4f})注意第二阶段学习率从 1e-4 降到 1e-5 是必须的。预训练权重已经接近最优解学习率太大会直接破坏学好的特征验证集准确率反而会下降。4. 花卉识别训练避坑数据集和训练中的 5 个高频踩坑记录4.1 坑一训练集随机划分导致同一张图出现在训练集和验证集现象验证集准确率高达 99%但把新拍的花卉照片放进测试集准确率掉到 70% 以下。原因用random.shuffle划分数据时如果原始图片集中同一个品种的多张照片是连续拍摄的背景、角度几乎一样随机划分会把相似度极高的图片同时分到训练集和验证集模型等于“开卷考试”。解决划分前先按图片的文件名或者拍摄时间聚类确保同一个文件夹、同一批拍摄的照片整体进训练集或整体进验证集。更规范的做法是直接用sklearn.model_selection.StratifiedShuffleSplit按类别做分层划分并固定随机种子from sklearn.model_selection import train_test_split import numpy as np # 假设 file_list[i] 属于 class_list[i] X np.array(file_list) y np.array(class_list) X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 )stratify参数保证训练集和验证集中每个类别的比例和原始数据集一致避免某个类别在验证集中一张都没有。random_state42固定随机种子保证每次划分结果一致方便复现实验。4.2 坑二图像尺寸不一致导致训练中断现象训练到一半报RuntimeError: The size of tensor a (224) must match the size of tensor b (250)。原因数据集中混入了尺寸异常的图片或者 transforms 没有正确处理。比如CenterCrop(224)要求输入图片短边必须大于等于 224否则会报错。解决在预处理阶段统一做Resize(256)后再CenterCrop(224)不要直接对原始尺寸做CenterCrop。同时加一层保护逻辑transform transforms.Compose([ transforms.Resize((256, 256)), # 强制统一尺寸避免短边不足 transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(...) ])4.3 坑三类别不平衡导致模型忽略小样本类别现象少数类别比如某种罕见兰花的召回率接近 0模型几乎把所有图片都预测为样本量最大的类别。原因CrossEntropyLoss默认对每个类别一视同仁样本多的类别在 loss 中占比更大模型自然偏向学大类别。解决在CrossEntropyLoss中传入类别权重让少数类别的错误被放大。权重的计算方式是类别样本数的倒数或总样本数 / 类别数 / 该类样本数。from sklearn.utils.class_weight import compute_class_weight import numpy as np class_weights compute_class_weight( class_weightbalanced, classesnp.unique(all_labels), yall_labels ) class_weights torch.tensor(class_weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)加了类别权重后训练初期的 loss 值看起来会偏高这是正常的——模型在被迫关注小类别。如果之后验证集整体准确率下降但小类别召回率上升说明类别均衡起了作用此时可以对大类别做欠采样或对小类别做过采样把整体准确率和小类别召回率平衡到可接受范围。4.4 坑四加载预训练权重时类别数不匹配现象RuntimeError: Error(s) in loading state_dict for ResNet: size mismatch for fc.weight: copying a param with shape torch.Size([1000, 512]) from checkpoint, the shape in current model is torch.Size([5, 512])。原因torchvision.models.resnet18(pretrainedTrue)默认输出 1000 类的权重替换fc层后结构变了直接load_state_dict必然报错。出现这个报错恰恰说明你的修改生效了——权重加载时发现最后全连接层维度对不上。解决如果用的是models.resnet18(weights...)方式创建模型替换fc层之后权重会自动跳过不匹配层不会报错。如果手动写了load_state_dict需要加strictFalse参数state_dict torch.load(resnet18_pretrained.pth) model.load_state_dict(state_dict, strictFalse) # 跳过维度不匹配的 fc 层4.5 坑五训练 loss 不下降但准确率乱跳现象前几个 epoch 的 loss 稳定在 2.3 左右不动准确率在 10%-20% 之间乱跳。原因学习率过高导致优化器在损失曲面来回震荡或者没有冻结预训练层、直接用大学习率微调整个网络。解决先把学习率降到 1e-4 以下确认 loss 开始单调下降后再逐步调高。如果仍然不降检查数据加载器是否正常——打印几个 batch 的图片和标签确认图片没有被翻转成上下颠倒或者标签和图片错位。5. 花卉识别模型的验证与推理从 PyTorch 模型到实际可用的分类器5.1 编写推理脚本单张图片分类的完整代码训练完成后把模型保存下来写一个推理脚本用于实际预测。这里有一个工程陷阱torch.save(model.state_dict())只保存权重参数不保存模型结构加载时必须先创建模型再载入权重。而torch.save(model)整体保存则可能因为类定义路径变化导致反序列化失败。我的建议是统一用state_dict方案。import torch from PIL import Image from torchvision import transforms def load_model(model_path, num_classes, device): model create_model(num_classesnum_classes) state_dict torch.load(model_path, map_locationdevice) model.load_state_dict(state_dict) model.to(device) model.eval() return model def predict_image(model, image_path, class_names, device): transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(image_path).convert(RGB) input_tensor transform(img).unsqueeze(0) # 加 batch 维度 input_tensor input_tensor.to(device) with torch.no_grad(): outputs model(input_tensor) probabilities torch.softmax(outputs, dim1) top_prob, top_class torch.topk(probabilities, 3) results [] for i in range(3): idx top_class[0][i].item() prob top_prob[0][i].item() results.append((class_names[idx], prob)) return results # 使用示例 device torch.device(cuda if torch.cuda.is_available() else cpu) class_names [daisy, rose, sunflower, ...] results predict_image(model, test_images/unknown_flower.jpg, class_names, device) for name, prob in results: print(f{name}: {prob:.4f})几个细节值得注意。torch.no_grad()在推理时必须加否则 PyTorch 会为每个中间变量构建计算图内存占用成倍增加而且没有任何用处。torch.topk返回概率最高的前三个类别这在花卉识别场景比只返回最高分更实用——因为相似品种之间概率差距可能只有几个百分点返回 Top-3 能帮你判断模型是“确定”还是“犹豫”。5.2 分类结果评估准确率、混淆矩阵与单类指标训练结束后的评估不能只看整体准确率。对花卉识别这种细粒度任务混淆矩阵能直接告诉你模型在哪些品种之间容易混淆——比如“月季”和“玫瑰”被反复搞混说明它们的特征过于相似需要考虑增加数据或换更强的模型。我习惯用 sklearn 生成混淆矩阵并可视化。import numpy as np from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt def evaluate_model(model, dataloader, class_names, device): model.eval() all_preds [] all_labels [] with torch.no_grad(): for inputs, labels in dataloader: inputs inputs.to(device) outputs model(inputs) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) # 混淆矩阵 cm confusion_matrix(all_labels, all_preds) print(混淆矩阵:\n, cm) # 每个类别的精确率、召回率、F1 report classification_report(all_labels, all_preds, target_namesclass_names) print(report)如果发现混淆矩阵中某两个类别频繁互认可以尝试两个方向第一增加这两个类别的训练样本特别是不同角度、不同光照下的图片第二把模型从 ResNet18 升级到 ResNet50更强的特征提取能力有可能捕捉到更细微的差异。5.3 模型导出从 PyTorch 到 TorchScript 或 ONNX 的部署转换训练和验证都通过后如果要把模型集成到移动端或服务端需要把 PyTorch 模型转换成 TorchScript 或 ONNX 格式。这里以 ONNX 导出为例因为它跨平台兼容性最好能在 TensorRT、OpenVINO 等推理框架中直接使用。model.eval() onnx_path flower_classifier.onnx dummy_input torch.randn(1, 3, 224, 224).to(device) # 导出 ONNX torch.onnx.export( model, dummy_input, onnx_path, export_paramsTrue, opset_version11, do_constant_foldingTrue, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}} ) print(fONNX 模型已导出到 {onnx_path})提示dynamic_axes允许推理时动态变化 batch 大小这在部署服务时非常有用。如果固定 batch 为 1可以去掉这个参数推理性能会稍微提升。6. 提升花卉识别精度的三个进阶技巧从“能跑”到“能用”当你的基础模型已经能达到 85%-90% 的验证集准确率时接下来的三个技巧能帮你再往上提几个百分点。这些不是理论推演而是我实际调参时反复验证有效的操作。第一个技巧是学习率衰减策略。上面第 3 节的训练代码用了固定学习率但实际训练中随着 epoch 增加固定学习率会在接近最优解时来回震荡导致损失无法收敛到更小值。用 PyTorch 内置的学习率调度器能自动降低学习率from torch.optim.lr_scheduler import ReduceLROnPlateau scheduler ReduceLROnPlateau(optimizer, modemax, factor0.5, patience3, verboseTrue) for epoch in range(num_epochs): train_loss, train_acc train_one_epoch(...) val_loss, val_acc evaluate(...) scheduler.step(val_acc) # 以验证集准确率为监控指标patience3表示连续 3 个 epoch 验证集准确率没有提升时学习率减半。这种策略比你手动盯着跑到第多少个 epoch 去改学习率要可靠得多。第二个技巧是数据增强加 MixUp。MixUp 是简单有效的正则化方法把两张训练图片按比例混合标签也按同样比例混合。对花卉识别来说这能迫使模型关注更鲁棒的特征而不是死记某张图的背景纹理。def mixup_data(x, y, alpha0.2): MixUp 增强按 Beta 分布采样混合系数 lam np.random.beta(alpha, alpha) batch_size x.size()[0] index torch.randperm(batch_size).to(x.device) mixed_x lam * x (1 - lam) * x[index] mixed_y lam * y (1 - lam) * y[index] return mixed_x, mixed_y注意 MixUp 的alpha不宜太大0.2 左右比较合适。值太大会让混合后的图片“四不像”模型反而学不到有效特征。第三个技巧是 Test Time AugmentationTTA也就是推理时做多次增强取平均。对单张图片分别做水平翻转、中心裁剪、缩放后再预测把多次预测结果取平均。这个方法能稳定提升准确率约 1-2 个百分点尤其当测试图片的角度和训练集差异较大时效果明显。代价是推理时间翻三到五倍离线评估时值得用线上实时推理建议谨慎取舍。最后说一个我自己的习惯每次训练实验我都会把模型权重、训练配置学习率、batch size、epoch 数、验证集准确率三样东西记在一个表格里。这么做的好处是当你尝试了十几次超参调整后翻开表格一眼就能看出哪个配置最稳定而不是靠记忆“上次好像用的 1e-4 还是 5e-5”。花卉识别这个项目最大的魅力就是数据、代码、参数全都自己可控每一次改进都能看到明确的量化反馈。希望这篇笔记能帮你在自己的数据集上少走几步弯路。本文还有配套的精品资源点击获取