scikit-learn 模型转 ONNX 时,应把训练时的预处理一起转出,再用完全相同的原始输入比较两个实现的标签和概率。只生成 .onnx 文件,不能证明转换正确,也不能说明推理一定更快。
下面训练一个 StandardScaler + LogisticRegression 管道,把 Iris 数据分类模型转换成 ONNX,并在 CPU 上比较 45 条独立测试样本。Iris 是教学数据,这个示例用于核对转换过程,不是生产模型评测。

先确定哪些部分需要一起转换
如果原始训练流程是“输入四列数值 → 标准化 → 逻辑回归”,导出的模型也应包含标准化。只导出最后的分类器、却继续喂未缩放的原始数据,会改变预测含义。反过来,导出了整个管道以后再在外面缩放一次,也会造成重复预处理。
sklearn-onnx 官方管道教程说明 to_onnx 可以根据样本推断输入类型,并强调部署前比较输出。转换是否受支持还取决于具体估计器、预处理步骤、算子版本和运行时;自定义 Python 函数不能假设可自动转换。
使用已核验的环境
本文在 Windows、Python 3.11.15 下运行,主要版本为 scikit-learn 1.9.1、skl2onnx 1.20.0、ONNX 1.23.1、ONNX Runtime 1.30.0、NumPy 2.4.6。建立并激活独立 Python 环境后安装:
python -m venv .venv
# Windows PowerShell
.\.venv\Scripts\Activate.ps1
python -m pip install "scikit-learn==1.9.1" "skl2onnx==1.20.0" "onnx==1.23.1" "onnxruntime==1.30.0" "numpy==2.4.6"
本例把输入统一为 float32,target_opset 显式为 17。opset 不是随意填得越大越好;换目标运行时之前,要重新核对其算子支持和模型兼容性。
保存为 convert_iris.py,训练、导出并比较
代码保留三个输出:分类标签、概率矩阵与类别标签。设置 zipmap=False 是为了得到容易与 NumPy 比较的概率矩阵;output_class_labels=True 让类别顺序也可核对。这两项依据 官方分类器输出选项教程。
from pathlib import Path
import json
import numpy as np
import onnx
import onnxruntime as ort
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from skl2onnx import to_onnx
X, y = load_iris(return_X_y=True)
# 统一进入两个实现的原始输入类型。
X = X.astype(np.float32)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, stratify=y, random_state=42)
classifier = LogisticRegression(max_iter=1000)
model = Pipeline([("scale", StandardScaler()), ("classifier", classifier)])
model.fit(X_train, y_train)
converted = to_onnx(
model, X_train[:1], target_opset=17,
options={id(classifier): {"zipmap": False, "output_class_labels": True}})
onnx.checker.check_model(converted)
onnx.save(converted, "iris_pipeline.onnx")
session = ort.InferenceSession("iris_pipeline.onnx", providers=["CPUExecutionProvider"])
for item in session.get_inputs():
print("input:", item.name, item.type, item.shape)
for item in session.get_outputs():
print("output:", item.name, item.type, item.shape)
if len(session.get_inputs()) != 1:
raise ValueError("本例只处理一个二维数值输入")
labels, probabilities, classes = session.run(
None, {session.get_inputs()[0].name: X_test})
expected_labels = model.predict(X_test)
expected_probabilities = model.predict_proba(X_test)
np.testing.assert_array_equal(classes, model.classes_)
np.testing.assert_array_equal(labels, expected_labels)
np.testing.assert_allclose(probabilities, expected_probabilities, rtol=1e-4, atol=1e-6)
result = {"samples": len(X_test), "label_matches": int(np.sum(labels == expected_labels)),
"classes": classes.tolist(),
"max_absolute_probability_difference": float(
np.max(np.abs(probabilities - expected_probabilities))),
"probability_shape": list(probabilities.shape)}
print(json.dumps(result, indent=2))
Path("conversion_result.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
python convert_iris.py
怎样判断这次转换通过
先看输入输出打印信息。本次实际运行的输入为 X: tensor(float) [None, 4],输出为标签、三列概率和三个类别标签。None 表示第一维未固定;四列特征的次序仍必须与训练时一致。
| 检查 | 本次教学运行输出 | 作用 |
|---|---|---|
| 类别顺序 | [0, 1, 2] 与 model.classes_ 一致 | 防止把概率列解释成错误的类别 |
| 分类标签 | 45/45 一致 | 确认这批测试输入的最终判定一致 |
| 概率形状 | 45×3 | 确认样本数和类别数匹配 |
| 概率最大绝对差 | 约 1.19×10−7 | 量化两个实现的数值差异 |
这些是上述教学代码的一次实际输出,不代表任意模型的转换精度。断言使用 rtol=10−4、atol=10−6,只是本例选择的数值容差;真实任务应按预测边界和业务误差要求确定,不能只扩大容差使检查通过。
程序无断言报错,并生成 iris_pipeline.onnx 和 conversion_result.json,才算本例转换与比较完成。即使概率差很小,靠近分类阈值的样本仍可能改变最终标签;替换真实模型后要加入边界样本、缺失处理、极端值和各类输入的覆盖。
换成自己的模型时保留四份契约
- 输入契约:字段名与顺序、dtype、维度、缺失处理,以及每列的单位;如果模型仅接受数组,单靠数组形状无法发现两列交换。
- 预处理契约:哪些步骤已经在 ONNX 图内,哪些仍在外部服务执行。
- 输出契约:标签含义、概率列对应的 classes_、正类和下游阈值。
- 验证契约:比较数据版本、样本范围、数值容差和任务指标。转换一致性通过后,再独立测试速度与内存。
如果要在图形界面查看是否保留预处理,可以用 Netron 打开 iris_pipeline.onnx。实际部署消费者如何核对输入与运行一次,可参照站内 ONNX Runtime CPU 推理教程。需要量化时先保留这个已验证的原模型,再参照 动态量化与输出比较教程另做精度检查。
常见失败怎样定位
- MissingShapeCalculator 或找不到转换器:核对报错中的具体步骤是否受支持;将管道拆开排查,必要时使用官方支持的自定义转换器流程。不要删除关键预处理后宣称转换成功。
- 输入类型错误:确认是 float32,并使用 session.get_inputs() 给出的输入名;本例不适用于文字或多个不同类型输入。
- 输出无法直接相减:先查看类型。默认 ZipMap 可能返回字典列表,不能把它当二维概率数组;使用匹配的输出选项或按类别展开。
- 标签一致而概率不一致:检查类别顺序、预处理重复、float32/float64 差异,再按代表性样本定位。不能只比较前几条输出。
本文实测仅覆盖这一个数值分类管道与 CPU 执行提供器,未验证自定义估计器、文本管道、GPU 或其他设备。没有提供加速承诺,转换后的文件仍需在实际目标环境验收。文档核验日期:2026-10-01。
Ai菜鸟网。发布者:AI小管家,转载请注明出处:https://www.alyyhw.com/30466.html