Skip to content
This repository was archived by the owner on Jan 13, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _doc/notebooks/numpy_api_onnx.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# Numpy API for ONNX\n",
"# Introduction to a numpy API for ONNX\n",
"\n",
"This notebook shows how to write python functions similar functions as numpy offers and get a function which can be converted into ONNX."
]
Expand Down
104 changes: 14 additions & 90 deletions _doc/sphinxdoc/source/api/npy.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@

.. _l-numpy-onnxpy:

Numpy API for ONNX
==================
Complete Numpy API for ONNX
===========================

The numpy API is meant to simplofy the creation of ONNX
graphs by using functions very similar to what numpy implements.
This page only makes a list of the available
functions. A tutorial is available at
:ref:`l-numpy-api-for-onnx`.

.. contents::
:local:
Expand Down Expand Up @@ -42,7 +48,7 @@ is called.
print(y)

Annotations are mandatory to indicate inputs and outputs type.
As a result, the returned function is strict about types
The decorator returns a function which is strict about types
as opposed to numpy. This approach is similar to what
:epkg:`tensorflow` with `autograph
<https://www.tensorflow.org/api_docs/python/tf/autograph>`_.
Expand Down Expand Up @@ -77,97 +83,15 @@ OnnxVar
Available numpy functions implemented with ONNX operators
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

.. autosignature:: mlprodict.npy.numpy_onnx_impl.abs

.. autosignature:: mlprodict.npy.numpy_onnx_impl.acos

.. autosignature:: mlprodict.npy.numpy_onnx_impl.acosh

.. autosignature:: mlprodict.npy.numpy_onnx_impl.amax

.. autosignature:: mlprodict.npy.numpy_onnx_impl.amin

.. autosignature:: mlprodict.npy.numpy_onnx_impl.arange

.. autosignature:: mlprodict.npy.numpy_onnx_impl.argmax

.. autosignature:: mlprodict.npy.numpy_onnx_impl.argmin

.. autosignature:: mlprodict.npy.numpy_onnx_impl.asin

.. autosignature:: mlprodict.npy.numpy_onnx_impl.asinh

.. autosignature:: mlprodict.npy.numpy_onnx_impl.atan

.. autosignature:: mlprodict.npy.numpy_onnx_impl.atanh

.. autosignature:: mlprodict.npy.numpy_onnx_impl.ceil

.. autosignature:: mlprodict.npy.numpy_onnx_impl.clip

.. autosignature:: mlprodict.npy.numpy_onnx_impl.compress

.. autosignature:: mlprodict.npy.numpy_onnx_impl.concat

.. autosignature:: mlprodict.npy.numpy_onnx_impl.cos

.. autosignature:: mlprodict.npy.numpy_onnx_impl.cosh

.. autosignature:: mlprodict.npy.numpy_onnx_impl.cumsum

.. autosignature:: mlprodict.npy.numpy_onnx_impl.det

.. autosignature:: mlprodict.npy.numpy_onnx_impl.dot

.. autosignature:: mlprodict.npy.numpy_onnx_impl.einsum

.. autosignature:: mlprodict.npy.numpy_onnx_impl.erf

.. autosignature:: mlprodict.npy.numpy_onnx_impl.exp

.. autosignature:: mlprodict.npy.numpy_onnx_impl.expand_dims

.. autosignature:: mlprodict.npy.numpy_onnx_impl.hstack

.. autosignature:: mlprodict.npy.numpy_onnx_impl.isnan

.. autosignature:: mlprodict.npy.numpy_onnx_impl.mean

.. autosignature:: mlprodict.npy.numpy_onnx_impl.log

.. autosignature:: mlprodict.npy.numpy_onnx_impl.pad

.. autosignature:: mlprodict.npy.numpy_onnx_impl.prod

.. autosignature:: mlprodict.npy.numpy_onnx_impl.reciprocal

.. autosignature:: mlprodict.npy.numpy_onnx_impl.relu

.. autosignature:: mlprodict.npy.numpy_onnx_impl.round

.. autosignature:: mlprodict.npy.numpy_onnx_impl.sign

.. autosignature:: mlprodict.npy.numpy_onnx_impl.sin

.. autosignature:: mlprodict.npy.numpy_onnx_impl.sinh

.. autosignature:: mlprodict.npy.numpy_onnx_impl.sqrt

.. autosignature:: mlprodict.npy.numpy_onnx_impl.squeeze

.. autosignature:: mlprodict.npy.numpy_onnx_impl.sum

.. autosignature:: mlprodict.npy.numpy_onnx_impl.tan

.. autosignature:: mlprodict.npy.numpy_onnx_impl.tanh

.. autosignature:: mlprodict.npy.numpy_onnx_impl.unsqueeze

.. autosignature:: mlprodict.npy.numpy_onnx_impl.vstack
All functions are implemented in submodule :ref:`f-numpyonnximpl`.

ONNX functions executed python ONNX runtime
+++++++++++++++++++++++++++++++++++++++++++

Same function as above, the import goes from
`from mlprodict.npy.numpy_onnx_impl import <function-name>` to
`from mlprodict.npy.numpy_onnx_pyrt import <function-name>`.
These function are usually not used except in unit test or as
reference for more complex functions. See the source on github,
`numpy_onnx_pyrt.py <https://github.com/sdpython/mlprodict/
blob/master/mlprodict/npy/numpy_onnx_pyrt.py>`_.
2 changes: 2 additions & 0 deletions _doc/sphinxdoc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@
'openmp': 'https://www.openmp.org/',
'ONNX': 'https://onnx.ai/',
'onnx': 'https://github.com/onnx/onnx',
'Op': ('https://github.com/onnx/onnx/blob/master/docs/Operators.md',
('https://github.com/onnx/onnx/blob/master/docs/Operators.md#{0}', 1)),
'ONNX Operators': 'https://github.com/onnx/onnx/blob/master/docs/Operators.md',
'ONNX ML Operators': 'https://github.com/onnx/onnx/blob/master/docs/Operators-ml.md',
'ONNX Zoo': 'https://github.com/onnx/models',
Expand Down
102 changes: 84 additions & 18 deletions _doc/sphinxdoc/source/tutorial/numpy_api_onnx.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@

.. _l-numpy-api-for-onnx:

Numpy API for ONNX
==================
Create ONNX graphs with an API similar to numpy
===============================================

Many people came accross the task of converting a pipeline
including a custom preprocessing embedded into a
Expand All @@ -11,7 +11,11 @@ including a custom preprocessing embedded into a
is to create an ONNX graph for every :epkg:`scikit-learn`
model included in a pipeline. Every converter is a new implementation
of methods `predict`, `predict_proba` or `transform` with
:epkg:`ONNX Operators`. Every custom function is not supported.
:epkg:`ONNX Operators`. But that does not include custom function.
Writing a converter can be quite verbose and requires to know
the :epkg:`ONNX Operators`, similar to :epkg:`numpy` but not
the same.

The goal here is to make it easier for users and have their custom
function converted in ONNX.
Everybody playing with :epkg:`scikit-learn` knows :epkg:`numpy`
Expand Down Expand Up @@ -64,32 +68,35 @@ Following example shows how to replace *numpy* by *ONNX*.

ONNX runtimes are usually more strict about types than :epkg:`numpy`
(see :epkg:`onnxruntime`).
By default a function must be implemented for the same input type
and there is not implicit cast. There are two important elements
A function must be implemented for the same input type
and there is not implicit cast. There are three important elements
in this example:

* Decorator :func:`onnxnumpy_default <mlprodict.npy.onnx_numpy_wrapper.onnxnumpy_default>`:
it parses the annotations, creates the ONNX graph and initialize a runtime with it.
* Annotation: every input and output types must be specified. They are :class:`NDArray
<mlprodict.npy.onnx_numpy_annotation.NDArray>`, shape can be left undefined by element
type must be precised.
* Types: `1` is different than `np.float32(1)`, the right type must be used.

`onnx_log_1` is not a function but an instance of class
:class:`wrapper_onnxnumpy <mlprodict.npy.onnx_numpy_wrapper.wrapper_onnxnumpy>`.
This class implements method `__call__` to behave like a function
and holds an attribute of type
:class:`OnnxNumpyCompiler <mlprodict.npy.onnx_numpy_compiler.OnnxNumpyCompiler>`.
This class contains an ONNX graph and a instance of a runtime.
The following lines lists some usefull attributes.

* `onnx_log_1`: :class:`wrapper_onnxnumpy <mlprodict.npy.onnx_numpy_wrapper.wrapper_onnxnumpy>`
* `onnx_log_1.compiled`: :class:`OnnxNumpyCompiler <mlprodict.npy.onnx_numpy_compiler.OnnxNumpyCompiler>`
* `onnx_log_1.compiled.onnx_`: ONNX graph
* `onnx_log_1.compiled.rt_fct_.rt`: runtime, by default
:class:`OnnxInference <mlprodict.onnxrt.onnx_inference.OnnxInference>`

The ONNX graph `onnx_log_1.compiled.onnx_` looks like this:

.. gdot::
:script: DOT-SECTION
:warningout: DeprecationWarning

from typing import Any
import numpy as np
Expand All @@ -107,12 +114,20 @@ This class contains an ONNX graph and a instance of a runtime.
oinf = onnx_log_1.compiled.rt_fct_.rt
print("DOT-SECTION", oinf.to_dot())

There is a fundamental different between :epkg:`numpy` and
:epkg:`ONNX`. :epkg:`numpy` allows inplace modifications.
The simple instruction ``m[:, 0] = 1`` modifies an entire column
of an existing array. :epkg:`ONNX` does not allow that, even if the
same operator can be achieved, the result is a new array.
See section :ref:`l-inplace-modification-onnx` for more
details.

Available functions
+++++++++++++++++++

This tool does not implement every function of :epkg:`numpy`.
This a work in progress. The list of supported function is
available at :ref:`l-numpy-onnxpy-list-fct`.
This a work in progress. The list of supported functions is
available at :ref:`f-numpyonnximpl`.

Common operators `+`, `-`, `/`, `*`, `**`, `%`, `[]` are
supported as well. They are implemented by class
Expand Down Expand Up @@ -188,7 +203,7 @@ Use onnxruntime as ONNX runtime

By default, the ONNX graph is executed by the Python runtime
implemented in this module (see :ref:`l-onnx-python-runtime`).
It is a mix of :epkg:`numpy` and C++ implementations but it does
It is a mix of :epkg:`numpy` and C++ implementations and it does
not require any new dependency. However, it is possible to use
a different one like :epkg:`onnxruntime` which has an implementation
for more :epkg:`ONNX Operators`. The only change is a wrapper
Expand Down Expand Up @@ -263,7 +278,7 @@ as an argument of `to_onnx`.

target_opset = 11

@onnxnumpy_np(op_version=target_opset)
@onnxnumpy_np(op_version=target_opset) # first place
def onnx_log_1(x: NDArray[Any, np.float32]) -> NDArray[(None, None), np.float32]:
return npnx.log(x + np.float32(1))

Expand All @@ -280,7 +295,7 @@ as an argument of `to_onnx`.

onx = to_onnx(pipe, X_train[:1], rewrite_ops=True,
options={LogisticRegression: {'zipmap': False}},
target_opset=target_opset)
target_opset=target_opset) # second place

oinf = InferenceSession(onx.SerializeToString())
print(oinf.run(None, {'X': X_test[:2]})[1])
Expand All @@ -291,10 +306,10 @@ Same implementation for float32 and float64
Only one input type is allowed by default but there is a way
to define a function supporting more than one type with
:class:`NDArrayType <mlprodict.npy.onnx_numpy_annotation.NDArrayType>`.
When calling function `onnx_log_1`, input are detected and
When calling function `onnx_log_1`, inputs are detected,
an ONNX graph is generated and executed. Next time the same function
is called, if the input type is the same as before, it reuses the same
ONNX graph and same runtime. Otherwise, it will generate a new
is called, if the input types are the same as before, it reuses the same
ONNX graph and same runtime. Otherwise, it generates a new
ONNX graph taking this new type as input. The expression
`x.dtype` returns the type of this input in order to cast
the constant `1` into the right type before being used by
Expand Down Expand Up @@ -327,10 +342,56 @@ There are more options to it. Many of them are used in
:ref:`f-numpyonnxpyrt`. It is possible to add arguments
with default values or undefined number of inputs. One
important detail though, a different value for an argument
(not an input) means the ONNX graph has to be different.
Everytime input type or an argument is different, a new ONNX
(not an input) means the ONNX graph has to be different because
this value is stored in the graph instead of being an input.
Everytime an input type or an argument is different, a new ONNX
graph is generated and executed.

.. _l-inplace-modification-onnx:

How to convert inplace modifications
++++++++++++++++++++++++++++++++++++

As mentioned earlier, there is no way to modify a tensor inplace.
Every modification implies a copy. A modification can be done
by creating a new tensor concatenated from other tensors or by using
operators :epkg:`Op:ScatterElements` or :epkg:`Op:ScatterND`.
Instruction ``v[5] = 3.5`` is correct with numpy. Class :class:`OnnxVar
<mlprodict.npy.onnx_variable.OnnxVar>` replaces that instruction
with operator :epkg:`Op:ScatterElements`.

Operator `[] (__setitem__)` must return the instance itself (`self`).
That's why the design is different from the other methods. Instead of
returning a new instance of :class:`OnnxVar
<mlprodict.npy.onnx_variable.OnnxVar>`, it replaces the only input.
However, that require the operator `[]` to follow a copy.
``v[5] = 3.5`` may not be valid but ``v = v.copy(); v[5] = 3.5`` always is.
Current implementation only supports one dimensional tensor.
Operators :epkg:`Op:ScatterElements` or :epkg:`Op:ScatterND` are not
really meant to change only one element but to change many of them.

.. gdot::
:script: DOT-SECTION

from typing import Any
import numpy as np
import mlprodict.npy.numpy_onnx_impl as npnx
from mlprodict.npy import onnxnumpy_default, NDArray

# The ONNX function
@onnxnumpy_default
def onnx_change_element(x: NDArray[Any, np.float32]) -> NDArray[Any, np.float32]:
shape = x.shape
v = x.reshape((-1, )).copy()
v[4] = np.float32(5)
return v.reshape(shape)

onx = onnx_change_element.compiled.onnx_
oinf = onnx_change_element.compiled.rt_fct_.rt
print("DOT-SECTION", oinf.to_dot())

Instructions using slice is also supported: ``v[:5] = 3.5``, ``v[5:] = 3.5``, ...

Common errors
+++++++++++++

Expand Down Expand Up @@ -361,6 +422,11 @@ the conversion to ONNX :meth:`to_algebra
x = np.random.rand(2, 3).astype(np.float32)
print(onnx_log_1(x))

The execution does not fail but returns an instance of class
:class:`OnnxVar <mlprodict.npy.onnx_variable.OnnxVar>`. This
instance holds all the necessary information to create the ONNX
graph.

Missing annotation
^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -505,6 +571,6 @@ There are a couple of ways to fix this example. One way is to call
:func:`to_onnx <mlprodict.onnx_conv.convert.to_onnx>` function with
argument `rewrite_ops=True`. The function restores the default
converter after the call. Another way is to call function
:func:`register_rewritten_operators <mlprodict/onnx_conv/
register_rewritten_converters.register_rewritten_operators>`
:func:`register_rewritten_operators
<mlprodict.onnx_conv.register_rewritten_converters.register_rewritten_operators>`
but changes are permanent.
Loading