简介计算机视觉中草药叶片识别数据包面向图像分类与植物识别任务适合刚开始接触深度学习分类项目的学习者以及需要现成数据集调试YOLOv5分类流程的开发者。数据包已按train与val划分训练集4800张、验证集1400张共涵盖80个类别类别字典以json文件提供便于复现实验与模型评估。包内共2000个文件包括1998张jpg图片、1个json类别映射文件及1个Python可视化脚本脚本可随机抽取4张图片展示并保存到当前目录方便快速预览数据形态、核对标签与图像对应关系。整体压缩包大小约175MB目录结构简洁拿到后可直接接入图像分类训练管线也可作为中草药叶片识别、细粒度分类等场景的既定数据起点。目前已有119人学习下载对需要快速获得划分好训练/验证集的入门者颇具实用价值。1. 计算机视觉数据里的中草药叶片识别分类先解决数据、字典和可视化这三件套很多人拿到一批中草药叶片照片第一反应就是丢进 ResNet 里训练结果测试集指标虚高、验证集不稳定、类别顺序混乱问题几乎全出在数据侧而不是模型侧。这里分享一套我做中草药叶片识别分类项目时固定会用的数据三件套划分好的数据train/val/test 按类别分层切分、类别字典文件class_to_idx 映射、Python 数据可视化脚本分布统计、尺寸检查、图像网格。这套流程适合正在做计算机视觉大作业、科研实验或者小规模识别原型的同学原始图片到位后一两个小时就能把数据准备工作做完后续换模型、调参都会顺畅很多。2. 划分好的数据怎么做按类别分层切分训练/验证/测试集2.1 为什么全局随机打乱会让测试集指标失真一张张图片直接全局随机打乱表面上看起来最公平。但中草药叶片数据有一个很典型的特点同一个植株往往会拍多张不同角度的照片或者同一批叶片是在同一时间、同一背景下拍摄的。全局随机打乱时同一植株的不同照片可能被拆进训练集和测试集模型在训练时已经见过这个植株的背景、光照和叶脉纹理测试分数自然虚高。这个高不是模型的真实识别能力。另一个问题是类别不均衡。中草药叶片数据集里薄荷、艾草这类常见药草样本常常很多而一些相对少见的药用植物可能只有几十张甚至几张。全局随机打乱时样本少的类别在三个集合里的分配比例完全看运气验证集和测试集里可能一个样本都分不到训练时验证 loss 像玄学一样上下乱跳。正确的做法是按类别做“分层划分”先按类别把样本收集在一起逐类打乱再按同一个比例切出训练、验证、测试三份。这样每个类别都以接近相同的比例进到三个集合里样本数少的类别也能保证在验证和测试里留下痕迹。2.2 分层划分脚本 split_dataset.py核心代码与参数说明我一般会提前约定一个原始目录结构raw_images/类别名/图片文件。类别名可以是中文比如银杏、枸杞、薄荷脚本统一用目录名作为类别名。import os import random import shutil from pathlib import Path from collections import defaultdict def collect_samples(root: str): 遍历原始目录按类别名收集所有图片路径 samples defaultdict(list) root_path Path(root) # 只读一层子目录目录名就是类别名 for class_dir in sorted(root_path.iterdir()): if not class_dir.is_dir(): continue for img_file in class_dir.rglob(*): if img_file.suffix.lower() in (.jpg, .jpeg, .png, .bmp, .webp): samples[class_dir.name].append(img_file) return samples def stratify_split(samples: dict, ratios(0.7, 0.15, 0.15), seed42): 对每个类别单独洗牌后按比例切分 train / val / test random.seed(seed) splits {train: [], val: [], test: []} for class_name, paths in samples.items(): random.shuffle(paths) n len(paths) n_train int(n * ratios[0]) n_val int(n * ratios[1]) train paths[:n_train] val paths[n_train:n_train n_val] test paths[n_train n_val:] for split_name, split_paths in [(train, train), (val, val), (test, test)]: for p in split_paths: splits[split_name].append((class_name, p)) return splits def copy_splits(splits: dict, dest: str): 按目标结构复制图片dest/训练集或验证集或测试集/类别名/图片 dest_root Path(dest) for split_name, items in splits.items(): for class_name, src_path in items: out_dir dest_root / split_name / class_name out_dir.mkdir(parentsTrue, exist_okTrue) shutil.copy2(src_path, out_dir / src_path.name) if __name__ __main__: samples collect_samples(raw_images) splits stratify_split(samples) copy_splits(splits, dataset)这段代码的核心逻辑是“逐类收集、逐类洗牌、逐类截断”。先按类别收集避免了全局随机打乱带来的类别遗漏逐类洗牌保证每个类别内部的样本顺序不固定最后按比例截断得到三个集合。copy2 会保留文件的修改时间和元数据这在后续做数据溯源时有用如果是超大文件数据集也可以把 copy2 换成 os.symlink 做软链接节省磁盘空间。参数默认值说明ratios(0.7, 0.15, 0.15)训练、验证、测试的样本占比三项相加需为 1.0seed42随机种子固定后多次运行得到的划分完全一致destdataset输出根目录内部自动生成 train / val / test 三个子目录一个小提醒如果某个类别只有一张图片这个脚本会把这张唯一图片分到 train 里val 和 test 对该类别就是空目录后面训练时这个类别的测试指标无法计算。遇到这种情况要么想办法补充该类别的图片要么在脚本里加一个保护逻辑把样本数不足 3 的类别单独打印出来提醒。2.3 划分后必做的完整性校验划分完不能直接开训先跑一段快速校验确认三个集合各自有多少张图、每个类别是否都出现了。我见过有人划分完发现某个类在测试集里整类消失训练了十几个小时才反应过来验证集指标为什么对不上。def check_split(dataset_root: str): 检查每个集合的类别数和样本数并找出缺失类 from pathlib import Path split_dirs [train, val, test] all_class_names set() for split_name in split_dirs: split_dir Path(dataset_root) / split_name if not split_dir.exists(): print(f[{split_name}] 目录不存在) continue class_names [d.name for d in split_dir.iterdir() if d.is_dir()] all_class_names.update(class_names) class_counts { c: len(list((split_dir / c).glob(*.*))) for c in class_names } total sum(class_counts.values()) print(f[{split_name}] 共 {total} 张类别 {len(class_names)} 个) print(f 样本数最少: {min(class_counts.values())}最多: {max(class_counts.values())}) # 找出在某个集合里缺失的类别 for split_name in split_dirs: split_dir Path(dataset_root) / split_name existing {d.name for d in split_dir.iterdir() if d.is_dir()} if split_dir.exists() else set() missing all_class_names - existing if missing: print(f[警告] {split_name} 中缺失类别: {missing})这里有个细节glob(.) 只能匹配带扩展名的文件如果原始数据里混入了 .txt、.npy 之类的文件统计数会不准。我在实际项目中会把校验脚本里允许的扩展名名单和收集脚本保持一致。校验输出的类别数应该和原始目录一致样本数比例应该接近预设的 ratios一旦出现偏差先回 raw_images 检查是不是有某个类别目录里存在子目录或隐藏文件。3. 类别字典文件让中文叶片名和模型标签一一对应3.1 类别字典文件的结构class_to_idx 与 idx_to_class训练一个图像分类模型时模型只能输出整数索引但中草药叶片数据集的类别名通常是中文比如“银杏叶”“薄荷叶”。这个中间层必须有稳定的映射规则每个类别名对应一个整数索引并且这个顺序在训练、验证、推理三个阶段保持一致。类别字典文件通常是一个 JSON里面至少包含三块内容class_to_idx 负责把类别名映射成整数Pytorch 的 Dataset 需要用这个顺序给每张图贴标签idx_to_class 负责把整数映射回类别名推理阶段拿到模型输出后需要查这个反查表才能显示中文名num_classes 记录总类别数方便初始化模型的全连接层。结构上idx_to_class 的键必须是字符串因为 JSON 规范要求键只能是字符串训练脚本里要派上用场时再用 int() 转回来。3.2 自动生成字典的脚本 build_class_dict.py手写字典容易出事的场景有两个一是中英文混合命名导致大小写不一致二是新增类别后忘记同步索引。所以我从不在训练脚本里手动维护字典而是写一段脚本从划分好的目录自动生成。import json from pathlib import Path def build_class_dict(train_dir: str, save_path: str class_to_idx.json): 从 train 目录自动生成类别字典文件 train_path Path(train_dir) # sorted 保证多次生成结果一致避免出现顺序漂移 class_names sorted([ d.name for d in train_path.iterdir() if d.is_dir() ]) class_to_idx {name: idx for idx, name in enumerate(class_names)} idx_to_class {str(idx): name for idx, name in enumerate(class_names)} dict_data { class_to_idx: class_to_idx, idx_to_class: idx_to_class, num_classes: len(class_names), source: str(train_path), } with open(save_path, w, encodingutf-8) as f: json.dump(dict_data, f, ensure_asciiFalse, indent2) print(f生成完成共 {len(class_names)} 个类别) print(f前 5 个类别{class_names[:5]}) return class_to_idx if __name__ __main__: build_class_dict(dataset/train, class_to_idx.json)逻辑说明脚本以 train 目录为输入源而不是原始图片目录。为什么要用 train 目录因为 train 目录是最终参与训练的目录它的子目录名直接决定了标签的集合。如果从 raw_images 生成一旦 raw_images 里有多余的文件夹或者空目录字典里就会混入根本不会参与训练的类别最后在计算准确率时类别数和模型输出维度对不上。sorted 排序保证同一批数据多次生成字典的顺序完全一致不会因为文件系统遍历顺序不同而产生不同的索引分配。参数说明train_dir 传入已经划分好的训练目录save_path 是 JSON 文件输出路径。生成后用文本编辑器打开检查一下中文类别名是否显示正常num_classes 是否和实际的类别目录数一致。如果中文变成 \uXXXX 转义序列说明打开方式或者生成时没有正确指定编码改一下编辑器编码设置就能解决。3.3 字典文件与训练/推理脚本的三种常见约定类别字典文件本身只是一个 JSON真正让它发挥作用的是训练和推理脚本共同遵守的约定。第一个约定是“字典唯一来源”。训练脚本的 Dataset 从 class_to_idx.json 加载映射而不是遍历训练目录自己重建。推理脚本加载同一个字典文件这样训练和推理阶段的索引空间完全一致。有人图省事在训练脚本里现算一遍类别名到索引的映射推理时又按测试目录重新算一遍只要目录排列顺序稍有不同预测结果就整体错位。这种事我碰到过不止一次最后模型指标好看得一塌糊涂实际部署出来识别结果全对不上。第二个约定是“字典一旦生成就冻结”。新增加类别、删除类别时重新生成字典文件但训练和推理同步更新不保留新旧混用的状态。更稳妥的做法是在生成脚本里输出一个校验值把类别列表的哈希同时写进字典文件训练脚本启动时校验一遍发现哈希不一致立即报错。第三个约定是关于中英文名的。如果模型中需要显示英文类别名可以额外加一个字段例如 class_to_en但索引分配仍然以中文类别名的 sorted 顺序为基准。不根据英文名的字母顺序排序避免不同语言环境的排序规则差异导致索引漂移。4. Python 数据可视化脚本训练前先看清分布别急着跑模型4.1 可视化能暴露哪些数据问题划分好的数据和类别字典文件解决了“数据怎么组织”的问题但数据本身的图像质量、类别均衡度、尺寸一致性还不知道。很多人跳过这一步直接训练等到损失不下降才开始怀疑数据有问题那时再回头排查时间成本已经翻了几倍。可视化脚本存在的意义就是花十分钟输出几张图把数据质量问题暴露在训练之前。常见的数据问题有三类类别样本数不均衡少数类只有个位数样本模型在多数类上过拟合图像尺寸混乱有的 200×200有的 4000×3000训练时 dataloader 的 collate 会不停报错图像损坏或者拍摄内容不对比如混入了整株植物而不是叶片特写网格缩略图能一眼看出这类低级错误。4.2 类别分布柱状图发现样本不均衡第一张图是各类别样本数的柱状图。中草药叶片数据经常是金字塔分布常见药草样本充足珍稀品种样本稀少这张图能直接决定训练时要不要用类别加权损失函数。import matplotlib.pyplot as plt from pathlib import Path def plot_class_distribution(data_root: str, save_path: str class_distribution.png): 统计并绘制各目录下样本数分布 class_names [] counts [] for class_dir in sorted(Path(data_root).iterdir()): if not class_dir.is_dir(): continue # 统计该类别目录下所有图片文件 n len([p for p in class_dir.rglob(*) if p.suffix.lower() in (.jpg, .jpeg, .png, .bmp, .webp)]) class_names.append(class_dir.name) counts.append(n) fig, ax plt.subplots(figsize(10, 6)) bars ax.bar(range(len(class_names)), counts) ax.set_xticks(range(len(class_names))) ax.set_xticklabels(class_names, rotation90) ax.set_ylabel(样本数) ax.set_title(各类别样本数分布) # 在柱子顶部标注具体数值 max_count max(counts) if counts else 1 for i, v in enumerate(counts): ax.text(i, v max_count * 0.01, str(v), hacenter, fontsize8) fig.tight_layout() fig.savefig(save_path, dpi150) print(f已保存: {save_path})逻辑说明这段脚本遍历指定目录的一级子目录每个子目录名当作一个类别统计所有图片文件的数量并绘制柱状图。sorted 保证类别顺序和类别字典文件的顺序一致这样柱状图和字典对上号。柱子顶部的数值标签避免了只看视觉效果产生的误判比如 50 和 55 的差距在图上几乎看不出来但数字能直接告诉你数量级差异。参数说明data_root 强烈建议分别检查 train、val、test 三个目录而不是只检查 train。我遇到过 train 里类别均衡test 里某个类别只剩 1 张图的情况单独看 train 根本发现不了问题。save_path 可以按目录区分命名比如 train_distribution.png 和 test_distribution.png。这张图能直接回答一个问题是否需要做数据增强。如果样本数最少的类别有 300 张而最多的类别有 3000 张差距在 10 倍以内直接训问题不大如果差距达到 50 倍以上就要考虑对少数类做样本扩充或者在损失函数里给少数类更高的权重。4.3 图像尺寸和宽高比分布决定 resize 策略第二张图是图像尺寸和宽高比分布。中草药叶片的拍摄设备不统一手机、单反、扫描仪都会产出不同尺寸的照片。训练前必须知道尺寸范围否则随便定一个输入分辨率训练时要么大量图片被拉伸变形要么显存不够。from PIL import Image import matplotlib.pyplot as plt from pathlib import Path def plot_image_sizes(data_root: str, sample_limit: int 500, save_path: str image_size.png): 抽样本统计图像尺寸和宽高比分布 widths, heights [], [] # 限制抽样数量避免扫全量太慢 img_paths [ p for p in Path(data_root).rglob(*) if p.suffix.lower() in (.jpg, .jpeg, .png, .bmp, .webp) ][:sample_limit] for img_path in img_paths: try: with Image.open(img_path) as im: w, h im.size widths.append(w) heights.append(h) except Exception as e: print(f损坏或无法读取: {img_path} - {e}) fig, axes plt.subplots(1, 2, figsize(12, 5)) # 左图像素尺寸散点 axes[0].scatter(widths, heights, s8, alpha0.5) axes[0].set_xlabel(宽度) axes[0].set_ylabel(高度) axes[0].set_title(像素尺寸分布) # 右图宽高比直方图 ratios [w / h for w, h in zip(widths, heights) if h 0] axes[1].hist(ratios, bins30) axes[1].set_xlabel(宽高比) axes[1].set_title(宽高比分布) fig.tight_layout() fig.savefig(save_path, dpi150) print(f抽样 {len(widths)} 张已保存: {save_path})逻辑说明脚本用 PIL 读取图像尺寸不会真正解码全部像素数据速度很快。左边散点图能看出尺寸是否集中右边直方图能看出宽高比分布。如果宽高比集中在 0.7 到 1.3 之间直接缩放到 224×224 问题不大如果存在大量 2:1 甚至更长的细长叶片图就要考虑先做等比例缩放填补边或者中心裁剪而不是直接拉伸变形。参数说明sample_limit 控制抽样数量500 张足够看出整体趋势不必全量扫描。在实际项目里我会把保存的图片尺寸分布图放在数据集目录旁边后面换预处理策略时对照着看。4.4 样本网格缩略图快速看每类叶片长什么样第三张图是样本网格缩略图。分布统计只能告诉你数量不能告诉你图像内容是否正常。中草药叶片识别数据容易混入几种脏数据拍的是整株植物而不是叶片、背景占满整个画面、叶片被手遮挡、图片模糊到看不出叶脉纹理。这些问题靠柱状图发现不了网格图一眼就能看出来。import math import matplotlib.pyplot as plt from PIL import Image from pathlib import Path def plot_class_sample_grid(data_root: str, class_name: str, grid_size: int 16, save_path: str sample_grid.png): 对指定类别画 grid_size x grid_size 的缩略图网格 class_dir Path(data_root) / class_name img_paths [ p for p in class_dir.rglob(*) if p.suffix.lower() in (.jpg, .jpeg, .png, .bmp, .webp) ][:grid_size * grid_size] if not img_paths: print(f警告: {class_name} 下没有找到图片) return fig, axes plt.subplots(grid_size, grid_size, figsize(12, 12)) for ax, img_path in zip(axes.flat, img_paths): with Image.open(img_path) as im: im im.convert(RGB) ax.imshow(im) ax.axis(off) fig.suptitle(f{class_name} 样本缩略图, fontsize14) fig.savefig(save_path, dpi120) print(f已保存: {save_path})逻辑说明脚本针对单个类别输出缩略图网格每个格子里放一张图不保留坐标轴刻度保证视觉上只关注叶片本身。convert(RGB) 是为了兼容某些 PNG 图片带透明通道的情况如果不做转换imshow 只会显示其中一部分像素。这个脚本在实际使用中要写一个循环对所有类别各生成一张网格图然后拼成一张大图浏览。我一般把 grid_size 设成 16也就是一张图看 256 个样本每个类别的典型形态、背景干扰、拍摄角度都能看清楚。如果有人告诉你某类叶片有两种完全不同的形态比如银杏叶有扇形和裂叶形之分缩略图里就能确认这是不是两类需要拆开的样本。5. 中草药叶片数据准备避坑五个真实翻车现场5.1 验证集里丢了尾类样本现象训练时 loss 正常下降验证集准确率忽高忽低甚至出现验证集准确率高于训练集的异常情况。排查训练代码半天找不到问题最后打印验证集类别分布发现某个类别在验证集里整类缺失。原因划分数据时用了全局随机打乱而不是按类别分层划分。一共 5 类每类样本数分别是 1000、800、600、400、50总样本量 2850全局随机打乱后最后那类只有 50 张分到验证集的期望值不到 8 张随机波动可能导致 0 张。验证集没有这个类别计算准确率时就少了一个类别训练时这个类的 loss 也时有时无。解决改用分层划分每个类别单独打乱再切分这样样本数为 50 的类别也能保证在验证集里留出 7 到 8 张。划分完成后跑一遍 2.3 的完整性校验脚本确认每个类别在 train、val、test 三个集合里都出现。5.2 同一植株多张照片跨集合泄漏现象测试集准确率达到 98%模型部署到现场拍的新照片上准确率骤降到 70%。训练代码没问题模型架构也没问题。原因数据切分粒度是单张图片不是植株或拍摄批次。同一个植株的 10 张照片被随机分进了 train 和 test模型在训练时见过同样背景、同样叶脉纹理的照片测试时只是“回忆”而不是识别。解决如果原始数据里同一植株的照片是用同一个目录或文件名前缀组织的要按植株为单位分组划分先把植株分组再对组做随机分配保证同一个植株的所有照片要么全在训练集要么全在测试集。无法确认拍摄批次时至少要检查 train 和 test 里是否存在文件名前缀相似的照片把相似样本手动剔除。5.3 OpenCV 读中文路径返回 None现象用 cv2.imread 加载图片时某些图片返回 None程序在训练中途崩溃。把路径打印出来发现全是中文目录名当时代码里明明检查过文件存在。原因OpenCV 的 imread 函数在 Windows 和部分 Linux 环境下不支持非 ASCII 字符路径中文字符路径传入后直接失败返回 None。数据划分脚本复制目录时保留了中文目录名于是中草药叶片这种天然用中文命名的数据集就踩中了这个坑。解决统一使用 PIL 的 Image.open 读取图片PIL 对中文路径支持相对稳定。如果需要 OpenCV 的接口可以用 cv2.imdecode 配合 numpy 从字节流解码。还有一个更省事的做法在划分脚本 copy_splits 输出目录时顺便生成一份英文别名目录映射训练时用英文字母类别名字典里再记录英文名和中文名的对应关系。5.4 类别字典顺序在二次生成后错乱现象第一次训练时一切正常后来新增了两个类别重新生成了字典文件训练完的模型在推理时预测结果整体错乱把银杏叶识别成艾草。原因重新生成字典时 sorted 的排序结果和第一次不一致或者第一次生成时用了原始图片目录第二次用了 train 目录两个目录下子目录的排列顺序不同导致同一类别被分配了不同的索引。模型结构完全没变但标签空间变了推理时用新索引查字典自然错位。解决把 class_to_idx.json 作为唯一事实来源训练脚本和推理脚本都从同一个文件加载映射不各自现算。新增类别时对字典做一次全量重新生成生成后用 3.2 脚本里的打印结果对比原有字典确认旧类别的索引没有变化再开始训练。5.5 直接等比例缩放到正方形导致叶片变形现象训练集准确率始终上不去loss 降到一定程度就不再下降。检查预处理代码发现图像被直接 resize 到 224×224 正方形细长叶片宽高比接近 3:1拉伸后叶片形态完全失真。原因中草药叶片形状差异很大银杏叶接近扇形艾草叶细长鱼腥草叶接近心形。统一缩放到正方形相当于把所有叶片强制拉成同一个比例学习到的特征是变形后的假特征模型自然学不好。解决先用 4.3 的可视化脚本看一下宽高比分布如果大多数图片宽高比在 1:1 附近直接 resize 没问题。如果分布很散改用先等比例缩放然后填充边框letterbox的方案或者随机中心裁剪后再缩放到目标尺寸。填充边框的颜色可以选灰色或者训练集图像的平均像素值。6. 数据可用性的最后验证一个快速过拟合测试和错误样本检查6.1 用一个小卷积网络打出拟合信号数据准备完毕先别上大模型用一个小型卷积网络做一个过拟合测试。过拟合测试的目的是验证数据链路是否通畅、模型能否在训练集上把损失打到很低。如果小模型在训练集上都学不进去说明数据有问题这时候排查成本最低。import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image from pathlib import Path class LeafDataset(Dataset): 按类别字典加载中草药叶片图片 def __init__(self, root, class_to_idx, transformNone): self.samples [] self.class_to_idx class_to_idx self.transform transform for class_name, class_dir in enumerate(Path(root).iterdir()): if not class_dir.is_dir(): continue label class_to_idx[class_dir.name] for p in class_dir.rglob(*): if p.suffix.lower() in (.jpg, .jpeg, .png): self.samples.append((str(p), label)) 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((64, 64)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) dataset LeafDataset(dataset/train, class_to_idx, transform) loader DataLoader(dataset, batch_size16, shuffleTrue) # 极小的卷积网络2层卷积 全局池化 model nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(16, 32, kernel_size3, padding1), nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(32, len(class_to_idx)), )这个测试不用训练完整跑上 100 个 batch观察 loss 是否稳定下降。如果 loss 纹丝不动第一件事检查图像读取是否正常打印一批图像张量看看有没有全黑、全白或者数值范围不对的问题。这是我最依赖的快速验证手段能在 5 分钟内确认数据管线是通的。6.2 把预测错误的样本单独捞出来看过拟合测试通过后我还有一个习惯在测试集上跑一次推理把预测错误的样本路径和真实标签、预测标签一起输出到一个 CSV 文件再按类别分组把错误样本的缩略图拼出来。错误样本往往透露出数据层面的规律比如某两类叶片外形太像、某类样本背景干扰严重、某些图片本身就是错的。这些规律就是下一步做什么的答案该补数据补数据该拆类拆类该调增强调增强。如果错误样本集中在光照条件和拍摄角度上就要在数据层面增加模拟光照变化和视角变化的增强手段而不是盲目换更大的模型。如果错误样本两种类别五五开大概率是那两个类别在形态上确实难以区分要么接受精度上限要么引入叶片纹理特征来辅助判断。这一步做完数据准备才算真正收尾。这些细节是我从好几次翻车里攒出来的经验。第一次做中草药叶片识别分类时我直接全局随机划分数据测试集指标虚高到 97%现场测试只有 70%查了两天才发现是划分颗粒度的问题。从那以后我再也不跳过校验步骤宁可多花半小时把分布图、字典、错误样本清单全部过一遍也不愿意训练完再推倒重来。希望这套流程能帮你在数据准备阶段少走这些弯路。本文还有配套的精品资源点击获取