Note
Go to the end to download the full example code
Two ways to implement a converter#
There are two ways to write a converter. The first one is less verbose and easier to understand (see k_means.py). The other is very verbose (see ada_boost.py for an example).
The first way is used in Implement a new converter. This one demonstrates the second way which is usually the one used in other converter library. It is more verbose.
Custom model#
It basically copies what is in example :ref:`l-plot-custom-converter.
from skl2onnx.common.data_types import guess_proto_type
from onnxconverter_common.onnx_ops import apply_sub
from onnxruntime import InferenceSession
from skl2onnx import update_registered_converter
from skl2onnx import to_onnx
import numpy
from sklearn.base import TransformerMixin, BaseEstimator
from sklearn.datasets import load_iris
class DecorrelateTransformer(TransformerMixin, BaseEstimator):
"""
Decorrelates correlated gaussian features.
:param alpha: avoids non inversible matrices
by adding *alpha* identity matrix
*Attributes*
* `self.mean_`: average
* `self.coef_`: square root of the coveriance matrix
"""
def __init__(self, alpha=0.0):
BaseEstimator.__init__(self)
TransformerMixin.__init__(self)
self.alpha = alpha
def fit(self, X, y=None, sample_weights=None):
if sample_weights is not None:
raise NotImplementedError("sample_weights != None is not implemented.")
self.mean_ = numpy.mean(X, axis=0, keepdims=True)
X = X - self.mean_
V = X.T @ X / X.shape[0]
if self.alpha != 0:
V += numpy.identity(V.shape[0]) * self.alpha
L, P = numpy.linalg.eig(V)
Linv = L ** (-0.5)
diag = numpy.diag(Linv)
root = P @ diag @ P.transpose()
self.coef_ = root
return self
def transform(self, X):
return (X - self.mean_) @ self.coef_
data = load_iris()
X = data.data
dec = DecorrelateTransformer()
dec.fit(X)
pred = dec.transform(X[:5])
print(pred)
[[ 0.0167562 0.52111756 -1.24946737 -0.56194325]
[-0.0727878 -0.80853732 -1.43841018 -0.37441392]
[-0.69971891 -0.09950908 -1.2138161 -0.3499275 ]
[-1.13063404 -0.13540568 -0.79087008 -0.73938966]
[-0.35790036 0.91900236 -1.04034399 -0.6509266 ]]
Conversion into ONNX#
The shape calculator does not change.
def decorrelate_transformer_shape_calculator(operator):
op = operator.raw_operator
input_type = operator.inputs[0].type.__class__
# The shape may be unknown. *get_first_dimension*
# returns the appropriate value, None in most cases
# meaning the transformer can process any batch of observations.
input_dim = operator.inputs[0].get_first_dimension()
output_type = input_type([input_dim, op.coef_.shape[1]])
operator.outputs[0].type = output_type
The converter is different.
def decorrelate_transformer_converter(scope, operator, container):
op = operator.raw_operator
out = operator.outputs
# We retrieve the unique input.
X = operator.inputs[0]
# In most case, computation happen in floats.
# But it might be with double. ONNX is very strict
# about types, every constant should have the same
# type as the input.
proto_dtype = guess_proto_type(X.type)
mean_name = scope.get_unique_variable_name("mean")
container.add_initializer(
mean_name, proto_dtype, op.mean_.shape, list(op.mean_.ravel())
)
coef_name = scope.get_unique_variable_name("coef")
container.add_initializer(
coef_name, proto_dtype, op.coef_.shape, list(op.coef_.ravel())
)
op_name = scope.get_unique_operator_name("sub")
sub_name = scope.get_unique_variable_name("sub")
# This function is defined in package onnxconverter_common.
# Most common operators can be added to the graph with
# these functions. It handles the case when specifications
# changed accross opsets (a parameter becomes an input
# for example).
apply_sub(
scope, [X.full_name, mean_name], sub_name, container, operator_name=op_name
)
op_name = scope.get_unique_operator_name("matmul")
container.add_node("MatMul", [sub_name, coef_name], out[0].full_name, name=op_name)
We need to let skl2onnx know about the new converter.
update_registered_converter(
DecorrelateTransformer,
"SklearnDecorrelateTransformer",
decorrelate_transformer_shape_calculator,
decorrelate_transformer_converter,
)
onx = to_onnx(dec, X.astype(numpy.float32))
sess = InferenceSession(onx.SerializeToString(), providers=["CPUExecutionProvider"])
exp = dec.transform(X.astype(numpy.float32))
got = sess.run(None, {"X": X.astype(numpy.float32)})[0]
def diff(p1, p2):
p1 = p1.ravel()
p2 = p2.ravel()
d = numpy.abs(p2 - p1)
return d.max(), (d / numpy.abs(p1)).max()
print(diff(exp, got))
(6.04657619085458e-07, 0.0002951417065406967)
Let’s check it works as well with double.
onx = to_onnx(dec, X.astype(numpy.float64))
sess = InferenceSession(onx.SerializeToString(), providers=["CPUExecutionProvider"])
exp = dec.transform(X.astype(numpy.float64))
got = sess.run(None, {"X": X.astype(numpy.float64)})[0]
print(diff(exp, got))
(0.0, 0.0)
The differences are smaller with double as expected.
Total running time of the script: (0 minutes 0.073 seconds)