CNN中药识别实战:从数据预处理到ResNet18模型微调全流程

发布时间:2026/10/22 13:38:22
CNN中药识别实战:从数据预处理到ResNet18模型微调全流程 又到了一年毕设季后台收到不少同学问“基于深度学习的XX识别”这类题目到底怎么做。题面看着就一个“识别”真正动手你会发现难点根本不在训练而在前面的数据准备和后面的模型调优。这篇文章我把做“PythonCNN训练识别中药”这个毕设项目时的完整思路、踩坑记录和能直接复用的代码都写出来给正在做类似图像分类题目的同学一个参考。1. 接到中药识别题目的第一件事先搞清楚CNN到底在学什么1.1 “识别中药”本质上是一个图像分类问题从机器学习角度看这个题目要做的事情非常明确输入一张中药饮片或药材的图片让模型输出它是哪一味药。这是一个标准的图像多分类任务技术流程分成四段图像采集与预处理、卷积神经网络提取特征、全连接层完成分类、输出各类别概率。很多同学上来就直接调模型、跑训练结果Loss不下降也不知道为什么。我觉得先想明白CNN在学什么是更重要的事。卷积神经网络在这里做的工作可以理解为“用一层层卷积核去扫描图片”低层卷积核学到的是边缘、纹理、颜色块这些基础特征深层卷积核把这些基础特征组合成更高级的、属于某一类药材的判别特征。比如当归的横切面有放射状纹理枸杞表面有褶皱菊花的花瓣形态独特——CNN做的事就是自动找到这些特征而不是像传统图像识别那样靠人去设计颜色直方图、纹理算子。1.2 中药图像分类和猫狗分类不是一回事用现成框架跑通一个猫狗分类教程很容易因为ImageNet里本来就有大量猫狗图片预训练模型对这些类别的区分能力已经很强。但中药识别是另一个难度级别我自己做下来深有体会第一类间差异小。很多中药饮片切片之后外观非常相似比如两种根茎类药材横截面都是黄白色只是纹理疏密不同。这对CNN的特征提取能力要求更高浅层小网络往往学不到足够细微的区分特征。第二类内差异大。同一味药药材形态、切片厚度、干燥程度、拍摄光线都会带来巨大的外观差异。同一种枸杞产地不同颜色深浅都不一样。模型要把这些差异统统“容忍”掉只抓住真正用于判别的核心特征。第三相似背景干扰。实验桌上、牛皮纸袋里、塑料密封袋中拍的图背景差异极大。如果训练数据里只有单一背景模型很容易把背景特征当成分类依据换一个场景就废了。所以拿到题目后不要急着敲代码先把这个任务拆成“数据有没有搞头、模型用什么规模、评估怎么才算过”三个问题后面才能走得顺。2. 数据集处理比模型选型更决定成败的一步2.1 数据从哪来、怎么整理训练数据是这个项目的地基。常见来源有两种自己拍摄采集或者用公开的中药图像数据集。自己拍的话尽量固定拍摄角度和距离但要有意保留一些光照变化和背景变化不然模型学出来太“娇气”。用公开数据集则要注意类别目录结构一般按这个格式组织dataset/ ├── train/ │ ├── 金银花/ │ ├── 枸杞/ │ └── 菊花/ ├── val/ └── test/PyTorch的ImageFolder类可以直接按这种目录结构加载数据它会自动把每个子文件夹名称作为类别标签。训练集、验证集、测试集的划分比例我建议6:2:2而且要保证每个类别的图片在这三个集合里都均匀分布不能有类别的图片全堆在训练集里。2.2 预处理三步缩放、增强、转张量做中药识别预处理环节有几个关键选择缩放尺寸。输入CNN的图片一般统一到224×224这是主流分类网络的默认输入尺寸。药材图片拍摄时往往有大量背景留白直接用Resize会把药材主体压缩变形更好的做法是用CenterCrop或者先Resize到256再RandomCrop到224相当于给模型一个“以主体为中心”的视图。归一化。ImageNet预训练模型要求输入标准化到特定均值和标准差mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]。这组数值是ImageNet数据集上统计出来的RGB通道分布迁移学习时沿用即可。如果从零训练自己的网络这组值也能用不用改。数据增强策略。中药图片增强要格外小心不能过度。我见过有同学对药材图片做随机旋转90度、水平翻转、亮度抖动后模型在训练集上表现很好验证集上却崩了——原因是某些药材有方向性特征比如叶片类药材正反面的纹理不同过度旋转会产生大量不符合真实场景的样本模型学歪了。实际项目里比较稳妥的是这样几种随机水平翻转概率0.5随机旋转10~15度小幅不转90度亮度、对比度轻微抖动±20%随机仿射变换平移、缩放轻微组合我用torchvision.transforms组合的增强管道长这样from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform 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]) ])验证集和测试集不做增强只做缩放和归一化这样评估结果才反映模型对真实图片的泛化能力而不是增强之后的概率分布。2.3 类别不均衡是中药数据的常态药材数据很容易不均衡常见药材图片多冷门药材可能只有几十张。CNN在类别不均衡的数据上训练模型会倾向于把预测结果偏向样本量大的类别因为整体Loss主要由大类样本贡献。处理方法推荐两个一是对样本少的类别做额外数据增强比如对该类图片重复采样并加入更强的随机扰动二是在WeightedRandomSampler里按类别样本数的倒数设置采样权重让每个batch里各类别出现频率更均衡。我当时用的后者实现也不复杂from torch.utils.data import WeightedRandomSampler labels [samples[i][1] for i in range(len(samples))] class_counts torch.bincount(torch.tensor(labels)) weights 1.0 / class_counts[labels].double() sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue)效果上类别不均衡问题明显缓解小样本类别的recall提升显著。但要注意replacementTrue会让某些小类别样本在不同epoch里反复出现训练轮数不宜设太大否则容易过拟合。3. 模型搭建不是所有CNN都能直接拿去识别药材3.1 从零写网络还是用预训练模型这是毕设项目里最纠结的问题之一。从零写一个CNN可以完整展示网络结构的设计过程答辩时能讲的东西多但中药识别这种细粒度分类任务数据量通常有限从零训练小网络的收敛效果往往不理想最终准确率可能只有70%上下。我的建议是两步走先搭一个小型CNN把完整训练流程跑通保证“能运行、能出结果”然后用迁移学习加载在ImageNet上预训练过的模型比如ResNet18或ResNet50只重新训练最后一层全连接这样能大幅提升识别准确率而且代码量增加很少答辩时还能讲“利用预训练知识迁移”的亮点。我当时就是先用自研小网络验证了DataLoader和训练循环没问题再切换ResNet18做正式训练。两条路都值得体验一下进度反而更快。3.2 搭建ResNet18迁移学习模型用PyTorch加载预训练ResNet18并改成中药分类模型核心代码非常短import torch.nn as nn from torchvision import models def build_model(num_classes, use_pretrainedTrue): model models.resnet18(pretraineduse_pretrained) # ResNet18最后的全连接层输入维度是512 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model关键点在于替换model.fc原模型是针对1000类ImageNet设计的最后全连接层输出维度是1000我们要改成自己的类别数。替换后模型前面的卷积层保留了ImageNet学到的通用视觉特征边缘、纹理、形状只有最后全连接层从头训练训练速度和最终准确率都会理想很多。3.3 冻结特征层还是微调全部参数这里是第二个容易纠结的地方。常见做法有两种冻结backbone只训练全连接层。把前面所有卷积层参数设为requires_gradFalse训练时只更新fc层。优点是训练极快小数据量也不容易过拟合缺点是模型学不到中药特有的细粒度特征准确率上限偏低。全参数微调fine-tune。加载预训练权重后所有层都参与训练只是初始学习率设小一点比如1e-4。这是中药识别场景下效果更好的方案因为药材图片和ImageNet自然图像分布差异较大让深层卷积特征也适应药材数据识别能力会提升一截。我当时选了全参数微调用验证集对比过冻结特征层版本准确率约82%全参数微调版约91%提升明显。代价是训练时间更长、过拟合风险更高所以配合了数据增强和早停策略。3.4 自研小CNN的结构参考如果有同学想从零写网络练手我推荐一个结构简单但能用的基线import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x三个卷积块加两个全连接层的结构能跑通流程但不是精度上限。注意两点每个卷积后接BatchNorm2d可以显著加速收敛缓解梯度消失AdaptiveAvgPool2d((1,1))代替Flatten前的固定尺寸约束这样网络对输入尺寸的适应性更强。4. 训练过程实录学习率、过拟合和那些只有跑过才知道的事4.1 损失函数与优化器选择中药识别是多分类任务损失函数直接用nn.CrossEntropyLoss()它内部把Softmax和交叉熵计算合在一起了输出层不需要额外加Softmax。预测时取输出向量最大值对应的索引就是预测类别。优化器我推荐Adam默认参数lr0.001起步训练稳定、收敛快。做迁移学习时有个经验预训练模型微调的学习率通常要比从零训练小一个数量级我用的是lr1e-4。等训练后期Loss下降趋于平缓时切换成CosineAnnealingLR学习率调度让学习率周期性地衰减再回升有助于跳出局部极小值点。损失曲线是判断训练状态最重要的信号。正常情况下训练Loss应该持续下降并趋于平稳验证Loss先下降后略有上升这说明过拟合开始出现需要配合早停策略。4.2 一次典型的过拟合排查记录我在训练自研小CNN时遇到过非常典型的过拟合问题训练集准确率到了95%验证集却卡在72%上不去。当时的排查过程很有参考价值。第一步检查数据预处理。确认验证集没有做随机增强只有缩放和归一化排除了评估时受增强噪声干扰的可能。第二步检查数据泄露。我怀疑训练集和验证集里混入了完全相同或几乎相同的图片。中药图片数据集中常有连拍图同一味药换不同角度拍几张视觉上差异极小。我按文件哈希找出了训练集和验证集之间的重复图片把重复样本全部归入训练集验证集重新采样验证准确率小幅提升到76%问题部分缓解。第三步增强模型的正则化。在分类器前加Dropout(0.5)并加入WeightDecay1e-4。这一步后验证集准确率到78%提升不大。第四步换更强大的backbone。从自研小CNN换成预训练ResNet18全参数微调验证集准确率直接跳到89%。这才明白之前的主要瓶颈不是过拟合而是自研网络的特征提取能力太弱学不到药材的细粒度判别特征。这个排查过程说明一件事当训练集和验证集差距过大时不要只盯着正则化操作先确认模型容量是否匹配任务难度。模型太弱怎么防过拟合都是治标不治本。4.3 训练循环的关键细节训练循环里最容易出错的细节有三个模型模式切换。训练阶段要调用model.train()让Dropout和BatchNorm按训练模式运行验证和测试阶段必须调用model.eval()否则BatchNorm会用到错误的统计量预测结果波动很大。这个坑我见过太多次了。验证阶段关闭梯度计算。用with torch.no_grad():包裹验证代码块能省内存和计算时间。保存模型参数。我建议保存状态字典而不是整个模型torch.save(model.state_dict(), herbs_resnet18.pth)这样加载的时候只需重新实例化模型再load_state_dict跨环境部署更灵活。同时每次验证集准确率刷新到最高时都保存一份快照防止训练过程中后期崩掉丢失最佳权重。4.4 训练的超参数参考我给出一组经过验证可复用的参数设置供参考超参数自研小CNNResNet18微调优化器AdamAdam初始学习率1e-31e-4Batch Size3232Epochs50带早停30带早停权重衰减1e-41e-4学习率调度CosineAnnealingLRCosineAnnealingLRBatch Size的选择要综合考虑显存和BatchNorm表现。我试过64收敛更快但GPU显存吃紧小数据集上32和64最终精度差距不大32更稳。5. 从“训练完”到“能交差”模型评估与识别Demo的落地环节5.1 不能只看准确率要看分类报告和混淆矩阵毕设答辩时老师问得最多的问题之一就是“你这个模型对不同药材的识别能力到底怎么样”。单看整体准确率回答不了这个问题一定要看每个类别的精确率、召回率和F1分数以及混淆矩阵。用sklearn处理很简单from sklearn.metrics import classification_report, confusion_matrix report classification_report( all_labels, all_preds, target_namesclass_names, digits4 ) print(report) cm confusion_matrix(all_labels, all_preds)我从混淆矩阵里发现了一个有意思的误判模式模型把A药材错判成B药材的比例显著偏高。查了一下是因为两者切片后颜色、纹理高度相似属于“合理误判”。针对这个情况我做了两件事一是增加这两种药材的训练图片数量二是在数据增强时对这两种药材做更小幅度的色偏扰动强迫模型关注形状和纹理特征而不是颜色。调整后A药材的召回率从84%提到了92%。5.2 写一个可以实际用的中药识别脚本训练完成后把模型封装成一个独立的预测脚本既能作为项目交付成果演示也方便自己平时验证图片import torch from PIL import Image from torchvision import transforms from torchvision import models import torch.nn as nn class_names [金银花, 枸杞, 菊花, 当归, 甘草] # 根据实际类别修改 transform 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]) ]) def load_model(num_classes, weight_path): model models.resnet18() in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) model.load_state_dict(torch.load(weight_path, map_locationcpu)) model.eval() return model def predict_image(model, image_path): img Image.open(image_path).convert(RGB) tensor transform(img).unsqueeze(0) with torch.no_grad(): outputs model(tensor) probs torch.softmax(outputs, dim1).squeeze(0) top_idx torch.argmax(probs).item() top_prob probs[top_idx].item() return class_names[top_idx], top_prob if __name__ __main__: model load_model(num_classeslen(class_names), weight_pathherbs_resnet18.pth) name, prob predict_image(model, test_image.jpg) print(f识别结果为{name}置信度{prob:.4f})这个脚本所有环节都用CPU也能跑预测单张图片耗时几十毫秒演示完全够用。如果觉得命令行不够直观可以再加一层简单的图形界面用tkinter做一个“选择图片→显示预测结果与置信度”的小窗口代码量不大但演示效果好很多。5.3 评估时容易被忽略的两个小问题第一个是归一化统计量。中药图片可能偏暗偏黄不像自然图像那样色彩分布均匀但归一化时用ImageNet的mean/std仍然可行因为这只是对输入分布做标准化CNN的卷积核会在后续层自己调整特征分布。第二个是预测置信度的解读。单张图片预测置信度达到99%并不稀奇但低置信度样本往往更有分析价值。我在测试集中挑出了置信度低于0.5的错误样本发现基本都是拍摄模糊或药材残缺不全的图片。这在真实应用场景里就是“模型不知道自己在说什么”的信号可以向用户提示“请重新拍摄更清晰的照片”。最后再分享一个实战小技巧做中药识别这类细粒度图像分类项目很多人忽略了一个低成本高回报的操作查看模型预测错误的具体图片。训练结束后把所有测试集错误样本连同预测置信度输出到一个文件夹里按置信度排序浏览一遍。你会发现错误样本集中在几类情况里背景复杂的、光照过暗的、与相似药材并排摆放的。把这些发现写进论文的“错误分析”章节整个项目的深度立刻就不一样了。这个项目从数据处理到模型部署核心就一句话把普适的CNN技术适配到中药图像的特殊性上。数据上解决类间相似、类内多样的问题模型上借迁移学习的力量评估上用细粒度的指标看透模型的真实能力。做到这一步不管毕设题目怎么包装你都能拿出有说服力的成果。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询