onnxruntime-training, scikit-learn#

Links: notebook, html, PDF, python, slides, GitHub

Simple examples mixing packages. The notebook takes a neural network from scikit-learn (regression), converts it into ONNX and trains it with onnxruntime-training.

from jyquickhelper import add_notebook_menu
add_notebook_menu()
%matplotlib inline
%load_ext mlprodict
import warnings
from time import perf_counter
warnings.filterwarnings("ignore")

Data and first model#

from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split

data = load_diabetes()
X, y = data.data, data.target
y /= 100
X_train, X_test, y_train, y_test = train_test_split(X, y)
from sklearn.neural_network import MLPRegressor

nn = MLPRegressor(hidden_layer_sizes=(20,), max_iter=400)
begin = perf_counter()
nn.fit(X_train, y_train)
print("training time: %r" % (perf_counter() - begin))
training time: 0.9272374000000028
from sklearn.metrics import r2_score
r2_score(y_test, nn.predict(X_test))
0.4844054606180469

Conversion to ONNX#

With skl2onnx.

import numpy
from skl2onnx import to_onnx

nn = MLPRegressor(hidden_layer_sizes=(20,), max_iter=1).fit(X_train, y_train)
nn_onnx = to_onnx(nn, X_train[1:].astype(numpy.float32))

%onnxview nn_onnx

Training with pytorch + ONNX#

We could use onnxruntime-training only (see Train a linear regression with onnxruntime-training but instead we try to extend pytorch with a custom function defined with an ONNX graph, the one obtained by converting a neural network from scikit-learn into ONNX. First, let’s get the list of parameters of the model.

from onnx.numpy_helper import to_array

weights = [(init.name, to_array(init))
           for init in nn_onnx.graph.initializer
           if 'shape' not in init.name]
[w[0] for w in weights]
['coefficient', 'intercepts', 'coefficient1', 'intercepts1']

Class TorchOrtFactory creates a torch function by taking the ONNX graph and the weights to learn.

from deeponnxcustom.onnxtorch import TorchOrtFactory

try:
    fact = TorchOrtFactory(nn_onnx, [w[0] for w in weights])
except ValueError as e:
    print(e)
No CUDA runtime is found, using CUDA_HOME='C:Program FilesNVIDIA GPU Computing ToolkitCUDAv11.4'
List of weights to train must be sorted but is not in ['coefficient', 'intercepts', 'coefficient1', 'intercepts1']. You shoud use function onnx_rename_weights to do that before calling this class.

The function fails because the weights needs to be in alphabetical order. We use a function to rename them.

from deeponnxcustom.tools.onnx_helper import onnx_rename_weights

onnx_rename_weights(nn_onnx)
weights = [(init.name, to_array(init))
           for init in nn_onnx.graph.initializer
           if 'shape' not in init.name]
[w[0] for w in weights]
['I0_coefficient', 'I1_intercepts', 'I2_coefficient1', 'I3_intercepts1']

We start again.

fact = TorchOrtFactory(nn_onnx, [w[0] for w in weights])

Let’s create the torch function.

cls = fact.create_class()
cls
deeponnxcustom.onnxtorch.torchort.TorchOrtFunction_2140275442256
cls.__bases__
(deeponnxcustom.onnxtorch.torchort.TorchOrtFunction,)
cls.__bases__[0].__bases__
(torch.autograd.function.Function,)

Let’s train it.

from tqdm import tqdm
import torch


def from_numpy(v, device=None, requires_grad=False):
    v = torch.from_numpy(v)
    if device is not None:
        v = v.to(device)
    v.requires_grad_(requires_grad)
    return v


def train_cls(cls, device, X_train, y_train, weights, n_iter=20, learning_rate=1e-3):
    x = from_numpy(X_train.astype(numpy.float32),
                   requires_grad=True, device=device)
    y = from_numpy(y_train.astype(numpy.float32),
                   requires_grad=True, device=device)
    fact = torch.tensor([x.shape[0]], dtype=torch.float32).to(device)
    fact.requires_grad_(True)

    weights_tch = [(w[0], from_numpy(w[1], requires_grad=True, device=device))
                   for w in weights]
    weights_values = [w[1] for w in weights_tch]

    all_losses = []
    for t in tqdm(range(n_iter)):
        # forward - backward
        y_pred = cls.apply(x, *weights_values)
        loss = (y_pred - y).pow(2).sum() / fact
        loss.backward()

        # update weights
        with torch.no_grad():
            for w in weights_values:
                w -= w.grad * learning_rate
                w.grad.zero_()

        all_losses.append((t, float(loss.detach().numpy())))
    return all_losses, weights_tch


device_name = "cuda:0" if torch.cuda.is_available() else "cpu"
device = torch.device(device_name)
print("device:", device)

begin = perf_counter()
train_losses, final_weights = train_cls(cls, device, X_train, y_test, weights, n_iter=400)
print("training time: %r" % (perf_counter() - begin))
device: cpu
100%|██████████| 400/400 [00:00<00:00, 428.75it/s]
training time: 0.9546589000000054
from pandas import DataFrame

df = DataFrame(data=train_losses, columns=['iter', 'train_loss'])
df[6:].plot(x="iter", y="train_loss", title="Training loss");
../_images/mlpregressor_25_0.png