注意
跳转到末尾以下载完整的示例代码。
StandardScaler 的差异¶
一个 StandardScaler 执行非常基本的缩放。ONNX 中的转换假设 (x / y)
等同于 x * ( 1 / y)
,但这对于浮点数或双精度浮点数来说并不总是正确的(参见编译器会将除法优化为乘法吗)。即使差异很小,如果下一步是决策树,也可能会引入差异。一个微小的差异可能导致决策树走向不同的路径。让我们看看如何解决这个问题。
失败的示例¶
这不是一个典型的示例,它旨在基于计算机上 (x / y)
通常与 x * ( 1 / y)
不同的假设而失败。
import onnxruntime
import onnx
import os
import math
import numpy as np
import matplotlib.pyplot as plt
from onnx.tools.net_drawer import GetPydotGraph, GetOpNodeProducer
from onnxruntime import InferenceSession
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeRegressor
from skl2onnx.sklapi import CastTransformer
from skl2onnx import to_onnx
奇怪的数据。
X, y = make_regression(10000, 10, random_state=3)
X_train, X_test, y_train, _ = train_test_split(X, y, random_state=3)
Xi_train, yi_train = X_train.copy(), y_train.copy()
Xi_test = X_test.copy()
for i in range(X.shape[1]):
Xi_train[:, i] = (Xi_train[:, i] * math.pi * 2**i).astype(np.int64)
Xi_test[:, i] = (Xi_test[:, i] * math.pi * 2**i).astype(np.int64)
max_depth = 10
Xi_test = Xi_test.astype(np.float32)
一个简单的模型。
model1 = Pipeline(
[("scaler", StandardScaler()), ("dt", DecisionTreeRegressor(max_depth=max_depth))]
)
model1.fit(Xi_train, yi_train)
exp1 = model1.predict(Xi_test)
转换为 ONNX。
onx1 = to_onnx(model1, X_train[:1].astype(np.float32), target_opset=15)
sess1 = InferenceSession(onx1.SerializeToString(), providers=["CPUExecutionProvider"])
以及最大差异。
got1 = sess1.run(None, {"X": Xi_test})[0]
def maxdiff(a1, a2):
d = np.abs(a1.ravel() - a2.ravel())
return d.max()
md1 = maxdiff(exp1, got1)
print(md1)
322.39065126389346
图。
pydot_graph = GetPydotGraph(
onx1.graph,
name=onx1.graph.name,
rankdir="TB",
node_producer=GetOpNodeProducer(
"docstring", color="yellow", fillcolor="yellow", style="filled"
),
)
pydot_graph.write_dot("cast1.dot")
os.system("dot -O -Gdpi=300 -Tpng cast1.dot")
image = plt.imread("cast1.dot.png")
fig, ax = plt.subplots(figsize=(40, 20))
ax.imshow(image)
ax.axis("off")

(np.float64(-0.5), np.float64(2536.5), np.float64(1707.5), np.float64(-0.5))
新的流水线¶
修复转换需要将 (x * (1 / y)
替换为 (x / y)
,并且此除法必须使用双精度浮点数。默认情况下,sklearn-onnx 假设计算都在浮点数下进行。ONNX 1.7 规范不支持双精度缩放(输入和输出支持,但参数不支持)。解决方案需要修改转换(使用选项 ‘div’ 移除 Scaler 节点),并通过插入显式 Cast 来使用双精度浮点数。
model2 = Pipeline(
[
("cast64", CastTransformer(dtype=np.float64)),
("scaler", StandardScaler()),
("cast", CastTransformer()),
("dt", DecisionTreeRegressor(max_depth=max_depth)),
]
)
model2.fit(Xi_train, yi_train)
exp2 = model2.predict(Xi_test)
onx2 = to_onnx(
model2,
X_train[:1].astype(np.float32),
options={StandardScaler: {"div": "div_cast"}},
target_opset=15,
)
sess2 = InferenceSession(onx2.SerializeToString(), providers=["CPUExecutionProvider"])
got2 = sess2.run(None, {"X": Xi_test})[0]
md2 = maxdiff(exp2, got2)
print(md2)
2.9884569016758178e-05
图。
pydot_graph = GetPydotGraph(
onx2.graph,
name=onx2.graph.name,
rankdir="TB",
node_producer=GetOpNodeProducer(
"docstring", color="yellow", fillcolor="yellow", style="filled"
),
)
pydot_graph.write_dot("cast2.dot")
os.system("dot -O -Gdpi=300 -Tpng cast2.dot")
image = plt.imread("cast2.dot.png")
fig, ax = plt.subplots(figsize=(40, 20))
ax.imshow(image)
ax.axis("off")

(np.float64(-0.5), np.float64(2536.5), np.float64(4171.5), np.float64(-0.5))
此示例使用的版本
import sklearn
print("numpy:", np.__version__)
print("scikit-learn:", sklearn.__version__)
import skl2onnx
print("onnx: ", onnx.__version__)
print("onnxruntime: ", onnxruntime.__version__)
print("skl2onnx: ", skl2onnx.__version__)
numpy: 2.2.0
scikit-learn: 1.6.0
onnx: 1.18.0
onnxruntime: 1.21.0+cu126
skl2onnx: 1.18.0
脚本总运行时间: (0 minutes 2.268 seconds)