这几年做农业视觉项目我踩过最多的坑就是作物病害识别。原因很简单病害图像和自然图像差别太大背景杂乱、光照不均、病斑形态又千变万化常规的分类模型一上真实场景就翻车。水稻叶片病害识别更是典型中的典型——稻瘟病、纹枯病、白叶枯病病斑长相相似肉眼都容易弄混更别说让模型去区分。我这次做的“基于CNN与Transformer的水稻叶片病害图像识别模型”核心思路不是简单堆一个网络而是把CNN的局部纹理提取能力和Transformer的全局上下文建模能力揉在一起让模型既能看清病斑细节又能把握整片叶子的分布规律。整套流程从数据清洗、图像增强、模型设计到训练调参、错误分析每个环节都踩出了不少值得记录的细节。这篇内容就是把我完整的实操过程和想法拆开讲清楚适合正在做农业图像识别、或者想对比CNN和Transformer两种架构在实际任务中表现的读者参考。1. 项目趟坑记水稻叶片病害识别到底难在哪1.1 任务拆解不是单纯分类问题开始动手之前我先把任务拆了一层。表面上看这是一个图像分类问题——输入一张水稻叶片图片输出它属于健康、稻瘟病、纹枯病、白叶枯病中的哪一类。但实际做下去会发现它比普通的通用图像分类要麻烦得多。水稻叶片病害有很强的类内差异和类间相似性。同样是稻瘟病急性型病斑是暗绿色水渍状慢性型病斑是梭形、边缘褐色、中央灰白两种形态差异很大而纹枯病的早期病斑又和稻瘟病的慢性型病斑有明显重叠。如果模型只盯着局部颜色或纹理很容易把纹枯病早期的椭圆形病斑误判成稻瘟病。这也是我坚持要用Transformer做全局建模的原因——只靠卷积核看局部感受野很难把“病斑分布形态”这个关键判据利用起来。另外实际场景中的图像不是实验室里那种干净的单叶背景。叶片互相遮挡、泥土沾在叶面、光照角度变化、露水反光都会让模型学到无关的伪特征。我在项目里碰到的第一问题就是模型在验证集上看起来有97%的准确率但拿到田间新拍的图上立刻掉到80%以下。原因就是训练集里的背景纹理太单一模型把背景当成了分类线索。1.2 数据与场景的双重挑战水稻叶片病害识别的数据获取成本很高。一个完整的病害样本库需要有经验植保专家逐张标注而且病害发生有季节性不同地区、不同生育期的表现还不一样。我用的基础数据集来自某农业科研机构公开的水稻病害图像库一共包含5个类别健康、稻瘟病、纹枯病、白叶枯病、胡麻斑病原始图像约1.2万张分辨率从640×640到2000×2000不等。这个数据量做普通的分类任务勉强够用但要训练一个Transformer模型就明显偏少。ViT类模型动辄需要几千万张图像做预训练在小数据集上如果不做迁移学习、不做强数据增强收敛效果会非常差。我在项目里采用了“CNN迁移学习 Transformer分支微调”的混合策略而不是从头训练一个ViT就是为了绕开数据量不足这个硬伤。实际部署场景还要考虑推理速度。用ViT-Base直接推理一张图在普通GPU上大约需要30到50毫秒在嵌入式设备上可能要几百毫秒这还没算预处理和后处理。而很多田间检测场景用的是边缘设备处理器性能有限。所以我的模型设计目标从一开始就定成精度不低于95%、单帧推理时间控制在20毫秒以内桌面级GPU、模型参数量尽量控制在50M以下。2. 数据准备图像增强和标签处理是模型的地基2.1 数据集采集与清洗数据这块我花的时间比训练还要多。原始数据集里问题不少有的图像标注框漂移、有的类别标错、有的图像本身模糊到人眼都看不出病斑。清洗流程我分了三步走。第一步是去重和模糊检测。用感知哈希算法计算图像间的相似度把重复图删掉再用拉普拉斯算子的方差判断图像清晰度低于阈值的直接剔除。第二步是裁剪校验我写了个可视化脚本把每张图的标注框和类别文字画出来随机抽查20%的样本有问题的重新标注。第三步是背景干扰过滤因为原始数据集里的图像大部分是单叶拍摄田间场景图太少这会直接导致泛化性差后面增强环节必须专门补这一类样本。清洗之后有效样本量从1.2万张降到接近1万张。不要觉得可惜——脏数据训练出来的模型线上表现往往比线下评估差一大截前期清理多花的时间在后面调参会全部省回来。2.2 数据增强策略数据增强是整个项目里收益最大的一步。田间拍照有一个特点叶片角度随意、光照不统一、背景杂乱。如果只用随机翻转和裁剪模型学到的其实是“实验室单叶图”的分布而不是田间真实分布。我按严重程度把增强分成三个等级。基础级随机水平翻转、垂直翻转、随机旋转30度、随机裁剪后resize到224×224概率都是0.5。中等级颜色抖动亮度±20%、对比度±15%、饱和度±15%、随机高斯模糊、随机仿射变换。强等级随机擦除Random Erasing和CutMix。这里我特别提一下随机擦除对病害识别很有效——它强迫模型不能只依赖某一个局部病斑判断而是综合多个区域的信息这正好对应田间叶片部分被遮挡的真实场景。但增强不是越猛越好。我把AutoAugment一类的策略试过一轮发现训练集准确率进步验证集反而下降。原因也好理解过度的几何变换会改变病斑的真实形态比如把一个长条形病斑拉伸成圆形标签还是原来的病害名这就等于往训练集里灌噪声。2.3 数据集划分与标注校验划分数据集时我用了分层抽样的方式保证每个类别在训练集、验证集、测试集中都有相近的分布比例。比例设置是训练集70%、验证集15%、测试集15%。划分时有一个细节同一株水稻上拍的叶片因为物理上是同一片叶子不能让其中一张进训练集、另一张进测试集否则模型等于提前看到了答案测试分数会虚高。我按“图像来源批次”来分组同一批次的图像只能同时出现在一个集合里这样评估结果才有说服力。标注校验我额外做了一轮。怎么做的把训练好的模型在测试集上的预测结果和真实标签不一致的样本全部捞出来人工一个个看。这一步发现了不少原始标注错误比如有一张图标签是“白叶枯病”实际上病斑已经扩展到全叶枯死植保专家会把它归为“生理性枯死”这类别在数据里根本没有。遇到这种情况我就把样本删掉不强行归到任何一个类避免教模型错误映射。3. 模型选型与架构解析CNN和Transformer谁更适合叶片病害3.1 为什么先选CNN打底局部特征与迁移学习模型架构我一开始没直接上Transformer先把经典CNN跑了一遍摸底。ResNet50和EfficientNet-B0我都试了综合表现不错尤其是EfficientNet-B0在参数量只有5M左右的情况下Top-1准确率能到92.3%推理速度也快单帧在1080Ti上只用8毫秒。CNN的优势在于局部归纳偏置。叶片病害诊断非常依赖病斑的局部细节——病斑形状、边缘是否清晰、有没有霉层、孢子堆的分布这些都需要小卷积核逐层提取。而且CNN有成熟的ImageNet预训练权重我可以做标准迁移学习小数据集上也能稳定收敛不用像ViT那样需要超大规模预训练支撑。但CNN的短板同样明显感受野有限。虽然深层卷积核的理论感受野已经很大但实际训练中模型对远距离依赖关系的捕捉能力还是偏弱。举例来说水稻白叶枯病的典型特征是病斑从叶尖或叶缘开始向下延伸呈长条状——这个“延伸方向”和“从叶缘开始”的全局位置信息对分类很关键但CNN容易把注意力集中在局部病斑纹理上忽略分布趋势。3.2 Transformer的全局建模能力与ViT的短板Transformer这边我重点研究了ViTVision Transformer的实现思路。简单来说ViT把图像切成固定大小的patch比如16×16像素每个patch线性映射成一个token然后像处理文本一样通过自注意力计算任意两个patch之间的相关性。这个机制让模型天然具备全局视野——它可以同时看到叶尖和叶基的token捕捉到病斑从叶缘蔓延的整体趋势。我在水稻叶片数据上单独跑了ViT-Base/16微调实验。结果比较有意思在测试集上精度大约91.8%比EfficientNet-B0还低0.5个百分点。原因很直接ViT的强项是海量数据下的规模化能力在只有1万张图的小数据集上它缺乏CNN那样的内置归纳偏置容易过拟合而且训练收敛速度明显更慢。即便用了ImageNet预训练权重也需要更多epoch才能达到和CNN相近的水平。这里要说明一下ViT不是不能做小样本而是它对训练策略和正则化要求高得多。数据量不够、训练epoch不够、数据增强不够强ViT的表现就会被CNN压制。考虑到实际项目里不可能无限积累数据纯ViT方案我最后只作为对比基线没有作为主模型。3.3 混合架构设计与特征融合思路最终我选择的是“CNN主干 Transformer全局分支 特征融合”的混合架构。具体结构是这样的输入端统一resize到224×224。CNN分支用了EfficientNet-B0的前几层作为backbone提取浅层局部纹理特征图。Transformer分支把输入图像切成patch序列经过两层全局自注意力模块计算全局上下文关系。两个分支的输出在通道维度上做拼接送到一个轻量级注意力融合模块里让模型自己学习两个分支特征的权重分配。最后接全连接分类头输出5类概率。为什么选择“浅层CNN特征 少量Transformer层”而不是“完整CNN 完整ViT”做双塔因为双塔结构参数量会直接膨胀到80M以上推理时间也翻倍对实际部署不友好。我的思路是浅层CNN负责把局部纹理信息提取成紧凑特征Transformer分支负责补充全局位置和分布信息两个分支都不需要做太深反而能减少冗余。融合模块我尝试过三种方案。第一种是直接拼接特征后过全连接层简单但效果一般。第二种是SE注意力机制给每个通道学习一个权重这个效果提升明显Top-1到了94.8%。第三种是更复杂的门控融合网络参数量增加不少但精度提升不到0.5个百分点性价比太低最后没采用。4. 训练流程与关键参数调优4.1 损失函数、优化器与学习率策略损失函数我用的是标准的交叉熵损失没有引入额外的损失项。有人会问类别不平衡要不要用Focal Loss我看了数据分布健康图片数量大约是稻瘟病图片的1.8倍不平衡程度不算严重交叉熵加类别权重就够用了。如果某个类别样本特别少Focal Loss可以试但要注意γ参数调得过大容易让训练不稳定。优化器选了AdamW权重衰减设为0.01。这里有个细节AdamW和Adam的区别在于权重衰减实现方式不同AdamW能更平滑地解耦L2正则化对Transformer类模型尤其友好。CNN主干因为用了预训练权重学习率设置得比较低Transformer分支是随机初始化学习率可以稍微高一点。这就是分层学习率策略。我具体的参数配置如下基础学习率3e-5CNN分支、1e-4Transformer分支学习率调度Warmup Cosine Annealing前5个epoch线性从0升到5e-5然后余弦退火降到0Batch size64训练轮数60个epoch混合精度训练开启AMPfp164.2 完整训练流程与验证策略我把训练流程写成了一套可复用的pipeline大致分六个阶段。数据加载阶段用PyTorch的DataLoadernum_workers设成8开启pin_memory图像预处理在GPU上做归一化。模型初始化阶段加载EfficientNet的ImageNet权重冻结前两层后面的层全部微调。损失计算阶段按类别频率设定权重喂给交叉熵损失。反向传播阶段用AMP混合精度注意梯度裁剪设到1.0防止Transformer分支训练初期梯度爆炸。验证阶段除了常规的验证集指标我还额外记录每个类别的召回率和精确率。因为准确率在类别不平衡数据上会掩盖问题只看一个数字容易翻车。测试阶段则是在训练结束后用从未参与过训练和验证的独立测试集做一次完整评估结果就是模型交给业务方前的最终依据。这里有一个我特别想强调的操作每个epoch结束都保存checkpoint但只保留验证集准确率最高的三个不要无脑保存最后一个epoch的权重。我在训练到第20个epoch时验证准确率就到了94%后面虽然验证指标在波动但最后一个epoch反而掉到了92.5%——如果不保留最优checkpoint等于白训。4.3 参数量、FLOPs与推理速度的权衡训练之外部署性能是我非常关注的维度。我专门列了一张表对比了几种候选方案的参数规模。模型方案参数量单帧推理耗时2080TiTop-1 准确率EfficientNet-B05.3M8ms92.3%ViT-Base/1686M38ms91.8%CNNTransformer混合我的方案27M18ms95.4%混合方案比纯ViT快了一半多参数量只有它的三分之一但准确率反而高出3.6个百分点。这说明在小数据集的实际任务里盲目堆参数不是出路把两种架构的优势结合起来效果远比单一大模型要好。FLOPs方面混合模型一次前向计算约6.2G FLOPsViT-Base约17.6G FLOPs前者只有后者的三分之一。如果在边缘设备上部署这直接意味着发热更小、续航更长、并发能力更高。5. 实验结果对照与常见问题排查5.1 指标对比要看的不是Acc是每个类的Recall模型训练完我最终的测试集表现是Top-1准确率95.4%宏平均精确率95.8%宏平均召回率95.1%宏平均F1分数95.4%。单看数字很漂亮但逐类拆开看才有意思。稻瘟病和白叶枯病的召回率分别是94.6%和93.2%相对偏低。胡麻斑病和纹枯病的召回率反而都在96%以上。这个结果和预判一致——稻瘟病病斑形态多变白叶枯病又容易和生理性枯黄混淆这两类本来就是人工识别都容易出错的类别。模型性能到了这个水平继续盲目加网络深度已经没意义我反而把重心放在收集更多这两类疑难样本上做难例挖掘再训练。对比基线数据纯EfficientNet-B0的宏平均F1是91.7%纯ViT-Base的宏平均F1是90.9%混合模型直接高出3.7到4.5个百分点。这个差距在农业诊断场景里非常明显直接决定模型能不能在实际田间用。5.2 混淆矩阵与典型错误分析混淆矩阵看完我抽了大概3%的错误样本人工分析发现了三个典型问题。第一个是稻瘟病慢性和纹枯病早期互相混淆。这类错误占比接近错误样本的40%原因是两者在图像上都是椭圆形或梭形病斑颜色相似只是纹枯病病斑边缘更模糊。模型需要看到更细微的边缘纹理差异才能区分。针对这个问题我在数据增强里强化了锐化和边缘增强相当于人为放大两类之间的纹理差异。第二个问题是白叶枯病和生理性叶片黄化混淆。白叶枯病病斑从叶尖或叶缘开始发展生理性黄化则是整叶均匀变黄。模型如果只采样了局部图像区域很容易误判。这个恰恰验证了我加Transformer全局分支的必要性——全局分支确实把这一类的召回率提升了近6个百分点因为它更重视病斑的起始位置和蔓延方向。第三个问题是光线过暗的样本。低光照图像即便人眼也不好判断模型更容易输出低置信度的错误预测。我的应对策略是在后处理里加一个置信度阈值低于0.8的预测直接返回“无法判断建议重拍”而不是硬给一个标签。这在小规模试点的田间使用中评价很高——农业场景里告诉用户“拍得不清楚请重拍”比给一个错误结论负责任得多。5.3 训练中常见但没人明说的坑第一个坑验证集和训练集的来源如果分布差异过大验证Loss会抖动很厉害。我曾在一次实验里用了未经清洗的验证集结果模型训练过程中验证准确率忽高忽低一度以为训练崩了排查后发现是验证集里混了标注错误的图像。清洗之后曲线立刻平稳。验证集也是数据不是可以随便糊弄的。第二个坑Transformer分支的学习率不能和CNN设成一样。我第一版实验统一用了1e-4的学习率结果CNN分支在微调阶段就loss爆掉了原因是主干和新分支对学习率的敏感度完全不同。分层学习率是必选项不是可选项。第三个坑混合精度训练在卷积层上没问题但Transformer分支的自注意力计算偶尔会出现精度不稳定。我加了一个针对注意力层不做fp16计算的配置让注意力权重保持在fp32精度。这轻微牺牲了一点训练速度但换来的是loss曲线平稳训练过程不容易出现NaN。第四个坑数据增强里的随机裁剪比例不要设在0.08以下。太小会导致裁剪区域只是病斑的一小块失去全局叶片信息。我用了0.1到1.0的scale范围配合随机宽高比变化效果最稳。5.4 部署与落地的几点经验模型训练完之后落地时还有几个绕不开的问题。模型导出我选择ONNX格式再用推理引擎或TensorRT做加速。导出时注意把预处理里的归一化参数冻结进模型中避免部署端代码重复计算尤其是均值和标准差这些参数前后端不一致会导致结果对不上。另一个经验是模型量化。我把fp32模型转成fp16后精度几乎不掉直接推理速度提升约40%。再压到int8时准确率掉了约1.2个百分点考虑到农业场景里1%的误差意味着不少误检我最终选择保留fp16版本上线int8版本留给那些算力确实不够的边缘盒子。田间光照条件变化大是另一个部署痛点。我在项目里做了一个简单的白平衡优化在进入模型前先对图像做一次自适应直方图均衡化能显著改善逆光和阴影条件下识别的稳定性。这个预处理代码量很少但对真实场景的鲁棒性提升非常明显。最后多说一句我的实际体会做农业视觉项目模型架构的选择固然重要但真正决定项目上限的往往是数据质量和评估方式。把数据清洗做到位、把评估指标拆细、把难例收集当成一个持续迭代的闭环比强行堆一个更大规模的模型有效得多。我在这个项目里最后的模型迭代重点已经完全从网络结构调整转移到了疑难样本补充和预处理优化上。如果你也想复现类似的流程建议先从EfficientNet加少量Transformer层的混合结构开始在保证指标的同时把更多精力花在数据侧这应该是性价比最高的路径。