简介:这是一套面向图像分类实战的EfficientNet迁移学习工程,重点解决104种常见花卉的自动识别,适合希望从零开始掌握迁移学习、完成自定义分类项目的开发者。包内共2000个文件,以1993张花卉样本jpg为主,另有3个Python训练与评估脚本、2个json结果文件、1个txt文件与1个readme文档;其中脚本负责模型训练与评估,json保存各类指标结果,readme说明更换数据集的流程,压缩包约514.13MB,数据集与标签齐全,可直接运行。网络部分覆盖EfficientNet b0至b7八种结构,可自由选择是否加载官方预训练权重、是否冻结部分层,还提供Adam、SGD、AdamW三种优化器,配合多类别交叉熵损失和cos余弦退火学习率,便于在精度与速度间灵活权衡。训练与验证阶段均输出loss、准确率曲线,并生成混淆矩阵、召回率、精确率、F1值、特异度等系列评估图像与数值,各类别结果写入json文件;当前模型在训练集可达到约0.9准确率。按readme提示可替换数据集迁移到其他图像分类场景,目前已有124人学习或浏览。
1. 图像分类实战:EfficientNet迁移学习拿下104种花的识别,值得照着做
很多做图像识别的朋友一上来就追大模型,参数堆到几个亿,结果在自己那点数据集上还没一个预训练好的轻量网络好用。这篇文章要讲的EfficientNet迁移学习项目,就是用EfficientNet-B0做骨干网络,借助ImageNet预训练权重,在104种常见花卉数据集上做图像分类与图像识别。训练时间按小时算,单卡就能跑,最终准确率能到95%左右。这个方案轻量、可复现、适合快速落地,特别适合刚入门深度学习、想用有限算力跑通完整图像分类流程,或者要做植物识别类应用的工程师。
2. 选型与数据准备:EfficientNet为什么适合做迁移学习,数据集该怎么摆
2.1 EfficientNet轻量在哪里:复合缩放与B0的取舍
EfficientNet的核心是复合缩放:把网络的宽度(通道数)、深度(层数)、输入分辨率三个维度按固定系数一起放大,而不是像传统网络那样只加深或只加宽。基线模型B0参数只有约530万,在ImageNet上top-1准确率接近77%,这个性价比相当夸张。从B0到B7,参数和计算量逐级膨胀,精度提升却在递减,所以做迁移学习时B0和B1是最常用的起点。
对104种花卉这个任务来说,数据规模通常在几千到几万张,远小于ImageNet的百万级。这时候选B2、B3以上收益有限,训练时间和显存却成倍增加。我一般先在B0上跑通全流程,确认数据没大问题再考虑升级到B3。下表是B0到B3的差异,方便做取舍:
| 模型 | 参数量 | 预训练输入尺寸 | 相对推理耗时 | 适用场景 |
|---|---|---|---|---|
| EfficientNet-B0 | 530万 | 224x224 | 1x | 快速验证、小数据集、端侧部署 |
| EfficientNet-B1 | 780万 | 240x240 | 1.3x | 精度略高,训练时间可接受 |
| EfficientNet-B2 | 910万 | 260x260 | 1.8x | 数据较多、追求准确率 |
| EfficientNet-B3 | 1220万 | 300x300 | 2.6x | 单卡训练有余量、想榨精度 |
这里有个容易忽略的点:迁移学习制胜关键是预训练权重,而不是网络结构有多新。EfficientNet在ImageNet上学到的底层纹理、边缘、颜色特征对花卉识别完全适用,轻量网络在数据量不大的情况下反而不容易过拟合,这也是它适合做104类细粒度识别的原因。
2.2 104种花卉数据集的结构与划分
做图像分类项目,第一步是把数据整理成标准目录结构。常见做法是每个类别一个文件夹,文件夹名就是类别名。104个类别摆好后,用torchvision的ImageFolder直接加载,标签会按文件夹名称的字母序自动生成。
flower_data/ ├── train/ │ ├── 玫瑰/ │ │ ├── 001.jpg │ │ ├── 002.jpg │ │ └── ... │ ├── 荷花/ │ ├── 郁金香/ │ └── ... 共104个类 ├── val/ │ └── 104个类文件夹 └── test/ └── 104个类文件夹下面这个脚本按类别分层划分数据集,保证每个类别的图像按比例进入train/val/test,避免某些类别在验证集中消失:
import os import random import shutil src = "flower_data_all" # 原始数据:每个类一个文件夹 dst = "flower_data" ratios = (0.7, 0.15, 0.15) # train/val/test 比例 random.seed(42) # 固定随机种子,保证可复现 classes = [d for d in os.listdir(src) if os.path.isdir(os.path.join(src, d))] classes.sort() # 按字母序排列,使标签顺序稳定 for mode in ["train", "val", "test"]: os.makedirs(os.path.join(dst, mode), exist_ok=True) for cls in classes: imgs = [f for f in os.listdir(os.path.join(src, cls)) if f.lower().endswith((".jpg", ".jpeg", ".png"))] random.shuffle(imgs) n_train = int(len(imgs) * ratios[0]) n_val = int(len(imgs) * ratios[1]) splits = { "train": imgs[:n_train], "val": imgs[n_train:n_train + n_val], "test": imgs[n_train + n_val:], } for mode, files in splits.items(): out_dir = os.path.join(dst, mode, cls) os.makedirs(out_dir, exist_ok=True) for f in files: shutil.copy(os.path.join(src, cls, f), os.path.join(out_dir, f)) print(f"{cls}: total={len(imgs)}, " f"train={len(splits['train'])}, " f"val={len(splits['val'])}, " f"test={len(splits['test'])}")这段脚本的逻辑很简单:按类别遍历,打乱每类的图片列表,按比例切三段,复制到新目录。重点在于按类别分层随机,而不是把全量图片混在一起随机切。混合随机的问题在于某个类别数据少时,可能全被分到train,val里根本没有这个类,训练时模型从没见过该类样本的验证信号。
random.seed(42)保证了每次运行结果一致,104类数据划分结果可以复现,排查问题时不至于因为随机差异找不到根因。
2.3 数据加载与预处理:分辨率要和预训练权重匹配
数据加载直接用ImageFolder配合DataLoader,归一化的均值和标准差必须用ImageNet统计量[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]。原因很简单:预训练权重是在归一化后的ImageNet图像上训练的,推理和微调时输入分布不一致,结果就会打折扣。
from torch.utils.data import DataLoader from torchvision import datasets, transforms # ImageNet 统计量,与预训练权重匹配 IMAGENET_MEAN = [0.485, 0.456, 0.406] IMAGENET_STD = [0.229, 0.224, 0.225] # 训练集增强:适度即可,不要过度 train_transform = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.2, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3), transforms.ToTensor(), transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), ]) # 验证集/测试集只做缩放裁剪,不做随机增强 val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), ]) train_dataset = datasets.ImageFolder("flower_data/train", train_transform) val_dataset = datasets.ImageFolder("flower_data/val", val_transform) test_dataset = datasets.ImageFolder("flower_data/test", val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False, num_workers=4, pin_memory=True)参数说明:RandomResizedCrop(224)先从原图随机裁剪一块区域再缩放到224,scale=(0.2, 1.0)表示裁剪面积在原图的20%到100%之间,这相当于引入尺度变化,对花卉这种主体大小不固定的任务很有用。RandomRotation(15)角度范围15度,花卉照片各种角度都有,旋转增强能提升泛化能力。验证集Resize(256)再CenterCrop(224)是迁移学习标准做法,比直接缩放保留更多中心细节,也避免测试时因为随机裁剪造成指标不稳定。
B0的预训练输入是224x224,如果后续换B1就需要改成240,B2改260,B3改300,这个尺寸和模型必须对应,否则预训练权重的感受野和输入分布都对不上,精度会明显下降。
3. 迁移学习训练:冻结、分层学习率与关键参数
3.1 两种迁移模式:何时只训分类头,何时微调整个网络
迁移学习在图像分类项目里通常有两种做法。第一种是特征提取模式:冻结backbone的所有参数,只训练新替换的分类头。这种做法适合每类样本很少(比如几十张)的情况,backbone在ImageNet上学到的通用特征足够强,训练快且不容易过拟合。第二种是微调模式:解冻部分或全部骨干层,用较小学习率继续训练,让模型适应花卉数据的特有纹理和颜色分布。
以104种花为例子,每类几百张图时,我一般采用两阶段策略:第一阶段冻结backbone,只训分类头3到5个epoch,快速把分类器收敛到合理水平;第二阶段解冻backbone后面几个stage,用更小的学习率做全模型微调。这么做比一开始就全量微调稳定得多,原因是随机初始化的分类头梯度尺度大,如果一开始就反向传播到backbone,预训练权重容易被冲坏。
3.2 训练脚本逐行拆解:冻结策略与分组优化器
下面是一份可直接跑的PyTorch训练脚本核心部分:
import torch import torch.nn as nn from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR from torchvision import models device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 加载 ImageNet 预训练权重 model = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.IMAGENET1K_V1) in_features = model.classifier[1].in_features # 替换分类头:原分类头是 (Dropout, Linear(1280, 1000)) model.classifier[1] = nn.Linear(in_features, 104) model = model.to(device) # 冻结 backbone,只训练分类头(第一阶段) for param in model.features.parameters(): param.requires_grad = False # 分组参数:分类头 lr=1e-3,backbone lr=1e-4(第二阶段反向传播时生效) param_groups = [ {"params": model.classifier.parameters(), "lr": 1e-3}, {"params": model.features.parameters(), "lr": 1e-4}, ] optimizer = AdamW(param_groups, weight_decay=1e-4) # 解冻 backbone 后几个 stage(第二阶段使用) # for idx, stage in enumerate(model.features): # if idx >= 6: # for param in stage.parameters(): # param.requires_grad = True scheduler = CosineAnnealingLR(optimizer, T_max=30) criterion = nn.CrossEntropyLoss()逻辑说明:model.features是EfficientNet的backbone特征提取层,model.classifier[1]是最后的全连接层。替换分类头后,输出维度从1000变成104,这个新层是随机初始化的。注释中解冻代码表示当进入微调阶段时,从features的第6个stage开始允许梯度更新,前几个stage保留预训练参数不动。
这里参数分组是关键技巧:分类头的随机初始化参数学习率给高一些(1e-3),backbone参数给低一些(1e-4),因为预训练权重已经很接近最优解,学习率太大会破坏原有特征。AdamW比Adam多了正确的权重衰减实现,泛化效果更好,weight_decay=1e-4是迁移学习里比较稳妥的经验值。
3.3 训练循环:记录指标、保存最佳模型、早停
best_acc = 0.0 patience = 0 max_patience = 6 for epoch in range(30): # ---------- 训练 ---------- model.train() train_loss, train_correct, train_total = 0.0, 0, 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() # 梯度裁剪:防止预训练层梯度异常 nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() train_loss += loss.item() * images.size(0) _, preds = torch.max(outputs, 1) train_correct += (preds == labels).sum().item() train_total += labels.size(0) # ---------- 验证 ---------- model.eval() val_correct, val_total = 0, 0 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) val_correct += (preds == labels).sum().item() val_total += labels.size(0) train_acc = train_correct / train_total val_acc = val_correct / val_total print(f"Epoch {epoch+1:02d} " f"train_loss={train_loss / train_total:.4f} " f"train_acc={train_acc:.4f} " f"val_acc={val_acc:.4f}") # 保存验证集最优模型 if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_flower.pth") patience = 0 else: patience += 1 if patience >= max_patience: print("Early stop triggered") break scheduler.step()训练循环本身是常规流程,但有三个值得留意的点。
梯度裁剪max_norm=5.0是防止解冻backbone后预训练层出现梯度爆炸的保险。EfficientNet的BN层在迁移学习中容易出现梯度异常,裁剪后训练更稳。
Early Stopping的patience=6表示验证集准确率连续6个epoch不创新高就停止训练。104类花卉项目如果数据质量没问题,通常在15到25个epoch内收敛,早停阈值设太大反而浪费时间。
torch.save(model.state_dict())只保存权重不保存模型结构,加载时先构建模型再load_state_dict。这个习惯可以避免跨版本不兼容问题,也方便把B0换B3时直接复用训练代码。
3.4 关键训练参数速查表
| 参数 | 建议值 | 说明 |
|---|---|---|
| 优化器 | AdamW | 比SGD收敛快,迁移学习首选 |
| 分类头学习率 | 1e-3 | 新初始化的层需要较大学步长 |
| backbone学习率 | 1e-4 | 预训练权重微调幅度要小 |
| weight decay | 1e-4 | 抑制过拟合,过大反而欠拟合 |
| batch size | 32(B0/224) | 约占用4-6GB显存 |
| epoch | 30-50 | 配合早停,以验证集为准 |
| 输入尺寸 | 224(B0) | 与预训练权重严格匹配 |
| 梯度裁剪 | 5.0 | 防止BN层和深层梯度爆炸 |
batch size这里多说一句:如果显存不够,优先把batch降到16而不是强行开梯度累积。图像分类模型对batch size相对不敏感,16的batch在104类任务上损失几乎可忽略,调试阶段跑得更快。如果确实需要大batch,打开PyTorch的自动混合精度(AMP)可以省一半显存。
4. 避坑:EfficientNet花卉识别中常见的5个高频坑
4.1 验证集准确率高,真实场景却翻车
现象:拿测试集或新拍摄的照片推理,准确率和验证集差5到10个百分点,甚至更多,模型像换了个人。
原因:最常见的是数据划分不严谨。原始的104类花数据集里,同一个植物不同角度的照片往往来自同一个拍摄批次,如果随机划分时这些图片同时进了训练集和验证集,验证集就等于开卷考试。另外,可能把带水印、带背景框的图片当成了有效特征,模型学到的是环境信号而不是花卉本身,新场景一换背景准确率就崩。
解决:划分时必须按类别分层,前面脚本已经做了。同时检查训练集和验证集里有没有同source的重复或近似图片,比如从同一段视频抽的帧。我一般会在划分后把训练集和验证集的文件名做一个交集检查,确认没有同一棵植物在不同文件夹里。再进一步,把验证集换成网络上找的真实花卉照片,才能真正评估泛化能力。
4.2 训练早期loss不降反升,验证集准确率在50%以下徘徊
现象:第一个epoch loss比随机猜测还高,或loss在2到4之间震荡,val_acc一直上不去。
原因:分类头是随机初始化的,backbone被冻结时,如果分类头学习率设置过大,输出层权重在梯度方向上大幅震荡;反过来,如果backbone没有冻结且学习率设成了统一的1e-3,预训练权重会被冲坏。EfficientNet的BN层对梯度尺度特别敏感,这个问题会被放大。
解决:严格区分两个阶段。第一阶段冻结backbone只训分类头,学习率从5e-4到1e-3都可以;第二阶段解冻后再用1e-4以下的学习率。如果loss还是不正常,先不要动架构,把优化器换回SGD试试,有时AdamW的默认参数在极小数据集上反而不稳定。
4.3 训练集准确率99%,验证集只有85%
现象:train_acc一路冲到99%,val_acc卡在85%附近不动,两者差距持续拉大。
原因:过拟合。104类花的数据集,如果每类图片只有几十张,而模型训练了30个epoch以上,backbone特征会过度适配训练集的细节纹理和背景。原始ImageNet预训练权重本来能提供良好的泛化性,但微调时间太长把它破坏了。
解决:三管齐下。第一,数据增强加猛一点:RandomResizedCrop的scale下限从0.2降到0.08,加上RandomErasing随机遮挡一部分区域,迫使模型不能依赖局部单一特征。第二,weight decay从1e-4提到5e-4,分类头加dropout(EfficientNet默认有0.2,可以调到0.3)。第三,提前停微调,backbone解冻后只训练5到8个epoch,多数情况精度已经够用,继续训练收益很小。
4.4 CUDA out of memory:显存一炸,心态也炸
现象:训练到一半爆显存,或一加载数据就OOM。
原因:大部分时候不是模型太大,而是验证/测试时开了过大batch,或num_workers过高导致内存碎片。还有一种隐蔽情况:CenterCrop(224)之前如果Resize(256),数据加载器里的图像在进入模型前会被多份拷到显存,batch=64的val loader瞬间占用就能超过训练。
解决:先把batch降到16,用AMP混合精度把模型和前向计算切换成fp16,显存基本减半。再把num_workers调成2到4,pin_memory=True保持勾选。如果还爆,把输入尺寸临时改成192观察——虽然和预训练尺寸不符降低精度,但排查问题阶段可以快速跑通流程。等确认问题和数据量无关,再恢复224。
4.5 110类里总是混那三五对:相近花卉的识别难题
现象:混淆矩阵里错误高度集中在某几对类别——月季和玫瑰、鸢尾和菖蒲、百合和花木兰,其他类别表现正常。
原因:细粒度分类问题。这些花外形相似,颜色、花瓣层数、纹理差异很小。EfficientNet-B0只有530万参数,特征表达能力有限,尤其是在只做了默认图像增强的情况下,模型很难捕捉到足够细的区分线索。
解决:两个方向。第一个方向是给易混类单独做分类器:把易混类抽出来组成一个5到8类的子数据集,用B1重新训练一个子模型,推理时先用104类模型粗分类,碰到易混类再走子模型,逻辑简单但有效。第二个方向是升级到B3并把输入分辨率提到300,让模型能看到更多细节。如果还不行,就要考虑在损失函数上做文章,加一个辅助损失去拉大易混类特征的距离。
5. 验证与诊断:看见模型错在哪,才有迭代的依据
5.1 混淆矩阵定位易混对
训练完模型后,很多人只看总准确率就收工了。但104类分类项目真正值钱的信息在混淆矩阵里。下面这段代码把验证集预测结果和真实标签对齐,输出错误次数最多的TOP5类别对:
import torch import numpy as np from sklearn.metrics import confusion_matrix model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for images, labels in val_loader: images = images.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) class_names = val_dataset.classes # 104个类名 # 统计易混对:真实类为i,预测为j errors = [] for i in range(len(class_names)): for j in range(len(class_names)): if i != j and cm[i][j] > 0: errors.append((cm[i][j], class_names[i], class_names[j])) errors.sort(reverse=True) print("TOP5 易混对(真实类 -> 误判类 -> 次数):") for cnt, true_cls, pred_cls in errors[:5]: print(f" {true_cls} -> {pred_cls}: {cnt}次")这段代码执行后的输出会直接告诉你模型的盲区。比如结果显示郁金香经常被误判成睡莲,那就说明学习到的颜色特征多于形状特征——郁金香和睡莲在颜色上有较多重叠,而花瓣形态差异明显。这个诊断比盲目堆数据要精准得多。
5.2 把错分样本沉淀成hard set,给模型返工做准备
把每个验证集里预测错误的图片单独复制出来,构建一个hard set目录,这是迭代训练最重要的资产。下面的脚本把错误样本保存成“真实类名_预测类名_文件名.jpg”的格式,一眼就能看出问题类型:
import os import shutil hard_dir = "hard_set" os.makedirs(hard_dir, exist_ok=True) for img_path, true_cls, pred_cls in zip(val_images_paths, all_labels, all_preds): true_name = class_names[true_cls] pred_name = class_names[pred_cls] if true_name == pred_name: continue filename = os.path.basename(img_path) new_name = f"{true_name}_as_{pred_name}_{filename}" shutil.copy(img_path, os.path.join(hard_dir, new_name)) print(f"hard set 已生成,共 {len(os.listdir(hard_dir))} 张错误样本")这里val_images_paths需要在加载数据时提前保留文件路径,可以用val_dataset.samples取到:每个元素是(路径, 标签)。保存成hard set后,人工过一遍这些图片,区分三类问题:标注错误、图像质量差、模型确实分不清。前两类直接修正数据,第三类才值得调整模型和增强策略。
5.3 CAM可视化验证模型在看什么
准确率不能回答“模型为什么对”,CAM(类激活图)可以。EfficientNet-B0没有全局平均池化前的单一特征图很方便做CAM:
from torchvision.transforms import functional as F # 取最后一层卷积输出 final_conv = model.features[7][0] def get_cam(image_path, true_label): img = F.resize(F.to_tensor(Image.open(image_path).convert("RGB")), (224, 224)) img = F.normalize(img, IMAGENET_MEAN, IMAGENET_STD) img = img.unsqueeze(0).to(device) features = [] def hook_fn(module, input, output): features.append(output.detach()) handle = final_conv.register_forward_hook(hook_fn) output = model(img) handle.remove() # 取目标类别的梯度 model.zero_grad() one_hot = torch.zeros(1, 104).to(device) one_hot[0][true_label] = 1 output.backward(gradient=one_hot) weights = final_conv.weight.grad.mean(dim=(2, 3)) # 每个通道的权重 cam = torch.matmul(weights, features[0].squeeze(0).flatten(1)) cam = cam.reshape(7, 7).cpu().numpy() # 224/32 = 7 return camCAM图叠加到原图上就能看到模型激活区域集中在花瓣还是叶片。如果大量样本激活在背景上,说明模型学到了背景特征,数据增强里的RandomResizedCrop尺度范围要调大;如果激活集中在花蕊,说明分类依据是可靠的花部特征,confidence就更有参考价值。
5.4 验证指标不止准确率:单类召回率与置信度分布
104类分类任务,每类的召回率差异往往比总体准确率更值得关注。某类花样本少且形态多变,召回率可能只有70%,拖低整体表现。建议单独输出每个类的准确率、召回率、F1,找出最差的5个类,优先补充这些类的训练数据。
置信度分布也很直观:把验证集每张图片的softmax最大概率统计成直方图。如果大量图片置信度在0.5以下但预测正确,说明模型有潜力但训练不充分;如果置信度很高但预测错误,说明模型“过度自信地错”,这是典型的特征学习偏差,CAM可视化正好用得上。
6. 进阶技巧:从“跑通”到“能用”的四件小事
当B0模型稳定跑出94%-95%验证准确率后,如果还想再往上走,按性价比从高到低做这四件事。
第一,推理时TTA(Test Time Augmentation)。把验证/测试图片做水平翻转和多个尺度(0.8x、1.0x、1.2x)的预测,取平均概率作为最终结果。实现上只需循环三到五次前向推理,不需要改模型。以我的经验,这个操作几乎免费地提升0.5到1个点的准确率,尤其在易混类上效果明显。
第二,两级训练策略。先冻结backbone训分类头到收敛,把模型保存为checkpoint;再加载这个checkpoint,解冻backbone最后两到三个stage,分类头学习率降到2e-4、backbone学习率降到2e-5继续训。这个两阶段流程比一次性全量微调稳定得多,是迁移学习中比较可靠的做法。
第三,导出ONNX或TorchScript进行推理加速。训练和推理共用一套代码没问题,但部署到服务端或边缘设备时,导成ONNX再用ONNX Runtime推理,速度通常提升一到两倍。EfficientNet结构的算子对ONNX Runtime支持很好,不会像某些Transformer模型一样碰到OOP算子兼容问题。导出的同时记得固化图像的预处理流程:resize、normalize都放进模型里或固定在推理脚本里,否则很容易出现“训练时正常、部署时掉点”的坑。
第四,单类数据不足时的兜底策略。104类里通常有一到两类样本特别少,比如只有20到30张。与其硬训,我习惯的做法是从模型的hard set里找出被误判到这些类的样本,看看是“缺类内多样性”还是“类间相似”。样例少的花最好是收集更多该花不同角度、不同光照的图片,这个办法永远是第一优先。如果临时补充不了,就用类别加权损失给少样本类更高的惩罚权重,能起一点作用但别指望太多。
我做这个项目的最大教训是:模型结构的选择只影响几个点的准确率浮动,真正决定成败的是数据划分的严谨性和验证集设计的合理性。每次训练前先确认数据没有泄漏、没有错误标注,训练后先看混淆矩阵而不是只看acc。EfficientNet这个方案最大的价值就在于它足够轻、足够快,让你有充足的时间把迭代精力放到数据和诊断上,而不是花大量成本跟模型架构缠斗。希望帮到你。
本文还有配套的精品资源,点击获取