拓冰建站拓冰建站
首页 / 资讯中心 / 正文

苹果叶片病害多标签分类:LeNet与ViT对比及ArcFace损失实践

简介基于CNN网络LeNetViT的苹果树叶片病害分类识别系统配套设计报告与说明文档面向深度学习课程设计、毕业设计或图像分类项目实践也适合具备一定Python基础的学生参考学习。压缩包共47个文件包含14个Python源码涵盖LeNet与ViT的训练、测试、配置及ArcFaceLoss实现、3个Keras模型权重、1个Jupyter数据分析脚本、设计报告PDF与说明文档MD以及19张图表整体大小约17.35MB目录按LeNet、ViT和文档模块区分便于检索。目前已有43人学习/下载。资源基于Kaggle FGVC8CVPR 2021植物病理学数据集完整覆盖数据预处理、多标签分类问题转换、ArcFaceLoss与CrossEntropyLoss损失函数对比、模型训练与测试流程并给出LeNet约65%、ViT约60%的准确率结果及数据集裁剪效果评估。设计报告和说明文档详细记录项目结构、实验思路与运行方式便于快速复现和二次开发是课设/毕设的高质量参考资料。1. 从苹果叶片病害到CNN与VIT的分类选型第一次跑通Kaggle FGVC8CVPR2021苹果叶片病害数据集时最让我意外的不是模型效果而是标签格式同一张叶片上可能同时出现锈病和黑腐病数据集的Disease列里会用组合标签表达这种叠加情况。也就是说这个课设表面上写着病害分类实际做的是多标签分类单纯套CrossEntropyLoss会直接在训练阶段出错。这份资源把LeNet和Vision Transformer两条路线完整实现了一遍LeNet稳定在65%准确率自搭建的ViT也能到60%还附带ArcFaceLoss和CrossEntropyLoss的对比实验以及设计报告。对正在准备课设、毕设或者想从零跑通一个CNN和Transformer对比实验的读者来说最值得拆的是它的数据处理链路和模型头部的设计差异。2. LeNet网络结构与多标签数据预处理处理多标签分类第一步不是选模型而是把标签从字符串变成模型能学的张量。FGVC8的train.csv里每行记录一张图片对应的Disease名称同一张图可能映射到多个病害名称。如果沿用单标签思维直接对类别做argmax黑腐病和锈病同时出现时会直接丢掉一半的真值。这套项目采用multi-hot编码每个类别一个输出维度病害叠加的图会在对应多个维度上同时置1。2.1 数据加载与multi-hot编码data_preprocessing.py里做标签编码的核心逻辑是先把csv里所有标签拆词去重建立类别索引表再逐行映射def build_class_index(csv_path): all_labels set() df pd.read_csv(csv_path) for row in df.itertuples(): for label in str(row.Disease).replace(,, ).split(): all_labels.add(label.strip()) return sorted(all_labels) def encode_label(labels_text, class_index): vec torch.zeros(len(class_index), dtypetorch.float32) for label in labels_text.replace(,, ).split(): label label.strip() if label in class_index: vec[class_index[label]] 1.0 return vecbuild_class_index遍历整个csv收集全部独立标签encode_label对每一行生成一个与类别数等长的0/1向量。注意这里没有把multiple_diseases这类组合标签二次拆分成原始病害因为在FGVC8里它本身就是一个标注类别。如果自己改写成healthy置0同时给具体病害置1会改变数据原始分布设计报告里就得多解释一层所以原项目保持了multi-hot原样。数据加载部分输入尺寸统一为224x224与后续ViT对齐图片读取用PIL打开转RGB后resize没有用RandomResizedCrop因为多标签场景下随机裁剪太狠容易把病灶区域裁掉数据增强只用了随机水平翻转和ColorJitter。2.2 LeNet结构重建与全连接层调整LeNet-5是最经典的卷积神经网络结构之一原本为32x32灰度手写数字设计两个卷积块加三个全连接参数总量很小。直接搬来处理224x224的三通道叶片图第一个问题就是全连接层维度爆炸原始输入经过两次步长2的池化后特征图边长降到原图四分之一展平后维度接近五万老结构根本接不上。项目里的处理方式是保留卷积块在全连接之前插入AdaptiveAvgPool2dclass LeNet(nn.Module): def __init__(self, num_classes, in_channels3): super(LeNet, self).__init__() self.features nn.Sequential( nn.Conv2d(in_channels, 6, kernel_size5, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), nn.Conv2d(6, 16, kernel_size5), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), ) self.avgpool nn.AdaptiveAvgPool2d((4, 4)) self.classifier nn.Sequential( nn.Linear(16 * 4 * 4, 120), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(120, 84), nn.ReLU(inplaceTrue), nn.Linear(84, num_classes), ) def forward(self, x): x self.features(x) x self.avgpool(x) x torch.flatten(x, 1) return self.classifier(x)自适应池化的意义在于无论输入尺寸怎么换全连接层拿到的向量长度始终是1644256比写死维度再反推池化核参数要省心得多。训练时模型最后一层直接输出类别数的logitsforward里不加sigmoid把sigmoid交给BCEWithLogitsLoss内部处理。新手最容易犯的错就是先手动sigmoid再传给BCEWithLogitsLoss两者叠加导致收敛极慢loss长期徘徊在0.7附近不降。2.3 训练配置与准确率观察LeNet的训练入口是main_train.py通过--model参数区分跑哪套网络。CONFIG.py里集中管理数据路径、图片尺寸、学习率和epoch数复现时只需要在命令行覆盖参数python main_train.py \ --model lenet \ --data_dir data/FGVC8 \ --batch_size 64 \ --epochs 50 \ --lr 1e-3 \ --loss bce常用超参数参考参数推荐值说明batch_size64224x224输入下单卡无压力epochs50第35轮附近容易出现验证集过拟合lr1e-3Adam默认从这个量级起步weight_decay1e-4叶片纹理复杂不加正则很容易过拟合img_size224与ViT保持一致便于横向对比这套配置下LeNet验证集准确率能稳定落在63%~68%之间资源里记录的65%是常规水平。需要说明的是这个准确率用的是严格匹配一张图的multi-hot预测必须与标签完全一致才算对。如果改成单标签Top-1命中数字会再高一大截所以看到65%不要觉得是模型写得有问题多标签评估本来就比单分类严苛。3. Vision Transformer的结构拆解与训练观察ViT的核心思想可以拆成三个词Patch、Token、Attention。把224x224的叶片图切成16x16的小块每块拉直后经过线性投影变成token196个patch token加上一个分类专用的CLS token组成197个向量的序列再送进标准的Transformer Encoder。模型不再像CNN那样用卷积核扫过整张图而是让每个patch直接和所有其他patch做注意力交互。3.1 Patch Embedding与位置编码VIT/models.py里的PatchEmbedding用一层卷积同时完成切patch和线性投影卷积核大小等于patch_size步长也等于patch_size卷积输出通道数就是embedding维度。等效于把每个16x16x3的patch展开成768维向量再乘一个投影矩阵class PatchEmbedding(nn.Module): def __init__(self, img_size224, patch_size16, in_chans3, embed_dim576): super().__init__() self.grid_size img_size // patch_size self.num_patches self.grid_size ** 2 self.proj nn.Conv2d(in_chans, embed_dim, kernel_sizepatch_size, stridepatch_size) def forward(self, x): x self.proj(x) # [B, D, 14, 14] x x.flatten(2) # [B, D, 196] x x.transpose(1, 2) # [B, 196, D] return xembed_dim设置为576而不是ViT-Base的768是为了在单卡上把显存压住。12层Transformer加768维在224x224输入下显存占用很高课设场景一般会把深度降到6层、embedding降到576甚至512。因为自注意力是置换等变的token顺序被打乱结果不变所以必须显式加入位置编码。ViT实现里位置编码初始化为零向量维度是1x197xembed_dim和cat了CLS token之后的序列对齐class ViT(nn.Module): def __init__(self, num_classes, patch_size16, embed_dim576, depth6, num_heads8): super().__init__() self.patch_embed PatchEmbedding(224, patch_size, 3, embed_dim) self.cls_token nn.Parameter(torch.zeros(1, 1, embed_dim)) self.pos_embed nn.Parameter( torch.zeros(1, self.patch_embed.num_patches 1, embed_dim)) encoder_layer nn.TransformerEncoderLayer( d_modelembed_dim, nheadnum_heads, dim_feedforwardembed_dim * 4, dropout0.1, activationgelu, batch_firstTrue) self.encoder nn.TransformerEncoder(encoder_layer, num_layersdepth) self.head nn.Linear(embed_dim, num_classes) def forward(self, x): x self.patch_embed(x) cls self.cls_token.expand(x.size(0), -1, -1) x torch.cat([cls, x], dim1) x x self.pos_embed x self.encoder(x) return self.head(x[:, 0])CLS token是ViT和CNN在分类头上最大的结构差异。CNN通常靠全局平均池化把特征图压成一维ViT则是在序列前面放一个可学习的向量让它在若干层自注意力中不断聚合所有patch的信息最后只在这一位接全连接分类头。病害区域小、分布分散时CLS token理论上能比池化保留更多全局上下文。3.2 多头自注意力与CNN卷积的归纳偏置对比CNN和ViT最本质的差异在归纳偏置。卷积核只有3x3或5x5的局部感受野但它在空间上滑动靠堆叠层数逐步扩大感受野。ViT第一层就能看见整张图因为自注意力的attention矩阵是197x197的稠密矩阵每个patch都要和全部patch计算相似度。在苹果叶片病害这个小规模数据集上这个差异直接体现为数据效率CNN默认假设邻近像素语义相近几十个epoch就能把纹理特征学好ViT把邻近像素相近这个先验也交给数据去学数据量不够时训练曲线会很毛糙需要更长的warmup、更小的学习率。所以ViT在这份数据上跑出60%准确率并不算差它是在没有使用任何预训练权重的前提下从头训练的结果和LeNet的65%对比起来才有参考价值。训练命令python main_train.py \ --model vit \ --batch_size 32 \ --epochs 60 \ --lr 5e-4 \ --loss bce \ --vit_depth 6 \ --vit_embed_dim 576ViT的batch_size降到32是因为自注意力矩阵随序列长度平方增长197个token的注意力图在梯度回传时占用显存不小。如果显存仍然吃紧可以加梯度累积用--grad_accum 4等效模拟batch_size 128。3.3 ViT训练过程中的关键调参最值得说的调参点是学习率warmup。ViT在这个数据上前10个epoch的loss下降明显慢于LeNet一上来就用5e-4前几个batch的loss容易跳到NaN。我一般会在VIT/main_train.py里加前5个epoch的warmup逻辑学习率从1e-5线性升到5e-4再按余弦方式衰减。复现时注意logs目录下loss曲线的形状如果前10个epoch还在高位横盘不要急着加epoch先检查学习率是不是直接给大了。第二个观察是过拟合比LeNet更早。ViT参数总量比LeNet大几倍训练到30个epoch左右验证集指标就开始滞涨甚至回落训练loss却还在下降。缓解方案是加大Dropout和weight_decay或者把patch_size从16改成8。但patch减小后token数量会从196变成784注意力矩阵膨胀到784x784显存和训练时间都会明显上涨所以项目最终保留16这个折中值。4. ArcFaceLoss与CrossEntropyLoss的对比实验这个项目里最有报告价值的部分不是两个模型本身而是ArcFaceLoss和CrossEntropyLoss的横向对比。很多做课设的读者第一次接触ArcFace是在人脸识别里不知道它也能用在植物病害这种细粒度分类上。两种损失的本质区别在于监督信号的作用位置CrossEntropy直接约束分类得分ArcFace在角度空间里对类中心与特征之间的夹角加margin。4.1 两种损失函数的适用边界CrossEntropyLoss计算的是预测概率和真实标签的交叉熵它只要求目标类得分高于其他类特征分布没有显式约束类别多时类间混叠比较常见。ArcFace则先把特征向量和分类权重都做L2归一化让两者的点积等于夹角余弦再在目标类夹角上加上一个margin迫使同类样本在单位超球面上聚拢。用人话说CrossEntropy让模型选得对ArcFace让模型分得开。但ArcFace原生是单标签设计直接拿来做多标签病害识别会有两个坑。第一个坑是标签维度multi-hot里有多个1不能直接用CrossEntropyLoss需要的long型标签索引。第二个坑是margin作用方式负类夹角不动正类夹角被推开在softmax里是为了让目标得分相对更突出如果换成交叉熵或BCE这种逐维度独立优化的损失需要对每个输出维度单独判断正负。4.2 多标签ArcFace的工程实现ArcLoss.py里的实现思路是把ArcFace的角度margin和BCE的逐标签独立性组合在一起每个类别拥有独立的类中心向量正类样本在余弦空间套上margin负类保持不变最后用BCEWithLogits做多标签损失# ArcLoss.py — 多标签 ArcFace 变体 class ArcFaceLoss(nn.Module): def __init__(self, feat_dim, num_classes, margin0.3, scale32): super().__init__() self.margin margin self.scale scale self.weight nn.Parameter(torch.randn(feat_dim, num_classes)) def forward(self, features, labels): w F.normalize(self.weight, dim0) # [D, C] 每一列是一个类中心 x F.normalize(features, dim1) # [B, D] 归一化特征 cos_theta torch.mm(x, w) # [B, C] 余弦相似度 theta torch.acos(cos_theta.clamp(-1 1e-7, 1 - 1e-7)) target_mask labels.bool() target_logits torch.cos(theta self.margin) # 正类加 margin logits torch.where(target_mask, target_logits, cos_theta) return F.binary_cross_entropy_with_logits(self.scale * logits, labels)normalize(dim0)是对每一列做归一化对应每个类中心向量normalize(dim1)是对每个样本特征归一化。acos把余弦值反解成角度后加上margin再cos回去就得到带角度间隔的logits。scale32把余弦值从[-1,1]放大到[-32,32]让sigmoid输出的概率分布更尖锐避免梯度消失。torch.where根据multi-hot掩码决定哪个位置取带margin的logits。这个变体和官方人脸识别ArcFace的区别在于最后没有接softmax而是用sigmoid加BCE这样能同时处理多个正类标签。4.3 损失函数对比结果与复现建议从logs目录下的训练曲线看两个损失函数的对比实验有几个稳定现象。一个是ArcFace收敛更慢前15个epoch几乎看不到提升CrossEntropy在同样时间已经跑到55%附近另一个是ArcFace的最终准确率受随机种子影响大同一套超参数下训练三次最好和最差能差出3到4个百分点。对比维度CrossEntropyLossArcFaceLoss收敛速度快10轮内见效慢10~15个epoch最终准确率稳定方差较大多标签适配需要改BCEWithLogits逐类margin工程稍复杂特征可分性类间有混叠类中心更分散如果目标只是课设里的准确率指标CrossEntropy改成BCE是更省事的方案。ArcFace的价值不在那一个准确率数字上而在特征分布的可解释性把最后一层特征用PCA降到二维画散点图ArcFace组的同类样本明显聚得更紧这个现象写进设计报告里比单纯贴两个准确率更有说服力。5. 数据集裁剪评估与模型验证技巧5.1 裁剪尺寸对精度的实际影响FGVC8原始图片里叶片并不居中背景、土壤和相邻叶片都会进入画面。项目专门做了数据裁剪评估做法是给每个叶片外接框裁剪后缩放成不同尺寸训练对照组。我复现时的经验是用224x224做基准放大到288后LeNet准确率能小幅提升如果裁剪框太紧裁到病灶边缘准确率反而会掉1%以上。评估脚本通过CONFIG.py的img_size字段控制for size in 224 256 288; do python main_train.py \ --model lenet \ --img_size $size \ --batch_size 64 \ --epochs 30 \ --log_dir logs/lenet_crop_$size done比较各log_dir下的val_acc曲线差距在1%以内说明裁剪没丢关键信息超过3%说明外接框太紧把黑腐病斑或锈斑边缘切掉了。这类对照实验在答辩时非常实用直接展示了数据预处理对模型性能的影响程度。5.2 多标签模型的验证指标多标签分类不能只看准确率至少要看严格匹配准确率、宏平均F1和汉明损失。main_test.py里可以这样算def exact_match_accuracy(y_true, y_pred): return (y_true y_pred).all(dim1).float().mean().item() def macro_f1(y_true, y_pred): eps 1e-9 tp (y_true.bool() y_pred.bool()).sum(dim0).float() fp ((~y_true.bool()) y_pred.bool()).sum(dim0).float() fn (y_true.bool() (~y_pred.bool())).sum(dim0).float() precision tp / (tp fp eps) recall tp / (tp fn eps) f1 2 * precision * recall / (precision recall eps) return f1.mean().item()exact_match_accuracy要求模型输出的multi-hot预测与标签张量完全一致才算对macro_f1对每个病害类别单独算F1后取平均类别不平衡时比准确率更能反映真实效果。main_test.py加载模型时建议保留验证集上loss最低的检查点而不是最后一个epoch训练后期过拟合几乎必然发生best.pth通常能带来1%到2%的测试集提升。多标签预测还有一个容易被忽略的调优点sigmoid输出的阈值不一定就是0.5。类别不均衡时0.5往往过于保守可以在验证集上从0.3到0.7扫一遍取宏F1最高的点best_th, best_f1 0.5, 0.0 for th in np.arange(0.3, 0.7, 0.05): pred (probs th).float() f1 macro_f1(labels, pred) if f1 best_f1: best_th, best_f1 th, f1把扫出来的阈值写回CONFIG.py作为main_test.py的--threshold参数传入测试阶段就按调过的阈值输出。再把阈值扫描结果和裁剪对照表补进设计报告的验证部分整个项目从数据处理到损失函数再到阈值调优就都有数据支撑了。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门