{ "cells": [ { "cell_type": "markdown", "id": "28f96d8a-c7b3-4685-aa24-2b0ede14807f", "metadata": {}, "source": [ "# Tutorial" ] }, { "cell_type": "markdown", "id": "87ab6f0e-e0a4-4fa6-817d-4d350f498a40", "metadata": {}, "source": [ "## Introduction" ] }, { "cell_type": "code", "execution_count": 1, "id": "0b7cc883-5089-4407-b908-271dd1a24841", "metadata": { "tags": [] }, "outputs": [], "source": [ "import numpy as np\n", "import spox" ] }, { "cell_type": "markdown", "id": "249f3e02-1fd5-4142-a2d5-e5a2ae49662f", "metadata": { "tags": [] }, "source": [ "### Types" ] }, { "cell_type": "markdown", "id": "58fbc5ce-d274-451c-913e-a90c92d9f2d0", "metadata": {}, "source": [ "Before creating an ONNX model we will have to specify argument argument types, so that Spox can conduct inference and create a correct model. \n", "\n", "Spox exposes types like `Tensor`, `Sequence`, `Optional`. `Tensor` is the most common." ] }, { "cell_type": "markdown", "id": "47b7bfb7-68cc-4892-ac4c-7b4f3c14992c", "metadata": {}, "source": [ "In this example, we specify:\n", "\n", "\n", "- Vector of 32-bit floats\n", "- K by 2 matrix (K pairs) of 64-bit integers.\n", "- Array of strings of unknown shape." ] }, { "cell_type": "code", "execution_count": 2, "id": "7cffd312-03b8-4ff2-9a7f-4acc3ec045c6", "metadata": { "tags": [] }, "outputs": [], "source": [ "VectorF32 = spox.Tensor(np.float32, (None,))\n", "KPairsI64 = spox.Tensor(np.int64, ('K', 2))\n", "Strings = spox.Tensor(np.str_)" ] }, { "cell_type": "markdown", "id": "727bc6ea-afad-4b62-bde0-fd0de7fbb560", "metadata": {}, "source": [ "In shape dimensions:\n", "\n", "- Integers are used for constant, known dimensions.\n", "- Strings and `None` are used for dimensions unknown at runtime.\n", "\n", "- With strings we can add basic annotations for dimensions that are the same accross many shapes (and tensors).\n", "\n", "Sometimes (especially if shape inference fails), a shape is entirely unknown, including its rank. This may be an error when specifying the entire model, but is nevertheless possible.\n", "\n", "Spox provides some neater C-style string representations for this." ] }, { "cell_type": "code", "execution_count": 3, "id": "ed672f2b-326d-43b9-afd1-96d6c68db585", "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "VectorF32 = float32[?]\n", "KPairsI64 = int64[K][2]\n", "Strings = str[...]\n" ] } ], "source": [ "print(f\"{VectorF32 = !s}\")\n", "print(f\"{KPairsI64 = !s}\")\n", "print(f\"{Strings = !s}\")" ] }, { "cell_type": "markdown", "id": "2936ab50-dcf4-40bd-9e54-51ddfdac8a2e", "metadata": {}, "source": [ "### Vars" ] }, { "cell_type": "markdown", "id": "db277c34-6b87-4eb1-b179-4adefaa658f2", "metadata": {}, "source": [ "To start performing any interesting computation, you have to get your hands on an instance of `Var`.\n", "\n", "You could create a constant-valued Var with a respective operator, but it's more useful to create an **argument** (a placeholder for a value), which will later become a **model input**.\n", "\n", "The function used for creating an argument is `spox.argument(typ: Type) -> Var`." ] }, { "cell_type": "code", "execution_count": 4, "id": "9291d186-da97-4ed8-8a1a-2bea6464e200", "metadata": { "tags": [] }, "outputs": [], "source": [ "u = spox.argument(VectorF32)\n", "v = spox.argument(VectorF32)" ] }, { "cell_type": "markdown", "id": "6eaa952a-729d-47a3-853b-7e91afe7aff1", "metadata": {}, "source": [ "String representations for Vars tell you a bit on what information they store:" ] }, { "cell_type": "code", "execution_count": 5, "id": "7bebdd13-cd62-4c67-97b1-1325d91837fd", "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "u = arg of float32[?]>\n" ] } ], "source": [ "print(f\"{u = !s}\")" ] }, { "cell_type": "markdown", "id": "025d6828-d9ed-4436-9c9d-0d2ca99fd683", "metadata": {}, "source": [ "As we expected, `u` is an argument `Var`, which is a vector of 32-bit floats." ] }, { "cell_type": "markdown", "id": "ba35d8d2-a68c-4800-872a-a9f66fb0ef3c", "metadata": {}, "source": [ "## Construction" ] }, { "cell_type": "markdown", "id": "6a31fd23-2fcd-4ac5-8c04-f2b2fd77e656", "metadata": {}, "source": [ "### Importing opsets" ] }, { "cell_type": "markdown", "id": "8612cd9d-99a7-47e4-bff2-594b4b5f4571", "metadata": {}, "source": [ "In ONNX, operators live in domains (kind of like packages), which are versioned with natural numbers.\n", "\n", "- For example, the current (default) domain is `ai.onnx` at version 17.\n", "- Another standard domain that we often use, as it is useful for traditional ML, is `ai.onnx.ml` - currently at version 3.\n", "\n", "A domain and a version is often called an *opset* (operator set). Spox exposes them as Python modules, where all the definitions for included operators reside:" ] }, { "cell_type": "code", "execution_count": 6, "id": "f40575df-3b29-497d-805a-76653c6f4601", "metadata": { "tags": [] }, "outputs": [], "source": [ "import spox.opset.ai.onnx.v17 as op # op - ai.onnx@17\n", "import spox.opset.ai.onnx.ml.v3 as ml # ml - ai.onnx.ml@3" ] }, { "cell_type": "markdown", "id": "bc8511b5-fbc8-45a3-a9a4-ccfdef5149e5", "metadata": {}, "source": [ "These operators are exposed as *operator constructors*, which are just Python functions that you give Vars and other parameters to. \n", "They return more Vars.\n", "\n", "For example, the `ai.onnx@17::Add` operator has the constructor `add`:\n", "\n", "---" ] }, { "cell_type": "code", "execution_count": 7, "id": "98c79a5b-52c0-4b95-85b4-7cca0fe40a19", "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on function add in module spox.opset.ai.onnx.v17:\n", "\n", "add(A: spox._var.Var, B: spox._var.Var) -> spox._var.Var\n", " Performs element-wise binary addition (with Numpy-style broadcasting\n", " support).\n", " \n", " This operator supports **multidirectional (i.e., Numpy-style)\n", " broadcasting**; for more details please check `the\n", " doc `__.\n", " \n", " (Opset 14 change): Extend supported types to include uint8, int8,\n", " uint16, and int16.\n", " \n", " Parameters\n", " ==========\n", " A\n", " Type T.\n", " First operand.\n", " B\n", " Type T.\n", " Second operand.\n", " \n", " Returns\n", " =======\n", " C : Var\n", " Type T.\n", " Result, has same element type as two inputs\n", " \n", " Notes\n", " =====\n", " Signature: ``ai.onnx@14::Add``.\n", " \n", " Type constraints:\n", " - T: `tensor(bfloat16)`, `tensor(double)`, `tensor(float)`, `tensor(float16)`, `tensor(int16)`, `tensor(int32)`, `tensor(int64)`, `tensor(int8)`, `tensor(uint16)`, `tensor(uint32)`, `tensor(uint64)`, `tensor(uint8)`\n", "\n" ] } ], "source": [ "help(op.add)" ] }, { "cell_type": "markdown", "id": "a1372cfc-dff1-4302-8f8b-77db22e9d57e", "metadata": {}, "source": [ "**This doc is auto-generated with internal schema docs and data within the `onnx` reference implementation.**" ] }, { "cell_type": "markdown", "id": "663a04e8-446c-4ea5-b413-cce203b70027", "metadata": {}, "source": [ "---" ] }, { "cell_type": "markdown", "id": "5b2edffb-bc37-4a6c-b208-b32ee7c20802", "metadata": {}, "source": [ "### Applying operators" ] }, { "cell_type": "markdown", "id": "a2f2a562-8490-4af3-8302-d96c803e258c", "metadata": {}, "source": [ "We will now define the example network from the slides. Start with the necessary arguments:" ] }, { "cell_type": "code", "execution_count": 8, "id": "8bcfde4b-69ba-47e0-85ef-7a3399a78c59", "metadata": { "tags": [] }, "outputs": [], "source": [ "a = spox.argument(spox.Tensor(np.int64, (\"N\", \"M\")))\n", "b = spox.argument(spox.Tensor(np.int64, (\"N\", 1)))\n", "x = spox.argument(spox.Tensor(np.int64, (\"M\", 1)))" ] }, { "cell_type": "markdown", "id": "e2d096aa-f284-4be5-bfc6-049bcc3c7fbf", "metadata": {}, "source": [ "Now, apply all of the necessary operators. Notice that intermediate values are just Vars that we pass on, and Spox is typing them!" ] }, { "cell_type": "code", "execution_count": 9, "id": "134dd41c-de38-449c-90e8-7b8c5bd60917", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "Y of int64[N][1]>" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ax = op.matmul(a, x)\n", "ax" ] }, { "cell_type": "code", "execution_count": 10, "id": "11962719-388a-473d-b1d2-b2f6ce4a56cc", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "reduced of int64[1][1]>" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "summed = op.reduce_sum(op.add(b, ax))\n", "summed" ] }, { "cell_type": "markdown", "id": "36827e2c-54a0-4240-9349-6747498b7a83", "metadata": {}, "source": [ "As we're reshaping this into a scalar at the end (scalars have an empty shape), we need to construct a Var representing that constant.\n" ] }, { "cell_type": "code", "execution_count": 11, "id": "60d40d16-448d-428c-8eab-713429a798a2", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "output of int64[0] = []>" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "empty_shape = op.constant(value=np.array([], dtype=np.int64))\n", "empty_shape" ] }, { "cell_type": "code", "execution_count": 12, "id": "2f3387ce-0a44-4625-9a3b-41f53f48b77f", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "reshaped of int64>" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "result = op.reshape(summed, empty_shape)\n", "result" ] }, { "cell_type": "markdown", "id": "dbece477-a36b-4144-8a25-1bd5a049a53c", "metadata": {}, "source": [ "## Exceptions" ] }, { "cell_type": "markdown", "id": "f17a955f-7d4c-41bf-be36-9c041be9d3cc", "metadata": {}, "source": [ "This is a short demonstration that Spox will stop you from making ONNX type errors as soon as possible." ] }, { "cell_type": "code", "execution_count": 13, "id": "ea78eff4-7492-4445-8fcb-68b071a5acaa", "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "InferenceError: [ShapeInferenceError] (op_type:Add, node name: _this_): B typestr: T, has unsupported type: tensor(string) -- for Add: inputs [A: int64[N][M], B: str = ]\n" ] } ], "source": [ "try:\n", " # Add a matrix of integers to a string\n", " op.add(a, op.constant(value_string=\"abc\"))\n", "except Exception as e:\n", " print(f\"{type(e).__name__}: {e}\")" ] }, { "cell_type": "markdown", "id": "ca66e5b2-6b9a-4007-a6f1-a46b9b724542", "metadata": {}, "source": [ "The slightly unfortunate formatting of the error is due to the fact it is *hardcoded in ONNX's C++ implementation* of type inference. \n", "\n", "What we *can* read from it, though:\n", "\n", "- The traceback (not displayed here) does lead to the offending line - the failing operator is an Add.\n", "- We inputted types `int64[N][M]` and a `str` (as a side note, here Spox knows it's of constant value `\"abc\"`).\n", "- The input called `B` of type variable `T` - these are explained in the docstring - was a tensor of strings, which is unsupported." ] }, { "cell_type": "code", "execution_count": 14, "id": "50e83656-bc86-4e06-b981-83dbb227f979", "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "InferenceError: [ShapeInferenceError] Shape inference error(s): (op_type:Add, node name: _this_): [ShapeInferenceError] Incompatible dimensions\n", " -- for Add: inputs [A: int64[2] = , B: int64[3] = ]\n" ] } ], "source": [ "try:\n", " # Add mismatched-length vectors\n", " op.add(op.const([1, 2]), op.const([1, 2, 3]))\n", "except Exception as e:\n", " print(f\"{type(e).__name__}: {e}\")" ] }, { "cell_type": "markdown", "id": "f15899f8-a9d8-4b6f-925e-a85a54925c2c", "metadata": {}, "source": [ "## Building" ] }, { "cell_type": "markdown", "id": "941f688f-0e30-4e84-a900-ca866f1844b6", "metadata": {}, "source": [ "Once you've constructed the computation you want, the last step is to _build_ an `onnx.ModelProto`. First, locate all the results you would like to include and arguments you used to compute them:\n" ] }, { "cell_type": "code", "execution_count": 15, "id": "0ae3ff88", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "(arg of int64[N][M]>,\n", " arg of int64[N][1]>,\n", " arg of int64[M][1]>)" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "a, b, x" ] }, { "cell_type": "code", "execution_count": 16, "id": "a73be142-f6ab-40f1-9321-1a5ae85e50b2", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "reshaped of int64>" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "result" ] }, { "cell_type": "markdown", "id": "78eebbf0", "metadata": {}, "source": [ "Once you've reached this step there is nothing more to do than invoke the compilation process and get an ONNX model.\n", "\n", "The `build(inputs: dict[str, Var], outputs: dict[str, Var]) -> onnx.ModelProto` takes the 'signature' of the built model. The dictionaries specify the name and ordering of the model inputs/outputs." ] }, { "cell_type": "code", "execution_count": 17, "id": "88b84508-0f50-4558-a10a-2c6571de4a43", "metadata": { "tags": [] }, "outputs": [], "source": [ "onnx_model = spox.build({'a': a, 'b': b, 'x': x}, {'y': result})" ] }, { "cell_type": "markdown", "id": "2dad0871-7c01-4663-83dc-04f4b889f9b8", "metadata": { "tags": [] }, "source": [ "## Working with ONNX" ] }, { "cell_type": "markdown", "id": "e2b2ba1b", "metadata": {}, "source": [ "### Exploring" ] }, { "cell_type": "markdown", "id": "6c46efd5-6c10-48e0-ba9c-2c0744422434", "metadata": {}, "source": [ "We start with importing the ONNX reference implementation and ONNX Runtime (ORT)." ] }, { "cell_type": "code", "execution_count": 18, "id": "413e91c4-4e80-4eeb-9052-45b77dc7c80b", "metadata": { "tags": [] }, "outputs": [], "source": [ "import onnx\n", "import onnxruntime" ] }, { "cell_type": "markdown", "id": "406b4526-9204-4bf2-9296-bf1892bb3983", "metadata": {}, "source": [ "### Saving and visualisation" ] }, { "cell_type": "markdown", "id": "e3b93fed-02bf-4193-b322-4ff18a4b1670", "metadata": {}, "source": [ "Our model is serializable to a bunch of bytes (since it's a protobuf object):" ] }, { "cell_type": "code", "execution_count": 19, "id": "59feb2e1-66ac-4523-aee4-8d35edee39e4", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "onnx.onnx_ml_pb2.ModelProto" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "type(onnx_model)" ] }, { "cell_type": "code", "execution_count": 20, "id": "ff829ba6-07ac-40f2-bde8-722c8cc3ea8d", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "b'\\x08\\x08\\x12\\x04spox2\\x00:\\xef\\x03\\n&\\n\\x01a\\n\\x01x\\x12\\nMatMul_0_Y\\x1a\\x08MatMul_0\"\\x06MatMul:\\x00\\n&\\n\\x01b\\n\\nMatMul_0_Y\\x12\\x07Add_0_C\\x1a\\x05Add_0\"\\x03Add:\\x00\\nf\\n\\x07Add_0_C\\x12\\x13ReduceSum_0_reduced\\x1a\\x0bReduceSum_0\"\\tReduceSum*\\x0f\\n\\x08keepdims\\x18\\x01\\xa0\\x01\\x02*\\x1b\\n\\x14noop_with_empty_axes\\x18\\x00\\xa0\\x01\\x02:\\x00\\n?\\x12\\x11Constant_0_output\\x1a\\nConstant_0\"\\x08Constant*\\x12\\n\\x05value*\\x06\\x08\\x00\\x10\\x07B\\x00\\xa0\\x01\\x04:\\x00\\nd\\n\\x13ReduceSum_0_reduced\\n\\x11Constant_0_output\\x12\\x12Reshape_0_reshaped\\x1a\\tReshape_0\"\\x07Reshape*\\x10\\n\\tallowzero\\x18\\x00\\xa0\\x01\\x02:\\x00\\n2\\n\\x12Reshape_0_reshaped\\x12\\x01y\\x1a\\x0fIntroduce_0_id0\"\\x08Identity\\x12\\nspox_graphZ\\x15\\n\\x01a\\x12\\x10\\n\\x0e\\x08\\x07\\x12\\n\\n\\x03\\x12\\x01N\\n\\x03\\x12\\x01MZ\\x14\\n\\x01b\\x12\\x0f\\n\\r\\x08\\x07\\x12\\t\\n\\x03\\x12\\x01N\\n\\x02\\x08\\x01Z\\x14\\n\\x01x\\x12\\x0f\\n\\r\\x08\\x07\\x12\\t\\n\\x03\\x12\\x01M\\n\\x02\\x08\\x01b\\x0b\\n\\x01y\\x12\\x06\\n\\x04\\x08\\x07\\x12\\x00B\\x04\\n\\x00\\x10\\x0e'" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "onnx_model.SerializeToString()" ] }, { "cell_type": "markdown", "id": "5f619469-f321-475c-a381-c974350952c4", "metadata": {}, "source": [ "It also has a pretty-print:" ] }, { "cell_type": "code", "execution_count": 21, "id": "10fdd73d-4951-4afb-80f0-770e108ab1cd", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "ir_version: 8\n", "producer_name: \"spox\"\n", "doc_string: \"\"\n", "graph {\n", " node {\n", " input: \"a\"\n", " input: \"x\"\n", " output: \"MatMul_0_Y\"\n", " name: \"MatMul_0\"\n", " op_type: \"MatMul\"\n", " domain: \"\"\n", " }\n", " node {\n", " input: \"b\"\n", " input: \"MatMul_0_Y\"\n", " output: \"Add_0_C\"\n", " name: \"Add_0\"\n", " op_type: \"Add\"\n", " domain: \"\"\n", " }\n", " node {\n", " input: \"Add_0_C\"\n", " output: \"ReduceSum_0_reduced\"\n", " name: \"ReduceSum_0\"\n", " op_type: \"ReduceSum\"\n", " attribute {\n", " name: \"keepdims\"\n", " i: 1\n", " type: INT\n", " }\n", " attribute {\n", " name: \"noop_with_empty_axes\"\n", " i: 0\n", " type: INT\n", " }\n", " domain: \"\"\n", " }\n", " node {\n", " output: \"Constant_0_output\"\n", " name: \"Constant_0\"\n", " op_type: \"Constant\"\n", " attribute {\n", " name: \"value\"\n", " t {\n", " dims: 0\n", " data_type: 7\n", " name: \"\"\n", " }\n", " type: TENSOR\n", " }\n", " domain: \"\"\n", " }\n", " node {\n", " input: \"ReduceSum_0_reduced\"\n", " input: \"Constant_0_output\"\n", " output: \"Reshape_0_reshaped\"\n", " name: \"Reshape_0\"\n", " op_type: \"Reshape\"\n", " attribute {\n", " name: \"allowzero\"\n", " i: 0\n", " type: INT\n", " }\n", " domain: \"\"\n", " }\n", " node {\n", " input: \"Reshape_0_reshaped\"\n", " output: \"y\"\n", " name: \"Introduce_0_id0\"\n", " op_type: \"Identity\"\n", " }\n", " name: \"spox_graph\"\n", " input {\n", " name: \"a\"\n", " type {\n", " tensor_type {\n", " elem_type: 7\n", " shape {\n", " dim {\n", " dim_param: \"N\"\n", " }\n", " dim {\n", " dim_param: \"M\"\n", " }\n", " }\n", " }\n", " }\n", " }\n", " input {\n", " name: \"b\"\n", " type {\n", " tensor_type {\n", " elem_type: 7\n", " shape {\n", " dim {\n", " dim_param: \"N\"\n", " }\n", " dim {\n", " dim_value: 1\n", " }\n", " }\n", " }\n", " }\n", " }\n", " input {\n", " name: \"x\"\n", " type {\n", " tensor_type {\n", " elem_type: 7\n", " shape {\n", " dim {\n", " dim_param: \"M\"\n", " }\n", " dim {\n", " dim_value: 1\n", " }\n", " }\n", " }\n", " }\n", " }\n", " output {\n", " name: \"y\"\n", " type {\n", " tensor_type {\n", " elem_type: 7\n", " shape {\n", " }\n", " }\n", " }\n", " }\n", "}\n", "opset_import {\n", " domain: \"\"\n", " version: 14\n", "}" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "onnx_model" ] }, { "cell_type": "markdown", "id": "00888fe8-5e8f-4e4b-b5b8-6d1854b164c3", "metadata": {}, "source": [ "We could save it to an ONNX file and then visualise it at https://netron.app by a call like `onnx.save(onnx_model, \"example.onnx\")`." ] }, { "cell_type": "markdown", "id": "23c307bc-8224-406c-9d8f-b0d70db5015b", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "5a95890a-c6c5-4cae-a998-511f79677ebd", "metadata": {}, "source": [ "*Nota bene*: the extra Identity at the end is a technical requirement of ONNX in some cases. It can be easily got rid of by using a model optimizer after compilation." ] }, { "cell_type": "markdown", "id": "d3c9e2d0-8812-4286-beb9-5c06b71742e7", "metadata": {}, "source": [ "### Running" ] }, { "cell_type": "markdown", "id": "3c8435e5-d4c2-4b3d-a71c-0a3c97aa8eba", "metadata": {}, "source": [ "We use ONNX Runtime's Python bindings to load and run the model.\n", "\n", "Notice that Spox is completely independent of this, as we are loading the model from the filesystem." ] }, { "cell_type": "code", "execution_count": 22, "id": "7001a691-a69e-487c-bbcd-2e0ce6f00b2c", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "[array(21, dtype=int64)]" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "onnxruntime.InferenceSession(onnx_model.SerializeToString()).run(None, {\n", " 'a': np.array([[1, 1], [1, 0]]), # Fibonacci's sequence matrix\n", " 'x': np.array([[8], [5]]), # F_6, F_5\n", " 'b': np.array([[1], [-1]]) # 1 - 1 = 0\n", "}) # -> F_8" ] }, { "cell_type": "markdown", "id": "5d962dac-c676-4958-be87-7af114d856d4", "metadata": {}, "source": [ "## Summary" ] }, { "cell_type": "markdown", "id": "1bf827f5-2f7e-4171-b630-9a72af6ed22d", "metadata": {}, "source": [ "A slightly longer program computing Gramian Angular Fields (come up in image processing with NNs, but mostly look nice)." ] }, { "cell_type": "code", "execution_count": 23, "id": "85ab1eae-2bbe-45b2-a0d6-88636cdaed98", "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "u.type = float32[N][1][K], v.type = float32[1][M][K]\n", "gram.type = float32[N][M]\n" ] } ], "source": [ "# Arguments\n", "arg_u = spox.argument(spox.Tensor(np.float32, (\"N\", \"K\")))\n", "arg_v = spox.argument(spox.Tensor(np.float32, (\"M\", \"K\")))\n", "\n", "# Manipulate shapes a bit to exploit broadcasting for an outer product computation\n", "u = op.unsqueeze(arg_u, op.const([1]))\n", "v = op.unsqueeze(arg_v, op.const([0]))\n", "\n", "print(f\"{u.type = !s}, {v.type = !s}\")\n", "\n", "# Dot product, along the last axis\n", "def dot(x: spox.Var, y: spox.Var) -> spox.Var:\n", " # Multiply point wise \n", " return op.reduce_sum(op.mul(x, y), axes=op.const([-1]))\n", "\n", "# Main computation - u*v - sqrt(1 - u^2)*sqrt(1 - v^2)\n", "gram = op.sub(\n", " dot(u, v),\n", " dot(\n", " op.sqrt(op.sub(op.const(1.), op.mul(u, u))),\n", " op.sqrt(op.sub(op.const(1.), op.mul(v, v))),\n", " )\n", ")\n", "gram = op.squeeze(gram, axes=op.const([-1]))\n", "\n", "print(f\"{gram.type = !s}\")\n", "\n", "# Create a graph and run on example\n", "gaf_model = spox.build({'u': arg_u, 'v': arg_v}, {'gram': gram})\n", "gaf_session = onnxruntime.InferenceSession(gaf_model.SerializeToString())\n", "\n", "def gaf(us, vs):\n", " return gaf_session.run(None, {\n", " 'u': np.array(us, dtype=np.float32), 'v': np.array(vs, dtype=np.float32)\n", " })[0]" ] }, { "cell_type": "code", "execution_count": 24, "id": "75e193da-fc3c-4878-8526-0db6c47303e7", "metadata": { "tags": [] }, "outputs": [], "source": [ "import matplotlib.pyplot as plt" ] }, { "cell_type": "code", "execution_count": 25, "id": "9b0638e2-d73f-4458-b50c-15f084160284", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "example_gaf = gaf(\n", " [(np.sin(1.3*x), x/10, np.cos(x)) for x in np.linspace(-10, 10, 48)],\n", " [(np.sin(x), (x/10)**2, 0.7) for x in np.linspace(-10, 10, 48)]\n", ")\n", "plt.imshow(example_gaf)" ] }, { "cell_type": "code", "execution_count": null, "id": "afa59d6e-16b1-4287-a4f6-41819a4cac78", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.0" } }, "nbformat": 4, "nbformat_minor": 5 }