尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

EfficientNet图像分类实战:PyTorch训练与推理全流程解析

EfficientNet图像分类实战:PyTorch训练与推理全流程解析 简介这是一份基于PyTorch的EfficientNet图像分类实战代码包适合有一定深度学习基础、希望快速上手经典卷积网络的中级学习者与研究人员。压缩包共8个文件以5个Python训练/测试脚本为主附带2个编译缓存文件与1个训练好的.pth权重文件整体约38.27MB结构上覆盖数据加载、模型构建、训练与评估等核心环节可直接对照博文复现实验。目前已有1205人学习下载对于理解EfficientNet的缩放策略与迁移学习应用具有不错的参考价值。通过运行脚本读者能获得完整的分类流程示例、权重参数与模块化代码组织便于在此基础上替换数据集或调整超参数进一步开展自己的实验。1. 图像分类实战EfficientNet 源码包开箱即用拿到这个图像分类EfficientNet实战.zip的时候我第一反应是总算有一个不用从零搭工程就能直接跑的图像分类项目了。解压以后里面是dataset.py、train.py、Test.py、test1.py、model.pth和dataset/目录整体是一个基于 PyTorch 的 EfficientNet 分类训练 推理的完整链路。对新手来说最友好的地方在于它自带训练好的model.pth解压完不用等训练结束直接跑测试脚本就能看到结果对熟手来说训练脚本和数据集模块的代码结构很清晰改改路径就能换自己的数据。这份资源能解决的典型问题包括想快速验证 EfficientNet 在自己的图片分类任务上的效果、需要一份能看懂能改的训练/推理模板、或者面对网上零散代码不知道怎么拼出一条完整流程。适合那些手里有分类图片、但还没跑通 PyTorch 训练链路的人。下文我一步步把包拆开讲从文件职责、数据准备、训练参数一直讲到测试脚本和踩坑记录。2. 拆包先行五个文件与两条主链路的职责边界拿到一个工程包盲目打开train.py从头读到尾是最低效的做法。我的习惯是先看目录结构和文件命名推测出这个项目的训练数据和测试数据是怎么流转的。这份资源的结构很精简我建议你也先按文件清单过一遍再决定从哪行代码开始看。2.1 文件清单与各自分工先看整体结构。这个 zip 解压之后核心文件我整理成了下表后续文章会逐个展开。文件类型职责dataset/目录存放训练和测试用的图片数据__init__.pyPython将 dataset 目录标记为 Python 包方便from dataset import ...dataset.pyPython自定义 Dataset 类负责图片读取、标签映射、数据增强train.pyPython训练主脚本加载数据、构建模型、训练并保存权重Test.pyPython测试/验证脚本加载model.pth评估模型在测试集上的表现test1.pyPython轻量测试脚本通常是单张图片推理用于快速验证model.pth权重文件训练好的模型参数PyTorch 的 state_dict 格式从这些文件名基本能推出一条完整流程dataset.py负责把图片喂给模型train.py负责训练并产出model.pthTest.py和test1.py分别从“批量评估”和“单张验证”两个角度消费这个权重文件。这种命名方式很常见但有一个隐患Test.py和test1.py读者容易混淆不知道哪个是主测试脚本。后面我会专门讲它们的差别。2.2 两条主链路训练与推理怎么衔接如果只记一件事那就是这个包里有两条链路。第一条是训练链路dataset.py读图 →train.py定义网络、迭代训练 → 每若干轮保存一次model.pth。第二条是推理链路Test.py或test1.py加载同一个model.pth→ 对测试图片做同样的预处理 → 输出类别索引和置信度。这里我想强调一个容易被新手忽略的点推理脚本里的预处理必须和训练脚本完全一致。举例来说如果训练时对图片做了Resize(224)、Normalize(mean[0.485, 0.456, 0.406])那么测试脚本里也必须做一模一样的操作差一个像素或者差一个归一化参数模型输出的置信度都会失真甚至直接误判。我见过不少翻车现场训练准确率 95%一跑测试脚本就崩最后查下来不是模型问题而是Test.py里忘了写归一化。2.3 先跑通再深读推荐的上手顺序我给别人拆这种包时强烈建议的阅读顺序是先打开train.py读模型定义和超参再打开dataset.py读数据组织形式最后翻Test.py读推理逻辑。不要一上来就研究test1.py它是辅助验证脚本全局视角下没那么重要。从train.py里你能看到 EfficientNet 的版本选择常见的是 b0 或 b1、类别数num_classes、batch size、学习率、优化器和学习率调度策略。这些参数决定了训练是否收敛、收敛多快。而从dataset.py里你能看到这个项目的图片目录是按什么方式组织的是train/cat/xxx.jpg这种按类别分文件夹的形式还是单一文件夹配一个 label 文件的形式。这两种方式对应不同的实现写法我下一章展开。最后补一句如果只是先想验证环境通不通我建议先跑test1.py用单张图片看输出能走通说明依赖、权重和预处理管线基本没问题再回头跑训练不迟。这个习惯我一直沿用能省下大量调试时间。3. 数据侧准备dataset 模块与标签组织的常规做法图像分类项目里代码的难点往往不在网络结构而在数据怎么喂进去。这个包的dataset.py承担了图片路径读取、标签映射、尺寸调整和数据增强的工作。理解了它你就能把自己的数据集无缝替换进这个项目。3.1 最省事的目录结构按类别分子文件夹PyTorch 的torchvision.datasets.ImageFolder直接支持“根目录下每个类别一个子文件夹”的组织方式。这个包自带的dataset/目录应该也是按照这种思路摆放的结构如下dataset/ ├── train/ │ ├── cat/ │ │ ├── 001.jpg │ │ └── 002.jpg │ └── dog/ │ ├── 003.jpg │ └── 004.jpg └── val/ ├── cat/ └── dog/提示如果压缩包里已经是这种结构直接用ImageFolder即可如果不是比如所有图片平铺在一个目录、另有一个 CSV 记录标签就需要在dataset.py里自己写路径解析逻辑。这种按类别分目录的方式最直观也免去了手工维护 label 映射表的麻烦。ImageFolder会自动按照文件名排序生成类别索引比如cat对应 0、dog对应 1。这里的坑在于索引顺序由文件夹名字符串排序决定训练和测试必须沿用同一个映射关系。如果你训练代码里用的是ImageFolder自动生成的索引测试时又手动指定了另一个映射表结果必然错乱。3.2 自定义 Dataset 的标准写法虽然ImageFolder能覆盖很多场景但数据增强、缓存、样本均衡这些需求下自己写 Dataset 是更可控的做法。这个包里的dataset.py大概率是继承torch.utils.data.Dataset实现我给你一个常规实现模板和这个包的思路一致import os from PIL import Image from torch.utils.data import Dataset from torchvision import transforms class ImageClassificationDataset(Dataset): def __init__(self, root_dir, transformNone): self.root_dir root_dir self.transform transform self.classes sorted(os.listdir(root_dir)) # 按字母排序保证映射稳定 self.class_to_idx {cls: i for i, cls in enumerate(self.classes)} self.samples [] # 遍历每个类别文件夹收集所有图片路径和标签 for cls in self.classes: cls_dir os.path.join(root_dir, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): path os.path.join(cls_dir, fname) self.samples.append((path, self.class_to_idx[cls])) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] image Image.open(path).convert(RGB) # 统一转成三通道 if self.transform: image self.transform(image) return image, label这份代码里有几个点要重点看。convert(RGB)处理灰度图和 RGBA 图的通道差异很多图片集里混着单通道或多通道图片不做统一转换训练到一半就会报张量形状不匹配的错误。sorted(os.listdir(root_dir))保证了类别索引的确定性避免因为文件系统遍历顺序导致映射漂移。预处理部分我一般会在训练脚本里额外组装一套transformstrain_transform transforms.Compose([ transforms.RandomResizedCrop(224), # EfficientNet 输入尺寸 transforms.RandomHorizontalFlip(), # 常见数据增强 transforms.ToTensor(), # HWC - CHW并归一化到 [0,1] transforms.Normalize( # ImageNet 均值/标准差 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]) ])这里RandomResizedCrop会在每次训练时随机裁剪出一个区域并缩放到 224×224相当于做了尺度增强和裁剪增强的组合。验证集只用Resize(256)加CenterCrop(224)这是 ImageNet 评估的标准配合目的是保持评估结果的确定性不要引入随机性。需要说明的是EfficientNet 各版本输入尺寸略有差异b0 默认 224×224b1 用 240×240b2 用 260×260以此类推。如果你想换更大的模型版本记得同步修改这两处尺寸。3.3__init__.py在这个包里到底有什么用很多人在解压后会看到dataset/__init__.py。这个文件是空文件也没关系它的核心作用是把目录变成一个 Python 包。如果你在别的目录下写脚本想from dataset.dataset import ImageClassificationDataset就必须有这个文件存在。没有它Python 不认为dataset/是一个可导入的包import 会直接报错。提示PyTorch 项目里__init__.py还有一个常见用途就是在里面统一导出 Dataset 和模型类这样外部调用代码会很简洁。我见过有人删除这个文件后训练脚本突然起不来的情况。排查到最后发现是 import 路径崩了恢复这个空文件后一切正常。所以解压后看到任何__init__.py默认都保留不要因为文件为空就觉得没用而删掉。4. 训练链路从 train.py 到 model.pth 的超参数与保存策略训练脚本是整个包的核心。EfficientNet 作为一个基于神经架构搜索设计的分类网络用对了预训练权重和训练超参往往能在几轮内就达到不错的准确率。这一章我聚焦最关键的训练设置并展开说明从脚本到model.pth的完整链路。4.1 模型加载torchvision 还是 timmEfficientNet 在 PyTorch 生态里有两个主要来源torchvision.models和第三方库timm。这个实战包里的写法我无法完全确定但无论哪种核心逻辑类似import torch import torch.nn as nn def build_model(num_classes10, pretrainedTrue): # 方案一torchvision 自带 from torchvision.models import efficientnet_b0 model efficientnet_b0(weightsIMAGENET1K_V1 if pretrained else None) # 方案二timm 提供更多变体 # import timm # model timm.create_model(efficientnet_b0, pretrainedpretrained) # 替换最后的全连接层 in_features model.classifier[1].in_features model.classifier[1] nn.Linear(in_features, num_classes) return model这里的关键在最后两行。EfficientNet 的分类头是一个Linear层位于model.classifier这个Sequential里。替换分类头时必须先取到原全连接层的输入维度再用nn.Linear(in_features, num_classes)覆盖。如果你的num_classes不改成自己数据集的类别数模型会直接报 mismatch 错误。pretrainedTrue表示加载在 ImageNet 上预训练过的权重。这里有个选型建议如果你的数据集比较小几百到几千张强烈建议用预训练权重在这个基础上只训练最后几层如果从零训练训练轮数和数据量要求会高很多效果还不一定好。如果model.pth是通过这个脚本训出来的它内部的 state_dict 结构也必然匹配这种替换后的结构。4.2 超参数设置与训练循环主体训练脚本的核心部分我按常见写法整理了一个主干import torch import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classes10).to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-2) scheduler CosineAnnealingLR(optimizer, T_max50) best_acc 0.0 for epoch in range(50): model.train() train_loss, correct, total 0.0, 0, 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() _, predicted outputs.max(1) correct (predicted labels).sum().item() total labels.size(0) scheduler.step() # 每个 epoch 后做一次验证保存最优权重 val_acc evaluate(model, val_loader) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), model.pth) print(fepoch {epoch}: saved model.pth, val_acc{val_acc:.4f})我重点说几个影响成败的细节。优化器选AdamW而不是基础的Adam因为 AdamW 把权重衰减从动量里解耦训练稳定性更好这也是 PyTorch 官方 Fine-tuning 教程的默认选项。学习率 1e-3 是微调场景的常规起点如果你发现 loss 震荡明显可以降一个量级到 1e-4。学习率调度用的CosineAnnealingLR这样做的好处是让学习率从初始值平缓下降在后期能更细地逼近局部最优。保存策略我强烈推荐按“验证集最优”保存而不是每个 epoch 都覆盖。这个包里的model.pth如果按照固定轮数保存可能存的是最后一轮的权重而最后一轮并不一定是验证集最好的如果你需要复现看一眼训练日志里保存时的val_acc就能知道这份权重对应的收敛状态。4.3 GPU/CPU 切换与随机种子的坑device torch.device(cuda if torch.cuda.is_available() else cpu)这行代码决定了训练跑在什么设备上。这里有一个实际的坑在 GPU 上训练得到的权重在 CPU 上加载需要用map_locationcpu。我经常碰到有人拿着 GPU 训出来的model.pth在自己没 GPU 的机器上torch.load直接报错就是因为漏了这句话state_dict torch.load(model.pth, map_locationcpu)此外如果训练时设了随机种子复现性会好很多。我一般会在train.py开头加一段固定种子和 cudnn 配置的代码。对于图像分类训练固定种子主要影响的是数据打乱顺序和权重初始化不设的话每次训练结果会有轻微差异这属于正常现象但如果要对比实验效果把种子固定掉能减少无关变量。import random import numpy as np def set_seed(seed42): random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) np.random.seed(seed) set_seed(42)这段代码在每个生成随机数的环节都显式指定了种子。注意torch.cuda.manual_seed_all只对 CUDA 生效CPU 训练时调用它不会报错但没有实际作用。固定种子对训练可复现性有帮助但不能百分之百保证在所有硬件环境下逐位一致因为某些 CUDA 算子是非确定的。5. 避坑 / 排查五个最容易翻车的点与处理记录训练脚本和数据模块都有了但这部分才是真正决定你能不能跑通的关键。我基于这一类项目的共性翻车点整理了五条具体的踩坑记录每条我都按「现象 → 原因 → 解决」来写便于你对照排查。5.1 图片通道错误导致 shape 不匹配现象跑train.py时前几步正常某个 batch 突然崩掉报错信息类似Expected 3 input channels, got 1或者张量维度不一致。原因数据集中混有灰度图或 RGBA 图。灰度图只有单通道PyTorch 的卷积层要求输入是[B, 3, H, W]通道数对不上就会在 forward 过程里报错。解决在 Dataset 的__getitem__里强制Image.open(path).convert(RGB)保证所有图片统一转为三通道。RGBA 图转 RGB 时透明度信息会被丢弃但多数分类场景下这种损失可接受。如果这个包本身没有这行我建议你自己补上。5.2 权重文件加载时报 Unexpected key(s)现象torch.load(model.pth)能读出来但model.load_state_dict(state_dict)报Missing key(s)或Unexpected key(s)提示分类头的权重形状不对。原因model.pth是从某个特定num_classes的模型里保存的而你现在构建模型的类别数不一致或者保存的是整个模型对象而不是 state_dict加载方式也不一样。解决把model.classifier的输出维度改成和保存权重时一致的类别数。如果你不确定原始类别数可以在加载时打印state_dict.keys()和model.state_dict()的差异或者用strictFalse加载跳过形状不匹配的分类头参数再单独适配。最稳妥的方式是验证一下类别映射关系确认没有加载错权重。5.3 训练中断后覆盖了最优权重现象训练到中途因为显存不足断掉重新跑之后model.pth被覆盖最后发现准确率比之前保存的还低。原因脚本设置成每个 epoch 都保存一次且文件固定为同一名字断点恢复后没有单独保留历史最佳权重。解决保存时按轮次或按验证集指标分文件保存比如model_epoch{epoch}_acc{val_acc:.4f}.pth然后用符号链接或单独文件指向最优档位。从那次以后我每次写训练脚本都会强制把“保存最佳”和“保存最新”分开避免覆写。5.4 测试时自定义预处理与训练不一致现象训练准确率表现正常但用Test.py评估测试集准确率大幅跳水。原因这是最常见的坑之一。测试脚本里图片预处理缺少了归一化步骤或者尺寸缩放方式和训练不一致。解决把训练用transform和测试用transform拆成两个独立定义。训练用RandomResizedCrop测试用ResizeCenterCrop两边归一化参数保持一致。检查时可以逐张比较训练和测试脚本里同一张图片经过预处理之后像素值范围是否一致。5.5 Windows 下路径拼接反斜杠报错现象在 Windows 上解压并运行脚本偶尔报文件找不到且报错路径显示dataset\train\cat这种反斜杠格式。原因代码里用了字符串直接拼接路径例如root_dir / cls在 Windows 上反斜杠和正斜杠混用部分接口解析异常。解决统一用os.path.join拼接路径。如果代码已经用了os.path.join但还是报错大概率是某个图片文件名的编码问题。另外不要自己写mkdir目录后在路径里混用斜杠交给Path对象处理会省心很多。五条记录覆盖了从数据读取、模型加载、训练保存到测试评估的完整链路。按照这个顺序排查大部分运行时报错都能定位到具体环节。6. 验证与进阶从 Test.py 到批量评估的习惯最后一个环节我来展开Test.py和test1.py的用法并讲一个我常用的进阶验证思路。6.1 Test.py 与 test1.py 的定位差异在工程包里Test.py是这个项目的正式评估脚本一般做法是遍历整个测试集累计所有样本的预测结果和标签最后输出整体准确率。test1.py则更像是调试用的轻量脚本指定某一张图片打印类别索引和置信度。两者的核心推理代码是一致的区别只在于循环范围。这里我给出单张图片推理的参考实现通常长这样def predict_single_image(model, img_path, class_names): from PIL import Image import torch from torchvision import transforms 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]) ]) image Image.open(img_path).convert(RGB) tensor transform(image).unsqueeze(0) # 增加 batch 维度 model.eval() with torch.no_grad(): outputs model(tensor) probs torch.softmax(outputs, dim1) pred_idx torch.argmax(probs, dim1).item() confidence probs[0, pred_idx].item() return class_names[pred_idx], confidenceunsqueeze(0)把[3, 224, 224]的张量转换为[1, 3, 224, 224]因为在训练时输入都是 batch 形式的四维张量推理时也要保持同样形状。模型切换到eval()模式这一点非常重要它关闭了 dropout 和 batch norm 的统计更新直接决定推理结果是否稳定。6.2 批量评估脚本怎么改要把单张推理扩成批量评估只需把图片路径换成测试集目录循环收集预测结果即可。我一般会在Test.py里先打印每个类别的单类准确率再打印总体准确率。只输出一个平均数会掩盖某一个类别严重混淆的问题。单类准确率可以帮助你快速定位模型到底在哪些类别上表现不佳然后针对性地补数据或者调整类别权重。6.3 一个进阶验证技巧用混淆矩阵定位问题如果你发现总体准确率不低但个别类之间互相误判我的习惯是额外生成混淆矩阵。不用复杂的可视化库sklearn.metrics.confusion_matrixmatplotlib就能满足多数场景。它能直观看出哪些类别容易混淆比如“背景”和“建筑”、“猫”和“狗”对比混淆矩阵和训练集样本分布你就能确认是数据不均衡还是类别本身太相近。这样做的价值在于它把推理脚本从“能出结果”推进到了“能指导下一轮迭代”。从那以后每次拿到这种实战源码包我都强制先走一遍解压确认文件结构 → 检查预处理一致性 → 用单张图片快速验证模型和前向链路 → 跑批量评估 → 最后看混淆矩阵。这套流程能帮我快速判断一个包值不值得深入去读也能顺手排查出上述大部分问题。希望这篇拆解能帮你在 EfficientNet 图像分类这条路上少走几个弯路。本文还有配套的精品资源点击获取
返回列表