ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PyTorch模型转ONNX实现垃圾分类系统部署实战

PyTorch模型转ONNX实现垃圾分类系统部署实战 简介基于深度学习的垃圾分类系统项目包聚焦图像识别自动分类场景面向正在学习计算机视觉和Python工程化的开发者也适用于课程设计、毕业设计或小型智能应用开发参考。项目通过ONNX标准格式导入预训练模型打通了从图像预处理、特征提取到结果输出的完整流程并内置了简单的Web交互模块方便直观验证识别效果。压缩包体量很小仅12KB共含6个文件包括3个CSV数据表用户信息、标签映射、历史记录、2个Python脚本主应用与核心识别逻辑以及1个pyc缓存文件目录结构一目了然适合快速通读与二次改造。目前已有116人学习下载读者能从中学习到ONNX跨平台模型部署、Flask轻量级应用搭建以及垃圾分类系统的模块化组织方式对于入门深度学习项目落地很有帮助。1. 垃圾分类系统为什么最终让模型走 ONNX 导入这条落地方案当你打开一个名为“基于深度学习的垃圾分类系统模型使用ONNX导入.zip”的项目包眼睛别只盯着“垃圾分类”这几个大字真正决定这套系统能不能从训练环境搬到生产环境的关键是“ONNX导入”这个动作。训练阶段用 PyTorch 怎么折腾都行动态图、打印中间张量、随时改结构但到了部署阶段没人愿意在一个只有几十兆内存的容器里再装一整套 PyTorch 和 CUDA。ONNX 把训练好的网络固化成一张静态计算图交给 onnxruntime 这种轻量推理引擎去执行依赖少、启动快也方便以后换硬件。下面我会把从数据准备、PyTorch 训练、Pytorch 转 ONNX 导出再到 onnxruntime 推理和问题排查的完整过程讲透让新手能按步骤跑通让熟手能找到参数设置的边界。2. 把项目拆成数据、训练与推理三段ONNX 这个交接点怎么选2.1 数据集选型与类别映射垃圾图片分类的最小可用方案垃圾图片分类本质上是细粒度图像分类同类垃圾在不同光线、不同拍摄角度下差别很大。一个常见做法是从公开渠道收集垃圾分类图片再按业务需要整理成 515 个类别。比如可回收垃圾下的塑料瓶、易拉罐、纸箱可以拆成三个类也可以合并成一个“可回收物”。别小看这个决策类别太细样本不够模型容易过拟合类别太粗类内差异太大模型内部会学成几个子类再硬合并边界一团乱。为了不把时间浪费在手工整理路径上我一般先用一个脚本按类别目录划分训练集和验证集import os, random, shutil from collections import defaultdict root garbage_all # 原始数据garbage_all/类别名/*.jpg train_dir dataset/train val_dir dataset/val val_ratio 0.2 class_files defaultdict(list) for cls in os.listdir(root): cls_path os.path.join(root, cls) if not os.path.isdir(cls_path): continue files [os.path.join(cls_path, f) for f in os.listdir(cls_path) if f.endswith((.jpg, .jpeg, .png))] class_files[cls] files random.seed(0) for cls, files in class_files.items(): random.shuffle(files) split_idx int(len(files) * (1 - val_ratio)) for phase, subset in ((train, files[:split_idx]), (val, files[split_idx:])): target_dir os.path.join(train_dir if phase train else val_dir, cls) os.makedirs(target_dir, exist_okTrue) for f in subset: shutil.copy(f, os.path.join(target_dir, os.path.basename(f)))这个脚本的关键是按类别分别划分而不是对整个数据集做一次随机打乱。垃圾图片数据往往类别数量极不平衡比如“有害垃圾”可能只有两三百张而“其他垃圾”有几千张如果整体随机切分验证集里有可能一个有害样本都分不到最后模型对有害垃圾的召回率显示 100%实际部署时却频频漏检。random.seed(0)固定随机种子保证你每次跑出的划分一致后面做模型对比时才有公平基础。类别映射表建议用数字 id 固化而不是让代码去猜字符串。下面是我常用的一套最小类别方案类别ID类别名典型物品训练样本建议0可回收物塑料瓶、纸箱、易拉罐≥8001厨余垃圾果皮、菜叶、剩饭≥8002有害垃圾电池、过期药品、灯泡≥3003其他垃圾卫生纸、碎陶瓷、烟蒂≥800如果某个类别实在凑不够样本不要用复制粘贴硬补数据那样会让模型记住重复图像而不是真正的类别特征。常见替代方案是调整损失函数的类别权重或者用数据增强中的随机裁剪、旋转、色彩抖动去扩充有效样本我一般会在训练脚本里用WeightedRandomSampler平衡采样。2.2 为什么用 PyTorch 训练却用 ONNX 导入推理训练和部署是两套逻辑。训练时你需要动态图带来的调试自由度能随时 print 张量形状能中途改网络结构PyTorch 的 eager 模式非常适合这种实验节奏。但当一个垃圾分类系统要作为服务交付时看中的是启动速度、内存占用和长时间运行的稳定性。继续让 PyTorch 参与推理意味着部署机器上要装一堆训练期的依赖哪怕你只用到其中 10% 的算子。ONNX 的价值就是在这个交接点上提供一种中立的模型表示。你可以在 PyTorch 里导出也可以在 TensorFlow 里导出最终交给 onnxruntime 统一执行。onnxruntime 针对 CPU 和 GPU 都做了算子融合与内存优化很多情况下推理速度比原生 PyTorch 还快一截。更重要的是部署端不一定用 Python可以用 C、Java、C# 写服务只要加载同一个.onnx文件就行。这也是项目标题里特意标出“模型使用ONNX导入”的原因训练框架生成权重但真正的运行环境只认识 ONNX 计算图。两者各管一段靠.onnx文件解耦。很多同学踩过的坑是只存.pth到部署环境才想起来没有原训练类定义加载直接变成黑匣子ONNX 相当于提前给模型补了一份可在任意机器上解释的存档至少不会因为缺文件就当场翻车。2.3 项目目录里你至少需要哪几个文件拿到这类 zip 包第一件事不是立刻跑 demo而是检查文件结构。一个能顺利落地的最小项目解压后通常长这样文件作用注意事项requirements.txt记录 onnxruntime、torch、opencv-python 等依赖版本锁大版本即可data/train 与 data/val训练/验证图片按类别分子目录类别目录名要与 classes.txt 顺序一致model.py定义网络结构训练、导出、部署复用同一份定义train.py训练入口保存 best_model.pth保存 state_dict不要保存整个 modelexport_onnx.py把 .pth 转成 .onnx是部署链路的枢纽infer.py加载 .onnx 并用 onnxruntime 推理单图/摄像头逻辑都放在这里classes.txt类别名按行排列第一行必须是类别 0其中最容易出问题的是classes.txt和model.py的对齐。训练时你用ImageFolder它按目录名字母排序生成标签如果classes.txt是手写的顺序不一致就会导致“模型把有害垃圾识别成可回收物”这种看似玄学、实则在映射表里就能发现的问题。我建议在训练脚本里把dataset.classes直接导出成classes.txt这样一劳永逸。3. 从 PyTorch 训练到 Pytorch 转 ONNX 导出opset、动态轴与校验3.1 训练一个轻量分类模型并保存权重文件导出 ONNX 的前提是你手上有一个真正训练过的模型。垃圾分类不是千类级识别任务我习惯用 ResNet18 或 MobileNetV3 做骨干网络把最后一层全连接改成类别数。下面是训练入口最需要注意的几段import torch import torch.nn as nn from torch.utils.data import DataLoader, WeightedRandomSampler from torchvision import datasets, models, transforms num_classes 4 epochs 10 device torch.device(cuda if torch.cuda.is_available() else cpu) transform 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_ds datasets.ImageFolder(dataset/train, transformtransform) val_ds datasets.ImageFolder(dataset/val, transformtransform) # 对类别不平衡做平衡采样 sample_weights [1.0 / train_ds.targets.count(t) for t in train_ds.targets] sampler WeightedRandomSampler(sample_weights, num_sampleslen(train_ds), replacementTrue) train_loader DataLoader(train_ds, batch_size32, shuffleFalse, samplersampler, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-4)这段代码里有一个对后续导出影响深远的细节预处理用了Resize((256,256))再CenterCrop(224)而不是直接Resize((224,224))。这么做一方面让训练时图片保留更多上下文另一方面也让部署端必须复刻“先放大再裁剪”的逻辑。如果你偷懒改成直接缩小模型推理时看到的物体比例和训练时不一致准确率会肉眼可见地掉。训练循环本身不复杂关键是每个 epoch 在验证集上评估一次只保留最优 checkpointbest_acc 0.0 for epoch in range(epochs): model.train() 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() model.eval() correct total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) preds model(images).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) acc correct / total if acc best_acc: best_acc acc torch.save(model.state_dict(), checkpoints/best_model.pth) print(fepoch {epoch1} acc {acc:.3f})保存state_dict而不是torch.save(model, ...)是我吃过亏后养成的习惯。整个模型保存会把一堆类引用、优化器状态一并序列化换一台机器或改了目录结构加载时经常报“cannot pickle”或类不存在。只保存参数导出脚本里用统一结构实例化再load_state_dict干净且跨环境友好。3.2 torch.onnx.export 的导出脚本五个必须锁定的参数权重固定后写一个export_onnx.py。这里没有特别多花活但每个参数都可能成为部署端的坑。直接看代码import torch import onnx from model import create_model num_classes 4 model create_model(num_classes) model.load_state_dict(torch.load(checkpoints/best_model.pth, map_locationcpu)) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, rubbish_classifier.onnx, export_paramsTrue, opset_version14, do_constant_foldingTrue, input_names[input], output_names[logits], dynamic_axes{ input: {0: batch_size}, logits: {0: batch_size} } )五个关键参数逐个说。export_paramsTrue表示把训练好的参数打包进 ONNX 文件这是必须的如果设成 False导出的只是空骨架推理结果全是随机数。opset_version是算子集合的版本号。太老比如 9可能导致新激活函数没有对应导出太新比如 18则需要部署端的 onnxruntime 也够新。我一般锁 14因为它在导出Resize、BatchNorm、Softmax这些常用算子时都比较稳老版本 onnxruntime 也大多支持。如果你部署环境特别老优先降到 12 而不是 9。do_constant_foldingTrue会把计算图里与输入无关的常量计算提前折叠减少运行时节点数模型体积也会小一些。input_names和output_names是给计算图节点起名字部署时通过这个名字喂数据取输出也靠它命名不统一是低级但高频的坑。dynamic_axes是最需要动脑的参数。这里把 batch 维度标成了动态意味着推理时一次可以传 1 张也可以一次传 16 张计算图会按实际传入的 batch 形状执行。代价是有轻微性能损耗。如果你确定只做单图请求可以不设dynamic_axesONNX 文件会更小、推理略快但一旦需求变成“一次批量识别 10 张”就要重新导出所以为了灵活性我更推荐保留 batch 动态。提示导出前一定记得调用model.eval()。漏掉这一步会让 BatchNorm 层使用训练时的统计量导出出的模型在单图推理时行为异常而你在导出阶段几乎看不出问题。3.3 导出后先校验onnx.checker 与形状对齐模型导出后别直接拿去上线先在本地做一次体检import onnx onnx_model onnx.load(rubbish_classifier.onnx) onnx.checker.check_model(onnx_model) # 结构不合法会直接抛异常 graph onnx_model.graph print(输入:, graph.input[0].name, graph.input[0].type.tensor_type.shape) print(输出:, graph.output[0].name, graph.output[0].type.tensor_type.shape) ops {node.op_type for node in graph.node} print(算子集合:, sorted(ops))check_model能发现一半的导出问题比如权重缺失、节点输入不匹配。打印出的输入形状会直接告诉你dynamic_axes有没有生效如果维度是[1,3,224,224]说明 batch 维没变成动态如果是[None,3,224,224]就对了。None在 ONNX 里就表示任意数值。还有一道更重要的验证拿几张训练集图片先走 PyTorch 模型拿输出再走 ONNX 模型拿输出对比 logits 的差异。差异在1e-3量级算正常如果大到0.1以上优先检查导出前是否model.eval()、是否加载了正确权重、预处理是否一致。这一步相当于给部署买一份“后悔药”总比在用户现场排查强。4. 用 ONNXRuntime 把 .onnx 模型接进垃圾分类系统4.1 加载模型与第一次推理session 初始化与输入输出部署端不再需要 PyTorch核心依赖只有一个 onnxruntime。第一次加载模型时先别急着写完整业务把输入输出摸清楚import onnxruntime as ort import numpy as np session ort.InferenceSession( rubbish_classifier.onnx, providers[CPUExecutionProvider] ) input_info session.get_inputs()[0] output_info session.get_outputs()[0] print(输入名:, input_info.name) print(输入形状:, input_info.shape) print(输入类型:, input_info.type) print(输出名:, output_info.name) dummy np.random.randn(1, 3, 224, 224).astype(np.float32) result session.run([output_info.name], {input_info.name: dummy}) print(输出 shape:, result[0].shape)providers参数控制了推理后端。如果你装的是onnxruntime-gpu可以把[CUDAExecutionProvider, CPUExecutionProvider]写在列表里onnxruntime 会优先尝试 CUDA失败时回退 CPU。但如果 CUDA 库版本不匹配启动时可能直接报找不到动态库因此我建议在项目初期先用 CPU 跑通全流程再考虑 GPU 加速避免把精力耗在环境问题上。有一点必须强调session.get_inputs()[0].type如果是tensor(float)喂进来的 numpy 数组必须转成float32。很多初版代码把 OpenCV 读出的uint8矩阵直接传入session 不会自动帮你转型结果要么报错要么输出概率看起来像随机数。4.2 图像预处理复用训练参数归一化、尺寸与通道顺序ONNX 图只认识数值不认识你在 PyTorch 里写的transforms对象。所以推理端的预处理必须把训练端每一行代码翻译过来。下面是我常用的一个函数import cv2 import numpy as np def preprocess(image_bgr): # 1. OpenCV 读出来是 BGR而训练时用的是 PIL 的 RGB必须转 image_rgb cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) # 2. 训练时 Resize((256, 256)) 后 CenterCrop(224) h, w image_rgb.shape[:2] scale 256 / max(h, w) new_w, new_h int(round(w * scale)), int(round(h * scale)) resized cv2.resize(image_rgb, (new_w, new_h)) top (resized.shape[0] - 224) // 2 left (resized.shape[1] - 224) // 2 cropped resized[top:top224, left:left224] # 3. ToTensor 相当于除以 255再按 ImageNet 统计值归一化 x cropped.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406], dtypenp.float32) std np.array([0.229, 0.224, 0.225], dtypenp.float32) x (x - mean) / std # 4. HWC - NCHW并扩展 batch 维 x np.transpose(x, (2, 0, 1)) x x[np.newaxis, ...].astype(np.float32) return x这段代码最容易错的是前两步。训练时先放大到短边 256再做 224x224 的中心裁剪推理时必须完整复刻。如果图省事直接cv2.resize(image, (224,224))相当于把训练时的裁剪步骤删了物体比例完全不同准确率下降是必然的。颜色通道也一样PIL 读进网络的是 RGBOpenCV 默认读成 BGR不转的话模型等于在看一幅红蓝互换的图。下面这张表建议贴在项目文档里以后排查“训练和部署结果不一致”问题时会非常有用阶段训练端 (torchvision)部署端 (OpenCV)缩放Resize((256,256))按比例缩到短边 256裁剪CenterCrop(224)手动居中剪到 224x224颜色PIL 读入 RGBBGR - RGB数值ToTensor 除以 255先转 float32 再除以 255归一化Normalize(mean,std)相同 mean/std 逐通道减除维度输出 NCHWtranspose 到 NCHW 后再加 batch 维最后一行的transpose是隐藏 boss。DataLoader 给出的张量是(B,C,H,W)而你手动读图得到的是(H,W,C)不做轴交换的话通道维和宽维搅在一起模型不会报错但输出概率跟随机一样。这种错误比显式异常难查得多最好的办法是写一个单测把同一张图分别喂给预处理函数和 torchvision 的 transform 流程对比输出张量。4.3 一条完整的单图推理函数与类别打码输出把预处理和 session 调用拼成一个函数def predict(image_bgr, session, class_names): x preprocess(image_bgr) input_name session.get_inputs()[0].name logits session.run(None, {input_name: x})[0] probs np.exp(logits - logits.max(axis1, keepdimsTrue)) probs probs / probs.sum(axis1, keepdimsTrue) idx int(np.argmax(probs[0])) return class_names[idx], float(probs[0][idx])session.run(None, ...)里的None表示返回模型所有输出对于单输出模型没问题但如果以后改成输出logits和feature_map就建议用输出名列表精确指定防止拿错张量。softmax 手动实现时先减去最大值这一步是为了防止 logits 很大时指数溢出和 PyTorch 内部Softmax算子的处理逻辑一致。读class_names时一定要记住第一行对应的是类别 0。如果你习惯把类别列表写成从 1 开始数模型输出的idx0会对应到第二个类别整个系统悄无声息地全错位。我建议把上面这个函数存成infer.py单独跑几张测试图打印出(类别名, 置信度)确认和训练时的分布一致再继续接入摄像头或服务接口。实际做摄像头实时识别时有一个性能大坑不要在每帧图像处理函数里创建InferenceSession。模型加载一次可能几百毫秒推理一帧只要几十毫秒每一帧都重新加载会直接把实时性拖垮。正确做法是在程序启动时创建好 session后续只调用session.run如果需要多线程并发最常见的方式是每个线程单独创建 session而不是共用一个因为InferenceSession不是线程安全的。5. ONNX 导入与落地的常见问题排查五次翻车记录和排查路径5.1 导入失败session 创建时报 opaque 或 cannot load现象ort.InferenceSession(rubbish_classifier.onnx)直接抛异常报错信息里有 “Failed to load model” 或 “opaque” 关键字有时还会提示某个算子 “Unsupported operator”。原因通常有两种一是导出时opset_version设得太高部署端 onnxruntime 版本老不认新算子二是模型图里包含某个 onnxruntime 没实现的算子。解决先看报错里有没有给出具体算子名再回到export_onnx.py把opset_version下调到 12 重新导出如果仍然失败把那个算子替换成等价组合。比如部分自定义激活函数在 ONNX 里没有专门算子可以用Clip或Sigmoid组合替代。还有一个笨办法是升级 onnxruntime 到最新稳定版但升级前要做好回归测试因为新版也可能改变算子行为。5.2 推理结果全是随机猜归一化管道不一致现象训练时怎么测都有 90% 以上准确率同一个 checkpoint 导出 ONNX 后在测试集上准确率掉到 10% 左右基本跟随机一样。原因第一嫌疑就是部署端预处理和训练端不一致。常见错误包括忘了减均值除以方差、用 OpenCV 读图没转 RGB、直接 Resize 到 224 而没有先放大到 256 再中心裁剪。解决把训练脚本里的transforms每一行注释出来逐项对着部署端代码检查。更可靠的验证方法是拿同一张图先走一遍训练框架的预处理再走一遍部署端preprocess打印中间张量的均值和形状确认差异在1e-6量级内。我见过不少项目在这一点上踩坑最后发现只是cv2.cvtColor漏了。5.3 模型文件 200MB 以上压缩包里的权重到底该不该带现象解压 zip 后发现.onnx文件有 200MB 甚至更大部署到边缘设备时内存紧张每次启动加载都要等好几秒。原因十有八九是网络太宽。ResNet50/101 这类大模型权重占大头而部署推理只需要前向计算图的权重不需要优化器状态。解决第一优先换轻量骨干比如 MobileNetV3 或 EfficientNet-LiteONNX 文件能缩到 10~30MB精度损失往往在 1 个点以内如果非要保留 ResNet导出时确保do_constant_foldingTrue并去掉导出脚本里不必要的中间张量输出。还要注意压缩包理应只带部署必需文件训练 checkpoint 和优化器状态别一股脑塞进部署目录否则连同用户共享文件一起拖慢下载。5.4 导出后 batch 固定成 1摄像头实时推理被拖垮现象单张图片离线推理很快一旦接实时视频流每帧都调一次session.run帧率只有个位数设备发热严重。原因除了硬件算力不足更多是导出时没把 batch 维设为动态导致模型固定接受形状[1,3,224,224]你就算在客户端攒了 4 帧也只能一帧一帧喂。解决导出时设置dynamic_axes{input: {0: batch_size}, logits: {0: batch_size}}推理时可以一次投喂[4,3,224,224]的 batch节省重复调用的开销。不过动态 batch 也不是银弹它会让计算图多出动态 shape 分支逻辑单帧延迟可能略微上升如果并发量很大更值得投入的方向是服务端批量推理队列而不是一味压榨单个 session。5.5 精度下降 0.3——是 ONNX 玄学还是算子精度差异现象原始 PyTorch 模型 Top-1 准确率 92.0%ONNX 模型测出来 91.7%整体看起来没毛病但心里总觉得是哪里出了问题。原因ONNX 导出时会对 BatchNorm 层做折叠把卷积和归一化合并成一个算子浮点计算顺序变化后结果会有一点点不同。卷积算子在不同实现下也可能使用不同的累加策略导致 logits 在高位一致、低位有1e-3级别的差异。解决拿 10 张图片对比两边的 logits差异量级在1e-3就是正常现象不是代码 bug如果准确率差距超过 1%优先检查预处理和输出后处理这两个环节造成的差异远比算子精度大。要追求可复现可以把导出时opset_version、onnxruntime 版本都固定下来写进 requirements.txt避免环境升级后结果漂移。6. 验证与进阶用测试集算指标再试 int8 量化加速6.1 用测试集算准确率与类别召回单张图片跑出置信度不算完事ONNX 导入后的系统要上线必须用部署同款的.onnx模型在验证集上重新算一遍指标。我是这样做的import onnxruntime as ort import numpy as np session ort.InferenceSession(rubbish_classifier.onnx) all_preds, all_labels [], [] for images, labels in val_loader: batch np.array([preprocess_from_pil(img) for img in images], dtypenp.float32) logits session.run(None, {input: batch})[0] preds np.argmax(logits, axis1) all_preds.extend(preds) all_labels.extend(labels.numpy()) correct sum(p l for p, l in zip(all_preds, all_labels)) print(fOverall Acc: {correct / len(all_preds):.3f}) # 按类别打印召回 from collections import Counter for cls_id in range(num_classes): mask [l cls_id for l in all_labels] if sum(mask) 0: cls_correct sum(p cls_id for p, l in zip(all_preds, all_labels) if l cls_id) print(fclass {cls_id} recall: {cls_correct / sum(mask):.3f})关注点应该放在“其他垃圾”和“有害垃圾”这两类的召回率上。如果“其他垃圾”被大量分到“可回收物”说明模型决策边界倾向把纹理不明显的物品归到样本多的类这时候去调后处理比盲目增加训练轮数更有效比如在输出 softmax 概率上加一个置信度阈值低于阈值返回“不确定”让系统在真实场景里宁可拒绝也不乱扔。6.2 int8 量化模型瘦身一半还有多少精度如果你的部署环境是 CPU 且对吞吐有要求ONNX 导入后还可以再做一步 int8 动态量化。这是最省交付成本的加速方式from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( rubbish_classifier.onnx, rubbish_classifier_int8.onnx, weight_typeQuantType.QUInt8, op_types_to_quantize[Conv, MatMul] )量化后模型体积通常能减到原来的三分之一左右推理延迟在 CPU 上也有明显下降。但我不建议拍脑袋直接上生产。量化对权重分布敏感有些模型量化后准确率只掉 0.5%有些会掉 5% 甚至更多。正确动作是量化完立刻跑一遍 6.1 的验证脚本比较 int8 和 fp32 的准确率差距。如果掉点有点大可以把op_types_to_quantize改成[Conv]让 MatMul 保持浮点精度损失更小只是收益打折。我自己在这个环节有过一次深刻教训项目里把 ONNX 量化后单张推理确实快了一上多线程并发反而变慢原因是动态量化在运行时需要把 int8 权重反量化回浮点线程多到一定程度后内存带宽反而成了瓶颈。所以量化前一定要压测至少跑 100 张图的平均耗时同时观察 CPU 占用率别被单次加速的数据迷惑。6.3 一个经验ONNX 不是终态是接口做了一两个模型上线的项目后我越来越觉得 ONNX 的意义不在于给你一个“最优部署格式”而是让你把训练和部署彻底分开。训练团队可以继续换模型、调参部署团队只需要维护一套读取.onnx的推理代码。就算下次换成 MobileNetV3 或者别的网络只要导出的输入输出协议不变业务代码一行都不用改。这种解耦带来的长期收益比单独把某个模型的数据再压几个百分点更值钱。希望我的这些踩坑和验证习惯能帮你少走一点弯路也希望你的垃圾分类系统一导入就跑通别让预处理这种小问题卡住整个项目。希望帮到你。本文还有配套的精品资源点击获取
返回列表