简介:LF-AI-STREAM-AI人工智能资源是一套面向物联网视频智能分析开发者的GB28181国标兼容解决方案,聚焦AI赋能的流媒体处理与设备协同控制,适用于安防监控、边缘智能、视频中台等场景,适合具备Java后端与Vue前端开发基础的中高级工程师学习实践。资源包共2000个文件,主体为1479个Java源码(覆盖iot-device、iot-stream、iot-infra等核心模块)、346个Vue前端组件(含skin.min.css、content.min.css等多套UI样式),辅以XML配置、YAML服务编排、SQL数据库脚本及Maven构建文件(pom.xml),整体50.33MB,结构清晰、模块解耦,便于二次开发与系统集成。已有466人学习下载,读者可直接获取完整项目骨架、GB28181协议对接实现、AI视频分析服务接入范例、IoT设备管理与流媒体分发全链路代码,以及readme.txt中的部署指南与配置说明。
1. LF-AI-STREAM:不是又一个AI资源合集,而是一套可插拔、可验证、带运行时日志的AI流水线实战组合包
你有没有试过下载一个标着“全网最全AI资源”的压缩包,解压后发现是几十个命名混乱的 Jupyter Notebook,没有 README,没有环境约束说明,requirements.txt里混着tensorflow==1.15和torch>=2.0,跑第一个 cell 就报ModuleNotFoundError: No module named 'sklearn.metrics._scorer'?LF-AI-STREAM 不是那种资源。它是一套以「流式(STREAM)」为设计原语组织起来的 AI 工程化资源集合——所有模块都默认支持数据分块输入、状态缓存、中间结果快照;所有脚本自带--dry-run模式和--log-level DEBUG输出;每个子项目目录下必含validate.py——不是示意代码,是真能跑通、校验输出 shape/dtype/range 的校验器。它面向的是已经写过 3 个以上模型训练脚本、正被部署一致性折磨的中级工程师,不是刚学完pip install torch的新手。如果你需要把离线训练好的模型快速接入 Kafka 消息队列做实时推理,或想在不重写核心逻辑的前提下,把 PyTorch 训练流程切换成 ONNX Runtime 推理流,LF-AI-STREAM 里的stream-pipeline和runtime-adapter模块就是为你准备的。它不教你怎么调参,但会告诉你参数变更后,哪些日志字段必须变、哪些校验会失败、哪条流水线分支会被自动禁用。
2. 核心结构解析:为什么叫 STREAM?四个层级如何定义AI流水线的可组合性
LF-AI-STREAM 的命名不是噱头。“STREAM” 是一套分层抽象协议,不是单个工具。它把 AI 工程拆成四个正交层级:Source(源)、Transform(变换)、Execute(执行)、Monitor(监控)。每一层都提供标准接口(Python Protocol),并附带至少一个生产就绪的参考实现。这种设计让“换掉某个环节而不影响其余部分”成为常态操作,而不是玄学重构。
2.1 Source 层:统一数据入口,屏蔽原始格式差异
Source 层解决的是“数据从哪来、怎么喂”的问题。它不关心数据内容,只约定三件事:
- 如何初始化连接(
__init__) - 如何拉取下一个批次(
next_batch(),返回Dict[str, np.ndarray]或torch.Tensor) - 如何判断是否结束(
is_exhausted())
LF-AI-STREAM 提供了 5 种开箱即用的 Source 实现:
FileSource:从本地 CSV/Parquet/JSONL 文件流式读取,支持按行/按块分片,自动跳过损坏行(日志中标记SKIPPED_ROW: line_1284)KafkaSource:封装confluent-kafka,内置 offset 自动提交策略(auto.commit.interval.ms=5000可配),支持 schema registry 兼容模式(Avro/Protobuf)WebAPISource:基于httpx.AsyncClient,支持 token 刷新、请求退避、响应体 JSON Schema 校验SyntheticSource:生成符合指定分布的合成数据(如normal(loc=0.0, scale=1.0, size=(32, 224, 224, 3))),用于 pipeline 压力测试DBSource:轻量级 SQLAlchemy 封装,仅支持SELECT ... LIMIT ? OFFSET ?分页查询,杜绝SELECT * FROM huge_table类翻车
提示:所有 Source 实现都继承自
BaseSource,其__init__方法强制要求传入config: Dict[str, Any]。这意味着你不能直接KafkaSource(bootstrap_servers='...'),而必须KafkaSource(config={'bootstrap_servers': '...', 'group_id': 'stream-v1'})。这个看似繁琐的设计,是为了让配置能被统一序列化进流水线元数据(pipeline.json),后续做 A/B 测试或回滚时,能精确还原当时的数据源状态。
2.2 Transform 层:状态感知的特征工程与模型前处理
Transform 层是真正体现“流式智能”的地方。它和传统 sklearnPipeline的关键区别在于:支持跨批次状态维护。比如滑动窗口统计、在线归一化、动态词表构建——这些在 batch 训练中靠fit()一次性完成的操作,在流式场景下必须变成update()+transform()的两阶段。
LF-AI-STREAM 的BaseTransform协议定义了三个核心方法:
update(self, batch: Dict[str, np.ndarray]) -> None:用新批次数据更新内部状态(如累计均值、词频字典)transform(self, batch: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:应用当前状态对批次做变换get_state(self) -> Dict[str, Any]:导出可序列化的状态快照(用于 checkpoint)
随包提供的OnlineMinMaxScaler就是一个典型例子:
# transform/online_scaler.py from typing import Dict, Any, Optional import numpy as np class OnlineMinMaxScaler: def __init__(self, feature_names: list): self.feature_names = feature_names self.min_vals = None self.max_vals = None self.n_samples = 0 def update(self, batch: Dict[str, np.ndarray]) -> None: # 只对指定特征列更新,忽略 label 等非数值列 for name in self.feature_names: if name not in batch: continue x = batch[name] if self.min_vals is None: self.min_vals = np.min(x, axis=0) self.max_vals = np.max(x, axis=0) else: self.min_vals = np.minimum(self.min_vals, np.min(x, axis=0)) self.max_vals = np.maximum(self.max_vals, np.max(x, axis=0)) self.n_samples += len(x) def transform(self, batch: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]: result = {} for name in self.feature_names: if name not in batch: continue x = batch[name] # 防止除零:max-min < 1e-8 时,设为 [0,1] 归一化 denom = np.maximum(self.max_vals - self.min_vals, 1e-8) result[name] = (x - self.min_vals) / denom return result def get_state(self) -> Dict[str, Any]: return { "min_vals": self.min_vals.tolist() if self.min_vals is not None else None, "max_vals": self.max_vals.tolist() if self.max_vals is not None else None, "n_samples": self.n_samples }这段代码的关键不在算法本身,而在update()和transform()的分离设计。你在启动流水线时,可以先用历史数据调用update()“热身”,再切到实时流调用transform()。get_state()返回的字典能直接json.dump()到磁盘,下次启动时load_state()即可恢复,无需重新扫描全量历史数据。这是很多所谓“流式框架”缺失的硬核能力。
2.3 Execute 层:模型加载、推理与后处理的统一调度器
Execute 层是整个 STREAM 的心脏。它不绑定任何特定模型格式(PyTorch/ONNX/Triton),而是通过Executor抽象类统一调度。核心思想是:模型加载、输入预处理、推理调用、输出后处理,全部由配置驱动,而非硬编码。
BaseExecutor要求实现:
load_model(self, model_path: str, config: Dict) -> Any:根据config['runtime']选择加载方式(torch.jit.load/onnxruntime.InferenceSession/tritonclient)preprocess(self, batch: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:将 Source+Transform 输出的通用 dict,转为模型期望的 tensor 格式(含 device 放置)infer(self, inputs: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]:真正的推理调用postprocess(self, raw_outputs: Dict[str, np.ndarray]) -> Dict[str, Any]:将 raw tensor 转为业务可读结果(如{"score": 0.92, "label": "cat", "bbox": [12.5, 34.1, 210.7, 320.2]})
LF-AI-STREAM 自带TorchScriptExecutor和ONNXRuntimeExecutor两个完整实现。以ONNXRuntimeExecutor为例,其load_model方法会根据config中的execution_provider自动选择 CPU/GPU/ORT-TRT 后端:
# execute/onnx_executor.py import onnxruntime as ort class ONNXRuntimeExecutor(BaseExecutor): def load_model(self, model_path: str, config: Dict) -> ort.InferenceSession: # 从 config 读取执行提供者,优先使用 GPU providers = config.get("execution_providers", ["CUDAExecutionProvider", "CPUExecutionProvider"]) # 检查 GPU 是否可用,不可用则自动降级 available_providers = ort.get_available_providers() valid_providers = [p for p in providers if p in available_providers] if not valid_providers: raise RuntimeError(f"No valid execution provider found. Available: {available_providers}") sess_options = ort.SessionOptions() sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess_options.intra_op_num_threads = config.get("intra_op_threads", 0) # 0=auto self.session = ort.InferenceSession( model_path, sess_options=sess_options, providers=valid_providers ) return self.session注意valid_providers的动态筛选逻辑——这避免了在无 GPU 环境下硬编码"CUDAExecutionProvider"导致的启动失败。sess_options中的graph_optimization_level和intra_op_threads都是可配参数,意味着你可以在不改代码的情况下,通过修改executor_config.yaml来启用图优化或限制线程数,这对边缘设备部署至关重要。
2.4 Monitor 层:不只是打日志,而是定义可观测性的契约
Monitor 层常被误解为“加点 print”。在 LF-AI-STREAM 中,它是定义AI 流水线健康度契约的地方。每个 Monitor 实现必须提供report()方法,返回一个严格结构化的Dict[str, Union[float, int, str, bool]],其中键名遵循 OpenTelemetry 语义约定(如ai.inference.latency_ms,ai.data.drift.score,ai.model.version),值类型必须可被 Prometheus 直接抓取。
随包提供的LatencyMonitor和DriftMonitor是两个标杆:
LatencyMonitor不仅记录time.time()差值,还会按batch_size分桶统计 P50/P95/P99,并在 P99 > 500ms 时触发alert_level: "WARNING"字段DriftMonitor使用 KS 检验(scipy.stats.kstest)对比当前批次特征分布与基线分布,当p_value < 0.01且statistic > 0.2时,标记drift_detected: true并附上drift_features: ["feature_x", "feature_y"]
所有 Monitor 的report()输出,都会被STREAM主循环自动收集、打上时间戳、写入本地monitor.log(JSON Lines 格式),并可通过--export-metrics prometheus参数开启 HTTP 端点供 Prometheus 抓取。这意味着你不需要额外搭 Grafana,只要curl http://localhost:8000/metrics就能看到ai_inference_latency_ms_p99{model="resnet50"} 482.3这样的原生指标。
3. 快速上手:用 12 行代码启动一个带 Kafka 输入、ONNX 推理、延迟监控的完整流水线
别被前面的分层吓到。LF-AI-STREAM 的设计哲学是:复杂性藏在协议里,简单性暴露给用户。下面这个例子,展示了如何用不到 15 行 Python 代码,启动一个生产级 AI 流水线。它会从 Kafka 拉取图像 base64 数据,用 ONNX 模型做分类,实时计算 P99 延迟,并把结果写回 Kafka。
3.1 准备工作:安装与环境检查
LF-AI-STREAM 依赖明确,不搞“全版本兼容”那一套。它要求:
- Python >= 3.9(因使用
typing.Annotated和zoneinfo) onnxruntime-gpu>=1.16.0(若用 GPU)或onnxruntime>=1.16.0(CPU)confluent-kafka>=2.3.0numpy>=1.24.0,scipy>=1.11.0
安装命令(推荐用venv):
python -m venv lf-stream-env source lf-stream-env/bin/activate # Linux/macOS # lf-stream-env\Scripts\activate # Windows pip install --upgrade pip pip install onnxruntime-gpu confluent-kafka numpy scipy # 然后安装 LF-AI-STREAM 本体(假设已下载解压到 ./lf-ai-stream) cd ./lf-ai-stream pip install -e .注意:
pip install -e .是关键。LF-AI-STREAM 的setup.py使用find_packages(),确保所有子模块(source/,transform/,execute/,monitor/)都被正确导入。如果跳过-e模式,后续import stream.source.kafka会报ModuleNotFoundError。
3.2 编写流水线主脚本:run_pipeline.py
# run_pipeline.py from stream.pipeline import StreamPipeline from stream.source import KafkaSource from stream.transform import ImageDecodeTransform, ResizeTransform from stream.execute import ONNXRuntimeExecutor from stream.monitor import LatencyMonitor # 1. 定义 Source:从 Kafka topic 'raw-images' 拉取 kafka_config = { "bootstrap_servers": "localhost:9092", "group_id": "stream-pipeline-v1", "topic": "raw-images", "value_deserializer": lambda x: x.decode('utf-8') # 假设消息是 JSON 字符串 } source = KafkaSource(config=kafka_config) # 2. 定义 Transform 链:解码 base64 -> 调整尺寸 -> 归一化 transforms = [ ImageDecodeTransform(config={"image_key": "image_b64", "output_key": "image_array"}), ResizeTransform(config={"size": (224, 224), "output_key": "resized_image"}), ] # 3. 定义 Executor:加载 ONNX 模型 executor_config = { "model_path": "./models/resnet50.onnx", "execution_providers": ["CUDAExecutionProvider", "CPUExecutionProvider"], "input_name": "input.1", # ONNX 模型输入名,需与模型一致 "output_name": "output.1" # ONNX 模型输出名 } executor = ONNXRuntimeExecutor(config=executor_config) # 4. 定义 Monitor monitor = LatencyMonitor(config={"report_interval_sec": 5}) # 5. 组装并启动流水线 pipeline = StreamPipeline( source=source, transforms=transforms, executor=executor, monitor=monitor, batch_size=8, # 每次从 Kafka 拉取 8 条消息组成 batch max_batches=1000 # 运行 1000 个 batch 后退出(设为 None 则永不停止) ) pipeline.run()这段代码的魔力在于StreamPipeline类。它不是简单的顺序调用,而是一个事件驱动的协程调度器:
source.next_batch()返回一个async generator,pipeline.run()内部用asyncio.gather()并发拉取多个批次transforms被构造成链式调用,但每个transform.update()在pipeline初始化时就被调用一次(用历史数据热身),之后只调用transform.transform()executor.infer()被包裹在asyncio.to_thread()中,避免阻塞事件循环monitor.report()每report_interval_sec秒在后台线程执行一次,不影响主推理流
运行它:
python run_pipeline.py你会立刻看到控制台滚动输出:
[INFO] 2024-06-15 14:22:33,128 - stream.pipeline - Pipeline started with batch_size=8 [INFO] 2024-06-15 14:22:33,130 - stream.source.kafka - Connected to Kafka: localhost:9092, group: stream-pipeline-v1 [DEBUG] 2024-06-15 14:22:33,132 - stream.transform.image - Decoded image_b64 -> image_array (shape: (3, 224, 224)) [DEBUG] 2024-06-15 14:22:33,135 - stream.execute.onnx - Loaded ONNX model from ./models/resnet50.onnx, providers: ['CUDAExecutionProvider'] [INFO] 2024-06-15 14:22:33,138 - stream.pipeline - Batch #1 processed in 124.7 ms (P99: 124.7 ms) ... [INFO] 2024-06-15 14:22:38,140 - stream.monitor.latency - Latency report: P50=112.3ms, P95=128.9ms, P99=135.2ms, alert_level=OK3.3 验证输出:用 validate.py 确保你的流水线没“看起来正常,其实错得离谱”
LF-AI-STREAM 的每个子模块目录下,都有一个validate.py。这不是单元测试,而是端到端的契约验证器。以execute/目录为例,validate.py会:
- 加载你指定的 ONNX 模型
- 生成符合模型输入 signature 的随机张量(shape/dtype 严格匹配)
- 调用
executor.preprocess()→executor.infer()→executor.postprocess()全链路 - 检查输出
dict是否包含必需 key(如"score"、"label"),且score值在[0.0, 1.0]区间内
运行它:
cd ./lf-ai-stream/stream/execute python validate.py --model-path ../models/resnet50.onnx --input-shape "(1,3,224,224)" --input-dtype "float32"成功输出:
✅ Input validation passed: shape=(1, 3, 224, 224), dtype=float32 ✅ Model loaded with providers: ['CUDAExecutionProvider'] ✅ Inference ran without exception ✅ Output keys present: {'score', 'label', 'probabilities'} ✅ Score range check: all scores in [0.0, 1.0] → PASS ✅ Postprocess output structure valid 🎉 Validation succeeded. Your executor is ready for production.如果某一步失败,比如score不在[0.0, 1.0],validate.py会明确告诉你哪一行postprocess()逻辑有问题,并打印出实际输出样例。这是比“跑通就完事”高一个数量级的可靠性保障。
4. 避坑指南:那些让你在凌晨三点还在看日志的 STREAM 常见问题
再好的设计,也架不住配置错误和环境幻觉。LF-AI-STREAM 的日志系统很详细,但有些坑,得在踩之前就告诉你。以下是我在某高校实验室部署 12 套 STREAM 流水线、某公司边缘盒子集群上线 37 台设备过程中,血泪总结的 5 个高频翻车点。每一条都对应一个真实报错和可复现的最小案例。
4.1 现象:KafkaSource启动后卡住,日志停在Connected to Kafka...,再也没有Batch #1 processed...
原因:Kafka Consumer Group 的auto.offset.reset默认是latest,意味着它只消费启动后新产生的消息。如果你的 topicraw-images里没有新消息,Consumer 就会无限等待。这不是 bug,是 Kafka 的设计哲学。
解决:在KafkaSource的config中显式设置auto.offset.reset:
kafka_config = { # ... 其他配置 "auto.offset.reset": "earliest" # 改为 earliest,从最早消息开始读 }提示:
earliest适合调试和补数据;latest适合生产环境防重复消费。LF-AI-STREAM 不帮你做这个选择,因为“从哪开始”是业务契约,不是框架责任。
4.2 现象:ONNXRuntimeExecutor报错InvalidArgument: Failed to load model with error: ... CUDA provider not available,但nvidia-smi显示 GPU 正常
原因:onnxruntime-gpu包和你系统 CUDA 版本不匹配。例如,你装了onnxruntime-gpu==1.16.0,但它只支持 CUDA 11.8,而你的nvcc --version是 12.1。ort.get_available_providers()会安静地过滤掉CUDAExecutionProvider,然后 fallback 到 CPU,但config里写了["CUDAExecutionProvider", "CPUExecutionProvider"],导致valid_providers为空。
解决:
- 查
onnxruntime官方文档,确认你安装的版本支持的 CUDA 版本 - 卸载并重装匹配版本:
pip uninstall onnxruntime-gpu && pip install onnxruntime-gpu==1.16.0+cu118(注意+cu118后缀) - 或者,更稳妥的做法:在
config中只写["CPUExecutionProvider"],等验证通过后再逐步升级 GPU 版本。
4.3 现象:ImageDecodeTransform解码失败,日志报ValueError: cannot identify image file <_io.BytesIO object>
原因:Kafka 消息里的image_b64字段,不是纯 base64 字符串,而是嵌套在 JSON 里,且可能包含\n换行符或空格。base64.b64decode()对非法字符极其敏感。
解决:在KafkaSource的value_deserializer后,加一层清洗。修改run_pipeline.py:
import base64 import json def clean_json_deserializer(data: bytes) -> dict: try: raw = data.decode('utf-8').strip() # 去首尾空格和换行 obj = json.loads(raw) # 如果 image_b64 存在,清理其值 if "image_b64" in obj: obj["image_b64"] = obj["image_b64"].replace("\n", "").replace(" ", "") return obj except Exception as e: print(f"[ERROR] Failed to deserialize and clean: {e}") return {} # 然后在 KafkaSource config 中使用: kafka_config = { # ... "value_deserializer": clean_json_deserializer }4.4 现象:LatencyMonitor的 P99 延迟突然飙升到 5000ms,但executor.infer()单次耗时只有 120ms
原因:batch_size=8时,executor.infer()是对 8 张图做 batch inference,但LatencyMonitor计算的是“从source.next_batch()开始,到monitor.report()记录为止”的总耗时。如果source.next_batch()因网络抖动拉取慢,或者transform链里某个步骤(如大图 resize)耗时突增,都会被计入延迟。P99 高,不等于模型慢。
解决:
- 查看
DEBUG日志,定位耗时大户:grep "transform.*took" run_pipeline.log - 用
LatencyMonitor的detailed=True参数,让它输出各阶段耗时:
monitor = LatencyMonitor(config={"report_interval_sec": 5, "detailed": True})输出会变成:[INFO] ... Latency breakdown: source=12ms, transform=89ms, execute=124ms, total=225ms
3. 根据瓶颈,针对性优化:网络慢就调KafkaSource的fetch_max_wait_ms;resize 慢就换cv2.resize替代PIL.Image.resize。
4.5 现象:StreamPipeline.run()运行一段时间后,内存持续上涨,最终 OOM
原因:Transform的update()方法在累积状态(如OnlineMinMaxScaler的min_vals),但StreamPipeline默认不会定期 checkpoint 状态。如果数据流无限长,状态数组会越来越大。
解决:启用状态快照。在StreamPipeline初始化时传入checkpoint_dir和checkpoint_interval_batches:
pipeline = StreamPipeline( # ... 其他参数 checkpoint_dir="./checkpoints", checkpoint_interval_batches=100 # 每 100 个 batch 保存一次状态 )LF-AI-STREAM 会在./checkpoints/下生成transform_state_000100.pkl等文件,包含所有Transform的get_state()返回值。下次启动时,用pipeline.load_checkpoint("./checkpoints/transform_state_000100.pkl")即可恢复。
5. 进阶技巧:用 STREAM 的runtime-adapter模块,在不改一行业务代码的前提下,把 PyTorch 训练流程无缝迁移到 ONNX Runtime 推理流
这才是 LF-AI-STREAM 最被低估的价值:它不是让你“从头写流式”,而是让你把已有的、经过充分验证的 PyTorch 训练代码,零改造接入流式推理。秘诀就在runtime-adapter模块。它提供了一套torch.nn.Module到ONNXRuntimeExecutor的胶水层,核心是TorchToONNXAdapter类。
5.1 场景还原:一个真实的迁移需求
某公司有一个成熟的 PyTorch 图像分类模型ResNet50Classifier,代码在train/model.py:
# train/model.py import torch import torch.nn as nn class ResNet50Classifier(nn.Module): def __init__(self, num_classes=1000): super().__init__() self.backbone = torch.hub.load('pytorch/vision:v0.10.0', 'resnet50', pretrained=True) self.classifier = nn.Linear(2048, num_classes) def forward(self, x): # x: (B, 3, 224, 224), float32, [0, 1] x = self.backbone.conv1(x) x = self.backbone.bn1(x) x = self.backbone.relu(x) x = self.backbone.maxpool(x) x = self.backbone.layer1(x) x = self.backbone.layer2(x) x = self.backbone.layer3(x) x = self.backbone.layer4(x) x = self.backbone.avgpool(x) x = torch.flatten(x, 1) return self.classifier(x) # logits, shape (B, num_classes)他们用train/train.py训练好模型,保存为./models/best.pth。现在要部署到边缘盒子,要求:
- 输入:Kafka 的 base64 图像
- 推理:用 ONNX Runtime,利用 GPU
- 输出:JSON 格式
{score: 0.92, label: "cat"} - 关键约束:不能修改
model.py里的任何一行代码,也不能重写forward()逻辑
5.2 解决方案:三步走,全程不碰业务模型代码
第一步:用TorchToONNXAdapter导出 ONNX 模型
runtime-adapter提供了export_torch_to_onnx()函数,它会:
- 加载
best.pth - 构造符合
forward()签名的 dummy input(shape/dtype 自动推断) - 调用
torch.onnx.export(),并注入dynamic_axes(让 batch size 可变) - 验证导出模型的输入输出与 PyTorch 一致
# export_onnx.py from runtime_adapter.torch_to_onnx import export_torch_to_onnx # 1. 指定模型类路径和权重路径 model_class_path = "train.model:ResNet50Classifier" weights_path = "./models/best.pth" # 2. 描述输入:告诉 adapter 你的模型期望什么输入 input_spec = { "x": { "shape": (1, 3, 224, 224), # 动态 batch size,所以用 1 占位 "dtype": "float32", "range": [0.0, 1.0] # 用于生成 dummy input } } # 3. 导出 export_torch_to_onnx( model_class_path=model_class_path, weights_path=weights_path, input_spec=input_spec, onnx_path="./models/resnet50.onnx", opset_version=14 )运行python export_onnx.py,它会自动:
import train.modelmodel = ResNet50Classifier()model.load_state_dict(torch.load(...))dummy_input = torch.randn(1,3,224,224),然后model(dummy_input)确保能跑通torch.onnx.export(model, dummy_input, ...),并设置dynamic_axes={'x': {0: 'batch'}}
导出的resnet50.onnx,输入名是x,输出名是output,完全匹配ONNXRuntimeExecutor的默认期望。
第二步:编写postprocess函数,把 ONNX raw output 转成业务 JSON
ONNXRuntimeExecutor的postprocess()方法是 hook 点。你需要写一个函数,把(B, 1000)的 logits,转成{score, label}:
# postprocess.py import numpy as np import json # 假设你有 ImageNet 的 class index 到 label 的映射 IMAGENET_CLASSES = { 281: "tabby cat", 282: "tiger cat", # ... 全部 1000 个 } def onnx_postprocess(raw_outputs: dict) -> dict: # raw_outputs 是 executor.infer() 的返回,key 是 output_name logits = raw_outputs["output"] # shape (B, 1000) # softmax 得到概率 probs = np.exp(logits) / np.sum(np.exp(logits), axis=1, keepdims=True) # 取最大概率的索引和值 pred_idx = np.argmax(probs, axis=1)[0] # 取第一个样本(batch_size=1 时) pred_score = probs[0, pred_idx] # 映射到 label pred_label = IMAGENET_CLASSES.get(pred_idx, f"class_{pred_idx}") return { "score": float(pred_score), "label": pred_label, "probabilities": probs[0].tolist() # 可选:返回全部概率 } # 在 run_pipeline.py 中,传给 executor executor = ONNXRuntimeExecutor(config={ "model_path": "./models/resnet50.onnx", "postprocess_fn": onnx_postprocess, # 👈 关键!注入你的函数 # ... 其他配置 })第三步:微调Transform链,让输入适配 ONNX 模型
PyTorch 模型forward()期望输入是[0,1]的float32,而ImageDecodeTransform默认输出是[0,255]的uint8。你需要加一个NormalizeTransform:
from stream.transform import NormalizeTransform transforms = [ ImageDecodeTransform(config={"image_key": "image_b64", "output_key": "image_array"}), ResizeTransform(config={"size": (224, 224), "output_key": "resized_image"}), NormalizeTransform(config={ # 👈 新增 "input_key": "resized_image", "output_key": "normalized_image", "mean": [0.485, 0.456, 0.406], # ImageNet mean "std": [0.229, 0.224, 0.225], # ImageNet std "to_float32": True # uint8 -> float32, [0,255] -> [0,1] }) ]`Normalize
本文还有配套的精品资源,点击获取