简介:这是一份基于PyTorch的mobilenetv2图像分类实战资源包,完整覆盖从模型训练到TensorRT部署的端到端流程,适合深度学习图像分类与模型加速方向的开发者和学生。包内数据以植物幼苗数据集为基础,共包含12个类别,文件总计2000个,其中2454张PNG图片构成主要训练样本,8个Python脚本和2个pyc编译文件覆盖数据集加载、模型搭建、训练验证、预测及onnx/TensorRT转换推理等关键环节,压缩包大小约936MB。目前已有1854人学习下载。借助此资源包,读者可系统掌握torchvision.models中mobilenetv2的调用方法、自定义数据集的方式,以及Cutout和Mixup两种数据增强技巧;还能学会使用余弦退火动态调整学习率,完成训练和验证,并载入训练好的模型进行预测。此资源包还演示了PyTorch模型转ONNX及ONNX转TensorRT的完整流程,并分别实现推理,帮助读者建立从分类建模到部署加速的整体认知。
1. mobilenetv2 图像分类从训练到部署:卡在 TensorRT 这一步的看过来
mobilenetv2(PyTorch 版)从训练走到 TensorRT 部署,中间隔着的不是代码量,而是一堆只有踩过才知道的坑——ONNX 导出的动态轴、TensorRT 对 BN 层的融合、FP16 在某些层上的精度抖动,随便碰上一个,推理结果就从 95% 准确率变成噪声。这套实战资源把一条完整链路打通了:从 PyTorch 环境配置、MobileNetV2 结构复现、迁移学习训练,到导出 ONNX、构建 TensorRT 引擎,最后给出端到端的性能验证脚本。适合两类人:刚学完 PyTorch 基础、想用真实图像分类项目练手的初学者,以及模型已经训练完、卡在部署提速环节的工程师。不绕理论,按步骤跑通为止。
2. PyTorch 环境与 MobileNetV2 模型:先让训练链路跑通再谈加速
2.1 环境配置:CUDA、PyTorch、TensorRT 的版本搭配
很多人上来就装最新版,结果 PyTorch 能跑、TensorRT 加载引擎报INVALID_ENGINE,最后发现是 CUDA 运行时版本和 TensorRT 编译版本对不上。我一般先把版本矩阵定死,再动手装。
conda create -n mobilenetv2 python=3.10 -y conda activate mobilenetv2 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121这里cu121对应 CUDA 12.1 的预编译版本,PyTorch 2.x 全系列都支持。装完之后验证一下 GPU 是否真的可用:
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"输出True NVIDIA GeForce RTX 3060这类结果才算通过。如果torch.cuda.is_available()返回False,九成是 PyTorch 装了 CPU 版,或者 CUDA 驱动版本太老,先把 NVIDIA 驱动升到 525 以上再看。
TensorRT 这边我建议直接用 NVIDIA 官网下载的 tar 包,不要走 apt,因为 apt 源里的版本往往和 PyTorch 的 CUDA 版本错位。解压后把lib目录加进环境变量:
export LD_LIBRARY_PATH=/opt/TensorRT-8.6.1.6/lib:$LD_LIBRARY_PATH pip install /opt/TensorRT-8.6.1.6/python/tensorrt-8.6.1-cp310-none-linux_x86_64.whl我用的这台机器是 TensorRT 8.6,如果你下到 10.x,配置方式完全一样,只是 Python wheel 的文件名不同。下表是这套资源里经过验证的版本组合:
| 组件 | 版本 | 说明 |
|---|---|---|
| Python | 3.10 | 兼容性最好,Torch 和 TRT 的 wheel 都全 |
| CUDA Toolkit | 12.1 | 和 PyTorch 的 cu121 对应 |
| PyTorch | 2.x | 用 torchvision 自带的 mobilenet_v2 预训练权重 |
| TensorRT | 8.6 或 10.x | 8.6 稳定,10.x 支持更多新算子 |
| onnx / onnxsim | 最新 | 用于导出和简化模型 |
2.2 MobileNetV2 结构拆解:倒残差块、深度可分离卷积与线性瓶颈
MobileNetV2 的核心是倒残差块(Inverted Residual Block),和 ResNet 的残差块方向相反——先 1x1 升维,再做 3x3 深度可分离卷积,最后 1x1 降维,降维后不跟 ReLU,改叫线性瓶颈。这个设计是为了避免低维特征经过 ReLU 后被截断丢失信息。
import torch import torch.nn as nn class ConvBNReLU(nn.Sequential): def __init__(self, in_planes, out_planes, kernel_size=3, stride=1, groups=1): padding = (kernel_size - 1) // 2 super().__init__( nn.Conv2d(in_planes, out_planes, kernel_size, stride, padding, groups=groups, bias=False), nn.BatchNorm2d(out_planes), nn.ReLU6(inplace=True) ) class InvertedResidual(nn.Module): def __init__(self, in_planes, out_planes, stride, expand_ratio): super().__init__() hidden_dim = int(round(in_planes * expand_ratio)) self.use_residual = stride == 1 and in_planes == out_planes layers = [] if expand_ratio != 1: layers.append(ConvBNReLU(in_planes, hidden_dim, kernel_size=1)) layers.extend([ ConvBNReLU(hidden_dim, hidden_dim, kernel_size=3, stride=stride, groups=hidden_dim), nn.Conv2d(hidden_dim, out_planes, kernel_size=1, bias=False), nn.BatchNorm2d(out_planes) ]) self.conv = nn.Sequential(*layers) def forward(self, x): if self.use_residual: return x + self.conv(x) return self.conv(x)expand_ratio控制升维倍数,MobileNetV2 里大部分块是 6 倍,也就是把通道数临时放大 6 倍再压缩回来,参数量没增加,但每个卷积处理的信息更多。groups=hidden_dim是深度可分离卷积分组卷积的写法,每个通道独立卷,这是 MobileNet 系列参数量远小于 VGG、ResNet 的关键。
实际工程里不需要手写整个网络,torchvision 已经内置了完整结构,上面这段代码是用来理解结构、方便改造成自定义变体的。如果你只是做分类任务,直接调 torchvision 的接口就行,后文训练部分就用内置版本。
2.3 数据加载与预处理:归一化参数为什么是 0.485、0.456、0.406
图像分类的预处理有一套固定的统计值,来自 ImageNet 数据集的 RGB 三通道均值和标准差。很多人会在这里翻车——训练时用了一组归一化参数,部署时忘了用同一组,结果推理结果和训练时完全对不上。
from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_data = datasets.ImageFolder("dataset/train", transform=train_tf) val_data = datasets.ImageFolder("dataset/val", transform=val_tf) train_loader = DataLoader(train_data, batch_size=64, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_data, batch_size=64, shuffle=False, num_workers=4, pin_memory=True)RandomCrop(224)配合Resize((256, 256))是训练集常用的数据增强组合,先把图放大到 256,再随机裁 224,相当于给模型提供了一定程度的平移不变性。验证集不能随机,所以用CenterCrop。
DataLoader里的num_workers=4在 Linux 下能让数据读取和 GPU 训练并行,Windows 下要注意,多进程有时会报错,建议改成 0 或 2。pin_memory=True对 GPU 训练有加速效果,配合to(device, non_blocking=True)能减少数据传输阻塞。
3. 迁移学习训练:用预训练权重把收敛时间压缩到三分之一
3.1 数据集组织与类别映射
这套资源默认的数据集目录结构是标准的ImageFolder格式,训练集和验证集分开,每个子文件夹代表一个类别,文件夹名就是类别名。
dataset/ ├── train/ │ ├── cat/ │ ├── dog/ │ └── bird/ └── val/ ├── cat/ ├── dog/ └── bird/datasets.ImageFolder会自动按文件夹名生成类别索引,train_data.classes就是类别列表,train_data.class_to_idx是类别到索引的映射字典。训练之前先打印出来确认一下,避免类别顺序乱掉导致评估指标对不上号。
如果你的数据是 CSV 标注或者 JSON 格式,需要先转换成这个目录结构。数据量少的时候(比如每类只有几十张),Transfer Learning 依然能跑,这就是热词里提到的 1-shot、5-shot 小样本场景。每类只有一张图时,预训练权重还能撑住,但学习率必须压到 1e-4 以下,而且只能微调最后一层分类头。
3.2 训练脚本:迁移学习、权重衰减、标签平滑与学习率调度
训练的核心是加载 ImageNet 预训练权重,然后替换最后的全连接层。MobileNetV2 的classifier是一个Sequential,由 Dropout 和 Linear 组成,替换索引为 1 的那一层即可。
import torch import torch.nn as nn import torch.optim as optim from torchvision import models num_classes = len(train_data.classes) model = models.mobilenet_v2(weights=models.MobileNet_V2_Weights.IMAGENET1K_V1) # 冻结特征提取层,只训练分类头,适合数据量少的场景 for param in model.features.parameters(): param.requires_grad = False model.classifier[1] = nn.Linear(model.last_channel, num_classes) model = model.to(device) criterion = nn.CrossEntropyLoss(label_smoothing=0.1) optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9, weight_decay=5e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)label_smoothing=0.1是标签平滑,把 one-hot 的 1 和 0 变成 0.9 和 0.1/num_classes,防止模型对训练集过度自信,泛化会好一些。weight_decay=5e-4就是 L2 正则化在 PyTorch 里的落地写法,SGD 的权重衰减直接加在参数更新上,能抑制过拟合。
冻结model.features后,优化器只更新分类头,训练速度快很多。如果你的数据集每一类超过 500 张图,可以解冻部分特征层,把学习率设成 1e-4,效果会更好。判断标准很简单:训练集准确率很快到 99%、验证集卡在 80% 附近,就是过拟合,此时加强数据增强或者加大 weight_decay;如果两边都上不去,说明学习率太高或者数据有问题。
余弦退火CosineAnnealingLR让学习率从初始值平滑降到接近 0,比固定学习率在分类任务上平均能涨 1~2 个点。T_max=50要和训练轮数一致。
3.3 训练循环:保存最佳模型与日志记录
训练循环本身不复杂,关键是每轮结束要跑验证集,并且只在验证准确率创新高时保存权重,这样即使后面训练发散,手里也有一份最优模型。
best_acc = 0.0 for epoch in range(50): model.train() running_loss = 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() running_loss += loss.item() model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() acc = correct / total scheduler.step() if acc > best_acc: best_acc = acc torch.save(model.state_dict(), "mobilenetv2_best.pth") print(f"epoch {epoch+1} | loss {running_loss/len(train_loader):.4f} | val_acc {acc:.4f}")torch.max(outputs, 1)返回每个样本得分最高的类别索引,predicted == labels得到布尔掩码,sum().item()统计正确数。保存的mobilenetv2_best.pth是纯状态字典,加载时要先建模型再load_state_dict,这个文件也是后续转 ONNX 的输入。
50 轮训练在单张 RTX 3060 上用 batch size 64 跑差不多 30 到 40 分钟,ImageNet 预训练权重起跑,收敛曲线会非常漂亮。如果发现验证集准确率一直徘徊在 60% 以下,先检查数据预处理——归一化参数用错或者图片尺寸不一致是最常见的原因。
4. 模型转换:PyTorch 到 ONNX,再到 TensorRT 的完整落盘流程
4.1 导出 ONNX:动态 batch 与 opset 版本选择
训练完成后的模型是.pth状态字典,TensorRT 不认识这个格式,需要先导出成 ONNX。导出前必须把模型切到 eval 模式,并且固定 batch 维度或者显式声明动态轴。
import torch from torchvision import models model = models.mobilenet_v2() model.classifier[1] = torch.nn.Linear(model.last_channel, 3) model.load_state_dict(torch.load("mobilenetv2_best.pth", map_location="cpu")) model.eval() dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, "mobilenetv2.onnx", input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, opset_version=17, do_constant_folding=True )dynamic_axes声明第 0 维是动态的,这样导出的 ONNX 模型可以接受 batch 为 1、4、8、16 任意值。如果你的 TensorRT 版本是 8.6 之前的,opset_version=17可能解析不了,改成 13 就行——不要在 TensorRT 装得老的情况下追求高 opset,能吃进去比什么都重要。do_constant_folding=True会把能算的常量折叠掉,减小模型体积。
导出后用 onnxsim 做一次简化,能去掉很多 TensorRT 不友好的多余节点:
pip install onnxsim onnx python -m onnxsim mobilenetv2.onnx mobilenetv2_sim.onnx简化前后的模型在推理结果上完全一致,但简化后的 ONNX 转 TensorRT 成功率更高。这个步骤是血泪经验,我之前跳过 onnxsim,结果 TensorRT 报FAILED: Assertion failed: inputs.size() == outputs.size(),简化后一次通过。
4.2 构建 TensorRT 引擎:trtexec 与 Python API 两种方式
TensorRT 引擎有两种构建方式。命令行工具 trtexec 适合快速验证和测性能,Python API 适合集成到自动化流程里。先看 trtexec 的用法:
trtexec --onnx=mobilenetv2_sim.onnx --saveEngine=mobilenetv2_fp16.engine \ --fp16 \ --minShapes=input:1x3x224x224 \ --optShapes=input:8x3x224x224 \ --maxShapes=input:16x3x224x224 \ --workspace=2048minShapes、optShapes、maxShapes三个参数对应动态 batch 的三种输入形状,TensorRT 会按optShapes做最优 kernel 选择。--workspace=2048单位是 MiB,也就是给 TensorRT 最多 2GB 显存做层融合和 kernel 选择,设太大会让构建时显存暴涨,设太小可能触发cannot allocate memory。
Python API 构建的好处是可以把构建过程封装进训练脚本,训练完成直接出引擎:
import tensorrt as trt logger = trt.Logger(trt.Logger.WARNING) builder = trt.Builder(logger) network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser = trt.OnnxParser(network, logger) with open("mobilenetv2_sim.onnx", "rb") as f: success = parser.parse(f.read()) if not success: for i in range(parser.num_errors): print(parser.get_error(i)) config = builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 2 * 1024 * 1024 * 1024) config.set_flag(trt.BuilderFlag.FP16) engine = builder.build_serialized_network(network, config) with open("mobilenetv2_fp16.engine", "wb") as f: f.write(engine)注意EXPLICIT_BATCH这个 flag,ONNX 导出时如果用了dynamic_axes,构建网络就必须开显式 batch 模式,否则 TensorRT 会认为 batch 维是固定的。build_serialized_network返回的是序列化后的引擎字节流,直接写盘,部署时反序列化就能用。
4.3 推理验证:引擎加载与精度一致性检查
引擎构建完先别急着上生产,跑一次推理对比 PyTorch 原始输出,确认精度没有丢失。这一步在部署时是强制流程,防止 FP16 转出来结果完全不对还在排查下游业务。
import numpy as np import tensorrt as trt logger = trt.Logger(trt.Logger.WARNING) runtime = trt.Runtime(logger) with open("mobilenetv2_fp16.engine", "rb") as f: engine = runtime.deserialize_cuda_engine(f.read()) context = engine.create_execution_context() # 动态 batch 必须显式设置输入 shape context.set_input_shape("input", (1, 3, 224, 224)) # 分配输入输出缓冲区 input_buf = np.random.randn(1, 3, 224, 224).astype(np.float32)加载引擎后,context.set_input_shape是动态 batch 推理的必需步骤,不设会直接报INVALID_SHAPE。TensorRT 10.x 里缓冲区分配方式有变化,推荐用tensorrt.tensorrt.Input的allocate_array接口,但 8.6 用cuda.mem_alloc更稳。
精度对比的做法是:把同一张预处理后的图片分别喂给 PyTorch 模型和 TensorRT 引擎,比较 softmax 输出的最大差异。差异小于 1e-2 说明转换没问题,如果输出全是 NaN 或者分类结果完全不同,大概率是预处理不一致,看下一章的避坑指南。
5. TensorRT 部署避坑指南:五条实战踩坑记录
5.1 ONNX 导出成功,但 TensorRT 解析时报错
现象:trtexec 加载 ONNX 时报Unsupported operator或者Graph conflicts with provided input shape,还没开始构建引擎就退出。
原因:opset 版本和 TensorRT 支持的算子不匹配,或者 ONNX 图里残留了未折叠的冗余节点。我在 TensorRT 8.6 上试过 opset 17 的某些导出,某些版本的 onnx 导出器产生的Resize节点 TRT 解析不了。
解决:先把 opset 降到 13 重新导出,再跑一遍 onnxsim 简化。如果还报错,用python -m onnxruntime.tools.convert_onnx_models_to_onnx做版本转换,或者直接换 TensorRT 10.x。另外把do_constant_folding=True打开,能消掉大部分冗余常量节点。
5.2 引擎构建成功,推理结果全是错的
现象:TensorRT 引擎能正常输出,但输出的置信度分布和 PyTorch 完全对不上,分类结果变成随机猜测。
原因:这不是 TensorRT 的问题,是预处理不一致。训练时 PyTorch 侧做了ToTensor+Normalize,但部署侧喂给 TensorRT 的是 0~255 的原始图像,或者归一化参数写错。
解决:部署脚本里把预处理写在 numpy 层,保证和训练侧完全一致。我一般会写一个preprocess()函数,输入 BGR 或 RGB 图像,输出(1, 3, 224, 224)的 float32 数组,里面带上 Resize、CenterCrop、Normalize,然后把这个函数的输出直接作为 TensorRT 的输入。比对脚本用np.max(np.abs(trt_output - torch_output))检查差异。
5.3 FP16 精度掉点明显,准确率从 95% 掉到 88%
现象:FP16 引擎跑起来速度很快,但验证集准确率明显下降,集中在某几个类别上。
原因:MobileNetV2 的倒残差结构里有大量 BatchNorm 层,FP16 下部分通道的动态范围不够,激活值被截断,导致特征信息丢失。这种情况通常只出现在个别 sensitive layer 上,不是全模型都坏。
解决:用 TensorRT 的层黑名单机制,把敏感层强制留在 FP32。做法是先构建一个全部 FP16 的引擎,用验证集找出分类错误集中的层,然后在构建配置里对这些层设置precision = trt.float32。更快的做法是直接跑一遍校准,给这些层加上set_precision接口:
for i in range(network.num_layers): layer = network.get_layer(i) if layer.name in sensitive_layers: layer.precision = trt.float32 layer.set_output_type(0, trt.float32)如果不想逐层排查,还有一个通用方案:FP16 整体掉点超过 1% 时,改用 INT8 加校准数据集,精度通常能回到 FP32 水平,延迟只比 FP16 高一点点。
5.4 动态 batch 推理时显存直接爆掉
现象:引擎能加载,单张图推理正常,但并发请求一上来就CUDA out of memory。
原因:构建引擎时maxShapes设成了 16,workspace又给了 2GB,TensorRT 会为最大 batch 预留 kernel workspace。实际业务并发根本到不了 16,白占显存。
解决:按真实业务峰值设置optShapes和maxShapes。如果线上并发高峰是 4,maxShapes设 8 就够了,多出来的空间留给 TensorRT 做 kernel 调优。workspace 设置成1 << 30(1GB)对 MobileNetV2 这种轻量网络已经非常充裕,没必要给 2GB。显存占用和延迟是跷跷板,没有绝对最优,只能按场景调。
5.5 PowerShell 里 conda 命令无法识别
现象:在 Windows PowerShell 里执行conda activate mobilenetv2,终端直接报无法将“conda”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。
原因:conda 安装时没有写入 PowerShell 的环境变量,或者没有执行过conda init。这个坑在开发环境配置里出现频率极高。
解决:先执行conda init powershell,然后关闭重开终端。如果还不行,直接用 Anaconda 安装路径下的完整 python 路径:
C:\Users\xxx\anaconda3\envs\mobilenetv2\python.exe -m pip install torch torchvision绕过 conda activate,直接调环境里的 python 解释器,效果一样。这套资源里部署脚本全部用相对路径和命令行参数,不会依赖特定的 shell 环境。
6. 端到端性能验证:用 CUDA Event 给推理延迟做个体检
部署完成后的第一件事不是看 FPS,而是用 CUDA Event 测端到端延迟。CUDA Event 走的是 GPU 时间线,比 Python 层面的time.time()准得多,尤其能过滤掉 CPU 侧数据拷贝和 Python 解释器调度的抖动。
import torch import numpy as np import tensorrt as trt import pycuda.autoinit import pycuda.driver as cuda # 假设 engine 和 context 已经加载,input_buf 和 output_buf 已分配 start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) # 先预热 20 次,把 TensorRT 的 kernel 加载和 CUDA context 初始化排除掉 for _ in range(20): context.execute_v2(bindings=[input_buf, output_buf]) torch.cuda.synchronize() times = [] for _ in range(100): start.record() context.execute_v2(bindings=[input_buf, output_buf]) end.record() torch.cuda.synchronize() times.append(start.elapsed_time(end)) print(f"avg: {np.mean(times):.2f} ms | p95: {np.percentile(times, 95):.2f} ms")p95比平均延迟更能反映真实用户体验,因为偶发的调度抖动会把平均值拉高。同一台机器上,PyTorch 的 FP32 推理延迟通常在 8~12ms,TensorRT FP16 能压到 2~4ms,吞吐提升接近 3 倍。显存占用也会跟着降,因为 TRT 的 kernel 融合减少了中间张量存储。
验证精度的脚本我建议做成固定的回归测试:跑完 val 集,把 TensorRT 输出的 top-1 准确率和 PyTorch 训练时的结果对比,偏差超过 0.5% 就报警。从那以后我每次做完模型转换,都强制走一遍「加载引擎 → 精度回归 → p95 延迟」三段式体检,不再凭感觉上线。这套资源里的脚本已经把三段流程串好了,换数据路径、换类别数就能直接用。希望帮到你。
本文还有配套的精品资源,点击获取