☰
CNN与Transformer混合架构:水稻叶片病害识别实战与移动端部署
2026/10/21 3:05:11 网站建设 项目流程

做农业视觉识别这行的朋友应该都有体会:模型在实验室里跑得再好,一到真实田间的照片上就露馅。光照忽明忽暗、叶片上沾着泥土和水珠、同一种病害在不同水稻品种上表现完全不一样——这些才是常态。我去年跟某农业科研团队合作过一个水稻叶片病害识别项目,目标很朴素:拍一张叶子照片,告诉农户这是稻瘟病、白叶枯还是胡麻斑,准确率尽量高,模型尽量小,最好能在普通安卓机上跑起来。这个项目让我把CNN和Transformer从“论文里的模块”变成了“手里的工具”,今天把完整的思路和踩过的坑写出来。

这篇文章适合两类人:一类是刚入门深度学习、想找个农业方向练手的同学,可以照着本文把整个流程跑通;另一类是已经在做图像分类、但想尝试CNN与Transformer混合架构、又不想只停留在“换个backbone”层面的研究者。文章会从任务本身出发,讲到数据预处理、模型结构设计、训练调参、评估可视化,最后聊到落地部署时那些文档里不会写的问题。

1. 水稻叶片病害识别的任务本质:三个麻烦之处

1.1 病害分类难在“类间差异小、类内差异大”

水稻常见病害里,稻瘟病、纹枯病、白叶枯、胡麻斑这几种最容易混淆。稻瘟病急性发作时是灰绿色水渍状斑点,慢性的则是褐色梭形斑,病斑周围还有黄色晕圈;胡麻斑也是褐色斑点,但形状偏椭圆,像芝麻粒。两者在颜色和形状上高度重叠,连植保专家都要结合发病部位和天气情况综合判断,模型却只能靠一张叶子图硬猜。

更麻烦的是同一种病在不同生育期的表现。稻瘟病在分蘖期的病斑小而多,到了抽穗期往往变成大块坏死。同一个模型要同时应付这些形态变化,数据里就得覆盖足够多的生长阶段和光照条件。

1.2 真实数据的“脏”与“乱”是常态

我拿到手的第一批数据,是从几个试验田和农户手机里收集的,数量不小,但质量参差不齐。有的照片对焦不准、叶片只占画面三分之一,有的图是正午强光下拍的,高光区域直接过曝,叶脉纹理全被白花花的光斑吃掉。还有一批图是黄昏时拍的,整体偏暗偏黄,肉眼看着都费劲。

这些数据直接送进网络训练,模型很可能学到的是“光照偏黄=稻瘟病”这种假相关。所以数据清洗必须做在前面,规则很简单:

  • 叶片区域占全图比例低于20%的直接丢弃或裁剪预处理
  • 严重过曝、欠曝、对焦模糊的图片剔除
  • 对于同一病斑有多张连续拍摄的,只保留清晰度最高的一张,防止同源数据在训练集和验证集之间重复出现
  • 标注有争议的图片单独放到待确认目录,找植保人员二次复核

这步做完,数据从两万多张缩到一万四千多张。听起来亏了三分之一,但后面模型收敛速度和最终准确率都证明这一步非常值。

1.3 数据增强不只是“翻翻转转”

基础增强方案:随机水平翻转、随机旋转15度、随机亮度对比度扰动、随机高斯噪声,这些都有用。但实际效果提升更明显的是两种针对性增强。

第一种是随机遮挡(Random Erasing),模拟叶片被其他叶子挡住、或者病斑区域被水珠反光盖住的情况,强制模型不要只依赖病斑中心那一小块特征。第二种是色彩抖动,不只在HSV空间调亮度和饱和度,还会对RGB通道单独做小幅度扰动,用来模拟不同手机摄像头白平衡不一致的现象。

另外我也把CutMix引入了训练。CutMix会把一张图的病斑区域切下来贴到另一张图上,标签也按面积比例混合。这对叶片病害这种局部纹理特征很敏感的识别任务特别有效,相当于人为制造了大量“一片叶子同时有健康部分和染病部分”的样本,模型被迫学会区分区域特征而不是整图记忆。实测下来,Top-1准确率提高了大约1.8个百分点。

提示:CutMix不是无脑加。如果你的病害区域占比本身很高,CutMix会把多个病斑叠在一起,导致特征严重错位。建议先统计数据里病斑面积占比的分布情况,再决定CutMix使用的混合比例。

2. 数据集与预处理:项目成败的第一道分水岭

2.1 数据集结构与类别梳理

我使用的数据集总共包含7个类别:稻瘟病、纹枯病、白叶枯、胡麻斑、稻曲病、细菌性条斑病、健康叶片。其中健康叶片占比最初高达35%,因为农户觉得正常的叶子没有病,拍起来顺手,导致类别严重不均衡。

针对不均衡问题,我没有一上来就死磕Focal Loss,而是先做了更朴素的操作:对少数类样本做过采样复制,对多数类做随机欠采样,把各类数量拉到一个相对接近的范围。然后分类权重仍然保留,等于双保险。经验是,过采样配合简单交叉熵在多数情况下比直接上Focal Loss更稳,Focal Loss的两个超参数太敏感,搜索成本高。

2.2 图像尺寸的选择

常见分类网络的输入是224×224,但叶片病害的病斑往往很小。224分辨率下,一个直径10像素的病斑可能被下采样到2像素,信息全丢。我做了两组对照实验:一组用224×224输入,一组用384×384输入,同样的骨干网络,后者准确率高了将近3个点。

代价很明显:显存占用和训练时间都上去了。考虑到目标设备是手机,最终方案是训练阶段用384分辨率,推理阶段保持384输入。CNN的骨干部分把空间分辨率从384降到96,Transformer模块在96×96的feature map上做全局建模,计算量还能接受。

2.3 标签噪声的代价

农业数据的标注误差比工业界数据高得多,这是做这个领域必须接受的现实。我抽样复核了500张图,发现约有6%的标签是错误的,其中很多是稻瘟病和胡麻斑混淆。

处理这类标签噪声的方法,实际效果最好的是“预测一致性筛选”:先用一个训练好的模型对训练集做预测,把模型高置信但标注不同的样本挑出来,人工核对。这个方法不需要额外标注成本,只借助已有模型和标注者的抽查即可完成。最后修正了约500张图的标签,整体准确率又涨了约1个百分点。

3. 模型设计:CNN提取细节、Transformer建模全局,两者怎么配合

3.1 单用CNN或纯Transformer的各自短板

纯CNN(比如ResNet50)对局部纹理的提取非常强,病斑的形状、边缘、颜色变化这些信息由浅层卷积就能抓到。但随着网络加深,感受野变大,全局上下文信息逐渐混入,反而容易把局部细节“抹平”。而且卷积网络的归纳偏置太强,需要很多层才能建立起长距离依赖,叶片上两个相距很远的病斑是否属于同一病害,这种关联很难建模。

纯Transformer(比如ViT)能建模全局关系,但它靠的是注意力机制对所有patch两两交互。没有卷积提供的局部先验,就要求数据量非常大才能学到有意义的纹理特征。水稻病害数据集通常在十万张以内,直接训ViT容易过拟合,而且ViT对图像分辨率变化非常敏感,换了输入尺寸位置编码就要重插值,很麻烦。

3.2 混合架构的整体思路:卷积做主、Transformer做全局校准

我最终采用的方案是“CNN主干 + Transformer编码器尾巴”的组合。CNN主干(选用跨阶段局部网络结构)负责把图像逐步下采样,并输出一个空间分辨率适中的feature map,相当于把原始图像压缩成一组高语义、中分辨率的特征;然后把feature map按空间位置切成patch序列,塞进一个小型Transformer编码器里,让注意力在全局范围内重新校准每个区域的特征。

这样设计逻辑很清晰:CNN先干它擅长的活——提取局部纹理和边缘信息;Transformer再干CNN不擅长的活——把不同位置的病斑信息关联起来。两者分工明确,而不是把Transformer模块硬塞到每个阶段里,那样既拖慢速度,又容易稀释卷积特征。

3.3 token化与位置编码:特征图怎么变成序列

假设CNN主干输出的是feature map,尺寸为通道数C、高H、宽W。我把这个feature map直接视为一个长度为H×W的token序列,每个token的特征维度就是C。这样就不需要做标准的patch embedding操作,省了一组卷积参数。

由于原始图像尺寸固定384且网络结构固定,CNN输出的feature map尺寸也是固定的96×96,所以位置编码可以选择可学习的绝对位置编码,效果够用。实际项目中没有选择相对位置编码,因为模型只在固定分辨率上推理,不需要跨分辨率泛化。

以下是Transformer编码器核心定义,可以当作参考:

import torch import torch.nn as nn class TransformerBlock(nn.Module): """ 标准Transformer编码层:Norm -> Attention -> DropPath -> Norm -> MLP -> DropPath """ def __init__(self, dim, num_heads, mlp_ratio=4.0, drop_path=0.1): super().__init__() self.norm1 = nn.LayerNorm(dim) self.attn = nn.MultiheadAttention(dim, num_heads, batch_first=True) self.norm2 = nn.LayerNorm(dim) self.mlp = nn.Sequential( nn.Linear(dim, int(dim * mlp_ratio)), nn.GELU(), nn.Linear(int(dim * mlp_ratio), dim), ) self.drop_path = DropPath(drop_path) if drop_path > 0 else nn.Identity() def forward(self, x): x = x + self.drop_path(self.attn(self.norm1(x), self.norm1(x), self.norm1(x))[0]) x = x + self.drop_path(self.mlp(self.norm2(x))) return x

要注意的是,这里的多头注意力接收的query、key、value是同一个输入,也就是自注意力,三层都走同一个归一化后的特征。这样写的好处是结构简洁,梯度传播更稳。

3.4 消融实验的设计:证明Transformer这一层确实起作用

加Transformer层不是拍脑袋,我做了一组完整的消融实验来确认每个模块的贡献:

模型配置Top-1准确率参数量备注
纯CNN主干91.2%8.4M基线
CNN主干 + 1层Transformer92.8%9.1M提升明显
CNN主干 + 2层Transformer93.3%9.8M效果最好
CNN主干 + 4层Transformer93.1%11.2M收益饱和
CNN主干 + 2层Transformer + CutMix94.5%9.8M最终方案

从表里能清楚看到,从0到2层Transformer的提升最大,4层之后准去率反而微微下降。这符合预期:96×96的token数量是9216个,对于自注意力来说计算量不小,层数加深会稀释已经由CNN提取好的局部特征。最终方案只保留2层。

注意:Transformer的token数量是H×W,也就是96×96等于9216个token,attention矩阵是9216×9216,显存消耗很大。如果主干输出分辨率再大一些,显存就扛不住了。这也是为什么早期实验没有直接在原图上做ViT,而是让CNN先把分辨率降下来。

4. 训练阶段的实战配置与调参记录

4.1 超参数选择的思路:不是照抄而是推演

优化器选了AdamW,初始学习率3e-4。很多人一上来就用1e-3甚至1e-4,但我更建议根据batch size和模型规模来推算。CNN主干参数量约8.4M,加上Transformer头共9.8M,batch size设为32,学习率3e-4是一个合理的起点。如果batch size翻倍到64,学习率可以尝试调到5e-4,但要注意warmup阶段适当拉长。

warmup用5个epoch,先把学习率从1e-5线性升到3e-4,再用余弦退火在剩下65个epoch里降到1e-6。这个设计的目的是让前几个epoch模型在平稳状态下探索,避免初始梯度过大把预训练权重冲坏。总训练70个epoch,是几组实验里效果和时间的平衡点。

训练参数清单如下:

参数值理由
输入尺寸384×384保留更多病斑细节
batch size32对应3e-4学习率的经验组合
优化器AdamW配合权重衰减效果稳定
权重衰减5e-2略高,抑制小数据集过拟合
初始学习率3e-4中等规模模型的常见起点
warmup5 epoch稳定初期训练
学习率策略余弦退火后期细粒度收敛
总epoch70验证集准确率开始饱和的节点
混合精度FP16显存节省约40%,速度提升明显

4.2 损失函数:标签平滑的意外收获

分类头直接接全连接层,输出7个类别的logits,损失函数用交叉熵加上标签平滑,smoothing设为0.1。标签平滑的本质是给正确标签的置信度“打折扣”,把一部分概率分配给其他类别,防止模型对训练样本过分自信。

这个设置在农业病害数据上特别实用,理由是标签噪声高。如果模型对某个错误的标签给出100%的置信,梯度会把参数往错误方向猛推;标签平滑等于给每个样本都留了一点余地。实测下来,标签平滑让验证集准确率提升了约0.8个点,对病斑特征不明显的胡麻斑类别尤其明显。

4.3 训练过程监控:不只是看loss曲线

训练时我同时记录训练loss、验证loss、Top-1准确率和Top-5准确率。这中间有个容易忽略的点:如果验证loss下降但训练loss不降,多半是模型容量不够,需要加深或者加宽;如果训练loss降但验证loss不降甚至上升,才是过拟合信号,这时应该加正则而不是继续调学习率。

我还开启了EMA(指数移动平均)对模型参数做平滑。EMA的衰减系数设为0.999,作用是让最终使用的模型参数是过去训练轨迹的平均值,而不是最后一个batch更新后的瞬时值。这个方法对任何视觉任务都能白捡0.3~0.5个点,也不增加推理时间,强烈建议在训练脚本里顺手加上。

4.4 显存不足与训练速度的务实解法

384输入、batch size 32,在单张24G显存的卡上很宽裕,但如果只有12G的卡就会有压力。我试过的方案里,性价比最高的是梯度累积:把batch size从32拆成4个micro-batch,每个micro-batch为8,梯度累积4步再更新一次参数。效果与batch size 32基本一致,显存占用能降一半。

另一个有效手段是mix precision训练,这句话听起来很常规,但在模型定义里要小心哪些层不适合FP16。LayerNorm和最后的分类头建议保留FP32精度,否则会偶发loss变成NaN的情况。我用PyTorch原生AMP时,给这两个部分加了一层float32的强制转换,之后训练再没出过数值问题。

5. 评估模型时我关注的五项指标与可视化验证

5.1 准确率之外,混淆矩阵才是关键

最终模型在测试集上的Top-1准确率是94.5%,听起来不错,但区分细看时发现问题都集中在两个地方:稻瘟病和胡麻斑互相混淆,贡献了将近一半的误判;白叶枯和细菌性条斑病在叶片症状相似时也会混淆。

这提醒了一个很实际的问题:如果只是做“有病害还是没病害”的二分类,模型性能完全够用;但如果农户需要的是“该喷哪种药”,混淆的代价就会直接落到经济层面。所以评估时必须看每一类别的精确率和召回率,而不是被总体准确率蒙蔽。

5.2 Grad-CAM可视化的正确打开方式

可视化能告诉我们模型真正的依据。我选了一批测试集图片,用Grad-CAM生成热力图,发现绝大多数正确预测的热力区域都集中在病斑周围,这很健康。但有几张错误样本暴露了模型的小心思:它聚焦在叶片边缘的枯黄区域,而不是中央的典型病斑。这类叶片如果病斑太小,模型就会退而求其次去借鉴边缘枯黄特征。

这种问题怎么解决?我尝试把边缘枯黄的样本单独提出来,做了一次简单的“难例挖掘”,把这部分数据往训练集和测试集里多塞一些。经过三四轮迭代,混淆率明显下降。可视化的价值不在于好看,而在于指引数据迭代方向。

5.3 测试集上的鲁棒性考验

实验室测试集都是在受控条件下拍的,为了模拟真实使用场景,我额外构造了一个“脏测试集”:对每张测试图随机叠加亮度扰动、模糊和高斯噪声,强度和训练时保持一个量级。模型在这个脏测试集上准确率降到91.8%,下降幅度约2.7个点,属于可接受范围。

如果脏测试集准确率掉得太多,就该回看数据增强策略,而不是盲目加大模型。有一次实验我把颜色抖动幅度调得过大,训练集loss一度降得很漂亮,但脏测试集反而更差,说明过强的增强把模型逼成了“色盲”。这个平衡点需要反复试。

6. 从实验室到田边:部署与长期维护的真实问题

6.1 模型压缩与量化:从94.5%到92.8%但能跑起来

训练好的模型大约9.8M参数,FP32权重约39MB,对手机来说不算大,但推理速度不够理想。我先把模型转换为ONNX格式,再转成TensorRT做FP16推理,速度提升接近3倍。随后尝试了INT8量化,准确率掉到92.8%,掉了约1.7个点,换来的是模型体积只剩4MB,推理时间进一步缩短。

INT8量化时最需要注意的是校准集选择。我用训练集里各类别均匀抽样的500张图做校准,效果比随机抽1000张好。因为随机抽样会让健康叶片的占比过高,校准后的量化阈值严重偏向多数类,少数病害类的激活值被过度压缩,识别率会明显下降。

6.2 真实田间部署:模型的“水土不服”

实验室跑得再好,到了田里照样可能出状况。农户拍摄时叶片往往不在画面中央,图像里有手指、泥土背景,甚至整张图是好几片叶子叠在一起。模型如果只见过单叶居中图片,遇到这种场景就会手足无措。

对策是在上线前做“场景适配”:收集农户实拍图,专门标注一批包含复杂背景的图片,微调模型。我在微调时用了更小的学习率(1e-5),只更新最后两个Stage和分类头,防止前面的卷积特征被新场景冲乱。三四个epoch后,实拍图片上的准确率就从不忍直视的79%拉到了88%。

6.3 持续迭代:病害识别不是一次训练就结束的

水稻病害有显著的季节性,早稻和晚稻的病害分布不同,不同年份的气候差异也会影响病害形态。所以我把模型做成“持续更新”的节奏:每季收集新样本,验证准确率明显下降时触发自动再训练。

实际操作上,我会保留每个季度的模型快照,新数据只对上一季度模型做增量训练。这样既避免旧知识被遗忘,又能逐步适应新出现的病害形态和拍摄设备差异。一个来自真实项目的体会是:永远不要把“训练一次、上线永久”当作目标,农业视觉识别天然就是一个需要随季节进化的系统。

6.4 算力与网络限制下的取舍

农户端设备良莠不齐,有的手机算力低,加载4MB的INT8模型没问题,但跑一次推理可能要800毫秒。我用了一个很务实的折中方案:在低端设备上先跑一个轻量的二分类模型判断“有没有病害”,对于判断为有病害的图片,再上传到服务器跑七分类细粒度识别。

这样做的理由是:“有没有病”对算力的要求远低于“是什么病”,轻量模型在端上只需要两百毫秒就能出结果,用户体验基本不受影响。只有当端上模型判断有病害时,才需要网络上传,大幅降低了对流量和时延的要求。

一些实操中的体会

做这个项目最大的体会是,混合架构不是“把两个模块堆起来”就行了,干燥的视觉任务里,能解释清楚每个模块的作用,比单纯追求更高一个点的准确率更有价值。CNN负责局部纹理、Transformer负责全局建模,这个分工思路几乎可以迁移到任何细粒度图像识别任务,包括其他作物病害、工业表面缺陷检测、甚至医学影像分类。

另外训练过程中遇到的多数问题,根子都不在网络结构而在数据。模型误判稻瘟病和胡麻斑,不是因为卷积不够深,而是因为标注数据里这两类本身就长得像。把数据洗干净、把增强做对、把评估看透,比换更花哨的网络结构更管用。先跑通基线,再逐项验证每个模块的贡献,最后再做压缩部署——这套流程值得每个做落地项目的同行参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询