资讯详情

资讯详情

ResNet152植物分类迁移学习:从数据契约到训练微调与推理部署

简介resnet152_plant.zip 是一份面向深度学习初学者与计算机视觉实践者的植物病害识别迁移学习方案围绕 ResNet152 预训练模型展开帮助读者解决 38 类植物叶片病害的精细分类问题可用于课程设计、农业 AI 项目复现与算法练手。压缩包共 2000 个文件、约 503.25MB以 5412 张 jpg 叶片图像构成数据集主体辅以 6 个 py 脚本承载训练与推理流程、2 个 pth 权重文件供直接加载或继续微调另有 json 与 txt 记录类别映射和说明信息。资源已有 1433 人学习下载具备一定参考基础。其价值在于把数据准备、增强策略、输出层改造、优化器与学习率设置、评估指标到部署落地串成完整链路读者可据此复现 99.6% 识别准确率的训练思路理解预训练特征迁移与全连接层替换的关键细节并对照权重与脚本快速验证改造效果减少从零搭建的试错成本。1. 从压缩包到可用模型resnet152_plant.zip 到底装了什么拿到 resnet152_plant.zip 的人十有八九第一反应是解压后找 train.py 直接跑真正决定它能不能用的不是 ResNet152 这串名字而是压缩包里的植物图像目录、标签顺序和预处理是否与权重对齐。这份资源的核心价值在于用 ResNet152 做迁移学习把通用视觉特征迁到植物叶片或整株图像分类上适合已经有一批带类别目录的图像、想快速跑通训练到推理链路的从业者。它不解决采集问题也不替你做标签清洗如果你的数据只有零散图片没有类别结构先补目录再谈模型。下面按“拆包核对—训练—推理—排查”的顺序拆。2. 先立数据契约植物图像目录、标签映射与 ResNet152 输入管线2.1 目录审计先数类别再看每类样本resnet152_plant.zip 这类资源最常见的翻车点不是模型结构而是数据目录和标签顺序。解压后先别改代码先做一次目录审计不管包内是 train/val/test 三级目录还是 images 加分类型标签文件先把类别名、每类样本数、图片后缀分布打印出来。很多植物分类数据会有“同一类别多个别名”“文件名带采集批次”“某几类只有十几张图”的情况这些信息直接决定后面能不能用默认的 ImageFolder 管线。常见的 ImageFolder 目录约定是每个类别一个文件夹文件夹名就是类别名。比如data/plant/train/类别A/*.jpg、data/plant/val/类别A/*.jpg。如果你拿到的是平铺图片加 CSV 标签不要硬套 ImageFolder先写一个小脚本转成目录结构或者自定义 Dataset 读 CSV。目录审计这一步做完你至少要知道类别数是否等于模型输出数、训练集和验证集的类别是否完全一致、有没有空文件夹、有没有非图片文件混在类别目录里。# 在解压后的数据根目录执行先看两层目录结构 find data/plant -maxdepth 3 -type d | sort | head -80 # 统计每个类别目录下的图片数量后缀按常见格式过滤 find data/plant/train -type f \( -iname *.jpg -o -iname *.jpeg -o -iname *.png -o -iname *.webp \) \ | awk -F/ {print $(NF-1)} | sort | uniq -c | sort -nr上面第一条命令只做结构观察-maxdepth 3防止在深层缓存目录里刷屏第二条命令用awk取文件所在目录名也就是类别名再按数量倒序。执行后如果发现某个类别明显偏少先记下来后面要么补样本要么在训练时用类别权重别等到混淆矩阵里看到它被全部分错才回头查。图片后缀统计也有用有些包内混了.gif或.bmpPIL 能打开但训练管线默认只读 RGB统一转成三通道更稳。图片完整性检查不要省。植物图像采集现场常见半截文件、零字节文件、扩展名和内容不一致的文件这些样本在 DataLoader 里会直接抛异常训练跑到一半断掉。我一般会在正式训练前跑一遍PIL.Image.verify()把坏图路径写进bad_images.txt再决定删除还是重新导出。from pathlib import Path from PIL import Image, UnidentifiedImageError data_root Path(data/plant/train) bad [] suffixes {.jpg, .jpeg, .png, .webp, .bmp} for p in data_root.rglob(*): if p.is_file() and p.suffix.lower() in suffixes: try: with Image.open(p) as im: im.verify() # 只校验文件头不保证像素完整解码 except (UnidentifiedImageError, OSError) as e: bad.append((str(p), repr(e))) Path(bad_images.txt).write_text( \n.join(f{p}\t{e} for p, e in bad), encodingutf-8 ) print(bad images:, len(bad))这段脚本的data_root指向训练集根目录suffixes按你实际数据扩展verify()之后图片对象不能继续读像素所以只用于校验。坏图清单落盘后人工抽查前二十条确认是传输损坏还是格式不受支持。注意不要一上来就全删有些.png只是带透明通道转成 RGB 后仍可用真正零字节或截断的文件才需要剔除。2.2 ResNet152 的 224 输入不是随便定的ResNet152 在通用视觉预训练里通常按 224×224 的输入训练迁移到植物分类时最稳的做法是让输入尺寸、归一化参数和预训练权重保持一致。训练阶段可以用RandomResizedCrop(224, scale(0.7, 1.0))做尺度扰动验证和推理阶段用Resize(256)加CenterCrop(224)。这个组合不是玄学它保留了比 224 稍大的视野再裁中心区域减少推理时因缩放方式不同带来的分布偏移。归一化参数建议直接沿用预训练权重的默认值mean 为[0.485, 0.456, 0.406]std 为[0.229, 0.224, 0.225]。你可以因为植物叶片颜色重要而减少颜色抖动但不要轻易改归一化均值方差。改了就相当于让预训练卷积核看到另一种色彩分布冻结训练时还能勉强收敛解冻后很容易出现验证指标先升后崩。常见做法是训练增强里加轻微ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.02)色相幅度不要大水平翻转对叶片通常安全垂直翻转要谨慎因为叶脉方向和病斑位置可能带语义。环节输入尺寸关键参数说明训练RandomResizedCrop 224scale(0.7,1.0)保留主体避免过度裁掉病斑验证Resize 256 CenterCrop 224插值默认与推理严格一致推理Resize 256 CenterCrop 224不随机单图和批量共用同一套 transform归一化224×224×3预训练 mean/std不要随意替换如果显存吃紧可以把训练输入降到 192 或 160但验证和推理也要同步降不要训练 224、推理 160。ResNet152 对输入尺寸敏感尤其是小病斑、小叶脉这类细粒度特征降太多会直接损失判别力。显存不够优先降 batch size 或用混合精度不要先砍输入分辨率。2.3 用 ImageFolder 固定 class_to_idx 并保存 classes.jsonImageFolder 会按文件夹名排序生成class_to_idx这个映射在训练集和验证集之间必须一致。问题在于如果你后来往训练目录里加了一个新类别文件夹或者验证集目录名大小写不同映射就可能变。训练时标签 0 是类别 A推理时标签 0 变成类别 B模型输出看着正常实际全错。这类问题在植物分类项目里很常见因为类别名经常带品种、批次、采集日期人工改名后忘了同步。import json from pathlib import Path from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.02), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(data/plant/train, transformtrain_tf) val_ds datasets.ImageFolder(data/plant/val, transformval_tf) assert train_ds.class_to_idx val_ds.class_to_idx, 训练/验证类别映射不一致 classes train_ds.classes Path(classes.json).write_text( json.dumps({classes: classes, class_to_idx: train_ds.class_to_idx}, ensure_asciiFalse, indent2), encodingutf-8, ) train_loader DataLoader(train_ds, batch_size16, shuffleTrue, num_workers4, pin_memoryTrue, drop_lastTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)这段代码先构造两套 transform训练集带随机裁剪、翻转和轻微颜色扰动验证集只做确定性的缩放裁剪。assert那行是数据契约的保险丝训练集和验证集的class_to_idx不一致就直接停不要带着错位标签继续跑。classes.json保存类别列表和映射后面推理脚本只认这个文件不再依赖目录当前状态。batch_size16是 ResNet152 在 224 输入下的保守起点显存足够再往上加num_workers按 CPU 核数调整Windows 环境下如果 DataLoader 卡住先把 workers 设成 0 排查。类别不平衡时不要只在损失函数里拍一个权重。可以用WeightedRandomSampler让每类被采样的概率接近也可以给 CrossEntropyLoss 传 class weights。两者选一个即可同时用容易让训练节奏变得难以判断。验证集必须保持原始分布不要对验证集做重采样否则指标会虚高。3. 迁移学习怎么接ResNet152 分类头替换、冻结策略与分层学习率3.1 ResNet152 深但小数据更该迁移ResNet152 的层数深直接从小规模植物图像从零训练常见结果是训练集 loss 很快降下去验证集准确率卡住甚至倒退。不是模型不够强而是数据量撑不起这么多参数。迁移学习的思路是保留预训练卷积层提取边缘、纹理、颜色块的能力只把最后的分类头换成本地类别数再根据数据量决定解冻多少层。植物叶片识别和通用物体识别在低层特征上有大量重叠纹理、叶缘、病斑形态都能复用所以 ResNet152 在这里的优势是特征表达够深代价是显存和训练时间更高。选 ResNet152 而不是更浅的骨干通常是因为类别细粒度较高、类间差异小比如不同品种的叶片、不同病害阶段的斑块。如果数据只有几百张、类别差异又很明显ResNet50 甚至更小的模型可能更稳。资源包给的是 ResNet152不代表所有场景都要硬上先看验证集规模再决定是否换骨干。换骨干时分类头替换、冻结策略、输入管线都可以沿用这也是把工程写成配置的好处。3.2 替换 fc 之后先打印可训练参数ResNet152 的最后一层全连接层默认输出 1000 类迁移到植物分类要替换成自己的类别数。替换时不要只写model.fc nn.Linear(2048, num_classes)就结束先把可训练参数名和形状打印出来确认没有漏掉旧分类头也确认新分类头初始化正常。常见错误是加载权重时strictTrue结果因为 fc 形状不匹配直接报错或者先加载权重再替换 fc把刚训练好的头又覆盖掉。import torch import torch.nn as nn from torchvision import models def build_resnet152(num_classes, pretrainedTrue): try: weights models.ResNet152_Weights.DEFAULT if pretrained else None model models.resnet152(weightsweights) except AttributeError: # 兼容旧接口没有 weights 参数时退回 pretrained model models.resnet152(pretrainedpretrained) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return model num_classes 12 # 改成你的类别数 model build_resnet152(num_classes, pretrainedTrue) for name, p in model.named_parameters(): if p.requires_grad: print(name, tuple(p.shape))这段代码先按当前 torchvision 的weights接口取预训练权重取不到再退回旧pretrained参数避免换环境就报错。in_features从原模型读不硬编码ResNet152 这里是 2048但写死没有好处。替换model.fc后新全连接层默认requires_gradTrue打印可训练参数时你应该看到fc.weight和fc.bias形状分别是(num_classes, 2048)和(num_classes,)。如果还看到旧分类头的参数名说明替换顺序有问题。加载自己保存的 checkpoint 时先构建同类别数的模型再load_state_dict。如果只是加载预训练骨干可以用strictFalse并检查缺失键和多余键不要看到没报错就以为加载成功。missing_keys里出现大量layer开头通常意味着权重根本没对上。3.3 冻结 backbone 与解冻 layer4 的两段式训练小数据迁移学习我一般分两段第一段冻结除 fc 外的所有参数只训练分类头让新头先适应特征分布第二段解冻layer4和 fc用更小的学习率微调高层特征。第一段 epoch 不用多3 到 5 轮足够验证集 macro F1 不再明显上升就进入第二段。第二段不要一上来解冻全部层ResNet152 全部解冻对小数据很容易过拟合显存也会明显增加。冻结时有一个容易忽略的细节把参数requires_grad设为 False 后BatchNorm 层在model.train()模式下仍会更新 running mean 和 running var。这会让冻结阶段的高层统计量发生漂移后面解冻时出现指标突然崩。常见做法是在每个训练 epoch 开始前把所有 BatchNorm 层设成 eval 模式只保留 Dropout 的训练行为ResNet152 里 Dropout 很少主要就是 BN。import torch.nn as nn def set_freeze(model, freeze): for name, p in model.named_parameters(): if name.startswith(fc.): p.requires_grad True else: p.requires_grad not freeze def bn_eval_when_frozen(model, frozenTrue): if not frozen: return for m in model.modules(): if isinstance(m, nn.BatchNorm2d): m.eval() def make_param_groups(model, head_lr3e-4, backbone_lr3e-5, wd1e-4): head, backbone [], [] for name, p in model.named_parameters(): if not p.requires_grad: continue if name.startswith(fc.): head.append(p) else: backbone.append(p) groups [{params: head, lr: head_lr, weight_decay: wd}] if backbone: groups.append({params: backbone, lr: backbone_lr, weight_decay: wd}) return groupsset_freeze(model, True)只放开 fcset_freeze(model, False)全部放开实际两段式训练里第二段可以只解冻layer4把layer1到layer3继续冻结这样显存和过拟合风险都更可控。bn_eval_when_frozen要在每个 epoch 的model.train()之后调用否则 BN 又回到训练模式。make_param_groups把 head 和 backbone 分成不同学习率组head 用3e-4backbone 用3e-5这是 AdamW 下比较稳的起点如果你用 SGD学习率要换一个量级重新试不要把 AdamW 的值直接套过去。第二段解冻时学习率调度建议带一点 warmup尤其是从冻结切到解冻的那几个 step。没有 warmup 时layer4的梯度第一次回传可能很大训练 loss 会突然抖一下。简单做法是前 200 个 step 线性从 0 升到设定学习率再接 cosine。这个细节不写进模型结构但很影响可复现性。4. 训练脚本落地损失函数、优化器、早停和可恢复的检查点4.1 一个能跑完的训练循环骨架训练循环不要写成“只要 loss 降就保存”。植物分类常见的类别不平衡会让 accuracy 看起来不错但少数类全错。验证指标至少同时记录 accuracy 和 macro F1保存模型时按 macro F1 选最优。下面这个骨架包含混合精度、梯度裁剪、训练/验证切换和早停计数代码不依赖特定版本号按你环境里的 torch 接口跑。import copy import torch import torch.nn as nn from sklearn.metrics import f1_score, accuracy_score def train_one_epoch(model, loader, criterion, optimizer, scaler, device, frozenTrue, grad_clip5.0): model.train() if frozen: for m in model.modules(): if isinstance(m, nn.BatchNorm2d): m.eval() total_loss, all_preds, all_targets 0.0, [], [] for images, targets in loader: images images.to(device, non_blockingTrue) targets targets.to(device, non_blockingTrue) optimizer.zero_grad(set_to_noneTrue) with torch.cuda.amp.autocast(enabledscaler.is_enabled()): logits model(images) loss criterion(logits, targets) scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip) scaler.step(optimizer) scaler.update() total_loss loss.item() * images.size(0) preds logits.argmax(dim1) all_preds.extend(preds.detach().cpu().tolist()) all_targets.extend(targets.detach().cpu().tolist()) return (total_loss / len(loader.dataset), accuracy_score(all_targets, all_preds), f1_score(all_targets, all_preds, averagemacro)) torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() total_loss, all_preds, all_targets 0.0, [], [] for images, targets in loader: images images.to(device, non_blockingTrue) targets targets.to(device, non_blockingTrue) logits model(images) loss criterion(logits, targets) total_loss loss.item() * images.size(0) all_preds.extend(logits.argmax(dim1).cpu().tolist()) all_targets.extend(targets.cpu().tolist()) return (total_loss / len(loader.dataset), accuracy_score(all_targets, all_preds), f1_score(all_targets, all_preds, averagemacro))train_one_epoch里frozenTrue时把 BN 设 eval解决冻结阶段统计量漂移scaler在 CPU 环境下可以传torch.cuda.amp.GradScaler(enabledFalse)代码不会因为没 GPU 直接崩。clip_grad_norm_的grad_clip5.0是防止解冻初期梯度爆炸的保险不是越多越好。evaluate用torch.no_grad()关掉梯度验证集 batch size 可以比训练大因为不存激活。损失函数先用nn.CrossEntropyLoss(label_smoothing0.1)。标签平滑对植物分类这种可能有人工标注噪声的数据有帮助尤其是病斑边界模糊、类别间有过渡状态时。如果类别严重不平衡再给weight传类别权重但不要把 label smoothing 和过大的类别权重一起叠训练会变得很钝。优化器优先 AdamWweight_decay1e-4数据量很大、调参经验足时再试 SGD momentum。4.2 batch size、学习率和 weight decay 怎么改ResNet152 在 224 输入下显存占用不低batch size 16 是保守值。显存够就试 32训练更稳但每轮迭代次数减少显存不够不要硬扛用梯度累积把accum_steps2或 4 加上去。梯度累积时 loss 要除以累积步数optimizer.step()和scaler.update()只在累积边界执行否则等效学习率会变大。参数保守起点可调范围调整依据batch size16832显存和梯度噪声head lr3e-41e-41e-3新分类头收敛速度backbone lr3e-51e-51e-4解冻后验证指标是否抖weight decay1e-41e-51e-3过拟合程度epochs302060早停触发位置patience6410验证集规模学习率不要只看训练 loss。head 学习率太大时训练 loss 降得很快验证 macro F1 来回摆backbone 学习率太大时解冻后的第一轮验证指标经常直接掉。我的习惯是第二段先跑 2 个 epoch 看验证曲线如果比第一段末尾还低就把 backbone lr 再降一半。weight decay 对 ResNet152 这种深模型有用但不要用 weight decay 去救标签错误标签错的时候正则只会让模型更保守。drop_lastTrue在训练集样本数不能被 batch size 整除时丢掉最后一批避免 BatchNorm 在极小 batch 上统计量不稳。验证集不要 drop_last否则每轮指标会随丢掉的样本波动。pin_memoryTrue在 GPU 环境能加快主机到设备拷贝CPU 环境可以关掉。4.3 验证指标与 checkpoint 里该存什么checkpoint 不要只存model.state_dict()。至少存model_state、optimizer_state、scheduler_state、epoch、best_f1、classes、input_size、mean、std。植物分类项目换人接手时最容易缺的是类别顺序和预处理参数只给一个权重文件推理脚本只能靠猜猜错就是全盘错。把classes.json的内容一起塞进 checkpoint推理时先读 checkpoint 再构建模型能省很多后悔药。def save_checkpoint(path, model, optimizer, scheduler, epoch, best_f1, classes, input_size224): torch.save({ model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), scheduler_state: scheduler.state_dict() if scheduler else None, epoch: epoch, best_f1: best_f1, classes: classes, input_size: input_size, mean: [0.485, 0.456, 0.406], std: [0.229, 0.224, 0.225], }, path) # 训练循环里的保存逻辑示意 best_f1, patience_counter -1.0, 0 for epoch in range(start_epoch, epochs): tr_loss, tr_acc, tr_f1 train_one_epoch( model, train_loader, criterion, optimizer, scaler, device, frozenis_frozen ) va_loss, va_acc, va_f1 evaluate(model, val_loader, criterion, device) if va_f1 best_f1 1e-4: best_f1 va_f1 patience_counter 0 save_checkpoint(best.pt, model, optimizer, scheduler, epoch, best_f1, train_ds.classes) else: patience_counter 1 if patience_counter 6: break保存逻辑按 macro F1 判断1e-4是防止浮点噪声触发保存。patience_counter到 6 就停验证集很小的时候可以放宽到 8但不要无限等。断点恢复时先加载 checkpoint再恢复 optimizer 和 scheduler最后从epoch 1继续。只恢复模型权重不恢复优化器状态学习率调度会从第一步重新开始训练曲线会对不上。训练日志至少每轮打印train_loss/train_f1/val_loss/val_f1/lr。如果 val_loss 持续升、val_f1 不升说明过拟合或学习率过大如果 train_f1 和 val_f1 都低先查标签和预处理不要先加 epoch。深度学习训练曲线有时很玄学但数据契约、类别映射、预处理对齐这三件事做完大部分“玄学”都会变成可解释的排查项。5. 推理与排查单图预测、批量导出和五个高频翻车点5.1 单图推理预处理必须复用验证变换推理脚本最常见的错误是“训练用了归一化推理只做了 ToTensor”。模型看到的输入分布变了输出置信度会整体偏移表现就是所有图片都预测成同一类或者 top1 分数很低但 top2 看起来合理。单图推理必须复用验证集的Resize(256) CenterCrop(224) Normalize并且从 checkpoint 里读classes不要从当前目录重新扫。import torch from PIL import Image from torchvision import transforms def load_model_from_ckpt(ckpt_path, devicecuda): ckpt torch.load(ckpt_path, map_locationcpu) classes ckpt[classes] model build_resnet152(len(classes), pretrainedFalse) model.load_state_dict(ckpt[model_state]) model.to(device).eval() return model, classes, ckpt def predict_one(model, classes, image_path, devicecuda, topk3): tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) image Image.open(image_path).convert(RGB) x tf(image).unsqueeze(0).to(device) with torch.no_grad(): probs torch.softmax(model(x), dim1)[0] scores, idx probs.topk(min(topk, len(classes))) return [(classes[i], float(s)) for s, i in zip(scores, idx)] model, classes, ckpt load_model_from_ckpt(best.pt, devicecpu) print(predict_one(model, classes, data/plant/test/sample.jpg, devicecpu))load_model_from_ckpt用pretrainedFalse构建同类别数模型再加载训练权重避免推理时又去下载预训练权重。map_locationcpu让脚本在没有 GPU 的机器上也能加载再决定是否.to(device)。predict_one里convert(RGB)处理带 alpha 通道的 PNGunsqueeze(0)补 batch 维softmax后取 topk。输出的分数是模型置信度不是校准后的概率阈值判断要谨慎。5.2 批量预测与混淆矩阵导出批量预测不要用for循环一张张走模型DataLoader 能批处理就批处理。需要保留原始路径方便人工复核。ImageFolder 的samples里有路径和标签但推理目录如果没有子文件夹就自己写一个只读图片的 Dataset返回路径和 tensor。预测结果输出 CSV字段至少包含path,pred,score,label有标签时顺便算混淆矩阵。import csv from pathlib import Path from torch.utils.data import Dataset, DataLoader from sklearn.metrics import confusion_matrix, classification_report class ImagePathDataset(Dataset): def __init__(self, paths, tf): self.paths [str(p) for p in paths] self.tf tf def __len__(self): return len(self.paths) def __getitem__(self, i): from PIL import Image img Image.open(self.paths[i]).convert(RGB) return self.tf(img), self.paths[i] val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) paths sorted(Path(data/plant/test).rglob(*.jpg)) ds ImagePathDataset(paths, val_tf) dl DataLoader(ds, batch_size32, shuffleFalse, num_workers0) rows, y_true, y_pred [], [], [] model.eval() with torch.no_grad(): for images, batch_paths in dl: probs torch.softmax(model(images.to(cpu)), dim1) scores, idx probs.max(dim1) for p, i, s in zip(batch_paths, idx.tolist(), scores.tolist()): rows.append({path: p, pred: classes[i], score: s}) with open(predictions.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[path, pred, score]) writer.writeheader() writer.writerows(rows)这段批量脚本只做预测不依赖测试目录的类别结构num_workers0是为了避免某些环境下多进程读图卡住数据量大再往上加。utf-8-sig让 CSV 在常见表格软件里打开不乱码。要算混淆矩阵时要求测试目录本身有类别子文件夹用 ImageFolder 或从路径里解析真实标签再调confusion_matrix(y_true, y_pred, labelsclasses)。混淆矩阵不要只看图导出 CSV 后按行看召回率少数类被错分到哪个类一目了然。5.3 五个高频翻车点现象、原因、解决现象一训练 loss 一直降验证准确率不动预测全是一个类。原因通常是训练集和验证集类别映射不一致或者推理时用了另一套预处理。ImageFolder 按目录名排序训练和验证目录只要有一个类别名不同标签就错位推理时如果只做 ToTensor 没做 Normalize模型输入分布直接变。解决是把classes.json作为唯一标签源训练、验证、推理都读同一份推理脚本里打印 tensor 的 min/max/mean和训练时的 batch 统计对比差太多就查 transform。现象二冻结阶段正常解冻 layer4 后第一轮验证指标突然掉。原因多半是 backbone 学习率太大或者冻结阶段 BatchNorm 的 running stats 被训练集小 batch 带偏。解决是解冻后 backbone lr 先降到 head 的十分之一前 200 个 step 加 warmup冻结阶段每个 epoch 把 BN 设 eval。还有一个常见原因是解冻时把layer1到layer3也放开了小数据下高层特征被快速覆盖。先只解冻layer4验证稳定后再考虑全部解冻。现象三训练刚开始就显存爆炸batch size 降到 1 才能跑。ResNet152 在 224 输入下激活占用高batch 32 很容易超。先确认没有在验证阶段保留梯度evaluate必须加torch.no_grad()再开混合精度把 batch 降到 8 或 16还不够就用梯度累积把accum_steps4等效成更大的 batch。不要为了省显存把输入降到 96 还指望小病斑识别分辨率损失不是调参能补回来的。现象四验证集指标高得离谱上线后一塌糊涂。植物图像经常同一植株、同一叶片拍多张随机划分会把相似图片分到训练和验证两边模型其实在背样本。解决是按植株 ID、采集批次或拍摄时间做分组划分同一组只进训练或验证。没有分组信息时至少按文件名前缀粗分并保留一个完全独立的测试集。测试集只在最后跑一次不要拿测试集调早停。现象五换一台机器推理类别名全乱或路径读不到。原因通常是推理脚本重新扫目录生成类别列表而目录里多了临时文件夹或者路径里带中文、空格脚本用字符串拼接读文件失败。解决是推理只读 checkpoint 里的classes路径统一用pathlib.PathCSV 用 UTF-8 写出。换机器前把best.pt、classes.json和一张样例图放在同一目录先跑单图预测确认输出类别名和训练时一致再跑批量。6. 把模型交付出去TTA、拒识阈值和一次可复现的验收训练到best.pt只是半成品交付前我一般做三件事轻量 TTA、拒识阈值、固定验收集。TTA 不要堆太多植物分类用原图和水平翻转两张平均就够了垂直翻转、多尺度裁剪容易把不确定性放大。代码上就是推理时对同一张图做两次 transformsoftmax 后取平均再 argmax。收益通常小于数据清洗但比单纯调阈值稳。拒识阈值解决“模型必须给一个答案”的问题。业务里遇到不属于训练类别的植物模型仍会输出高置信度错误类。做法是设一个max_prob阈值低于阈值输出unknown阈值在验证集上按目标拒识率扫不要拍脑袋定 0.9。表格里这三个动作的边界要清楚动作目的关键参数适用边界水平翻转 TTA降低单次裁剪随机性2 视图平均叶片方向不敏感时拒识阈值拦截分布外样本max_prob 阈值验证集有 unknown 样本时TorchScript 导出本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →