与 GaussianProcessorRegressor 的差异:使用双精度

GaussianProcessRegressor 涉及许多矩阵运算,可能需要双精度。sklearn-onnx 默认使用单精度浮点数,但对于这个特定模型,最好使用双精度。让我们看看如何使用双精度创建 ONNX 文件。

训练模型

一个在 Boston 数据集上使用 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/_gpr.py:660: ConvergenceWarning: lbfgs failed to converge (status=2):
ABNORMAL_TERMINATION_IN_LNSRCH.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.cn/stable/modules/preprocessing.html
  _check_optimize_result("lbfgs", opt_res)
/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])
[199.16400146 144.28936768 109.57861328 183.99377441 169.99847412
 112.01519775 179.21099854 130.21044922 189.19836426 136.98278809]
[557056.]

差异似乎相当大。让我们通过查看最大的差异来确认一下。

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))
[556952.34741211 556953.92102051 556957.08154297 556958.05334473
 556961.00817871]
min(Y)-max(Y): 39.0 341.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])
[199.16298882]

新的差异看起来好多了。

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.00544517 0.0063448  0.00640814 0.00701725 0.00797183]
min(Y)-max(Y): 39.0 341.0

大小增加

结果是,ONNX 模型几乎大了两倍,因为每个系数都存储为双精度而不是浮点数。

size32 = len(onx.SerializeToString())
size64 = len(onx64.SerializeToString())
print("ONNX with floats:", size32)
print("ONNX with doubles:", size64)
ONNX with floats: 29226
ONNX with doubles: 57050

return_std=True

GaussianProcessRegressor 是一个模型,它为预测函数定义了额外参数。如果调用时带有 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([[199.16298882],
       [144.28235043],
       [109.58064467],
       [183.99437845],
       [169.99982479]]),
 array([668.01881677, 785.8806495 , 561.56004495, 541.05432348,
         0.        ])]

让我们与 scikit-learn 的预测结果比较。

pprint.pprint(gpr.predict(X_test[:5], return_std=True))
(array([199.16400146, 144.28936768, 109.57861328, 183.99377441,
       169.99798584]),
 array([1.01514412, 1.00504366, 1.01094833, 1.01163485, 1.0076308 ]))

看起来不错。让我们做更详细的检查。

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)
[ 952.49009449  969.80341373  995.20442978 1069.49191637 1079.44581069]

存在一些差异,但这似乎是合理的。

此示例使用的版本

print("numpy:", numpy.__version__)
print("scikit-learn:", sklearn.__version__)
print("onnx: ", onnx.__version__)
print("onnxruntime: ", rt.__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 分钟 5.965 秒)

由 Sphinx-Gallery 生成的示例集锦