☰
500张果树水果图像分类实战:YOLOv8与ResNet18训练全流程
2026/10/1 18:41:49 网站建设 项目流程

简介:面向图像分类与深度学习初学者的7类常见水果标注数据集,类别包括草莓、甜瓜、橙子、苹果等,已按训练集与测试集划分并完成预处理,可直接输入分类网络训练,免去自行整理、清洗图片的环节。压缩包共587个文件,以584张jpg样本图为主体,另含记录类别名称与对应关系的json文件、一键可视化数据集的Python脚本show.py及1张示意缩略图,整体约23.19MB,体量轻、易下载。训练与测试目录分别按类别存放,配合show脚本可快速抽查样本,确认标注与分布是否合理;json文件也便于读取标签、统计各类别数量,目录结构清晰,可直接接入常见图像分类框架。目前已有218人学习使用,适合用于图像分类课程设计、算法对比、网络改进或数据增强实验,拿到即可直接训练与验证。

1. 七类果树水果图像分类数据集:500张已标注数据到底能做什么

500 张已标注的果树水果图像,是个尴尬的规模——不到 cifar10 数据集的十分之一,离工业场景的数万张更远。但这种“7种常见果树水果图像分类数据集”恰恰是课程设计、毕业设计和算法预研里出现频率最高的一档:类别固定、标注明确、一两个晚上跑完一轮完整实验,还能把图像分类从数据校验到训练评估的流程走通。

它的价值不在直接产出产品模型,而在用最小成本把图像分类模型的全链路跑明白:对比图像分类算法、验证迁移学习、测数据增强边界都够用。已标注省掉了最劝退的环节,约500张保证单卡能跑、迭代够快。适合刚接触图像分类的新手,也适合想快速验证小样本思路的工程师。

接下来按我实际做这类项目的顺序展开:先拆数据、再跑基线、然后调参、最后讲坑。

2. 拆解这份数据:七类构成、已标注的两种形态与开训前校验

2.1 七类怎么定:从“长在果树上”推导类别边界

标题里“长在果树上”不是白写的。做图像分类数据集,类别空间的物理边界先要立住:西瓜、草莓是贴地藤生果实,葡萄是藤本,都不算“长在果树上”。所以这类7类数据集通常从苹果、梨、桃、杏、李、樱桃、橙子/橘子、芒果、柿子、石榴这些树生水果里选七种。它们靠颜色、形状、表面纹理区分,而不是靠叶子——这对分类算法是友好设定,模型学的应该是果实本体。

这个边界直接决定训练难度。树生水果在自然果园拍摄,背景里全是叶子与枝干,绿色纹理占比高,果实有遮挡和重叠;同一类在不同光照、不同角度下外观差异很大。类别之间还有天然的混淆对:苹果和梨在绿色品种、背光条件下容易互认,桃和杏在颜色与个头上有重叠,橙子和芒果的橙色色域部分相交。500张摊到7类,平均每类也就70张上下,少的类可能只有三四十张——这意味着模型稍不注意就把背景纹理当成了判别特征,这个隐患从数据层面就种下了,后面所有增强和调参都在跟它对抗。

2.2 已标注标的是什么:目录即标签与清单文件两种格式

“已标注”三个字,在分类任务里和检测、分割完全不同。检测要 bbox,分割要掩码,对照 coco2017数据集结构,那是 JSON 里挂 annotations、每个对象带 bbox 和 segmentation 的重结构;而图像分类的标注只需要回答“这张图属于哪个类”。拿到这类数据集首先要确认标注形态,我见过的就是两种。

第一种是 ImageFolder 目录结构,目录名即类别名:

fruit_dataset/ train/ apple/ apple_001.jpg apple_002.jpg ... pear/ pear_001.jpg ... peach/ orange/ mango/ cherry/ persimmon/ val/ apple/ ... ...

第二种是标签清单文件,JSON 或 CSV 一行一条:

[ {"image": "apple_001.jpg", "label": "apple"}, {"image": "pear_003.jpg", "label": "pear"}, {"image": "peach_011.jpg", "label": "peach"} ]

两种形态本质等价,但落地体验差别很大,对比如下:

维度目录结构(ImageFolder)清单文件(JSON/CSV)
解析成本框架直接支持,无需解析需写读取代码,与文件名强耦合
类目调整改目录名即可要同步改清单,容易漏
常见出错点混入空目录或隐藏文件image 字段与实际文件失配
适用场景单标签分类、YOLOv8 classify多标签或带额外元数据

我一般拿到手先写个十行脚本把清单转成目录结构,统一用 ImageFolder 处理,后续训练代码就不用为标注格式写分支:

import json, os, shutil with open("labels.json", "r", encoding="utf-8") as f: items = json.load(f) dst_root = "fruit_dataset" for item in items: src = os.path.join("images", item["image"]) dst_dir = os.path.join(dst_root, item["label"]) os.makedirs(dst_dir, exist_ok=True) shutil.copy(src, os.path.join(dst_dir, item["image"]))

这段脚本会把“images 目录 + labels.json”转成标准的 train/类名/图 结构。注意写的时候保留原始文件,不要在原目录上做 move,转换脚本出问题时还有后悔药可吃。

2.3 开训前必做的三项校验:数量盘点、损坏扫描、划分检查

下载完第一时间不要配环境,先把数据盘一遍。约500张听着不多,但解压后的问题通常不少:空目录、半截图片、隐藏文件、类别缺失。这类数据集下载后第一件事不是一张张看照片,而是按文件清单做机器校验。

先从数量盘点开始,一段脚本看清每个 split、每个类的真实数量:

import os root = "fruit_dataset" allowed_ext = {".jpg", ".jpeg", ".png"} for split in ["train", "val", "test"]: split_dir = os.path.join(root, split) if not os.path.isdir(split_dir): continue class_counts = {} for cls in os.listdir(split_dir): cls_dir = os.path.join(split_dir, cls) if not os.path.isdir(cls_dir): continue files = [f for f in os.listdir(cls_dir) if os.path.splitext(f)[1].lower() in allowed_ext] class_counts[cls] = len(files) total = sum(class_counts.values()) print(f"{split}: {total} 张, 各类数量: {class_counts}")

这段脚本的关键是只统计图片扩展名,把 .DS_Store、Thumbs.db、说明 txt 排除在外。输出后看两件事:有没有类别数量掉到均值一半以下;train 和 val 的类别分布是否接近。如果一个类在 val 里只有 2 张,后面评估指标会剧烈抖动,这一步就要先知道。

数量没问题再扫损坏图片。网盘、压缩包反复传输的文件最常见问题就是头部损坏或内容字段缺失,训练时读到会直接中断或拖慢。用 PIL 的 verify 快速全量扫一遍:

from PIL import Image import os root = "fruit_dataset" bad_files = [] for dirpath, _, filenames in os.walk(root): for f in filenames: if os.path.splitext(f)[1].lower() not in {".jpg", ".jpeg", ".png"}: continue path = os.path.join(dirpath, f) try: with Image.open(path) as im: im.verify() except Exception as e: bad_files.append((path, str(e))) print(f"损坏图片: {len(bad_files)} 张") for path, err in bad_files[:20]: print(path, err)

verify() 只解析文件结构不完整解码,速度快,适合全量扫。扫出来的图移到坏图目录,别直接删,万一模型效果差还能回头查是不是误删。

最后是划分检查。自带的 train/val 要看两类问题:一是有没有某个类只出现在 train 没出现在 val;二是划分时是否按文件名顺序切分——果园拍摄经常是连续快门,文件名相邻的图高度相似,顺序切分会让 val 里全是“见过面的近亲”,评测结果虚高。检查方法很简单,从 val 里每个类抽前几张和 train 对比,看是否几乎一样。有问题就重新按随机种子切分,train/val 参考 8:1:1 或 7:2:1。

3. 双线跑通训练:YOLOv8 classify 与 ResNet18 手工循环的最小实现

3.1 目录就绪后直接开训:yolov8 classify 最小命令

数据校验完、目录整理好之后,最省事的路径是 ultralytics 的 classify 模式。它直接消费 ImageFolder 目录结构,不需要额外写标注解析——这就是“yolov8训练自己的数据集”在分类场景下最简单的原因:不用导出 txt、不用管 anchor 和 bbox,目录对就行。先手工从 train 里留一份 val,然后跑:

cd ~/projects/fruit_cls yolo classify train \ data=./fruit_dataset \ model=yolov8n-cls.pt \ epochs=30 \ imgsz=224 \ batch=16 \ project=./runs \ name=fruit_baseline

data 指向数据集根目录,内部需要 train/ 和 val/ 两个子目录。model 填 yolov8n-cls.pt 会自动下载预训练权重并替换分类头;nano 版参数量最小,500 张数据撑不起更大模型,s 或 m 变体反而更容易过拟合。imgsz=224 是分类任务最常见的输入尺寸;epochs=30 在这个量级够看清收敛趋势;batch=16 在 8GB 显存上无压力,显存更大可以提到 32。跑完看 runs/fruit_baseline/ 下的 results.csv,重点看 val accuracy 随 epoch 的走势,以及 per-class 准确率表。只要不是全程抖成噪声,这个 baseline 就立住了。

3.2 从 YOLO 手里拿回控制权:ResNet18 训练循环逐行拆

YOLOv8 的 classify 适合跑 baseline,但想精细控制数据增强、冻结策略、学习率调度,还是要回到 torchvision 的 ImageFolder 加一个标准训练循环。控制权在调参阶段才是关键:你能明确知道每个改动发生在哪一层。这里刻意不用 torchvision 的封装接口,手写循环是为了后面能穿插冻结层、换优化器、插混淆矩阵,这些在封装里都要绕路。

import torch import torch.nn as nn from torchvision import datasets, transforms, models data_dir = "fruit_dataset" num_classes = 7 transform_train = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) transform_val = 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]) ]) train_set = datasets.ImageFolder(f"{data_dir}/train", transform=transform_train) val_set = datasets.ImageFolder(f"{data_dir}/val", transform=transform_val) train_loader = torch.utils.data.DataLoader(train_set, batch_size=32, shuffle=True, num_workers=4) val_loader = torch.utils.data.DataLoader(val_set, batch_size=32, shuffle=False, num_workers=4) model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) model.fc = nn.Linear(model.fc.in_features, num_classes) model = model.cuda() criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) for epoch in range(30): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.cuda(), labels.cuda() optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) model.eval() correct = total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.cuda(), labels.cuda() preds = model(images).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) acc = correct / total print(f"epoch {epoch+1:02d} | loss {running_loss/len(train_set):.4f} | val acc {acc:.4f}") scheduler.step()

几个参数值得单独说。RandomResizedCrop 的 scale 设 0.7~1.0 是故意保守的:500 张数据下裁剪比例太狠,模型永远看不到完整的果实,学不到整体形状。ColorJitter 的亮度对比度加 0.3,用来压制“背光 vs 顺光”这类光照偏差;但 saturation 别调太大,套袋或不熟的青果一旦被增强成红色,类别语义就被污染了。ResNet18 用 ImageNet 预训练权重初始化,fc 换成 7 输出,这是小样本分类的标准起手式。优化器不用 SGD,AdamW 配 3e-4 在 500 张规模下更容易稳住收敛。

3.3 别只看准确率:混淆矩阵与分类报告怎么读

训练收敛后,总的 val accuracy 只是一个数,掩盖了大量信息。7 类里有一类几乎全错,accuracy 可能只掉几个点,但你不知道是哪类、错成了哪类。图像分类模型在小数据集上翻车,几乎都翻在“某两类互认”,这时候必须看混淆矩阵:

import numpy as np import torch from sklearn.metrics import confusion_matrix, classification_report model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for images, labels in val_loader: images = images.cuda() all_preds.extend(model(images).argmax(dim=1).cpu().numpy()) all_labels.extend(labels.numpy()) report = classification_report(all_labels, all_preds, target_names=train_set.classes, digits=3) print(report) cm = confusion_matrix(all_labels, all_preds) print("混淆矩阵 (行=真实, 列=预测):") print(np.array2string(cm, formatter={'int': lambda x: f"{x:3d}"}))

注意 val_loader 在评估阶段必须保持 shuffle=False,否则 all_preds 和 all_labels 的对齐会在 batch 边界错位。

提示:评估阶段不要做任何随机变换,Resize 和 CenterCrop 这类确定性变换就够了。

看混淆矩阵有三个优先项。第一,对角线有没有接近 0 的类,说明这个类完全没有判别力。第二,最大的非对角元素是哪一对,这对模型和人应该“看起来像”——如果梨被大量认成橙子,模型学到的特征是颜色而非形状,要回去查增强和类内多样性。第三,看 classification_report 里 macro-F1 和 weighted-F1 的差距,差距大说明类别不平衡,这在避坑章会专门讲。

4. 参数调优与迁移学习:三个杠杆把小样本准确率推上一个台阶

4.1 必调参数一:输入尺寸与数据增强的力度

baseline 跑通后,第一个要动的是输入尺寸。imgsz=224 是启动值,不是默认最优值。500 张数据下不建议直接上 512:分辨率翻倍,模型要拟合的细节翻倍,样本量没变,过拟合风险比收益大。常见做法是保持 224,在增强上花功夫——增强是把 500 张图变成“有限但多样”的唯一手段。

torchvision 增强的顺序有讲究:先空间类(缩放、翻转、旋转),再颜色类(亮度、对比度、饱和度),最后 Normalize。顺序错了效果打折。增强力度在 500 张这个量级上是双刃剑:把 RandomResizedCrop 的 scale 下限从 0.7 降到 0.3,模型会频繁看到“树叶加果实残片”的组合,最后学的是绿色纹理而不是果实本体。经验参数如下:

增强操作建议取值作用风险
RandomResizedCrop scale(0.7, 1.0)模拟不同取景比例下限过低会学背景
RandomRotation±15°模拟手持抖动超过 30° 出现黑边和畸变
ColorJitter brightness/contrast0.2~0.3压制光照差异过大导致颜色分布失真
ColorJitter saturation/hue0 或极小保留果实颜色语义套袋、未熟果实会被污染

RandomErasing 和 Cutout 这类遮挡增强,我在这类数据上不建议全局启用。它们适合大样本正则化,每类七十张时遮一块就丢一块有效信息。如果想试,只用在最后的微调阶段,并且只遮很小的比例。

4.2 必调参数二:学习率、批大小与 epoch 的配合

小数据集最怕的不是欠拟合,是收敛过程没稳住就过拟合。500 张数据、ResNet18 大概 30 个 epoch 就能把 train accuracy 推到 99%,关键看 val 曲线什么时候开始背离。我的习惯是学习率从 3e-4 起,用 CosineAnnealing 调度,盯着 val loss 的拐点——如果 val loss 在 15~20 epoch 反弹,说明后续 epoch 都在记噪声,要么调小 lr,要么提前停。学习率这东西也带点玄学,但对这份数据,1e-3 起步大概率第一轮就震荡,1e-4 起步又太磨蹭,3e-4 是稳的。

批大小和小数据的配合经常被低估。batch=32 配 lr=3e-4 是稳定组合;把 batch 降到 8,梯度噪声变大,lr 要同步降到 1e-4 左右,否则曲线震荡。反过来,batch 提到 64,每 epoch 只有十步左右的更新,方向估计极不稳,我一般不建议在小数据上开大 batch。epoch 不必死守,用 EarlyStopping 更实际:连续 5 个 epoch val accuracy 不涨就停。到这个量级,30~50 epoch 足够收敛。

4.3 冻结与微调两步走:迁移学习的具体代码

500 张从零训练深度模型是走不通的,迁移学习是这个规模下唯一可靠的路。关键在冻结策略。我的做法分两步:第一步冻结全部 backbone,只训 fc 分类头;val accuracy 进平台期后,再解冻最后两三个 stage,用更小的学习率微调。一步到位全部解冻,很容易在第一个 epoch 就把预训练特征破坏掉,这在小数据集上是不可逆的。

# 第一步:冻结 backbone,只训分类头 for name, param in model.named_parameters(): if "fc" not in name: param.requires_grad = False optimizer = torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3, weight_decay=1e-4) # 跑 10~15 个 epoch 后进入第二步:解冻全部参数 for name, param in model.named_parameters(): param.requires_grad = True # 解冻后换小学习率,只微调后半部分 optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)

filter(lambda p: p.requires_grad, model.parameters()) 这行是关键:如果直接传 model.parameters(),requires_grad=False 的参数也会进优化器,PyTorch 不报错但白占显存,而且解冻后优化器里参数的梯度状态不一致。我习惯在切换冻结状态时重建优化器,而不是复用旧的,避免动量缓冲里残留冻结期的梯度历史。

还有一个常被忽略的迁移细节:类别数。ImageNet 预训练权重为 1000 类设计,fc 换成 7 输出后,backbone 输出的特征本身已经包含“果实、叶子、枝干”这类通用视觉概念。有人在 fc 前插 Dropout 或加一个 128 维中间层,我的经验是小数据集加中间层收益有限,反而多了要调的超参数,不如直接用 fc 输出。

5. 避坑实录:小样本水果分类最常见的五个翻车现场

5.1 现象:训练一启动就报 FileNotFoundError,报错路径在标注清单里有、文件夹里没有

原因:解压不完整,或者标注清单是先整理后补拍的,部分图片没进最终目录。这类数据从网盘、群文件分享出来,断点续传失败是最常见的罪魁祸首。

解决:不要一张张找,用 2.3 的 verify 脚本扩展一下,遍历清单里每个路径并检查 os.path.exists。缺失少的直接放弃这几张,缺失多则需要重新获取完整包;如果是清单转目录结构,就在转换时就过滤掉缺失项。这件事必须在训练前做,YOLOv8 读到一半报错会把整个 project 输出打乱,重跑又浪费一轮时间。我踩过这个坑之后养成了习惯:任何数据集先过一遍 数量盘点 + 损坏扫描,再谈训练。

5.2 现象:训练曲线正常,val accuracy 里某一类永远在 50% 上下,混淆矩阵显示它一大半被判成另一个相似类

原因:类别不平衡,且少的那类多样性不足。假设某类只有 28 张,训练集只分到 22 张,颜色、角度、光照的多样性撑不起一个类别的边界,模型只能退化到用底色投机。

解决:先确认数量,用 Counter 脚本看清每类真实张数。如果确实少,三个手段按顺序用。一是过采样:复制该类样本,在复制时用更强的颜色扰动生成变体。二是加权损失:按样本量反比算 CrossEntropy 的 class_weight。三是砍类:把难以区分的相近类合并,7 类并成 6 类——课程设计场景里这是合法操作,比硬保一个永远错的类好看得多。我的原则是:宁可类别少而准,不要类多而烂。

5.3 现象:val accuracy 显示 95%,你抽几张真实果园照片一测,套袋芒果、背光苹果几乎全错,模型把它们判成叶子或别的类

原因:模型学的是背景,不是果实。树生水果场景里树叶占比极高,如果样本里果实小、叶子大,模型学会“绿色纹理”就能在训练集拿高分,换一张没有果实的叶子图,它照样信心满满给出一个类。这是小样本分类最隐蔽的坑,准确率再高也说明不了问题。

解决:这不是调参能救的,要回数据。第一,检查样本里果实占比,大量图片果实不到三分之一时,考虑用检测模型或人工裁剪把果实先裁出来。第二,在验证集里加入负样本——不放任何果实的纯树叶、枝干、天空图,观察模型给这些图分了什么标签,如果集中偏向某一类,说明背景泄漏严重。第三,把 RandomResizedCrop 的 scale 下限抬回 0.7,强制模型看更大的果实局部。模型是黑匣子,但背景泄漏这个坑,用负样本一测就显形。

5.4 现象:train accuracy 冲到 100%,val accuracy 在第 20 个 epoch 后开始明显回退,30 轮时比平台期低 5 个点

原因:过拟合,而且往往是被增强“伪造”出来的过拟合。颜色增强过猛时,同一张原图会产生多个颜色分布差异极大的变体,模型在变体之间强行求不变性,最后把正常光照下的真实样本也“套滤镜”了——增强把数据分布拓宽到了真实分布的边界之外。

解决:把增强参数往回收。ColorJitter 的 saturation 和 hue 先砍掉或减半,只留 brightness/contrast 的 0.2~0.3。然后看 val loss 曲线,如果反弹拐点从第 15 轮推迟到第 25 轮,说明增强和训练时长不匹配,再配合 EarlyStopping 提前收手。另外注意,某些库默认开启的 MixUp/CutMix 在 500 张规模下会显著拖慢收敛,建议先关掉跑通,再考虑是否开启。

5.5 现象:val accuracy 高得离谱,接近 99%,把模型拿到网上随便找的果园图一测,准确率立刻崩到六成

原因:验证集泄漏。这类数据集常由同一个人在同一果园、同一天用连拍模式采集,文件名相邻的图是同一棵树上几秒内连拍的不同角度。按文件名顺序切分 train/val 时,相邻帧被分到两边,val 里的图和 train 的几乎一样,评估结果虚高,真实泛化能力被严重高估。这个坑几乎存在于所有带场景连续性的小型自然图像数据集。

解决:先验证再修正。方法一是从 val 每个类随机抽三张,和 train 里同类的图对比,肉眼判断相似度;方法二是按拍摄批次切分——文件名常带拍摄序号或时间戳,把连续段整体划入同一边,而不是逐张切。重新切分后用固定随机种子(比如 42)锁定划分,后续所有实验都用这一份,保证对比公平。血泪经验是:数据集的 val accuracy 不可信是常态,可信的是你重新划分后自己跑出来的数字。

6. 进阶验证:遮挡分析与模型对比,确认它学的真的是果实

6.1 遮挡敏感性分析:十行代码找到模型的视觉锚点

准确率和混淆矩阵只能告诉你“结果对不对”,不能告诉你“模型看的是什么”。小样本项目里这层验证尤其重要,因为数据量小,模型极可能走捷径。遮挡分析是我最常用的手段:用一个灰色方块在图片上滑动覆盖,记录每个位置覆盖后模型对正确类别的预测置信度,置信度大幅下降的位置就是模型做决策依赖的区域。

import torch import torch.nn.functional as F def occlusion_sensitivity(model, image, label, patch=32, stride=16): model.eval() h, w = image.shape[1], image.shape[2] conf = torch.zeros((h // stride, w // stride)) with torch.no_grad(): for i in range(0, h - patch + 1, stride): for j in range(0, w - patch + 1, stride): masked = image.clone() masked[:, i:i+patch, j:j+patch] = 0.0 logits = model(masked.unsqueeze(0).cuda()) conf[i // stride, j // stride] = F.softmax(logits, dim=1)[0, label] return conf

置零操作发生在 Normalize 之后,等价于把方块区域设为归一化均值,和灰色块效果一致。如果方块盖到果实中心时置信度崩掉,说明模型学对了;盖到叶子或空闲背景时置信度也崩,说明证据里有背景成分,结合 5.3 的负样本测试一起判断,能把模型的视觉锚点定位得很清楚。这一步做完,才算把这份数据的“模型能力”和“模型缺陷”都摸透。

6.2 把这份数据当评测台:轻量 Transformer 与 CNN 的对比习惯

500张的数据集不适合训练大模型,但非常适合当评测台。同一份随机划分、同一套增强,分别跑 ResNet18、YOLOv8n-cls 和 timm 里的轻量 Transformer(ViT-Tiny 或 DeiT-Tiny),对比它们在特征效率和过拟合速度上的差异。常见结果是 CNN 类在 500 张规模占优,Transformer 在 224 输入、50 epoch 下 val accuracy 略低且曲线更抖——这不是模型不行,是数据量不足以支撑注意力的自由度,这个结论反过来也说明小样本场景里 CNN 的归纳偏置更值钱。

我的习惯是把这些对比的 val accuracy、macro-F1、到达平台期的 epoch 数记成一张表,作为后续换数据、换任务时的参照基线。小数据项目的最后一关,就是先用遮挡分析和混淆矩阵说服自己“模型学的是果实”,再用同数据多模型对比说服别人“结论不挑模型”。这两个验证做完,这份约500张的已标注数据才算真正用透了。希望帮到你。

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

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

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

立即咨询