`_
+was introduced in onnxruntime to define custom losses in order to train
+a model with :epkg:`onnxruntime-training`. It is mostly used for this usage.
+
+.. code-block:: python
+
+ import onnxruntime.training.onnxblock as onnxblock
+ from onnxruntime.training import artifacts
+
+ # Define a custom loss block that takes in two inputs
+ # and performs a weighted average of the losses from these
+ # two inputs.
+ class WeightedAverageLoss(onnxblock.Block):
+ def __init__(self):
+ self._loss1 = onnxblock.loss.MSELoss()
+ self._loss2 = onnxblock.loss.MSELoss()
+ self._w1 = onnxblock.blocks.Constant(0.4)
+ self._w2 = onnxblock.blocks.Constant(0.6)
+ self._add = onnxblock.blocks.Add()
+ self._mul = onnxblock.blocks.Mul()
+
+ def build(self, loss_input_name1, loss_input_name2):
+ # The build method defines how the block should be stacked on top of
+ # loss_input_name1 and loss_input_name2
+
+ # Returns weighted average of the two losses
+ return self._add(
+ self._mul(self._w1(), self._loss1(loss_input_name1, target_name="target1")),
+ self._mul(self._w2(), self._loss2(loss_input_name2, target_name="target2"))
+ )
+
+ my_custom_loss = WeightedAverageLoss()
+
+ # Load the onnx model
+ model_path = "model.onnx"
+ base_model = onnx.load(model_path)
+
+ # Define the parameters that need their gradient computed
+ requires_grad = ["weight1", "bias1", "weight2", "bias2"]
+ frozen_params = ["weight3", "bias3"]
+
+ # Now, we can invoke generate_artifacts with this custom loss function
+ artifacts.generate_artifacts(base_model, requires_grad = requires_grad, frozen_params = frozen_params,
+ loss = my_custom_loss, optimizer = artifacts.OptimType.AdamW)
+
+ # Successful completion of the above call will generate 4 files in the current working directory,
+ # one for each of the artifacts mentioned above (training_model.onnx, eval_model.onnx, checkpoint, op)
+
+numpy API for onnx
+++++++++++++++++++
+
+See :ref:`l-numpy-api-onnx`. This API was introduced to create graphs
+by using numpy API. If a function is defined only with numpy,
+it should be possible to use the exact same code to create the
+corresponding onnx graph. That's what this API tries to achieve.
+It works with the exception of control flow. In that case, the function
+produces different onnx graphs depending on the execution path.
+
+.. runpython::
+ :showcode:
+
+ import numpy as np
+ from onnx_array_api.npx import jit_onnx
+ from onnx_array_api.plotting.text_plot import onnx_simple_text_plot
+
+ def l2_loss(x, y):
+ return ((x - y) ** 2).sum(keepdims=1)
+
+ jitted_myloss = jit_onnx(l2_loss)
+ dummy = np.array([0], dtype=np.float32)
+
+ # The function is executed. Only then a onnx graph is created.
+ # One is created depending on the input type.
+ jitted_myloss(dummy, dummy)
+
+ # get_onnx only works if it was executed once or at least with
+ # the same input type
+ model = jitted_myloss.get_onnx()
+ print(onnx_simple_text_plot(model))
+
+Light API
++++++++++
+
+See :ref:`l-light-api`. This API was created to be able to write an onnx graph
+in one instruction. It is inspired from the :epkg:`reverse Polish notation`.
+There is no eager mode.
+
+.. runpython::
+ :showcode:
+
+ import numpy as np
+ from onnx_array_api.light_api import start
+ from onnx_array_api.plotting.text_plot import onnx_simple_text_plot
+
+ model = (
+ start()
+ .vin("X")
+ .vin("Y")
+ .bring("X", "Y")
+ .Sub()
+ .rename("dxy")
+ .cst(np.array([2], dtype=np.int64), "two")
+ .bring("dxy", "two")
+ .Pow()
+ .ReduceSum()
+ .rename("Z")
+ .vout()
+ .to_onnx()
+ )
+
+ print(onnx_simple_text_plot(model))
diff --git a/_unittests/ut_validation/test_docs.py b/_unittests/ut_validation/test_docs.py
new file mode 100644
index 0000000..3b1307f
--- /dev/null
+++ b/_unittests/ut_validation/test_docs.py
@@ -0,0 +1,93 @@
+import unittest
+import sys
+import numpy as np
+from onnx.reference import ReferenceEvaluator
+from onnx_array_api.ext_test_case import ExtTestCase
+from onnx_array_api.validation.docs import make_euclidean, make_euclidean_skl2onnx
+
+
+class TestDocs(ExtTestCase):
+ def test_make_euclidean(self):
+ model = make_euclidean()
+
+ ref = ReferenceEvaluator(model)
+ X = np.random.rand(3, 4).astype(np.float32)
+ Y = np.random.rand(3, 4).astype(np.float32)
+ expected = ((X - Y) ** 2).sum(keepdims=1)
+ got = ref.run(None, {"X": X, "Y": Y})[0]
+ self.assertEqualArray(expected, got)
+
+ def test_make_euclidean_skl2onnx(self):
+ model = make_euclidean_skl2onnx()
+
+ ref = ReferenceEvaluator(model)
+ X = np.random.rand(3, 4).astype(np.float32)
+ Y = np.random.rand(3, 4).astype(np.float32)
+ expected = ((X - Y) ** 2).sum(keepdims=1)
+ got = ref.run(None, {"X": X, "Y": Y})[0]
+ self.assertEqualArray(expected, got)
+
+ @unittest.skipIf(sys.platform == "win32", reason="unstable on Windows")
+ def test_make_euclidean_np(self):
+ from onnx_array_api.npx import jit_onnx
+
+ def l2_loss(x, y):
+ return ((x - y) ** 2).sum(keepdims=1)
+
+ jitted_myloss = jit_onnx(l2_loss)
+ dummy1 = np.array([0], dtype=np.float32)
+ dummy2 = np.array([1], dtype=np.float32)
+ # unstable on windows?
+ jitted_myloss(dummy1, dummy2)
+ model = jitted_myloss.get_onnx()
+
+ ref = ReferenceEvaluator(model)
+ X = np.random.rand(3, 4).astype(np.float32)
+ Y = np.random.rand(3, 4).astype(np.float32)
+ expected = ((X - Y) ** 2).sum(keepdims=1)
+ got = ref.run(None, {"x0": X, "x1": Y})[0]
+ self.assertEqualArray(expected, got)
+
+ def test_make_euclidean_light(self):
+ from onnx_array_api.light_api import start
+
+ model = (
+ start()
+ .vin("X")
+ .vin("Y")
+ .bring("X", "Y")
+ .Sub()
+ .rename("dxy")
+ .cst(np.array([2], dtype=np.int64), "two")
+ .bring("dxy", "two")
+ .Pow()
+ .ReduceSum()
+ .rename("Z")
+ .vout()
+ .to_onnx()
+ )
+
+ ref = ReferenceEvaluator(model)
+ X = np.random.rand(3, 4).astype(np.float32)
+ Y = np.random.rand(3, 4).astype(np.float32)
+ expected = ((X - Y) ** 2).sum(keepdims=1)
+ got = ref.run(None, {"X": X, "Y": Y})[0]
+ self.assertEqualArray(expected, got)
+
+ def test_ort_make_euclidean(self):
+ from onnxruntime import InferenceSession
+
+ model = make_euclidean(opset=18)
+
+ ref = InferenceSession(
+ model.SerializeToString(), providers=["CPUExecutionProvider"]
+ )
+ X = np.random.rand(3, 4).astype(np.float32)
+ Y = np.random.rand(3, 4).astype(np.float32)
+ expected = ((X - Y) ** 2).sum(keepdims=1)
+ got = ref.run(None, {"X": X, "Y": Y})[0]
+ self.assertEqualArray(expected, got, atol=1e-6)
+
+
+if __name__ == "__main__":
+ unittest.main(verbosity=2)
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index f0f887b..89a4ed9 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -80,9 +80,6 @@ jobs:
- script: |
ruff .
displayName: 'Ruff'
- - script: |
- rstcheck -r ./_doc ./onnx_array_api
- displayName: 'rstcheck'
- script: |
black --diff .
displayName: 'Black'
@@ -177,9 +174,6 @@ jobs:
- script: |
ruff .
displayName: 'Ruff'
- - script: |
- rstcheck -r ./_doc ./onnx_array_api
- displayName: 'rstcheck'
- script: |
black --diff .
displayName: 'Black'
diff --git a/onnx_array_api/__init__.py b/onnx_array_api/__init__.py
index 316abf4..b2a711d 100644
--- a/onnx_array_api/__init__.py
+++ b/onnx_array_api/__init__.py
@@ -1,6 +1,6 @@
# coding: utf-8
"""
-(Numpy) Array API for ONNX.
+APIs to create ONNX Graphs.
"""
__version__ = "0.1.2"
diff --git a/onnx_array_api/light_api/_op_var.py b/onnx_array_api/light_api/_op_var.py
index 6b511c5..e2354eb 100644
--- a/onnx_array_api/light_api/_op_var.py
+++ b/onnx_array_api/light_api/_op_var.py
@@ -186,6 +186,90 @@ def RandomUniformLike(
"RandomUniformLike", self, dtype=dtype, high=high, low=low, seed=seed
)
+ def ReduceL1(self, keepdims: int = 1, noop_with_empty_axes: int = 0) -> "Var":
+ return self.make_node(
+ "ReduceL1",
+ self,
+ keepdims=keepdims,
+ noop_with_empty_axes=noop_with_empty_axes,
+ )
+
+ def ReduceL2(self, keepdims: int = 1, noop_with_empty_axes: int = 0) -> "Var":
+ return self.make_node(
+ "ReduceL2",
+ self,
+ keepdims=keepdims,
+ noop_with_empty_axes=noop_with_empty_axes,
+ )
+
+ def ReduceLogSum(self, keepdims: int = 1, noop_with_empty_axes: int = 0) -> "Var":
+ return self.make_node(
+ "ReduceLogSum",
+ self,
+ keepdims=keepdims,
+ noop_with_empty_axes=noop_with_empty_axes,
+ )
+
+ def ReduceLogSumExp(
+ self, keepdims: int = 1, noop_with_empty_axes: int = 0
+ ) -> "Var":
+ return self.make_node(
+ "ReduceLogSumExp",
+ self,
+ keepdims=keepdims,
+ noop_with_empty_axes=noop_with_empty_axes,
+ )
+
+ def ReduceMax(self, keepdims: int = 1, noop_with_empty_axes: int = 0) -> "Var":
+ return self.make_node(
+ "ReduceMax",
+ self,
+ keepdims=keepdims,
+ noop_with_empty_axes=noop_with_empty_axes,
+ )
+
+ def ReduceMean(self, keepdims: int = 1, noop_with_empty_axes: int = 0) -> "Var":
+ return self.make_node(
+ "ReduceMean",
+ self,
+ keepdims=keepdims,
+ noop_with_empty_axes=noop_with_empty_axes,
+ )
+
+ def ReduceMin(self, keepdims: int = 1, noop_with_empty_axes: int = 0) -> "Var":
+ return self.make_node(
+ "ReduceMin",
+ self,
+ keepdims=keepdims,
+ noop_with_empty_axes=noop_with_empty_axes,
+ )
+
+ def ReduceProd(self, keepdims: int = 1, noop_with_empty_axes: int = 0) -> "Var":
+ return self.make_node(
+ "ReduceProd",
+ self,
+ keepdims=keepdims,
+ noop_with_empty_axes=noop_with_empty_axes,
+ )
+
+ def ReduceSum(self, keepdims: int = 1, noop_with_empty_axes: int = 0) -> "Var":
+ return self.make_node(
+ "ReduceSum",
+ self,
+ keepdims=keepdims,
+ noop_with_empty_axes=noop_with_empty_axes,
+ )
+
+ def ReduceSumSquare(
+ self, keepdims: int = 1, noop_with_empty_axes: int = 0
+ ) -> "Var":
+ return self.make_node(
+ "ReduceSumSquare",
+ self,
+ keepdims=keepdims,
+ noop_with_empty_axes=noop_with_empty_axes,
+ )
+
def Selu(
self, alpha: float = 1.6732631921768188, gamma: float = 1.0507010221481323
) -> "Var":
diff --git a/onnx_array_api/light_api/model.py b/onnx_array_api/light_api/model.py
index def6cc1..090e29c 100644
--- a/onnx_array_api/light_api/model.py
+++ b/onnx_array_api/light_api/model.py
@@ -95,6 +95,8 @@ def unique_name(self, prefix="r", value: Optional[Any] = None) -> str:
:param value: this name is mapped to this value
:return: unique name
"""
+ if isinstance(value, int):
+ raise TypeError(f"Unexpected type {type(value)}, prefix={prefix!r}.")
name = prefix
i = len(self.unique_names_)
while name in self.unique_names_:
@@ -210,7 +212,10 @@ def make_node(
:return: NodeProto
"""
if output_names is None:
- output_names = [self.unique_name(value=i) for i in range(n_outputs)]
+ output_names = [
+ self.unique_name(prefix=f"r{len(self.nodes)}_{i}")
+ for i in range(n_outputs)
+ ]
elif n_outputs != len(output_names):
raise ValueError(
f"Expecting {n_outputs} outputs but received {output_names}."
@@ -233,6 +238,10 @@ def true_name(self, name: str) -> str:
Some names were renamed. If name is one of them, the function
returns the new name.
"""
+ if not isinstance(name, str):
+ raise TypeError(
+ f"Unexpected type {type(name)}, rename must be placed before vout."
+ )
while name in self.renames_:
name = self.renames_[name]
return name
@@ -242,6 +251,8 @@ def get_var(self, name: str) -> "Var":
tr = self.true_name(name)
proto = self.unique_names_[tr]
+ if proto is None:
+ return Var(self, name)
if isinstance(proto, ValueInfoProto):
return Var(
self,
@@ -267,7 +278,12 @@ def rename(self, old_name: str, new_name: str):
raise RuntimeError(f"Name {old_name!r} does not exist.")
if self.has_name(new_name):
raise RuntimeError(f"Name {old_name!r} already exist.")
- self.unique_names_[new_name] = self.unique_names_[old_name]
+ value = self.unique_names_[old_name]
+ if isinstance(value, int):
+ raise TypeError(
+ f"Unexpected type {type(value)} for value {old_name!r} renamed into {new_name!r}."
+ )
+ self.unique_names_[new_name] = value
self.renames_[old_name] = new_name
def _fix_name_tensor(
diff --git a/onnx_array_api/light_api/var.py b/onnx_array_api/light_api/var.py
index 9fc9b85..6da1ee3 100644
--- a/onnx_array_api/light_api/var.py
+++ b/onnx_array_api/light_api/var.py
@@ -298,3 +298,7 @@ def _check_nin(self, n_inputs):
if len(self) != n_inputs:
raise RuntimeError(f"Expecting {n_inputs} inputs not {len(self)}.")
return self
+
+ def rename(self, new_name: str) -> "Var":
+ "Renames variables."
+ raise NotImplementedError("Not yet implemented.")
diff --git a/onnx_array_api/validation/docs.py b/onnx_array_api/validation/docs.py
new file mode 100644
index 0000000..d1a8422
--- /dev/null
+++ b/onnx_array_api/validation/docs.py
@@ -0,0 +1,64 @@
+from typing import Optional, Tuple
+import numpy as np
+import onnx
+import onnx.helper as oh
+
+
+def make_euclidean(
+ input_names: Tuple[str] = ("X", "Y"),
+ output_name: str = "Z",
+ elem_type: int = onnx.TensorProto.FLOAT,
+ opset: Optional[int] = None,
+) -> onnx.ModelProto:
+ """
+ Creates the onnx graph corresponding to the euclidean distance.
+
+ :param input_names: names of the inputs
+ :param output_name: name of the output
+ :param elem_type: onnx is strongly types, which type is it?
+ :param opset: opset version
+ :return: onnx.ModelProto
+ """
+ if opset is None:
+ opset = onnx.defs.onnx_opset_version()
+
+ X = oh.make_tensor_value_info(input_names[0], elem_type, None)
+ Y = oh.make_tensor_value_info(input_names[1], elem_type, None)
+ Z = oh.make_tensor_value_info(output_name, elem_type, None)
+ two = oh.make_tensor("two", onnx.TensorProto.INT64, [1], [2])
+ n1 = oh.make_node("Sub", ["X", "Y"], ["dxy"])
+ n2 = oh.make_node("Pow", ["dxy", "two"], ["dxy2"])
+ n3 = oh.make_node("ReduceSum", ["dxy2"], [output_name])
+ graph = oh.make_graph([n1, n2, n3], "euclidian", [X, Y], [Z], [two])
+ model = oh.make_model(graph, opset_imports=[oh.make_opsetid("", opset)])
+ return model
+
+
+def make_euclidean_skl2onnx(
+ input_names: Tuple[str] = ("X", "Y"),
+ output_name: str = "Z",
+ elem_type: int = onnx.TensorProto.FLOAT,
+ opset: Optional[int] = None,
+) -> onnx.ModelProto:
+ """
+ Creates the onnx graph corresponding to the euclidean distance
+ with :epkg:`sklearn-onnx`.
+
+ :param input_names: names of the inputs
+ :param output_name: name of the output
+ :param elem_type: onnx is strongly types, which type is it?
+ :param opset: opset version
+ :return: onnx.ModelProto
+ """
+ if opset is None:
+ opset = onnx.defs.onnx_opset_version()
+
+ from skl2onnx.algebra.onnx_ops import OnnxSub, OnnxPow, OnnxReduceSum
+
+ dxy = OnnxSub(input_names[0], input_names[1], op_version=opset)
+ dxy2 = OnnxPow(dxy, np.array([2], dtype=np.int64), op_version=opset)
+ final = OnnxReduceSum(dxy2, op_version=opset, output_names=[output_name])
+
+ np_type = oh.tensor_dtype_to_np_dtype(elem_type)
+ dummy = np.empty([1], np_type)
+ return final.to_onnx({"X": dummy, "Y": dummy})
diff --git a/pyproject.toml b/pyproject.toml
index 3e85f19..4101adf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,15 +1,3 @@
-[tool.rstcheck]
-report_level = "INFO"
-ignore_directives = [
- "autoclass",
- "autofunction",
- "automodule",
- "gdot",
- "image-sg",
- "runpython",
-]
-ignore_roles = ["epkg"]
-
[tool.ruff]
# Exclude a variety of commonly ignored directories.
diff --git a/requirements-dev.txt b/requirements-dev.txt
index ce6ab52..5804529 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -19,7 +19,6 @@ Pillow
psutil
pytest
pytest-cov
-rstcheck[sphinx,toml]
sphinx-issues
git+https://github.com/sdpython/sphinx-runpython.git
ruff
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy