导出模型为 ONNX

API打磨师
2025-06-29 02:10
阅读 709

机器学习部署的“血泪”经验分享:从模型训练到服务上线那些事儿

这篇文章其实是源于我过去两年参与的一个图像识别项目。项目初期我们团队信心满满,模型在本地跑得飞起,在测试数据上准确率也漂亮得不像话,以为部署上线就是顺理成章的事儿。结果现实狠狠地给了我们一记响亮的耳光。

部署不是简单把模型 load 上去,然后扔到服务器就完事。它涉及到环境管理、性能调优、服务稳定性、监控报警等等多个环节。特别是在资源有限、高并发场景下,稍有不慎,系统就会崩溃或者响应迟缓,直接影响业务。

今天我就结合这个项目的实战经历,来聊聊我在机器学习部署过程中踩过的坑,以及一些我认为非常值得借鉴的最佳实践。


一、项目背景和挑战

一、项目背景和挑战

我们的目标是为一个电商客户搭建一个商品图像识别系统,用于自动识别用户上传的商品图片并打标签(比如“T恤”、“牛仔裤”等),以提升推荐系统的准确性。

整个项目大致分为两个阶段:

  1. 模型开发阶段:我们基于 PyTorch 搭建了 ResNet-50 的迁移学习模型,使用了自建的商品图像数据集进行训练。
  2. 模型部署阶段:模型需要在 AWS EC2 实例上提供 API 接口,支持每天数十万次的请求访问。

听起来是不是挺标准的?但等到部署阶段,问题接踵而至。


二、遇到的主要问题与挑战

二、遇到的主要问题与挑战

1. 环境不一致导致模型加载失败

我们在本地用的是 Python 3.8 + PyTorch 1.10,但是在云服务器上默认安装的是 Python 3.6 和 PyTorch 1.4。模型文件一加载就报错,提示某些新特性不支持。

🧨 小插曲:当时我们在凌晨上线的时候才发现这个问题,直接懵了两分钟才意识到是版本问题。

2. 模型预测速度太慢,QPS 上不去

一开始我们用 Flask 跑模型服务,单个请求处理时间平均 200ms,压测发现 QPS 勉强能到 15,完全扛不住预估的流量。

3. 显存占用太高,GPU 使用率不稳定

模型每次推理都要重新 load 到 GPU 上,而且 batch size 太小,显存利用率只有 30%,GPU 几乎处于空转状态。

4. 缺乏监控和服务治理机制

服务挂掉后没人知道,重启靠人工;日志杂乱无章;没有限流熔断,一炸全盘崩。


三、解决方案:一步步稳扎稳打

三、解决方案:一步步稳扎稳打

面对这些问题,我们没有慌,而是逐步优化架构、调整技术栈、完善运维流程。以下是我们的整体解决思路。

技术选型升级

模块 初始方案 最终方案 原因
Web框架 Flask FastAPI + Gunicorn + Uvicorn Worker 更好的异步支持,性能更好
模型服务化 单进程加载 TorchServe(后来换成自研) 提供更好的性能、批处理和版本控制
模型运行时 CPU 推理 GPU + ONNX Runtime 利用 GPU 加速推理
服务部署方式 单节点 EC2 Docker 容器 + Kubernetes 集群 支持弹性伸缩、滚动更新
日志监控 自定义 print Prometheus + Grafana + ELK 服务可视化,异常感知更快

架构图简化示意

[Client] → [Nginx/LB] → [Kubernetes Pods] → [ONNX + GPU 推理]
                     ↘ [Prometheus/Grafana/ELK]

四、代码示例:如何做模型转换与高性能推理?

四、代码示例:如何做模型转换与高性能推理?

为了提高推理效率,我们最终将模型导出为 ONNX 格式,并使用 onnxruntime 进行推理。

import torch.onnx

model.eval()
dummy_input = torch.randn(1, 3, 224, 224)  # 输入尺寸根据模型结构修改
torch.onnx.export(model, dummy_input, "resnet50.onnx", export_params=True)
# 使用 onnxruntime 推理
import numpy as np
import onnxruntime as ort

ort_session = ort.InferenceSession("resnet50.onnx")
outputs = ort_session.run(
    None,
    {'input': dummy_input.numpy()}
)

为了进一步提升吞吐量,我们将多个图片合并成 batch 进行批量推理,而不是单张处理。同时我们也利用了 CUDA 进行加速:

pip install onnxruntime-gpu  # 一定要装带 GPU 的版本

五、部署过程中的几个关键细节

1. 批处理 vs 并发模型推理

我们尝试过两种方式:

  • 同步逐条推理:每个请求进来自独立处理,缺点是吞吐低,GPU 无法有效利用。
  • 队列式批处理:收集一定数量的请求组成 batch,一次性推理。这种方式可以显著提高 GPU 利用率,但也引入了延时。

我们最后选择了一个折中方案——设置最大等待时间 + 最小批大小,达到其中之一就触发推理。

from collections import deque
import threading

class BatchPredictor:
    def __init__(self, model_path, max_batch_size=16, timeout=0.1):
        self.model = load_model(model_path)
        self.queue = deque()
        self.lock = threading.Lock()
        self.batching_thread = threading.Thread(target=self._process_loop)
        self.batching_thread.daemon = True
        self.batching_thread.start()
        
    def _process_loop(self):
        while True:
            with self.lock:
                if len(self.queue) < 1:
                    time.sleep(0.01)
                    continue
                batch = list(self.queue)[:self.max_batch_size]
                self.queue = deque(list(self.queue)[self.max_batch_size:])
            # perform batch inference
            predictions = self.model.predict(batch)

2. 模型热加载与服务降级机制

我们实现了一个简易的模型版本管理系统,允许通过 HTTP 触发模型 reload,这样可以在不停机的情况下完成模型更新。

同时在服务入口加了个简单的限流和熔断机制,防止雪崩效应。

from fastapi import FastAPI, HTTPException
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware

class RateLimitingMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, limit=100):
        super().__init__(app)
        self.limit = limit
        self.requests = {}

    async def dispatch(self, request, call_next):
        client_ip = request.client.host
        now = time.time()
        self.requests.setdefault(client_ip, []).append(now)
        self.requests[client_ip] = [t for t in self.requests[client_ip] if now - t < 60]
        if len(self.requests[client_ip]) > self.limit:
            raise HTTPException(status_code=429, detail="Too many requests")
        return await call_next(request)

六、部署之后的效果

最终我们达成了如下效果:

计算机视觉应用-2

指标 优化前 优化后
QPS 15 240+
平均延迟 ~200ms ~60ms
GPU 利用率 30% → 80%
故障恢复时间 依赖手动重启 自动重启 + 健康检查
支持多模型 ✅(支持灰度发布)

我们还实现了自动扩缩容,Kubernetes 根据 CPU/GPU 使用率自动调整实例数,成本降低了不少。


七、踩坑总结 & 经验建议

1. 环境一致性才是王道

不要低估环境差异带来的影响。哪怕是一点 minor 版本的变化,都可能导致灾难。我推荐大家使用容器镜像打包整个环境,包括 Python、CUDA、CUDNN 和模型文件。例如用 Dockerfile:

FROM nvidia/cuda:11.7.1-base
RUN apt-get update && apt-get install -y python3-pip
COPY resnet50.onnx /models/
COPY requirements.txt .
RUN pip install -r requirements.txt
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "80"]

2. 别一开始就追求复杂的技术栈

TorchServe、TensorRT、Seldon、TF Serving 听起来都很高级,但在早期验证阶段完全没必要。先跑通再说,再根据实际情况迭代升级。

3. 写好文档,写好日志

别想着以后再补文档,否则你一定会后悔。部署服务时,务必记录每一步做了什么配置,用了哪些命令。日志也一样,别只输出 info 级别,debug、warn、error 要分清楚。

4. 测试!测试!测试!

线上出问题往往是因为没覆盖某个 edge case。一定要写好单元测试、集成测试,还要模拟各种异常情况(网络中断、磁盘满、GPU 异常等)。

5. 不要忽略业务端对接体验

接口设计要简洁清晰,返回格式统一,错误码明确,文档齐全。我们曾因为接口设计不合理导致客户端不断报错,浪费了很多时间排查。


八、最后想说的话

AI模型训练过程-1

机器学习部署这件事儿,说难也难,说容易其实也有迹可循。关键是不要怕试错,也不要迷信大厂那一套架构,适合自己的才是最好的。

我个人最大的感悟是:一个 ML 工程师如果不了解部署和运维,就永远只是个“玩具开发者”。 只有当你把自己的模型真正放到生产环境,支撑起实际业务,才算是真正完成了闭环。

如果你正在准备或已经进入 ML 部署的阶段,我希望你看到这篇文章能少走些弯路,别像我当初那样被现实打得措手不及。

当然,也欢迎你在评论区留言交流你的经验和困惑。愿我们都能做出“跑得快、稳得住”的 AI 应用 💪


作者简介
一名曾在某电商平台负责 CV/NLP 相关算法工程化的全栈工程师,现专注于机器学习模型服务化与边缘部署方向。喜欢把理论落地,也热爱开源社区。欢迎关注我的 GitHub 或博客了解更多内容。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝