1. 为什么是ResNet torchvision一个非常现实的选型逻辑做水稻虫害分类很多人第一反应是“自己设计一个CNN”或者直接上最热的Vision Transformer。但以我这几年做农业图像项目的经验来看ResNet torchvision预训练权重往往才是性价比最高的起点。为什么三个很现实的原因。第一农业场景下的虫害数据集通常不大。常见的情况是每类几百到几千张图片总共几千张。这种规模从零训练一个深层网络非常容易过拟合——模型会把训练集的背景、光照、叶片纹理都背下来换一批图就“翻车”。而ResNet在ImageNet上已经学到的底层特征边缘、纹理、形状、颜色分布是通用的迁移过来只需要让它去学“水稻叶片上这个虫害的局部特征”数据需求就能大幅下降。第二torchvision的封装确实省心。模型定义、预训练权重下载、数据预处理、训练测试用的经典网络结构全都帮你准备好了。相比自己写残差块、自己找权重文件开发效率完全不在一个量级。对于刚接触深度学习分类任务的同学这是个非常友好的入口对于有经验的开发者也能把精力集中在数据处理和调参这种真正影响效果的地方。第三ResNet本身的设计非常成熟。它用残差连接解决了深层网络梯度消失的问题所以在ImageNet上有着从ResNet18到ResNet152的一系列选择。你可以根据显存和精度要求自由选择体量。我用下来14类虫害这种中等粒度任务ResNet50是最稳的选择——精度够用显存占用也比较合适单卡6GB以上就能轻松跑起来如果数据量特别少或者只想快速验证流程ResNet34也足够。模型参数量单张224x224推理显存占用batch32适合场景ResNet18约11M1GB以内快速验证、移动端部署ResNet34约21M1.2GB左右小数据集、轻量方案ResNet50约25M约2GB绝大部分农业图像分类任务ResNet101约44M约3GB类别多、类间差异极小的任务顺便说一句很多人纠结“要不要用Transformer”。我的看法是Vision Transformer确实在某些大数据集上表现更强但预训练ViT对数据量、学习率、正则化的敏感度都比ResNet高不少在几千张的虫害数据集上动辄要调很久。你只是需要一个能落地、效果可复现的方案ResNet的容错性显然更好。2. 数据和环境先把“饭”备好2.1 环境版本匹配是第一道坎PyTorch生态有个让人挺头疼的特点PyTorch和torchvision必须配套。如果你用新版PyTorch配旧版torchvision或者反过来导入的时候经常会直接报错提示找不到某个符号或模块。而且现在很多写教程的人用的是老版本的pretrainedTrue写法但新版torchvision已经把这个参数标记为即将弃用换成了weightsxxx的API。我的建议是直接用当前的主流稳定版组合。以我最近复现的环境为例Python 3.10PyTorch 2.x比如2.3或2.4torchvision 0.18或0.19与原版配套CUDA 11.8或12.1取决于显卡驱动版本NumPy、Pandas、Matplotlibscikit-learn后面做分类报告和混淆矩阵会用到安装时最好直接用官方源或者国内镜像一条命令解决pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118如果你不需要GPUCPU版本也能跑通全流程就是训练慢一些。验证安装是否成功import torch import torchvision print(torch.__version__) print(torchvision.__version__) print(torch.cuda.is_available())torch.cuda.is_available()输出True才说明GPU可用。这个检查一定要做很多人后面跑了好半天发现模型在CPU上白白浪费时间。2.2 数据目录结构按ImageFolder标准来整理虫害数据集的来源渠道很多。有的是从某农业合作项目拿到的有的是从公开数据集平台下载的有的甚至是自己在地里采集的照片。不管来源如何第一步都是把所有图片按类别放到对应的文件夹里。推荐的结构是rice_pests/ ├── train/ │ ├── 稻飞虱/ │ │ ├── img_001.jpg │ │ ├── img_002.jpg │ │ └── ... │ ├── 二化螟/ │ ├── 三化螟/ │ ├── 稻纵卷叶螟/ │ ├── 稻瘟病/ │ ├── 纹枯病/ │ └── ... (共14类) └── val/ ├── 稻飞虱/ │ ├── val_001.jpg │ └── ... └── ...用这种结构最大的好处是PyTorch的torchvision.datasets.ImageFolder可以直接读取它会把子文件夹名称自动映射成类别索引按名称排序不需要你手工去写一份图片路径和标签的对应表。整理的时候有几个容易踩的坑图片格式统一.jpg、.png、.jpeg混着没问题但一定要确认所有图片都能正常打开。某些损坏图片会在训练早期导致奇怪的报错或OOM我建议做一次全量体检用Pillow或OpenCV读一遍删掉无法解码的文件。类别数量确认14个类别文件夹一个都不能少。少一个类最终模型的输出层就只能分出13类而评估时标签索引也会错位。val集不要有“泄漏”很多人偷懒直接从train里复制了一部分图到val。如果train和val来自同一批图片的裁剪数据增强后模型实际上“见过”这些图片的变体评估结果会虚高。最好按地块或采集批次来划分保证val里的图片没有在同批次出现过。2.3 划分train/val请留出一块真正的验证集如果原始数据还是一个大目录你需要先划分。用train_test_split或者自己写一个简单的脚本import os import random import shutil source_root rice_pests_dataset # 原始数据的根目录 train_root rice_pests/train val_root rice_pests/val val_ratio 0.2 for cls_name in os.listdir(source_root): cls_path os.path.join(source_root, cls_name) if not os.path.isdir(cls_path): continue images os.listdir(cls_path) random.shuffle(images) val_count int(len(images) * val_ratio) os.makedirs(os.path.join(train_root, cls_name), exist_okTrue) os.makedirs(os.path.join(val_root, cls_name), exist_okTrue) for img in images[:val_count]: shutil.copy( os.path.join(cls_path, img), os.path.join(val_root, cls_name, img) ) for img in images[val_count:]: shutil.copy( os.path.join(cls_path, img), os.path.join(train_root, cls_name, img) )这个脚本很简单但注意两点一是随机种子不然每次划分结果都不一样二是使用copy而不是move保留原始数据尤其在数据还没有完全清洗时原始备份的容错性要好得多。划分完看一眼每个类别的图片数量分布。虫害数据集普遍存在类别不均衡——某个虫害好拍某个虫害特别难遇到。后续要不要做采样策略、怎么设计评估指标都取决于这一步的统计结果。3. 数据增强这一步决定了模型的上限3.1 为什么必须做数据增强虫害图片有一个很明显的特性背景干扰强、拍摄条件差异大。同一种虫害可能是在清晨的水稻田里拍的也可能在傍晚的灯光下补光的叶片角度、虫体大小、虫害程度都不一样。如果模型只见过“整齐划一”的图实际应用时泛化能力会很差。数据增强的本质是人为制造更多样的训练样本。随机裁剪能模拟虫体在不同画面位置水平翻转能模拟拍摄角度的变化颜色抖动能适应不同光照、不同手机的白平衡差异。这些扰动在ImageNet上可能只是锦上添花在农业数据集上几乎是必需的。但增强也要有度。某些虫害是靠颜色和纹理来区分的比如稻瘟病病斑的颜色深浅过强的色彩抖动反而会干扰判断。我常用的做法是用一个中等强度的增强组合不要让颜色变化的幅度太大。3.2 训练集和验证集的增强策略千万别混训练集和验证集的预处理逻辑是不一样的。训练集用随机增强验证集只做确定性的缩放和归一化。如果你给验证集也加随机裁剪每次评估结果都会变调试时没法判断修改是否真的有效。具体代码from torchvision import transforms # 训练集随机增强 train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), 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), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里的mean和std直接用了ImageNet的统计值。有人问“我用的是自己的数据要不要重新算均值方差”我的经验是对于迁移学习场景直接用ImageNet的统计值即可。因为预训练模型的输入分布就是这个改变它反而需要模型重新适应。只有当你是从零训练模型时才需要根据自己数据重新计算。3.3 Dataset和DataLoaderbatch这一层直接决定训练速度有了增强策略接下来是把数据放在可迭代的DataLoader里。ImageFolder加DataLoader的标准姿势from torch.utils.data import DataLoader from torchvision import datasets train_dataset datasets.ImageFolder(rootrice_pests/train, transformtrain_transform) val_dataset datasets.ImageFolder(rootrice_pests/val, transformval_transform) train_loader DataLoader( train_dataset, batch_size32, shuffleTrue, num_workers4, # Windows上建议设为0否则可能报错 pin_memoryTrue ) val_loader DataLoader( val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue ) print(训练集类别数:, len(train_dataset.classes)) # 应输出14 print(训练集图片数:, len(train_dataset)) print(验证集图片数:, len(val_dataset))batch_size的选择和显卡显存有关。我做过的实验里ResNet50在输入224x224时batch_size 32约占显存4~6GB不含优化器状态训练起来很流畅batch_size 64显存需求量大幅上升需要更大的卡如果显存只有4GB建议batch_size降到16同时把num_workers设成4训练速度依然可以接受。这里有一个常见的迷思batch_size和learning rate是有对应关系的。batch_size翻倍学习率通常也应该相应翻倍线性缩放法则。后面讲到优化器时会再说这一点。4. 模型改造把ResNet的“旧知识”接到14类输出上4.1 新版torchvision的weights参数用法torchvision 0.13之后官方推荐用weights代替pretrained参数。这样做的原因是为了更明确地区分“模型结构”和“权重来源”。在torchvision 0.19里旧写法仍能运行但会收到DeprecationWarning新写法是这样的import torchvision.models as models # 加载带ImageNet预训练权重的ResNet50 weights models.ResNet50_Weights.DEFAULT model models.resnet50(weightsweights)ResNet50_Weights.DEFAULT会自动下载权重到~/.cache/torch/hub/checkpoints/目录。下载需要联网一次之后本地就有缓存了。如果你所在网络下载比较慢也可以手动下载权重文件然后通过load_state_dict的方式加载。模型的默认分类输出是1000类ImageNet而我们要做14类分类所以需要替换最后一层全连接层num_classes 14 in_features model.fc.in_features # ResNet50这里是2048 model.fc torch.nn.Linear(in_features, num_classes)这里有个细节值得注意in_features不要写死成2048。虽然ResNet50的fc输入确实是2048但如果你换成ResNet18或ResNet101维度是不同的。动态读取model.fc.in_features可以让代码兼容多种ResNet结构省去改代码的麻烦。4.2 特征提取 vs 全量微调怎么选替换完分类头之后还有一个关键抉择是冻结卷积层只训练分类头还是全部层都参与训练冻结场景适用于数据量非常少比如每类只有几十张或者训练资源很紧张。这时模型相当于一个“固定特征提取器”只学习怎么把ImageNet特征映射到14类虫害上。这种方式在保持低过拟合率的同时训练速度极快。全量微调适用于数据量达到每类几百张以上或者虫害特征与普通物体差异较大比如稻瘟病病斑是细小密集的斑纹与ImageNet里的猫狗差异很大需要让底层特征也“适应”水稻图像的分布。我实际的建议是先把全连接层替换掉用一个较小的学习率全量微调所有层这通常是14类、每类200~500张这种规模下效果最好的方案。如果你的时间充裕可以先冻结训练5个epoch再解锁全部层微调这属于“两阶段迁移”的策略能避免一开始梯度就破坏预训练特征。冻结的写法for param in model.parameters(): param.requires_grad False # 只让最后一层参与训练 for param in model.fc.parameters(): param.requires_grad True全量微调的写法就不用改requires_grad但要特别注意学习率。4.3 分类头的维度逻辑很多人疑惑为什么fc输出之前要有2048维这个2048来自ResNet最后一个卷积块输出的通道信息7x7的空间尺寸被全局平均池化压成了1x1只剩下2048个通道。也就是说每张图片通过前面的卷积层最终被压缩成2048个数值的特征向量全连接层负责把这2048个值映射到14个类别得分上。如果把ResNet比作一个“视觉理解引擎”那么最后的全连接层就是“决策器”。预训练阶段决策器是针对1000类设计的我们换成14类之后决策器就要重新学因为我们随机初始化了它的权重而“视觉理解引擎”部分则带着ImageNet的常识直接上岗。这样理解之后你就能明白为什么迁移学习对小学生也很友好——你不需要一个天文数字级的训练集因为绝大部分视觉知识已经被预训练过程学到了。5. 训练配置损失函数、优化器与学习率策略5.1 损失函数14分类用交叉熵就对了分类任务的标准损失函数是交叉熵损失。PyTorch的实现是torch.nn.CrossEntropyLoss它内部已经包含了Softmax计算所以你不需要在模型的fc后再加一个Softmax层。criterion torch.nn.CrossEntropyLoss()有一个很容易被忽略的变体CrossEntropyLoss支持weight参数可以为每个类别设置不同的损失权重。如果某些类别数据特别少可以按图片数量的反比设置权重让模型对少数类更“敏感”class_counts [len(train_dataset) for cls in train_dataset.classes] total_count sum(class_counts) class_weights [total_count / count for count in class_counts] # 归一化可选防止权重值过大影响学习率 max_w max(class_weights) class_weights [w / max_w for w in class_weights] criterion torch.nn.CrossEntropyLoss( weighttorch.tensor(class_weights, dtypetorch.float32) )这个技巧我在做类别不平衡的数据集时经常用。但注意它本质上是在“强迫”模型多关注少数类可能以降低多数类准确率为代价。如果你的核心诉求是整体准确率不一定要加如果少数类的识别率也很重要那就值得一试。5.2 优化器AdamW和SGD都行但要用对PyTorch里两个主流选择AdamW收敛快对学习率不敏感新手友好。权重衰减项需要单独设置。SGD Momentum收敛相对慢但对“最后一点精度”的提升往往有帮助调好之后泛化能力略强。我的习惯是前期探索用AdamW快速看到效果后期精调再用SGD。如果你只想一步到位AdamW 小学习率 cosine衰减是最稳妥的组合。import torch.optim as optim optimizer optim.AdamW( model.parameters(), lr1e-4, # 迁移学习中小学习率很重要 weight_decay1e-4 )如果你是全量微调lr建议在1e-4到3e-4之间。如果只训练全连接层由于训练参数很少学习率可以适当大一些比如1e-3。这是一个很实用的调参逻辑可训练参数量越少学习率可以越大参数量越大学习率要越小。5.3 学习率调度warmup cosine省心且稳学习率调度最容易犯的错是一路恒定。在训练初期用太高的学习率预训练权重容易被冲乱在训练末端学习率不下降模型会在最优点附近震荡损失曲线总是看不齐。我推荐两个调度器配合使用from torch.optim.lr_scheduler import LinearLR, CosineAnnealingLR # 前3个epoch做线性warmup从极小学习率爬升到设定的lr warmup_scheduler LinearLR( optimizer, start_factor0.1, total_iters3 ) # warmup结束后用余弦退火逐步降低学习率 main_scheduler CosineAnnealingLR( optimizer, T_max20, # 总训练epoch数减去warmup的epoch数 eta_min1e-6 ) # 在训练循环中按epoch切换 for epoch in range(total_epochs): ... if epoch 3: warmup_scheduler.step() else: main_scheduler.step()这里有一种更简洁的做法PyTorch 2.x支持SequentialLR可以自动完成warmup到cosine的切换from torch.optim.lr_scheduler import SequentialLR warmup_scheduler LinearLR(optimizer, start_factor0.1, total_iters3) main_scheduler CosineAnnealingLR(optimizer, T_max20, eta_min1e-6) scheduler SequentialLR( optimizer, schedulers[warmup_scheduler, main_scheduler], milestones[3] )5.4 训练循环把验证放在每个epoch的末尾训练循环本身不难但有几个细节决定你的模型能不能真正“练出来”。下面是我习惯用的模板from tqdm import tqdm best_acc 0.0 total_epochs 23 device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) for epoch in range(total_epochs): model.train() running_loss 0.0 correct 0 total 0 for images, labels in tqdm(train_loader): images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() train_acc correct / total train_loss running_loss / len(train_loader) # 验证 model.eval() val_loss 0.0 val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_acc val_correct / val_total val_loss val_loss / len(val_loader) scheduler.step() print(fEpoch {epoch1}/{total_epochs} | fTrain Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | fVal Loss: {val_loss:.4f} | Val Acc: {val_acc:.4f}) # 保存验证集准确率最高的模型 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_resnet50_rice_pests.pth)注意几点务必调用model.train()和model.eval()。这两个调用会切换BatchNorm和Dropout的行为。如果不切验证阶段用的还是训练时的统计量结果会莫名其妙地变差。验证阶段用torch.no_grad()。不需要计算梯度能省下大量显存和计算时间。保存的是state_dict而不是整个模型。这样文件小部署和加载都方便。保存best模型的那一行不可省。如果只保存最后一轮的模型很可能因为学习率还没收敛完或者轻微过拟合而错过最优解。6. 评估与可视化准确率之外的信息量6.1 测试集上的评估流程在固定好测试集或者用val集做最终评估之后最常见也最基础的评估就是计算准确率。但一个数字并不足以说明模型表现如何。我每次做完训练至少要输出三类内容分类报告来自scikit-learn每个类别的精确率Precision、召回率Recall、F1分数混淆矩阵展示模型在哪些类别上容易混淆错误样例可视化把预测错的图拿出来看能直观地定位问题。评估代码import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) # 分类报告 print(classification_report( all_labels, all_preds, target_namestrain_dataset.classes, digits3 )) # 混淆矩阵 cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(12, 10)) plt.imshow(cm, cmapBlues) plt.colorbar() tick_marks np.arange(14) plt.xticks(tick_marks, train_dataset.classes, rotation45) plt.yticks(tick_marks, train_dataset.classes) plt.xlabel(Predicted) plt.ylabel(True) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)6.2 混淆矩阵怎么看三大典型问题第一类典型问题是两个类别互相混淆。比如“稻飞虱”和“叶蝉”外观相似模型在两者之间来回误判。这说明模型学到的特征还不足以区分这两个细分类别可能需要增加这两个类别的训练样本对图片做更细粒度的裁剪突出关键部位或者干脆考虑把这两个类合并如果业务允许。第二类典型问题是某一大类被系统性误判为另一大类。比如多种稻瘟病分别被分到“稻瘟病”的其他子类说明模型学到了这类特征的共同模式但缺乏决策边界。这种情况通常和标注的模糊性有关也许某些样本本身在人的眼里就很难区分。第三类典型问题是某个类别几乎全部被另一类“吃掉”。比如某个类别样本太少模型直接摆烂把所有样本都预测成多数类别。这说明类别不平衡已经严重到影响学习过程需要做类权重或过采样。6.3 错误样例可视化人眼审阅依然不可替代数字指标只能告诉你“哪里错了”不能告诉你“为什么错了”。我每次训练完都会写一个脚本随机抽取预测错误的图片连同真实标签和预测标签一起画出来import random misclassified_indices [ i for i in range(len(all_labels)) if all_preds[i] ! all_labels[i] ] sample_indices random.sample(misclassified_indices, min(12, len(misclassified_indices))) fig, axes plt.subplots(3, 4, figsize(12, 9)) for idx, ax in enumerate(axes.flat): img_idx sample_indices[idx] # 需要从val_dataset中取原始图片 img, true_label val_dataset[img_idx] true_name train_dataset.classes[true_label] pred_name train_dataset.classes[all_preds[img_idx]] # 反归一化以便显示 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) img img.numpy().transpose(1, 2, 0) img img * std mean img np.clip(img, 0, 1) ax.imshow(img) ax.set_title(f真: {true_name}\n预: {pred_name}, fontsize9) ax.axis(off) plt.tight_layout() plt.savefig(misclassified_examples.png, dpi150)看这12张图你往往会立刻发现一些“非模型层面”的问题有的图片其实是两类虫害同时存在标注本身就有歧义有的图片背景过于杂乱人眼都很难看出来有的是同一个虫害但虫龄阶段差异太大模型看到的根本是两种外观。这些发现反过来会指导数据清洗和标注规范的改进。7. 实操中绕不开的坑这些我全踩过7.1 “0.98”的训练损失看似完美验证集却一直在0.85徘徊这是我第一次带某同学做这个项目时遇到的典型问题。训练准确率不断上升甚至逼近100%验证集准确率却死活涨不上去。排查后发现问题出在RandomResizedCrop用了过高比例的裁剪训练时模型看到的是各种被裁剪过的局部叶片验证时看到的是完整叶片两者数据分布差距太大。解决方案是调整增强策略的幅度把裁剪尺度范围限制在相对较小的区间。同时检查增强前后图片的视觉效果让训练图片“看起来就像正常拍摄的照片”。7.2 同一个模型换了GPU跑出的结果却不一样有一次我把训练好的模型从办公电脑换到一台新的服务器上去复现同样的代码、同样的权重推理结果却有一小部分不一样。查了一圈原因在于不同显卡的浮点计算优化差异模型的BatchNorm在不同batch大小下统计量的微小差异甚至cuDNN算法选择的不确定性。所以在复现实验时建议固定随机种子、固定模型到评估模式、确定Batch大小并且在保存模型的同时保存一份推理配置说明def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed()7.3 类别不平衡时别死盯着准确率14类虫害里如果最常见的“稻飞虱”占了30%的样本模型可以把所有图片都预测成“稻飞虱”而获得30%的准确率。但这个模型显然没有用。所以类别不平衡场景下我主要看宏平均F1macro F1而不是准确率。宏平均F1对每个类别一视同仁少数类的表现会直接拉低总分。只有当宏平均F1稳定在某个较高水平时我才认为模型真正解决了“14类区分”的问题。7.4 训练中途Loss变成NaN训练过程中如果突然出现loss为NaN99%的原因是学习率过高导致梯度爆炸少部分原因是数据里有异常值比如所有像素都为0的图片。对付这种情况我的习惯是先把学习率降到当前的十分之一重新跑几个step检查输入数据中是否存在全零张量或非常异常的像素值在数据加载后加一个assert检查torch.isnan(images).any()是否为False给ClipGradNorm加一道保险torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0)这个clip_grad_norm_的作用是把梯度的范数限制在max_norm以内防止极端梯度把权重推出合理范围。很多老手在训练Transformer或大型CNN时都会默认加一行这个。8. 最终的扩展思考从训练完成到真正落地模型训练完之后整个流程还不算结束。至少还有两件值得做的事。第一件事是导出模型用于部署。如果你只是想离线预测只需要保存state_dict在推理脚本里重新构建模型再加载权重。如果你想接入ONNX、TensorRT或者移动端框架需要导出完整的计算图model.eval() dummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, resnet50_rice_pests.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} )dynamic_axes是为了让导出后的模型支持批处理大小可变的输入这在部署时非常常用。第二件事是检查数据标注的一致性。在我做过的多个农业图像项目中模型评估阶段发现的错误最后追根溯源基本都是标注问题。虫害识别的专业门槛高人工标注难免有模糊如果用“模型预测结果 人工复核”的方式反哺数据集迭代两三轮整个系统的准确率还会再上一个台阶。最后再说一个个人心得像水稻虫害分类这类农业AI项目模型的精度只是其中一个环节让农户或植保人员能拍出可用的图片、能在低端手机上流畅运行、能及时给出可解释的判断依据这些往往比把准确率从92%提到94%更值得投入精力。预训练ResNet帮我们轻松拿到了90分以上的起点剩下的10分靠的就不再是模型结构而是对农业场景本身的理解。以后你再遇到类似的多分类图像任务完全可以照着一套流程走整理数据、设计增广、加载预训练模型、微调全连接层、训练评估、可视化分析。这套流程的容错性和可复现性都很好踩过坑之后你也会慢慢总结出适合自己业务场景的那一套“微调配方”。