我一直觉得,中药房里那些老师傅抓药前扫一眼就能认出几十种药材的本事,放到计算机视觉领域就是一个非常典型的细粒度图像识别难题——很多中草药干燥之后,颜色、纹理、形态上的差异极其微妙,普通人根本分不清,模型想学好也不容易。最近我把一个基于PyTorch和EfficientNetV2的中草药识别分类系统从头到尾完整跑通了一遍,从数据准备、模型选型、训练调参到部署落地都踩了不少坑,今天就把这套完整的技术链路和源码设计思路整理出来,给想做类似图像分类项目的朋友参考。这套项目不仅能作为深度学习入门的完整实战案例,也适用于农产品识别、矿物标本分类、植物叶片识别这类“类间相似度高、类内差异大”的细分场景。
1. 中草药识别这个任务,难点不在模型而在数据
很多朋友一看到“深度学习图像分类”就急着选模型、调参数,实际上对于中草药识别这类细分领域,真正决定项目上限的往往是数据。模型结构再好,喂进去的数据各种脏、各种偏,结果一样惨不忍睹。我在项目启动阶段最深的体会就是:数据工作占了整个项目至少六成的精力。
1.1 数据采集与标注规范
中草药图像和通用物体分类(比如猫狗、汽车)有个本质区别:通用分类中同类物体的外观一致性较高,而中草药的形态会随着产地、采收季节、干燥工艺、拍摄角度发生巨大变化。同一个药材,切片和整株入镜完全像两个东西;自然光下和灯光下拍摄的色泽也差很多。所以采集阶段要有意识地覆盖多样性。
我当时做了一套采集规范,关键几点:
- 每类样本数量不低于800张,控制在800到1200张之间,总数在11个类别左右。太少模型学不到稳定的类内特征,太多则采集和标注成本直线上升。
- 每张图尽量覆盖不同角度、不同光线条件、不同状态(干品、鲜品、切片),条件允许的话加入部分自然背景的实拍图。
- 图像原始分辨率建议不低于800像素短边,后续统一缩放,保留足够的纹理细节给模型学。
- 类别标签以中文通用名为主,方便后面做界面展示,同时建立一张类别映射表,为训练时的数字标签做转换。
标注这个环节也容易出问题。中草药类别之间有些是“连坐”性质的相似——比如某两种药材放在一起,标注人员一旦对标准模糊,就会导致标签内部混入大量噪声,而这种噪声在训练时很难被模型“自己纠正”。我的做法是每张图至少经过两人交叉标注,不一致的样本单独抽出来由有经验的人定夺,宁可少要一批数据也不能让脏标签进训练集。
1.2 数据增强策略:既要丰富又要防过拟合
数据增强是这类小数据集项目里的“救命稻草”。我用的是PyTorch原生的torchvision.transforms组合管线,整个过程拆成训练期和验证期两套。
训练期的增强管线:
transform_train = transforms.Compose([ transforms.Resize((256, 256)), # 先放大一点,给随机裁剪留空间 transforms.RandomCrop((224, 224)), # 随机裁剪到模型输入尺寸 transforms.RandomRotation(15), # 小幅旋转,模拟拍摄角度偏差 transforms.RandomHorizontalFlip(), # 水平翻转 transforms.ColorJitter( brightness=0.3, contrast=0.3, saturation=0.3, hue=0.1 ), # 模拟不同光照条件 transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ])验证期管线相对简单:
transform_valid = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ])这里有个经验点:训练期的RandomCrop(224)前先放大到256,如果直接对原始尺寸做裁剪,很容易出现样本主体被切掉一半的情况。放大后再随机裁剪,即使裁偏也仍然保留大部分主体。我测试过直接Resize((224, 224))不做随机裁剪,Top-1准确率大概掉了2个百分点。
1.3 数据划分:留出验证集的边界感
数据划分看起来简单,但中草药这类数据有个特别容易踩的坑——同一次拍摄的连拍照片,或者同一批次药材的多次拍摄,彼此之间高度相关。如果这些照片被随机打乱分到训练集和验证集,验证集相当于“透题了”,模型没学到泛化能力,只是记住了这批药材的特征,验证集分数虚高。
所以我的划分原则是:按拍摄批次或来源分组,保证同一来源的图片整体进入同一个集合,不让它们跨集合出现。具体做法是给每张图加一个来源ID字段,在脚本里按来源ID划分训练集、验证集、测试集,比例大致是7:2:1。这样验证集的成绩才真实反映模型面对“没见过的药材批次”时的表现。
2. EfficientNetV2选型思路:为什么不选ResNet,也不用EfficientNetV1
模型选型是这类项目最容易被忽略但最体现功力的地方。中草药识别需要的不是尽可能“宽”的模型,而是能在固定算力预算内把训练和推理效率拉满的模型。我要用到的EfficientNetV2系列,恰好是这个方向上的一个标杆选择。
2.1 训练感知缩放与MBConv的升级
EfficientNetV2相对V1的改进,核心在于训练感知缩放(Training-Aware Scaling)和新的块结构。V1的NAS搜索空间没有考虑训练速度,导致搜索出来的结构虽然推理效率高,但训练时很慢。V2把训练时间纳入搜索空间,在早期层用Fused-MBConv代替部分MBConv,后面层继续使用MBConv结构。
这两个模块用大白话解释:
- MBConv是逆残差结构,先用1×1卷积升维,再用深度可分离卷积提取特征,最后用1×1卷积降维,中间嵌入SE注意力模块。相当于一个“先展开再总结”的信息处理过程。
- Fused-MBConv则是把前面的1×1卷积和深度可分离卷积合并成一个普通的3×3卷积,在计算量相近的情况下减少访存次数,加速训练。
我自己的理解是:V2在早层用Fused-MBConv(信息还在表层,不需要太高维的抽象),在深层层保留MBConv(需要更强的非线性特征提取),这个搭配有点像是“轻装快跑一段,再负重稳跑一段”的策略,比V1一味追求低FLOPs更务实。
2.2 具体选哪个版本:S还是M
EfficientNetV2系列提供了B0到XL等多个版本。中草药识别这类任务输入分辨率通常在224或256,类别数量在11个左右,不需要上XL这种体量的模型。在项目里我做了三个版本的对比:
| 模型 | 参数量 | Top-1精度(预训练权重志米) | 单张推理耗时(GPU) |
|---|---|---|---|
| ResNet-50 | 25.6M | 93.2% | 大约0.8倍基线 |
| EfficientNetV2-S | 21.5M | 95.8% | 大约1.0倍基线 |
| EfficientNetV2-M | 54.1M | 96.4% | 大约1.7倍基线 |
从数据看,EfficientNetV2-S是性价比最高的选择——比ResNet-50参数量更少、精度更高,推理速度和ResNet-50也基本在同一量级。EfficientNetV2-M虽然精度略高,但推理耗时增加明显,对后续要部署到边缘设备或做实时识别不太友好。所以我最终选定了efficientnet_v2_s作为主体模型架构。
2.3 迁移学习的必要性
中草药数据集就算做到1万张,和ImageNet的千万级数据量相比还是太小。从头训练EfficientNetV2-S几乎必然过拟合。我的做法是加载ImageNet预训练权重,然后把最后的全连接分类层替换成适合自己任务的输出维度。
这里有个很多新手容易犯的错误:只改了最后全连接层的输出大小,其他所有层的学习率都一样。正确的迁移学习策略通常是分层设置学习率——底层特征提取层的学习率调小一些(比如1e-4),新加的随机初始化分类头学习率调大一些(比如1e-3)。底层学习的是通用纹理、边缘等基础特征,改动的破坏性要小;顶层则是任务特化的部分,需要更大步长快速收敛。
model = models.efficientnet_v2_s(weights=models.EfficientNet_V2_S_Weights.IMAGENET1K_V1) in_features = model.classifier[1].in_features model.classifier = nn.Sequential( nn.Dropout(p=0.3, inplace=True), nn.Linear(in_features, num_classes) ) # 分层学习率 params_group = [ {'params': model.features.parameters(), 'lr': 1e-4}, {'params': model.classifier.parameters(), 'lr': 1e-3} ] optimizer = torch.optim.AdamW( params_group, weight_decay=1e-4 )3. 完整训练链路:从数据加载到模型评估
数据准备好、模型选好之后,接下来就是训练工程的问题了。训练过程不是简单地把数据灌进模型里跑几百轮,而是要关注验证集指标、避免过拟合、结合一些训练技巧让模型的精度和泛化能力都达到理想状态。
3.1 DataLoader与文件组织
整个项目我实现了PyTorch标准的数据加载流程。首先按类别组织文件夹结构,每一类的中草药图片放在对应的子文件夹下,文件夹名即类别名。这是最简单也最不容易出错的组织方式。
train_dataset = datasets.ImageFolder( root=train_dir, transform=transform_train ) valid_dataset = datasets.ImageFolder( root=valid_dir, transform=transform_valid ) train_loader = DataLoader( train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True ) valid_loader = DataLoader( valid_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True )ImageFolder会自动读取子文件夹名作为label,构建类别到索引的映射。这个映射后续要保存成一个JSON文件,部署推理时类别映射需要与之完全一致,不能训练完之后就把映射丢掉。
num_workers这个参数在没有GPU或低内存的机器上容易踩坑——不要盲目设大,PyTorch中每个worker都会把一批数据加载进内存,worker数超过CPU核心数反而会增加调度开销。我自己的机器是8核CPU,num_workers=4表现最平稳。
3.2 训练超参配置与EMA
训练超参这块我最终用的方案如下:
| 超参数 | 取值 | 说明 |
|---|---|---|
| 输入尺寸 | 224×224 | 与预训练权重一致 |
| Batch Size | 32 | 根据GPU显存调整,显存不够改为16也影响不大 |
| 优化器 | AdamW | 相比SGD收敛更稳定,配合权重衰减效果好 |
| 基础学习率 | 2e-4 | 针对预训练骨干参数 |
| 分类头学习率 | 1e-3 | 新初始化的层需要更大学习率 |
| 学习率调度 | CosineAnnealingLR | 余弦退火,减小后期震荡 |
| 训练轮数 | 100 | 早停策略兜底 |
| 标签平滑 | 0.1 | 缓解过度自信 |
| EMA动量 | 0.995 | 平滑参数更新,提升泛化 |
训练100轮是考虑了EarlyStopping机制后的保守设置,验证集准确率连续15轮不提升就停止并恢复最佳权重。实际项目中在第67轮左右就触发了早停,最终精度定格在95.8%。
EMA(指数移动平均)是很多实战教程不会写但确实有效的技巧。简单来说,就是维护一份模型参数的滑动平均副本,推理和验证时用这个副本而不是实时训练的参数。因为训练参数在后期会有震荡,EMA相当于对历史参数做了平滑平均,往往能带来0.3到0.5个百分点的精度提升。实现上通常用PyTorch自带的torch.optim.swa_utils配套的EMA工具,或者自己写一个备份参数的函数。
3.3 混合精度训练与显存优化
EfficientNetV2-S在224分辨率下,batch size 32大概需要6~8GB显存。如果用的是消费级显卡(比如某些显存较小的卡),混合精度训练几乎是必须的。PyTorch提供了非常方便的自动混合精度支持,老版本用apex,现在直接用torch.cuda.amp即可。
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for images, labels in train_loader: images, labels = images.cuda(), labels.cuda() optimizer.zero_grad() with autocast(): outputs = model(images) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()混合精度原理不复杂:FP16精度下前向和反向计算速度更快,显存占用减半,但梯度过小时FP16缓存不下,所以用Scale梯度缩放解决下溢问题。这里要注意的是,GradScaler必须配合autocast使用,而且scaler.step(optimizer)和scaler.update()的顺序不能反。
开了混合精度之后,我训练单轮时间大约缩短了35%,显存占用也明显下降,这个优化对项目落地的价值非常大。
3.4 评估与混淆矩阵分析
模型训练完,不能只看一个验证集准确率就收工。我额外从验证集中切出了一部分数据做测试集,跑了一轮完整的评估,统计了每类精确率、召回率和F1分数,并画了混淆矩阵。这一步很有必要——中草药识别系统的用户拍一张照片,模型给出的是一个类别推断,如果模型把药材A稳定地错认成药B,那就需要追查原因,很可能是这两种药材干燥后的视觉特征真的很接近。
我做了一组易混淆类别对照分析。大多数类别都能达到95%以上的单类召回率,但有几对药材之间的混淆比较明显。比如某组颜色相近的根茎类药材,模型在两者之间的误判率达到了7%左右。处理方式是在数据集中追加这两个类别的额外样本,并适当增强对应的增样比例,再重新微调。
这一步也体现了混淆矩阵的意义——准确率是高是低是一回事,错在哪里是另一回事,后者对数据优化有直接的指导价值。
4. 模型部署:从PyTorch权重到实际可用的识别服务
很多教程写到训练完、模型精度达标就戛然而止,但真实项目里还有一个绕不开的环节——部署。训练用的PyTorch环境通常笨重、需要GPU,而实际使用场景可能是一个Web后端,甚至是一个移动端App。模型必须经过转换和轻量化才能跑在目标环境里。
4.1 ONNX导出与推理加速
我选择的部署路径是先把PyTorch模型导出为ONNX格式,再通过ONNXRuntime进行推理。ONNX是一个开放式的模型交换格式,导出之后可以脱离PyTorch环境运行。
import torch.onnx model.eval() model = model.cuda() dummy_input = torch.randn(1, 3, 224, 224).cuda() torch.onnx.export( model, dummy_input, "herb_efficientnet_v2_s.onnx", export_params=True, opset_version=12, do_constant_folding=True, input_names=['input'], output_names=['output'] )导出过程中有个容易踩的坑:model.eval()和torch.no_grad()环境一定要做好,否则模型中如果有BatchNorm或Dropout层,导出的ONNX会在推理时产生和训练时不一致的行为。EfficientNetV2中有Dropout,更要注意这一点。
ONNXRuntime做CPU端推理时,我实测一张224×224的中草药图像大约需要113毫秒(2.3GHz主频的x86处理器),相比在原PyTorch动态图环境下的推理速度快了不少。如果追求更极致的速度,可以在有GPU的服务器上使用TensorRT作为后端,或者在Intel CPU上使用OpenVINO加速,这样单张推理时间可以进一步压到几十毫秒级别。但对于识别系统来说,113毫秒已经足够支撑正常的在线请求了。
4.2 模型轻量化:剪枝与量化
虽然EfficientNetV2-S本身参数量不大,但部署到资源受限环境(比如移动端或嵌入式设备)时,还是可以考虑做一轮轻量化。我尝试了两种手段,效果对比放在这里:
| 优化手段 | 模型大小 | 推理耗时(CPU) | 精度损失 |
|---|---|---|---|
| 原始FP32 | 82MB | 约113ms | 基准 |
| FP16量化 | 41MB | 约89ms | 忽略不计 |
| INT8量化(PTQ) | 21MB | 约57ms | 0.6% |
| 结构化剪枝30%+微调 | 51MB | 约78ms | 0.9% |
INT8量化在CPU上收益最大,但需要一张校准数据集来统计各层激活值的动态范围,校准集最好从训练集中取一些有代表性的样本,不能用太偏的样本,否则量化后的精度损失会明显放大。
4.3 类别映射和预处理一致性问题
部署环节最隐蔽的一个坑就是预处理不一致。训练时做了Resize、Normalize,部署推理时也必须用完全相同的参数。很多人本地测试精度很高,一上服务器就拉胯,八成是部署代码里的预处理管线和训练不一致。
我建议把预处理逻辑独立封装成一个函数,同时导出到推理代码中:
def preprocess_image(img_path): image = Image.open(img_path).convert('RGB') image = image.resize((224, 224), Image.BILINEAR) image = np.array(image, dtype=np.float32) image = image / 255.0 mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) std = np.array([0.229, 0.224, 0.225], dtype=np.float32) image = (image - mean) / std image = np.transpose(image, (2, 0, 1)) image = np.expand_dims(image, axis=0) return image同时,类别索引到中文名的映射表也要持久化保存,推理接口拿到模型输出的索引后,通过映射表还原成药材名称返回给上层业务。
5. 代码结构、复现步骤与中草药数据的坑
整套系统的代码结构按照工程化的方式组织,既有训练入口也有推理入口,不只是一个裸模型文件。下面把目录结构和复现步骤整理出来,方便照着做。
5.1 项目目录结构
herb_classifier/ ├── config.py # 全局配置,路径、超参数等 ├── data_preprocess.py # 数据划分,按来源ID分训练/验证/测试集 ├── train.py # 训练主脚本,支持混合精度、EMA、EarlyStopping ├── evaluate.py # 测试集评估,输出各类别指标和混淆矩阵 ├── export_onnx.py # 导出ONNX模型 ├── inference.py # ONNXRuntime推理脚本 ├── requirements.txt # 依赖列表 ├── models/ │ └── herb_model.py # EfficientNetV2-S封装 ├── data/ │ ├── train/ # 按类别分文件夹的训练图像 │ ├── valid/ # 按类别分文件夹的验证图像 │ └── test/ # 按类别分文件夹的测试图像 └── weights/ └── best_model.pth # 最佳模型权重5.2 训练全流程命令参考
前提条件是装好PyTorch、torchvision、onnxruntime这几个核心库。然后按顺序执行:
python data_preprocess.py python train.py python evaluate.py python export_onnx.py python inference.py训练过程中留意几个关键输出——每个epoch的train loss、valid loss、valid accuracy。如果trian loss持续下降但valid loss回升,说明过拟合;如果两边都降得很慢,则考虑调大学习率或检查数据增强是否过强。我在这类中草药数据集上发现,ColorJitter的强度不能给太大,否则模型学到的是“色偏不变性”,反而把药材本身的颜色特征抑制了,对识别不利。
5.3 中草药识别特的坑和我的处理经验
我踩过最大的坑是易混淆类别导致验证集指标虚高。前面提到按来源ID划分数据,其实最初版本没有这个划分,直接随机打乱。第一次训练就出现验证集精度96%以上、测试集只有89%的情况。排查后发现问题出在“同批药材多次拍摄导致的数据重叠”,这才意识到分组划分的重要性。
第二个坑是迁移学习的初始学习率。一开始我整个模型统一用2e-4的学习率,前面若干轮loss掉得很快,但后面验证集精度卡在92%上不去。后来改成分类头1e-3、骨干2e-4的分层策略,收敛速度和最终精度都上来了。迁移学习时新加的层学习率适当放大,是中小型数据集上很实用的技巧。
第三个坑和部署相关:ONNX模型在CPU推理时,第一次调用会有一个初始化耗时(包括内存分配和算子预设),如果这个初始化时间被计入单次推理延迟,会得出一个吓人的数字。真实的性能测试应该先跑若干次“预热”,再统计后续推理的平均耗时。
6. 融合前段识别接口的整体效果与实测表现
前面讲的都是训练和部署的单点细节,最后从整个识别系统的角度复盘效果。我最终整合了一个简单的Web识别接口:用户上传一张药材图片,后端调用ONNXRuntime推理模型,返回5个最可能的类别及其置信度。整个链路不算复杂,但实测效果对中草药识别场景足够实用。
6.1 实测性能数据
用独立的测试集(按来源ID划分,不参与训练)做了一轮完整评估:
| 指标 | 数值 |
|---|---|
| 测试集样本数 | 约2300张 |
| Top-1准确率 | 95.8% |
| Top-5准确率 | 99.1% |
| 平均单张推理耗时(CPU) | 约115ms |
| 模型大小 | 约82MB(FP32) |
Top-5准确率接近99%,意味着系统在实际使用中基本不会给出离谱的结果,即使第一候选错了,也会在后续候选中给出正确类别。这个特性对识别类产品非常重要——用户看到的不只是一个硬性答案,而是一个排序列表,可以极大改善体验。
6.2 真实拍摄场景下的表现
把模型放到自然场景下用手机拍摄测试,会遇到训练数据里没有的新分布:自动白平衡造成的色偏、手持拍摄带来的运动模糊、背景噪点等。在这些非理想输入下,Top-1准确率下降到91%左右,Top-5仍能维持在97%上下。说明模型学到的主要是纹理和形态特征,对小幅颜色偏移有一定鲁棒性,但亮度极暗或严重模糊的情况下还是会失效。
中途我遇到过一个非常有意思的情况:某个类别的干品和鲜品颜色差距极大,鲜品是绿色、干品是土黄色,模型偶然把绿色鲜品错认成另一个颜色相近的类别。解决思路是在增强管线里对这类样本增加RandomGrayscale的概率,弱化颜色主导地位,把注意力拉回形态和纹理上,效果确实有改善。
7. 训练脚本核心代码解读
很多刚接触PyTorch的人拿到项目源码第一反应是“看不懂”,其实整个训练脚本拆开来看,核心就那么几件事:加载配置、构造数据、定义模型、写循环、做评估。我把最核心的代码片段放出来,配合注释解读。
7.1 模型定义与类别数自动适配
class HerbClassifier(nn.Module): def __init__(self, num_classes): super().__init__() self.backbone = models.efficientnet_v2_s( weights=models.EfficientNet_V2_S_Weights.IMAGENET1K_V1 ) in_features = self.backbone.classifier[1].in_features self.backbone.classifier = nn.Sequential( nn.Dropout(p=0.3, inplace=True), nn.Linear(in_features, num_classes) ) def forward(self, x): return self.backbone(x)这里唯一需要解释的是classifier[1]这个索引。EfficientNetV2-S的classifier是一个Sequential容器,包含Dropout和Linear两个层,classifier[1]取到的是Linear层,它的in_features就是导入全连接层的输入维度。把这个维度取出来替换成新的分类头,就完成了迁移学习的适配。
7.2 EarlyStopping的逻辑
早停写起来很简洁,却能在实验跑十几小时后救你一命:
class EarlyStopping: def __init__(self, patience=15, delta=0): self.patience = patience self.delta = delta self.best_score = None self.counter = 0 self.early_stop = False def __call__(self, val_acc, model, save_path): score = val_acc if self.best_score is None: self.best_score = score self.save_checkpoint(model, save_path) elif score < self.best_score + self.delta: self.counter += 1 if self.counter >= self.patience: self.early_stop = True else: self.best_score = score self.counter = 0 self.save_checkpoint(model, save_path) def save_checkpoint(self, model, save_path): torch.save(model.state_dict(), save_path)保存最佳权重的时机是验证集准确率创新高时,而不是训练结束时。训练后期即便触发了早停,保存的权重也是整个训练周期里最理想的版本,不是最后一轮的权重。
7.3 EMA平滑的极简实现
用PyTorch实现EMA不需要引入额外库,在自己的Trainer里维护一份影子参数就行:
def ema_update(model, ema_model, alpha=0.995): with torch.no_grad(): for ema_param, param in zip(ema_model.parameters(), model.parameters()): ema_param.data.mul_(alpha).add_(param.data, alpha=1 - alpha)训练时每一轮epoch结束调用一次ema_update,验证和推理都用ema_model而非实时模型。这个技巧几乎没有额外的计算成本,却能稳定带来小幅精度提升,我习惯在所有图像分类项目中默认开启。
8. 这个项目后续还能怎么扩展
最后聊一下这个中草药识别系统后续的扩展空间。整个链路是通用的图像分类解决方案,只是应用场景换成了中草药,所以很多技术点是可以迁移复用的。
8.1 扩展思路:从分类到多标签与细粒度定位
当前的系统是单标签多分类——一张图对应一个最可能的药草类别。但实际使用中用户拍一张图里可能出现多种药材,这时候单标签模型就不够用了。可以考虑多标签分类方案,把最后一层改成Sigmoid激活,对每个类别独立计算概率,同时考虑引入目标检测模型(比如YOLO系列)先定位出图里几个独立的药材区域,再逐一送入分类模型。这样整个系统就从一个“认图”的系统升级成“认图里所有东西”的系统。
8.2 持续学习与数据迭代
中草药品种的覆盖范围是开放的,本地有上千种药材,不可能一次全部收集齐。后续会增加新类别,这时候不用重训整个模型——在已有预训练权重的基础上,对新增的类别数据做增量训练即可。实践中要注意的是新旧类别样本比例尽量均衡,否则模型会对旧类别产生“灾难性遗忘”。我试过在旧类别样本上按比例做采样,让新旧样本在一个batch中保持大致1:1的比例,增量训练一轮后,旧类别精度几乎不掉。
8.3 识别结果的解释性
图像分类模型是黑盒,用户得到一个“这是什么药”的结果,但并不理解为什么。后续可以引入Grad-CAM类热力图可视化,把模型关注的图像区域叠加到原图上展示给用户——如果模型注意力集中在药材主体上,结果就更有说服力;如果模型关注的是一堆背景噪点,说明特征学习有问题,需要继续优化数据。这类可视化对建立用户信任有很大帮助。
我在实际使用中最深的体会是,中草药识别这种细粒度领域的难点不是“模型有多深多宽”,也不是“训练有多花哨”,而是数据分布的覆盖、标注的一致性和部署链路的一致性。把这些基础工作做扎实,EfficientNetV2-S这套方案已经能交出相当可用的成绩单;反过来,如果数据本身是一团乱麻,换比EfficientNetV2更复杂的模型也只会放大数据里的错误,并不会自动把精度救回来。
希望这篇项目总结对打算入坑图像分类实战或正在为细粒度识别任务选型的朋友有帮助。如果你也在做类似的植物、药材或标本识别项目,可以从数据划分和预处理一致性这两个点先自查,我猜你大概率也会遇到我踩过的那几个坑。