切換至浮點數時的問題

scikit-learn 中的大多數模型都使用雙精度浮點數而非浮點數進行計算。深度學習中的大多數模型都使用浮點數,因為這是 GPU 最常見的情況。ONNX 最初是為了促進深度學習模型的部署而創建的,這解釋了為什麼許多轉換器假設轉換後的模型應使用浮點數。此假設通常不會損害預測,與雙精度預測相比,轉換為浮點數會引入小的差異。如果預測函式是連續的,則此假設通常為真,y = f(x),則dy = f'(x) dx。我們可以確定差異的上限:\Delta(y) \leqslant \sup_x \left\Vert f'(x)\right\Vert dxdx 是由浮點數轉換引入的差異,dx = x - numpy.float32(x)

然而,並非每個模型都是如此。為回歸訓練的決策樹不是連續函式。因此,即使是很小的 dx 也可能引入巨大的差異。讓我們來看一個總是產生差異的例子,以及一些克服此情況的方法。

深入探討問題

下面的範例是故意使其失敗。它包含具有不同數量級並四捨五入為整數的整數特徵。決策樹將特徵與閾值進行比較。在大多數情況下,浮點數和雙精度浮點數比較會得出相同的結果。我們用[x]_{f32}表示轉換(或轉換)numpy.float32(x)

x \leqslant y = [x]_{f32} \leqslant [y]_{f32}

但是,兩種比較得出不同結果的機率並非為零。下圖顯示了不一致的區域。

from skl2onnx.sklapi import CastTransformer
from skl2onnx import to_onnx
from onnxruntime import InferenceSession
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import make_regression
import numpy
import matplotlib.pyplot as plt


def area_mismatch_rule(N, delta, factor, rule=None):
    if rule is None:

        def rule(t):
            return numpy.float32(t)

    xst = []
    yst = []
    xsf = []
    ysf = []
    for x in range(-N, N):
        for y in range(-N, N):
            dx = (1.0 + x * delta) * factor
            dy = (1.0 + y * delta) * factor
            c1 = 1 if numpy.float64(dx) <= numpy.float64(dy) else 0
            c2 = 1 if numpy.float32(dx) <= rule(dy) else 0
            key = abs(c1 - c2)
            if key == 1:
                xsf.append(dx)
                ysf.append(dy)
            else:
                xst.append(dx)
                yst.append(dy)
    return xst, yst, xsf, ysf


delta = 36e-10
factor = 1
xst, yst, xsf, ysf = area_mismatch_rule(100, delta, factor)


fig, ax = plt.subplots(1, 1, figsize=(5, 5))
ax.plot(xst, yst, ".", label="agree")
ax.plot(xsf, ysf, ".", label="disagree")
ax.set_title("Region where x <= y and (float)x <= (float)y agree")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.plot([min(xst), max(xst)], [min(yst), max(yst)], "k--")
ax.legend()
Region where x <= y and (float)x <= (float)y agree
<matplotlib.legend.Legend object at 0x7f7383b6e6b0>

管線和資料

現在,我們可以建立一個範例,其中學習到的決策樹在此不一致區域中進行多次比較。這是透過將特徵四捨五入為整數來完成的,這是處理類別特徵時經常發生的情況。

X, y = make_regression(10000, 10)
X_train, X_test, y_train, y_test = train_test_split(X, y)

Xi_train, yi_train = X_train.copy(), y_train.copy()
Xi_test, yi_test = X_test.copy(), y_test.copy()
for i in range(X.shape[1]):
    Xi_train[:, i] = (Xi_train[:, i] * 2**i).astype(numpy.int64)
    Xi_test[:, i] = (Xi_test[:, i] * 2**i).astype(numpy.int64)

max_depth = 10

model = Pipeline(
    [("scaler", StandardScaler()), ("dt", DecisionTreeRegressor(max_depth=max_depth))]
)

model.fit(Xi_train, yi_train)
Pipeline(steps=[('scaler', StandardScaler()),
                ('dt', DecisionTreeRegressor(max_depth=10))])
在 Jupyter 環境中,請重新執行此儲存格以顯示 HTML 表示法或信任筆記本。
在 GitHub 上,HTML 表示法無法呈現,請嘗試使用 nbviewer.org 載入此頁面。


差異

讓我們重複使用第一個範例中實作的函式 比較 並檢視轉換。

def diff(p1, p2):
    p1 = p1.ravel()
    p2 = p2.ravel()
    d = numpy.abs(p2 - p1)
    return d.max(), (d / numpy.abs(p1)).max()


onx = to_onnx(model, Xi_train[:1].astype(numpy.float32), target_opset=15)

sess = InferenceSession(onx.SerializeToString(), providers=["CPUExecutionProvider"])

X32 = Xi_test.astype(numpy.float32)

skl = model.predict(X32)
ort = sess.run(None, {"X": X32})[0]

print(diff(skl, ort))
(191.14468356708568, 4.322660335343007)

差異非常顯著。ONNX 模型在每個步驟都保持浮點數。

scikit-learn

CastTransformer

我們可以嘗試在所有地方都使用雙精度浮點數。不幸的是,ONNX ML 運算子僅允許運算子 TreeEnsembleRegressor 使用浮點係數。我們可能希望折衷將正規化器的輸出轉換為 scikit-learn 管線中的浮點數。

model2 = Pipeline(
    [
        ("scaler", StandardScaler()),
        ("cast", CastTransformer()),
        ("dt", DecisionTreeRegressor(max_depth=max_depth)),
    ]
)

model2.fit(Xi_train, yi_train)
Pipeline(steps=[('scaler', StandardScaler()), ('cast', CastTransformer()),
                ('dt', DecisionTreeRegressor(max_depth=10))])
在 Jupyter 環境中,請重新執行此儲存格以顯示 HTML 表示法或信任筆記本。
在 GitHub 上,HTML 表示法無法呈現,請嘗試使用 nbviewer.org 載入此頁面。


差異。

onx2 = to_onnx(model2, Xi_train[:1].astype(numpy.float32), target_opset=15)

sess2 = InferenceSession(onx2.SerializeToString(), providers=["CPUExecutionProvider"])

skl2 = model2.predict(X32)
ort2 = sess2.run(None, {"X": X32})[0]

print(diff(skl2, ort2))
(191.14468356708568, 4.322660335343007)

這仍然會失敗,因為 scikit-learnONNX 中的正規化器使用不同的類型。轉換仍然會發生,而 dx 仍然存在。為了移除它,我們需要在 ONNX 正規化器中使用雙精度浮點數。

model3 = Pipeline(
    [
        ("cast64", CastTransformer(dtype=numpy.float64)),
        ("scaler", StandardScaler()),
        ("cast", CastTransformer()),
        ("dt", DecisionTreeRegressor(max_depth=max_depth)),
    ]
)

model3.fit(Xi_train, yi_train)
onx3 = to_onnx(
    model3,
    Xi_train[:1].astype(numpy.float32),
    options={StandardScaler: {"div": "div_cast"}},
    target_opset=15,
)

sess3 = InferenceSession(onx3.SerializeToString(), providers=["CPUExecutionProvider"])

skl3 = model3.predict(X32)
ort3 = sess3.run(None, {"X": X32})[0]

print(diff(skl3, ort3))
(2.0221857994329184e-05, 5.733250169110544e-08)

它成功了。這也表示當管線包含不連續函式時,很難變更計算類型。最好在使用決策樹之前,沿途保持相同的類型。

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

由 Sphinx-Gallery 產生的展示