简介:这份OpenCV水果识别样本围绕苹果、香蕉、梨子三类目标整理,适合正在学习图像预处理、特征提取、分类器训练与测试的计算机视觉入门者和课程实践者,可用于搭建水果分类实验、验证传统机器学习或深度学习流程。压缩包共1624个文件,以1618张jpg水果图像为主体,配有5个xml标注文件和1个iml工程配置,整体约17.82MB,便于直接划分训练集与测试集并开展样本清洗、数据增强等练习。目前已有12819人学习下载,说明其在相关课程与练手项目中具有一定参考价值。读者可据此复现从灰度化、直方图均衡化到特征描述与分类评估的完整链路,也能借多类别样本观察光照、角度和背景差异对识别效果的影响,为后续模型调参与泛化能力分析提供素材。
1. OpenCV 水果识别样本先看清:三分类的坑大多不在模型
手里这份 opencv 水果识别样本,苹果、香蕉、梨子各一个目录,量不大,属于典型的小样本集合。很多人拿到水果样本第一反应是「这点数据能训出什么」,然后直接上 CNN,跑出七十几的准确率就怀疑样本质量。实际情况往往相反——翻车的环节基本都在模型之前:路径读不进来、通道数不统一、同一张原图的增强版本同时落进训练集和验证集。三个类别的颜色差异足够明显,苹果偏红、香蕉偏黄、梨子青黄带斑点,这个特点让颜色特征就能撑起一个不差的 baseline,也正好把预处理、划分、评估这条链路完整走一遍。适合刚接触 OpenCV 的从业者,也适合需要一份干净三分类水果样本做对照实验的人。
2. 目录结构到 Mat:读图链路里藏着三个硬约束
2.1 先摸清目录布局和命名,再谈训练
样本包解压出来通常是这样的结构,类别名就是子目录名:
| 层级 | 示例 | 说明 |
|---|---|---|
| 根目录 | fruit_samples/ | 解压后的统一入口 |
| 类别目录 | apple/banana/pear/ | 通常一个类别一个文件夹 |
| 图片文件 | apple_001.jpg | 后缀可能混杂 jpg/png/jpeg |
| 干扰文件 | .DS_Store、Thumbs.db | 系统生成的隐藏文件,必须过滤 |
这个结构刚好和torchvision.datasets.ImageFolder的约定一致,后面迁移学习那条线可以直接用。但在读图之前,我一般会先做三件事:统计每个类别的文件数,确认没有某个类别只有十几张;统计后缀分布,确认没有混进 webp 或者无后缀文件;扫一遍文件名,确认没有空格和全角字符。这三步加起来不到二十行代码,但能把后面一半的报错挡在门外。
血泪经验是文件名里的空格。banana 003.jpg这种命名在cv2.imread里不一定出问题,但一旦走subprocess或者某些标注工具,路径就会被截断,最后表现为「文件明明在,就是读不到」。
2.2 批量读取与可读性校验
读图这一步我不用cv2.imread直连磁盘,改成先读字节流再解码。原因很实际:样本包里只要有一张图是中文名或者路径里带中文,imread在部分平台会静默返回None,而且不报错,直到训练时才炸。
import os import numpy as np import cv2 ROOT = "fruit_samples" # 样本包解压后的根目录 CLASSES = ["apple", "banana", "pear"] # 类别名要和子目录名严格一致 VALID_EXT = {".jpg", ".jpeg", ".png", ".bmp"} def load_image(path): """先读字节再解码,绕开非 ASCII 路径下 imread 返回 None 的问题""" buf = np.fromfile(path, dtype=np.uint8) if buf.size == 0: return None img = cv2.imdecode(buf, cv2.IMREAD_COLOR) # 强制三通道,丢掉 alpha return img records, bad = [], [] for idx, cls in enumerate(CLASSES): cls_dir = os.path.join(ROOT, cls) if not os.path.isdir(cls_dir): print("目录缺失:", cls_dir) continue for name in sorted(os.listdir(cls_dir)): if os.path.splitext(name)[1].lower() not in VALID_EXT: continue # 过滤 .DS_Store 之类的干扰文件 p = os.path.join(cls_dir, name) img = load_image(p) if img is None: bad.append(p) # 单独收集,不中断整个流程 continue records.append((p, idx, img.shape[0], img.shape[1])) print("可读样本:", len(records), "损坏样本:", len(bad)) for p in bad[:10]: print(" 打不开:", p)np.fromfile读的是原始字节,cv2.imdecode负责解码,两个组合起来对路径编码的容忍度比imread高很多。IMREAD_COLOR这个标志很关键,它会强制输出三通道 BGR,哪怕原图是灰度图或者带透明通道的 PNG,读出来也是统一的三通道,后面就不需要为通道数写分支判断。
bad列表要单独留着,不要读完就扔。这些文件要么是零字节的占位文件,要么是扩展名写错(把 webp 改成了 jpg),要么是真的损坏。把它们打印出来人工看一眼,能反推出样本包是不是在压缩传输中出过问题。
参数说明:VALID_EXT只放这几种是因为 OpenCV 对这几个解码最稳;如果样本里有webp,要么提前转码,要么确认opencv-python编译时带了对应支持,否则会读到None然后被误判成损坏文件。
2.3 尺寸、通道与色彩空间统一
样本包里的图尺寸几乎不可能一致,手机拍的是 3024×4032,网图可能是 800×600,还有裁剪过的小图。直接resize到 224×224 会有一个副作用:长宽比被强行拉伸,香蕉这种细长物体变形会很明显,颜色特征的分布也会跟着偏。我一般用「短边缩放 + 中心裁剪」的写法。
INPUT_SIZE = (224, 224) def preprocess(img, size=INPUT_SIZE): """短边等比缩放到目标尺寸,再中心裁剪,避免长宽比失真""" h, w = img.shape[:2] scale = max(size[0] / w, size[1] / h) # 取较大比例,保证缩放后不短于目标 nw, nh = int(round(w * scale)), int(round(h * scale)) # 缩小用 INTER_AREA 减少摩尔纹,放大才用 INTER_LINEAR interp = cv2.INTER_AREA if scale < 1 else cv2.INTER_LINEAR resized = cv2.resize(img, (nw, nh), interpolation=interp) x0 = (nw - size[0]) // 2 y0 = (nh - size[1]) // 2 return resized[y0:y0 + size[0], x0:x0 + size[1]] def to_hsv(img): """颜色特征一律在 HSV 空间算,BGR 三个通道对光照的耦合太强""" return cv2.cvtColor(img, cv2.COLOR_BGR2HSV)INTER_AREA和INTER_LINEAR的选择不是强迫症。INTER_AREA在缩小场景下是按区域取均值,能压掉高频噪点;INTER_LINEAR在放大时是双线性插值,边缘更平滑。两者的差异在小图上肉眼可见,而这个差异会直接传到后面的特征向量里。
色彩空间那块要注意:OpenCV 的 HSV 里 H 的取值范围是 0 到 180,不是 0 到 360。这个细节在写calcHist的区间参数时如果按 360 写,直方图的后半段会全是零,颜色特征直接废掉一半。翻车过的人应该都记得这个 180。
注意:样本里如果有偏暗的图(比如室内拍摄的香蕉),统一 resize 之后亮度不做归一化,颜色直方图会被弱光拉偏。后面第 3 章的训练脚本里我加了一步亮度归一,这里先把读图链路走通。
3. 传统特征路线:颜色直方图加 HOG 撑起的 baseline
3.1 为什么小样本要先做传统特征,而不是直接上 CNN
三个类别、每类几十张的量级,从头训一个 CNN 基本等于随机初始化加过拟合。这时候传统特征的价值就体现出来了:颜色直方图不需要训练,HOG 的梯度方向也不需要训练,整个流程只有 SVM 的几百个参数在学,泛化反而不容易崩,而且耗时以秒计,可以反复试参数。
这条路线还有一个隐性价值——它是后面 CNN 路线的对照组。如果 CNN 跑出来 85%,传统特征跑出来 80%,那说明数据本身的可分性就在这个水平附近,瓶颈在样本而不在模型;如果传统特征只有 50%,而 CNN 有 90%,那就要回头查是不是验证集划分出了问题,或者 CNN 把背景当成了捷径特征。这个判断依据比单看一个准确率数字有用得多。
需要说清楚的边界:颜色直方图对光照和饱和度敏感,HOG 对旋转和尺度敏感,两者拼接后是典型的手工特征,遇到复杂背景(比如水果放在木桌上、旁边有别的物体)就会明显掉点。所以这条线定位是 baseline 和对照,不是最终方案。
3.2 HSV 三维直方图:把颜色差异变成可计算的距离
苹果和香蕉的区分本质上是色相分布的差异。在 HSV 空间里,红色的 H 集中在 0 到 10 以及 170 到 180 两段,香蕉的黄色集中在 20 到 35,梨子的青黄落在 35 到 60。这个分布差异用三维直方图能直接刻画出来。
import cv2 import numpy as np def hsv_hist(img, bins=(8, 8, 8)): """三维 HSV 直方图,三通道各 8 个 bin,归一化后展平""" hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) hist = cv2.calcHist( [hsv], [0, 1, 2], None, bins, [0, 180, 0, 256, 0, 256] # H 上限是 180,不是 360 ) hist = cv2.normalize(hist, hist, norm_type=cv2.NORM_L1) # L1 归一化,抗尺寸差异 return hist.flatten().astype(np.float32)三个通道各取 8 个 bin,一共 512 维。bin 数不是越多越好:8 个 bin 对色相来说是每 22.5 度一格,刚好能把红、黄、青黄分开;如果加到 16 个 bin,维度翻倍,小样本下 SVM 更容易过拟合,而且相邻 bin 之间的计数很稀疏,特征反而更不稳定。
cv2.NORM_L1这步归一化不能省。它让直方图的总和恒为 1,这样一张近距离特写的苹果和一张远景的苹果,特征向量在同一量级上,SVM 才不会因为图片里水果占比不同而把两者判成不同类。视觉上这像是细节,实际对准确率的影响经常在两三个百分点。
参数怎么调:如果发现苹果和梨子的混淆特别严重,可以把 H 通道的 bin 加大到 12 或 16,让红色的位置更细;如果光照差异大导致 S 和 V 通道抖动,反过来把 S、V 的 bin 减到 4。这个调整要看混淆矩阵定位问题,不要盲调。
3.3 HOG 特征与 SVM:形状信息补上颜色分不开的部分
只有颜色特征的话,青苹果和梨子会互撞。HOG 统计的是局部梯度方向分布,能刻画出苹果的圆形轮廓和梨子的梨形轮廓,正好补上这块。
from skimage.feature import hog def hog_feat(img, size=(128, 128)): """灰度化后提 HOG,参数针对 128x128 的小图做过缩放""" gray = cv2.cvtColor(cv2.resize(img, size, interpolation=cv2.INTER_AREA), cv2.COLOR_BGR2GRAY) return hog( gray, orientations=9, # 9 个方向 bin,覆盖 0-180 度 pixels_per_cell=(16, 16),# 细胞 16x16,128 图上是 8x8 个细胞 cells_per_block=(2, 2), # 2x2 个细胞组成一个块,做块内归一化 block_norm="L2-Hys", # 对光照变化最稳的归一化方式 feature_vector=True, ).astype(np.float32)pixels_per_cell=(16, 16)是配合 128×128 输入定的。如果按默认的 8×8,128 的图上会切出 16×16 个细胞,特征维度翻到接近八千维,小样本下 SVM 训练会明显变慢而且更容易过拟合。block_norm选L2-Hys是因为它对高对比度边缘有截断,能压住反光造成的梯度异常。
把两个特征拼起来,接一个 RBF 核 SVM:
import numpy as np from sklearn.svm import SVC from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score # imgs 是预处理后的 BGR 图,labels 是 0/1/2 X = np.array([ np.hstack([hsv_hist(cv2.resize(im, (128, 128))), hog_feat(im)]) for im in imgs ], dtype=np.float32) y = np.array(labels) # stratify 保证三个类别在训练/测试里的比例一致,小样本必须加 X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) pipe = Pipeline([ ("scaler", StandardScaler()), # 颜色特征和 HOG 量纲差一个数量级,先标准化 ("svm", SVC(kernel="rbf", C=10, gamma="scale", probability=True)), ]) pipe.fit(X_tr, y_tr) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(pipe, X, y, cv=cv, scoring="accuracy") print("五折准确率:", np.round(scores, 3), "均值:", scores.mean().round(3))StandardScaler是必须的。HSV 直方图每个维度在 0 到 1 之间,HOG 的维度在 0 到 0.3 之间波动,量纲不同会让 RBF 核的距离计算被颜色特征主导。做完标准化两部分才在同一个尺度上参与距离计算。
gamma="scale"是 sklearn 的默认策略,取1 / (n_features * X.var())。小样本下这个自适应值通常比手调好,因为特征维度高、方差小,手调的gamma很容易偏大导致过拟合。C=10是经验值,如果五折的方差特别大(比如 0.62 到 0.91),说明模型对划分敏感,这时候把C降到 1 到 3 会更稳。
probability=True是给后面画混淆矩阵和看置信度用的,代价是训练时多了内部交叉验证,小样本下多花几秒,可以接受。
3.4 混淆矩阵比准确率信息量大得多
准确率是个被平均掉的数字。三个类别的样本量如果不完全相等,它会把少数类的错误藏起来。
from sklearn.metrics import confusion_matrix, classification_report y_pred = pipe.predict(X_te) print(classification_report(y_te, y_pred, target_names=CLASSES)) print(confusion_matrix(y_te, y_pred, labels=[0, 1, 2]))看混淆矩阵的时候重点看两件事:有没有某个类别的召回率明显低于其他类,以及错误是不是集中在某两个类之间。前者通常对应样本量不足或者该类内部差异太大(比如香蕉有未熟、成熟、过熟三种外观),后者通常对应特征区分度不够——苹果和梨子混淆就说明 HOG 的轮廓信息没起到作用,可以考虑加大 HOG 的权重或者换一个pixels_per_cell。
这里有个容易忽略的点:classification_report的target_names顺序必须和标签编码一致。CLASSES = ["apple", "banana", "pear"]对应标签 0、1、2,如果中间某个目录缺失,编码会错位,报告里的类名就全错位了。这类问题是典型的「数据没问题、报告看错」的翻车方式。
4. 迁移学习路线:小样本下冻结和解冻的取舍
4.1 小样本为什么优先迁移而不是从头训练
传统特征能到 80% 左右就到顶了,再往上需要模型自己学出「香蕉弯曲的角度」「梨子表面的斑点分布」这类不好手工描述的特征。从头训一个 CNN 在几十张每类的规模下几乎没有可能收敛好,但预训练模型不同——它的浅层卷积核已经学会了边缘、颜色块、纹理这些通用模式,直接拿来用,只需要重新训最后那层分类头。
这条路线的前提是输入分布别偏太远。水果照片和 ImageNet 里的自然图像分布接近,迁移效果通常不错;但如果样本全是纯白背景的摆拍图,而预训练模型见的多是复杂场景,浅层特征会有偏差,这时候微调的解冻层数要适当多一点。
4.2 数据增强与 ImageFolder 的组织方式
增强的策略要贴着水果这个场景设计。水平翻转是安全的,苹果和梨子翻转后还是苹果和梨子;但垂直翻转就要小心,倒过来的水果在现实中很少见,会让模型学到不该学的模式。颜色抖动是有必要的,因为样本里香蕉的成熟度差异很大,从青黄到带黑斑都有,ColorJitter 能让模型对这种自然差异更宽容。
from PIL import Image import torch from torch.utils.data import Dataset from torchvision import datasets, transforms tf_train = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), # 模拟不同拍摄距离 transforms.RandomHorizontalFlip(p=0.5), # 只做水平翻转 transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.05), # hue 幅度要小 transforms.RandomRotation(degrees=12), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) tf_eval = transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) class SubsetWithTransform(Dataset): """给训练/验证子集分别挂不同的 transform,避免增强泄漏到验证集""" def __init__(self, base_ds, indices, transform): self.base = base_ds self.indices = indices self.transform = transform def __len__(self): return len(self.indices) def __getitem__(self, i): path, label = self.base.samples[self.indices[i]] img = Image.open(path).convert("RGB") # 统一成 RGB,和预训练权重对齐 return self.transform(img), labelhue=0.05这个幅度是有意压小的。HSV 里色相偏移一旦过大,苹果可能被改成橙色,模型就学到了错误的颜色语义。亮度、对比度、饱和度可以放宽到 0.3,这三个维度的变化在真实拍摄条件下很常见。
SubsetWithTransform这个类是我吃过亏之后固定下来的写法。如果直接用random_split得到一个子集,然后在训练循环里手动对 batch 做增强,验证集很容易被同一套增强污染。用这个包装类,训练子集挂tf_train,验证子集挂tf_eval,物理隔离,不会串。
4.3 冻结主干、替换分类头、再解冻最后一块
import torch.nn as nn from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 第一步:冻住全部参数 for p in model.parameters(): p.requires_grad = False # 第二步:替换分类头,新层默认 requires_grad=True model.fc = nn.Linear(model.fc.in_features, 3) # 第三步:解冻最后一个残差块,让高层语义特征也能微调 for p in model.layer4.parameters(): p.requires_grad = True trainable = [n for n, p in model.named_parameters() if p.requires_grad] print("参与训练的参数:", trainable)冻结全部再单独解冻的顺序不能反。如果先解冻layer4再统一冻结,会把刚解冻的部分又关掉。layer4是 ResNet 里语义层级最高的一块,它的特征和水果这种具体物体类别最相关,解冻它的收益最大;再往前的layer3、layer2学的是纹理和局部结构,小样本下解冻它们带来的收益不明显,反而增加过拟合风险。
优化器的学习率也要分档,分类头用大学习率,解冻的layer4用小学习率:
import torch opt = torch.optim.AdamW([ {"params": model.fc.parameters(), "lr": 1e-3}, {"params": model.layer4.parameters(), "lr": 1e-4}, ], weight_decay=1e-4) crit = nn.CrossEntropyLoss()1e-4这个量级是给已经预训练好的层的,太大等于把学到的权重冲掉;1e-3给全新的分类头,让它快速找到方向。weight_decay=1e-4在小样本下配合早停,能明显压低过拟合。
训练循环里我固定记录训练损失、验证损失、验证准确率三条曲线,早停的判据用验证损失而不是准确率。准确率在小样本上会跳,验证损失更连续。耐心值设 5 个 epoch,触发就回滚到验证损失最低的那份权重。
4.4 分类报告之外,要回看被错分的原图
classification_report能告诉你哪个类别召回低,但告诉不了你为什么。真正有用的一步是把验证集里预测错误的样本路径打出来,按真实类别分组,人工扫一遍。
常见的发现有三类:一是背景干扰,比如苹果那张图旁边有一根香蕉的边角,模型被带偏;二是标注问题,某个文件放在了错误的类别目录;三是边缘样本,比如严重过熟的香蕉,人眼判断都要犹豫一下。前两类是数据问题,改数据;第三类才是模型能力边界,可以考虑扩充这类边缘样本而不是调模型。
这一步花的时间大概是调参的两倍,但收益也在这个量级。我见过准确率停在 0.82 上不去的案例,回看错误样本发现是十几个文件放错了目录,改完直接到 0.93。
5. 避坑与排查:水果样本落地时的五个真实翻车点
5.1 数据侧的三个坑
现象一:训练时DataLoader报stack expects each tensor to be equal size。原因通常是某个类别的图里有单通道灰度图,convert("RGB")没做,或者某张图是 4 通道 RGBA 的 PNG,ToTensor之后通道数不一致。解决方式是在读图那一步统一强制三通道,也就是第 2 章IMREAD_COLOR那个写法,或者自定义 Dataset 里Image.open(path).convert("RGB")不要省。这类错误不会在扫描阶段暴露,只在拼 batch 时炸,定位成本高,所以要在入口就堵死。
现象二:训练集准确率一直涨,验证集第一次评估就有 95%,后续不再提升。这是典型的增强泄漏。同一张原图被随机增强出多个副本,分别落进了训练集和验证集,验证集相当于见过原图。根源是很多人的划分方式是在ImageFolder之后对增强后的数据集做random_split,而增强是每次__getitem__时随机生成的,同一路径的图会被重复采到。正确做法是先按文件路径划分出训练/验证清单并落盘,再分别构造 Dataset,清单文件一旦固定就不再变动。第 6 章给了一份脚本。
现象三:香蕉类别的召回率明显偏低,混淆矩阵里大量被判成梨子。先别改模型。打开被错分的那批图看一眼,十有八九是过熟香蕉,表皮已经发黑偏褐,在 HSV 空间的色相分布确实往青黄方向偏了。这属于类别内部的自然变异,不是特征提取的问题。解决办法有两个方向:要么在样本层面补充这类边缘样本,让模型见过更多样的香蕉;要么在增强里加ColorJitter的 hue 和 saturation 幅度,模拟成熟度变化。前者更扎实,后者更快见效。
5.2 训练侧的两个坑
现象四:整体准确率 0.88 看着不错,但梨子的 F1 只有 0.6。这是类别不平衡加指标误读的组合。如果梨子样本只有苹果的三分之一,模型倾向于把不确定的梨子判成其他类,整体准确率仍然好看。判断方式是看classification_report里的macro avg而不是accuracy,前者对每个类别等权,后者被大类别主导。解决上,小样本下不建议直接上重采样,容易放大小类噪声,更稳的是在CrossEntropyLoss里传weight,权重按类别样本数的倒数归一化,改动一行代码,收益立竿见影。
现象五:微调之后模型对背景敏感,换个桌面颜色就崩。迁移学习在小样本上很容易学到背景捷径,尤其是样本里如果所有苹果都放在白瓷盘上、所有香蕉都放在木桌上,模型会直接学桌面纹理。排查方式是把验证集图片的背景做一次替换或裁剪,看准确率掉多少。掉得厉害就说明学的是背景。缓解手段是在增强里加RandomResizedCrop的scale下限调低(比如 0.5),逼模型只看局部;或者对背景做离线合成,把水果抠出来贴到随机背景上,这个做法在最后一章展开。
6. 把样本用扎实:固定划分清单加背景合成
小样本项目最容易被忽略的一件事是划分的复现性。每次跑实验都重新random_split,两次的验证集不同,指标就没法比较,调参变成玄学。我现在的习惯是先落一份 CSV 清单,把文件路径、类别、划分(train/val)三列写死,后面所有实验都读这份清单。
import csv, random, os from collections import defaultdict ROOT, CLASSES = "fruit_samples", ["apple", "banana", "pear"] random.seed(42) # 固定种子,保证清单可复现 rows = [] for idx, cls in enumerate(CLASSES): files = [f for f in os.listdir(os.path.join(ROOT, cls)) if f.lower().endswith((".jpg", ".jpeg", ".png"))] random.shuffle(files) n_val = max(1, int(len(files) * 0.2)) # 每类至少留一张验证 for i, f in enumerate(files): rows.append([os.path.join(ROOT, cls, f), idx, "val" if i < n_val else "train"]) with open("split.csv", "w", newline="", encoding="utf-8") as fp: csv.writer(fp).writerows(rows)这份清单的价值在于可追溯。哪天指标异常,直接回去看是不是某个类别在验证集里只分到一张图,而不是在一堆随机数里猜。
再往上一层是背景合成。水果样本的通病是拍摄场景单一,解决办法是离线做一批合成图:用 GrabCut 或者简单的阈值把水果从原背景抠出来,贴到随机选取的纯色、木纹、布料背景上,再叠一点亮度扰动。合成图只进训练集,绝不进验证集,否则验证指标就失去意义了。合成比例我一般控制在原样本量的 1 到 1.5 倍,太多会让模型偏向合成图的分布,反而在真实图上掉点。
验证合成是否有效,别看训练损失,看验证集里那些背景和训练集差异最大的样本,单独统计它们的准确率变化。这个子集的表现才是合成增强是否真的提升了泛化的证据。
从那以后我每次拿到一份新的水果样本,第一件事都是先写划分清单、再扫一遍可读性和尺寸分布,最后才开始动特征和模型。这个顺序颠倒过几次,每次都付出了双倍的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取