與 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/github/scikit-learn/sklearn/gaussian_process/kernels.py:419: 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])
[157.67407448 142.03651212 160.8484086  135.0034477  100.48107033
 171.43261057 134.12309522 167.4292642  155.90885873 201.31475888]
[155.5]

差異似乎相當重要。讓我們透過查看最大的差異來確認這一點。

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))
[2.24528426 2.36538915 2.37925239 2.38045369 2.63459276]
min(Y)-max(Y): 25.0 336.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])
[157.67407447]

新的差異看起來好多了。

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))
[7.92597632e-09 8.98936037e-09 9.23387233e-09 9.66474545e-09
 1.00339719e-08]
min(Y)-max(Y): 25.0 336.0

大小增加

因此,ONNX 模型的大小幾乎是原來的兩倍,因為每個係數都以雙精度而不是浮點數的形式儲存。

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

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 在首次呼叫預測方法時計算內部變數的事實。轉換器需要至少呼叫一次預測方法來初始化它們,然後再次轉換。

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([[157.67407447],
       [142.03651212],
       [160.8484086 ],
       [135.0034477 ],
       [100.48107033]]),
 array([1.00845167, 1.0054851 , 1.0139891 , 1.00514903, 1.01019988])]

讓我們與 scikit-learn 預測進行比較。

pprint.pprint(gpr.predict(X_test[:5], return_std=True))
(array([157.67407448, 142.03651212, 160.8484086 , 135.0034477 ,
       100.48107033]),
 array([1.00845384, 1.00548596, 1.01398906, 1.00515132, 1.01019995]))

看起來不錯。讓我們做更好的檢查。

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)
[2.54371954e-06 2.56724555e-06 2.58754006e-06 2.63925354e-06
 3.30249346e-06]

存在一些差異,但看起來是合理的。

此範例使用的版本

print("numpy:", numpy.__version__)
print("scikit-learn:", sklearn.__version__)
print("onnx: ", onnx.__version__)
print("onnxruntime: ", rt.__version__)
print("skl2onnx: ", skl2onnx.__version__)
numpy: 1.23.5
scikit-learn: 1.4.dev0
onnx:  1.15.0
onnxruntime:  1.16.0+cu118
skl2onnx:  1.16.0

腳本的總執行時間:(0 分鐘 3.428 秒)

由 Sphinx-Gallery 產生的圖庫