注意
转到末尾 下载完整的示例代码。
GaussianProcessRegressor 的差异:使用双精度¶
对 GaussianProcessRegressor 的许多矩阵运算可能需要双精度。sklearn-onnx 默认使用单精度浮点数,但对于此特定模型,最好使用双精度。让我们看看如何使用双精度创建 ONNX 文件。
训练模型¶
一个在波士顿数据集上使用 GaussianProcessRegressor 的非常基础的示例。
import pprint
import numpy
import sklearn
from sklearn.datasets import load_diabetes
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import DotProduct, RBF
from sklearn.model_selection import train_test_split
import onnx
import onnxruntime as rt
import skl2onnx
from skl2onnx.common.data_types import FloatTensorType, DoubleTensorType
from skl2onnx import convert_sklearn
dataset = load_diabetes()
X, y = dataset.data, dataset.target
X_train, X_test, y_train, y_test = train_test_split(X, y)
gpr = GaussianProcessRegressor(DotProduct() + RBF(), alpha=1.0)
gpr.fit(X_train, y_train)
print(gpr)
/home/xadupre/vv/this312/lib/python3.12/site-packages/sklearn/gaussian_process/kernels.py:452: ConvergenceWarning: The optimal value found for dimension 0 of parameter k1__sigma_0 is close to the specified upper bound 100000.0. Increasing the bound and calling fit again may find a better value.
warnings.warn(
/home/xadupre/vv/this312/lib/python3.12/site-packages/sklearn/gaussian_process/kernels.py:442: ConvergenceWarning: The optimal value found for dimension 0 of parameter k2__length_scale is close to the specified lower bound 1e-05. Decreasing the bound and calling fit again may find a better value.
warnings.warn(
GaussianProcessRegressor(alpha=1.0,
kernel=DotProduct(sigma_0=1) + RBF(length_scale=1))
首次尝试将模型转换为 ONNX¶
文档建议以下将模型转换为 ONNX 的方法。
initial_type = [("X", FloatTensorType([None, X_train.shape[1]]))]
onx = convert_sklearn(gpr, initial_types=initial_type, target_opset=12)
sess = rt.InferenceSession(onx.SerializeToString(), providers=["CPUExecutionProvider"])
try:
pred_onx = sess.run(None, {"X": X_test.astype(numpy.float32)})[0]
except RuntimeError as e:
print(str(e))
第二次尝试:可变维度¶
不幸的是,尽管转换顺利,但运行时无法计算预测。之前的代码片段对输入施加了固定维度,因此运行时会假设每个节点的输出具有固定维度。而此模型并非如此。我们需要通过将固定维度替换为空值来禁用这些检查。(见下一行)。
initial_type = [("X", FloatTensorType([None, None]))]
onx = convert_sklearn(gpr, initial_types=initial_type, target_opset=12)
sess = rt.InferenceSession(onx.SerializeToString(), providers=["CPUExecutionProvider"])
pred_onx = sess.run(None, {"X": X_test.astype(numpy.float32)})[0]
pred_skl = gpr.predict(X_test)
print(pred_skl[:10])
print(pred_onx[0, :10])
[100.315979 158.71972656 134.34814453 149.08435059 113.38873291
124.66339111 163.16369629 143.5949707 145.91064453 190.05358887]
[-53248.]
差异似乎相当大。让我们通过查看最大差异来确认这一点。
diff = numpy.sort(numpy.abs(numpy.squeeze(pred_skl) - numpy.squeeze(pred_onx)))[-5:]
print(diff)
print("min(Y)-max(Y):", min(y_test), max(y_test))
[53450.00366211 53452.51953125 53452.95153809 53455.44708252
53460.27514648]
min(Y)-max(Y): 39.0 332.0
第三次尝试:使用双精度¶
该模型使用了几次矩阵计算,而矩阵的系数数量级差异很大。如果转换后的模型坚持使用单精度浮点数,则很难近似 scikit-learn 所做的预测。需要双精度。
之前的代码需要两个更改。第一个更改表明输入现在是 DoubleTensorType
类型。第二个更改是额外参数 dtype=numpy.float64
告诉转换函数,所有实数常数矩阵(如训练的系数)将以双精度转储,而不是单精度浮点数。
initial_type = [("X", DoubleTensorType([None, None]))]
onx64 = convert_sklearn(gpr, initial_types=initial_type, target_opset=12)
sess64 = rt.InferenceSession(
onx64.SerializeToString(), providers=["CPUExecutionProvider"]
)
pred_onx64 = sess64.run(None, {"X": X_test})[0]
print(pred_onx64[0, :10])
[100.31582765]
新的差异看起来好多了。
diff = numpy.sort(numpy.abs(numpy.squeeze(pred_skl) - numpy.squeeze(pred_onx64)))[-5:]
print(diff)
print("min(Y)-max(Y):", min(y_test), max(y_test))
[0.00612018 0.00635238 0.00669051 0.00719862 0.00763542]
min(Y)-max(Y): 39.0 332.0
尺寸增加¶
因此,ONNX 模型几乎大了两倍,因为每个系数都存储为双精度,而不是单精度浮点数。
ONNX with floats: 29226
ONNX with doubles: 57050
return_std=True¶
GaussianProcessRegressor 是一个模型,它为 predict 函数定义了附加参数。如果使用 return_std=True
调用,该类会返回一个额外的结果,这需要在生成的 ONNX 图中反映出来。转换器需要知道需要一个扩展图。这通过选项机制完成(参见 带选项的转换器)。
initial_type = [("X", DoubleTensorType([None, None]))]
options = {GaussianProcessRegressor: {"return_std": True}}
try:
onx64_std = convert_sklearn(
gpr, initial_types=initial_type, options=options, target_opset=12
)
except RuntimeError as e:
print(e)
此错误突显了一个事实,即 scikit-learn 在第一次调用 predict 方法时计算内部变量。转换器需要通过至少调用一次 predict 方法然后再次转换来初始化它们。
gpr.predict(X_test[:1], return_std=True)
onx64_std = convert_sklearn(
gpr, initial_types=initial_type, options=options, target_opset=12
)
sess64_std = rt.InferenceSession(
onx64_std.SerializeToString(), providers=["CPUExecutionProvider"]
)
pred_onx64_std = sess64_std.run(None, {"X": X_test[:5]})
pprint.pprint(pred_onx64_std)
[array([[100.31582765],
[158.71855798],
[134.34822088],
[149.0801781 ],
[113.38941371]]),
array([0., 0., 0., 0., 0.])]
让我们与 scikit-learn 的预测进行比较。
pprint.pprint(gpr.predict(X_test[:5], return_std=True))
(array([100.315979 , 158.71972656, 134.34814453, 149.08435059,
113.38726807]),
array([1.01359849, 1.01227192, 1.00741782, 1.00668201, 1.0090939 ]))
看起来不错。我们做一个更好的检查。
pred_onx64_std = sess64_std.run(None, {"X": X_test})
pred_std = gpr.predict(X_test, return_std=True)
diff = numpy.sort(
numpy.abs(numpy.squeeze(pred_onx64_std[1]) - numpy.squeeze(pred_std[1]))
)[-5:]
print(diff)
[ 1.02410261 168.13500527 298.00327908 317.31394382 357.80585252]
存在一些差异,但似乎是合理的。
此示例使用的版本
print("numpy:", numpy.__version__)
print("scikit-learn:", sklearn.__version__)
print("onnx: ", onnx.__version__)
print("onnxruntime: ", rt.__version__)
print("skl2onnx: ", skl2onnx.__version__)
numpy: 2.3.1
scikit-learn: 1.6.1
onnx: 1.19.0
onnxruntime: 1.23.0
skl2onnx: 1.19.1
脚本总运行时间: (0 分钟 0.764 秒)