From 63a9de17931f211a16acf7525eeb449cbb45e08a Mon Sep 17 00:00:00 2001 From: Meriel von Stein Date: Sun, 10 Apr 2022 19:06:51 -0400 Subject: [PATCH 01/18] added Split operator --- dnnv/nn/operations/tensor.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index a9a4cf31..3c82d593 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -221,6 +221,22 @@ def from_onnx(cls, onnx_node, *inputs): return cls(*inputs, axes=axes, name=onnx_node.name) +class Split(Operation): + def __init__(self, x, axis, split, *, name: Optional[str] = None): + super().__init__(name=name) + self.x = x + self.axis = axis + self.split = split + + @classmethod + def from_onnx(cls, onnx_node, *inputs): + attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} + axis = attributes.get("axis") + split = attributes.get("split") + split = tuple(split) + return cls(*inputs, axis=axis, split=split, name=onnx_node.name) + + __all__ = [ "Cast", "Concat", @@ -235,4 +251,5 @@ def from_onnx(cls, onnx_node, *inputs): "Tile", "Transpose", "Unsqueeze", + "Split", ] From d379b0a7c21c4129bba2a195181517078613db00 Mon Sep 17 00:00:00 2001 From: Meriel von Stein Date: Mon, 9 May 2022 11:38:06 -0400 Subject: [PATCH 02/18] added tests for Split --- dnnv/nn/converters/onnx.py | 1221 ++++++------ dnnv/nn/converters/tensorflow.py | 1763 +++++++++-------- .../transformers/simplifiers/squeeze_convs.py | 85 +- .../test_converters/test_onnx/test_Split.py | 144 ++ .../test_tensorflow/test_Split.py | 134 ++ 5 files changed, 1832 insertions(+), 1515 deletions(-) create mode 100644 tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py create mode 100644 tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py diff --git a/dnnv/nn/converters/onnx.py b/dnnv/nn/converters/onnx.py index f20bb4a2..7fecc50d 100644 --- a/dnnv/nn/converters/onnx.py +++ b/dnnv/nn/converters/onnx.py @@ -1,599 +1,622 @@ -from dnnv.nn.operations.base import OutputSelect -import numpy as np -import onnx - -from collections import defaultdict -from typing import Any, Dict, List, Union - -from .. import operations -from ..graph import OperationGraph -from ..operations import Operation -from ..utils import NUMPY_TO_ONNX_DTYPE -from ..visitors import OperationVisitor - - -def convert(op_graph: OperationGraph, *, add_missing_optional_inputs=False): - converter = OnnxConverter( - op_graph, add_missing_optional_inputs=add_missing_optional_inputs - ) - model = converter.convert() - - return model - - -class OnnxConverter(OperationVisitor): - def __init__(self, op_graph: OperationGraph, add_missing_optional_inputs=False): - self.op_graph = op_graph - self.inputs: List[onnx.ValueInfoProto] = [] - self.outputs: List[onnx.ValueInfoProto] = [] - self.initializer: List[onnx.TensorProto] = [] - self.visited: Dict[Operation, onnx.NodeProto] = {} - self.op_counts: Dict[str, int] = defaultdict(int) - self.add_missing_optional_inputs = add_missing_optional_inputs - - def convert(self, name="onnx_model") -> onnx.ModelProto: - output_details = ( - self.op_graph.output_details - ) # TODO: don't rely on tensorflow converter - for op, (shape, dtype) in zip(self.op_graph.output_operations, output_details): - output_op = self.visit(op) - node = onnx.helper.make_tensor_value_info( - output_op.name, NUMPY_TO_ONNX_DTYPE[dtype], shape - ) - self.outputs.append(node) - - nodes = [n for n in self.visited.values() if isinstance(n, onnx.NodeProto)] - graph_def = onnx.helper.make_graph( - nodes, - name, - self.inputs, - self.outputs, - initializer=self.initializer, - ) - # TODO : make opset configurable - model_def = onnx.helper.make_model( - graph_def, - producer_name="dnnv", - ir_version=7, - opset_imports=[onnx.helper.make_opsetid("", 13)], - ) - model_def = onnx.shape_inference.infer_shapes(model_def) - onnx.checker.check_model(model_def, full_check=True) - return model_def - - def visit(self, operation: Operation) -> Union[onnx.NodeProto, onnx.ValueInfoProto]: - if operation not in self.visited: - result = super().visit(operation) - self.visited[operation] = result - return self.visited[operation] - - def generic_visit(self, operation: Operation): - if not hasattr(self, "visit_%s" % operation.__class__.__name__): - raise ValueError( - f"ONNX converter not implemented for operation type {type(operation).__name__}" - ) - return super().generic_visit(operation) - - def _to_onnx_proto( - self, value: Any, opname: str - ) -> Union[onnx.NodeProto, onnx.TensorProto, onnx.ValueInfoProto]: - if isinstance(value, Operation): - return self.visit(value) - elif isinstance(value, np.ndarray): - tensor_proto = onnx.numpy_helper.from_array(value, name=opname) - self.initializer.append(tensor_proto) - return tensor_proto - elif isinstance(value, (int, float)): - tensor_proto = onnx.numpy_helper.from_array( - np.array(value, dtype=f"{type(value).__name__}32"), name=opname - ) - self.initializer.append(tensor_proto) - return tensor_proto - raise ValueError(f"Unknown type for operand of {opname}: {type(value)}") - - def visit_Add(self, operation: operations.Add) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - - node = onnx.helper.make_node( - op_type, - inputs=[a.name, b.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Atan(self, operation: operations.Atan) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, inputs=[x.name], outputs=[opname], name=opname - ) - - return node - - def visit_AveragePool(self, operation: operations.AveragePool) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name], - outputs=[opname], - kernel_shape=list(operation.kernel_shape), - ceil_mode=operation.ceil_mode, - count_include_pad=operation.count_include_pad, - strides=list(operation.strides), - pads=list(operation.pads), - name=opname, - ) - - return node - - def visit_BatchNormalization( - self, operation: operations.BatchNormalization - ) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - scale = self._to_onnx_proto(operation.scale, f"{opname}.scale") - bias = self._to_onnx_proto(operation.bias, f"{opname}.bias") - mean = self._to_onnx_proto(operation.mean, f"{opname}.mean") - variance = self._to_onnx_proto(operation.variance, f"{opname}.variance") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name, scale.name, bias.name, mean.name, variance.name], - outputs=[opname], - epsilon=operation.epsilon, - momentum=operation.momentum, - name=opname, - ) - - return node - - def visit_Cast(self, operation: operations.Cast) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - to = operation.to - - node = onnx.helper.make_node( - op_type, inputs=[x.name], outputs=[opname], to=to, name=opname - ) - - return node - - def visit_Concat(self, operation: operations.Concat) -> onnx.NodeProto: - idx = self.op_counts["Concat"] = self.op_counts["Concat"] + 1 - opname = f"Concat_{idx}" - - inputs = [ - self._to_onnx_proto(x, f"{opname}.x{i}") for i, x in enumerate(operation.x) - ] - - node = onnx.helper.make_node( - "Concat", - inputs=[x.name for x in inputs], - outputs=[opname], - axis=operation.axis, - name=opname, - ) - - return node - - def visit_Conv(self, operation: operations.Conv) -> onnx.NodeProto: - idx = self.op_counts["Conv"] = self.op_counts["Conv"] + 1 - opname = f"Conv_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - w = self._to_onnx_proto(operation.w, f"{opname}.w") - inputs = [x.name, w.name] - if operation.b is not None: - b = self._to_onnx_proto(operation.b, f"{opname}.b") - inputs.append(b.name) - elif self.add_missing_optional_inputs: - b_ = np.zeros(w.shape[0], dtype=w.dtype) - b = self._to_onnx_proto(b_, f"{opname}.b") - inputs.append(b.name) - - node = onnx.helper.make_node( - "Conv", - inputs=inputs, - outputs=[opname], - kernel_shape=list(operation.kernel_shape), - strides=list(operation.strides), - dilations=list(operation.dilations), - group=operation.group, - pads=list(operation.pads), - name=opname, - ) - - return node - - def visit_ConvTranspose( - self, operation: operations.ConvTranspose - ) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - w = self._to_onnx_proto(operation.w, f"{opname}.w") - inputs = [x.name, w.name] - if operation.b is not None: - b = self._to_onnx_proto(operation.b, f"{opname}.b") - inputs.append(b.name) - elif self.add_missing_optional_inputs: - b_ = np.zeros(w.shape[1], dtype=w.dtype) - b = self._to_onnx_proto(b_, f"{opname}.b") - inputs.append(b.name) - - extra_attributes = {} - if operation.output_shape is not None: - extra_attributes["output_shape"] = list(operation.output_shape) - node = onnx.helper.make_node( - op_type, - inputs=inputs, - outputs=[opname], - auto_pad=operation.auto_pad, - dilations=list(operation.dilations), - group=operation.group, - kernel_shape=list(operation.kernel_shape), - output_padding=list(operation.output_padding), - pads=list(operation.pads), - strides=list(operation.strides), - name=opname, - **extra_attributes, - ) - - return node - - def visit_Div(self, operation: operations.Div) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - - node = onnx.helper.make_node( - op_type, - inputs=[a.name, b.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Dropout(self, operation: operations.Dropout) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - ratio = self._to_onnx_proto(operation.ratio, f"{opname}.ratio") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name, ratio.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Elu(self, operation: operations.Elu) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name], - alpha=operation.alpha, - outputs=[opname], - name=opname, - ) - - return node - - def visit_Expand(self, operation: operations.Expand) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - shape = self._to_onnx_proto(operation.shape, f"{opname}.shape") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name, shape.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Flatten(self, operation: operations.Flatten) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name], - outputs=[opname], - axis=operation.axis, - name=opname, - ) - - return node - - def visit_Gather(self, operation: operations.Gather) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - indices = self._to_onnx_proto(operation.indices, f"{opname}.indices") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name, indices.name], - outputs=[opname], - axis=operation.axis, - name=opname, - ) - - return node - - def visit_Gemm(self, operation: operations.Gemm) -> onnx.NodeProto: - idx = self.op_counts["Gemm"] = self.op_counts["Gemm"] + 1 - opname = f"Gemm_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - inputs = [a.name, b.name] - if operation.c is not None: - c = self._to_onnx_proto(operation.c, f"{opname}.c") - inputs.append(c.name) - elif self.add_missing_optional_inputs: - output_details = OperationGraph([operation]).output_details[0] - c_ = np.zeros(output_details.shape[1], dtype=output_details.dtype) - c = self._to_onnx_proto(c_, f"{opname}.c") - inputs.append(c.name) - - node = onnx.helper.make_node( - "Gemm", - inputs=inputs, - outputs=[opname], - alpha=operation.alpha, - beta=operation.beta, - transA=operation.transpose_a, - transB=operation.transpose_b, - name=opname, - ) - - return node - - def visit_GlobalAveragePool( - self, operation: operations.GlobalAveragePool - ) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Input(self, operation: operations.Input) -> onnx.ValueInfoProto: - idx = self.op_counts["Input"] = self.op_counts["Input"] + 1 - opname = f"Input_{idx}" - - shape = np.asarray(operation.shape).tolist() - if shape[0] < 0: - shape[0] = 1 - dtype = NUMPY_TO_ONNX_DTYPE[operation.dtype] - - node = onnx.helper.make_tensor_value_info(opname, dtype, shape) - self.inputs.append(node) - - return node - - def visit_MatMul(self, operation: operations.MatMul) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - - node = onnx.helper.make_node( - op_type, - inputs=[a.name, b.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_MaxPool(self, operation: operations.MaxPool) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name], - outputs=[opname], - kernel_shape=list(operation.kernel_shape), - ceil_mode=operation.ceil_mode, - strides=list(operation.strides), - dilations=list(operation.dilations), - pads=list(operation.pads), - storage_order=operation.storage_order, - name=opname, - ) - - return node - - def visit_Mul(self, operation: operations.Mul) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - - node = onnx.helper.make_node( - op_type, - inputs=[a.name, b.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_OutputSelect(self, operation: operations.OutputSelect) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - if operation.index != 0: - raise NotImplementedError( - "Support for operations with multiple ouputs is not yet implemented." - ) - - op = self._to_onnx_proto(operation.operation, f"{opname}.operation") - - node = onnx.helper.make_node( - "Identity", - inputs=[op.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Relu(self, operation: operations.Relu) -> onnx.NodeProto: - idx = self.op_counts["Relu"] = self.op_counts["Relu"] + 1 - opname = f"Relu_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - "Relu", inputs=[x.name], outputs=[opname], name=opname - ) - - return node - - def visit_Reshape(self, operation: operations.Reshape) -> onnx.NodeProto: - idx = self.op_counts["Reshape"] = self.op_counts["Reshape"] + 1 - opname = f"Reshape_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - shape = self._to_onnx_proto(operation.shape, f"{opname}.shape") - - if operation.allowzero: - # TODO : need to use newer onnx opset version - raise ValueError("Reshape allowzero is not yet supported") - - node = onnx.helper.make_node( - "Reshape", - inputs=[x.name, shape.name], - # allowzero=operation.allowzero, - outputs=[opname], - name=opname, - ) - - return node - - def visit_Sigmoid(self, operation: operations.Sigmoid) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, inputs=[x.name], outputs=[opname], name=opname - ) - - return node - - def visit_Sub(self, operation: operations.Sub) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - - node = onnx.helper.make_node( - op_type, - inputs=[a.name, b.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Tanh(self, operation: operations.Tanh) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, inputs=[x.name], outputs=[opname], name=opname - ) - - return node - - def visit_Transpose(self, operation: operations.Transpose) -> onnx.NodeProto: - idx = self.op_counts["Transpose"] = self.op_counts["Transpose"] + 1 - opname = f"Transpose_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - "Transpose", - inputs=[x.name], - outputs=[opname], - name=opname, - perm=list(operation.permutation), - ) - - return node +from dnnv.nn.operations.base import OutputSelect +import numpy as np +import onnx + +from collections import defaultdict +from typing import Any, Dict, List, Union + +from .. import operations +from ..graph import OperationGraph +from ..operations import Operation +from ..utils import NUMPY_TO_ONNX_DTYPE +from ..visitors import OperationVisitor + + +def convert(op_graph: OperationGraph, *, add_missing_optional_inputs=False): + converter = OnnxConverter( + op_graph, add_missing_optional_inputs=add_missing_optional_inputs + ) + model = converter.convert() + + return model + + +class OnnxConverter(OperationVisitor): + def __init__(self, op_graph: OperationGraph, add_missing_optional_inputs=False): + self.op_graph = op_graph + self.inputs: List[onnx.ValueInfoProto] = [] + self.outputs: List[onnx.ValueInfoProto] = [] + self.initializer: List[onnx.TensorProto] = [] + self.visited: Dict[Operation, onnx.NodeProto] = {} + self.op_counts: Dict[str, int] = defaultdict(int) + self.add_missing_optional_inputs = add_missing_optional_inputs + + def convert(self, name="onnx_model") -> onnx.ModelProto: + output_details = ( + self.op_graph.output_details + ) # TODO: don't rely on tensorflow converter + for op, (shape, dtype) in zip(self.op_graph.output_operations, output_details): + output_op = self.visit(op) + node = onnx.helper.make_tensor_value_info( + output_op.name, NUMPY_TO_ONNX_DTYPE[dtype], shape + ) + self.outputs.append(node) + + nodes = [n for n in self.visited.values() if isinstance(n, onnx.NodeProto)] + graph_def = onnx.helper.make_graph( + nodes, + name, + self.inputs, + self.outputs, + initializer=self.initializer, + ) + # TODO : make opset configurable + model_def = onnx.helper.make_model( + graph_def, + producer_name="dnnv", + ir_version=7, + opset_imports=[onnx.helper.make_opsetid("", 13)], + ) + model_def = onnx.shape_inference.infer_shapes(model_def) + onnx.checker.check_model(model_def, full_check=True) + return model_def + + def visit(self, operation: Operation) -> Union[onnx.NodeProto, onnx.ValueInfoProto]: + if operation not in self.visited: + result = super().visit(operation) + self.visited[operation] = result + return self.visited[operation] + + def generic_visit(self, operation: Operation): + if not hasattr(self, "visit_%s" % operation.__class__.__name__): + raise ValueError( + f"ONNX converter not implemented for operation type {type(operation).__name__}" + ) + return super().generic_visit(operation) + + def _to_onnx_proto( + self, value: Any, opname: str + ) -> Union[onnx.NodeProto, onnx.TensorProto, onnx.ValueInfoProto]: + if isinstance(value, Operation): + return self.visit(value) + elif isinstance(value, np.ndarray): + tensor_proto = onnx.numpy_helper.from_array(value, name=opname) + self.initializer.append(tensor_proto) + return tensor_proto + elif isinstance(value, (int, float)): + tensor_proto = onnx.numpy_helper.from_array( + np.array(value, dtype=f"{type(value).__name__}32"), name=opname + ) + self.initializer.append(tensor_proto) + return tensor_proto + raise ValueError(f"Unknown type for operand of {opname}: {type(value)}") + + def visit_Add(self, operation: operations.Add) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + + node = onnx.helper.make_node( + op_type, + inputs=[a.name, b.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Atan(self, operation: operations.Atan) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, inputs=[x.name], outputs=[opname], name=opname + ) + + return node + + def visit_AveragePool(self, operation: operations.AveragePool) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + outputs=[opname], + kernel_shape=list(operation.kernel_shape), + ceil_mode=operation.ceil_mode, + count_include_pad=operation.count_include_pad, + strides=list(operation.strides), + pads=list(operation.pads), + name=opname, + ) + + return node + + def visit_BatchNormalization( + self, operation: operations.BatchNormalization + ) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + scale = self._to_onnx_proto(operation.scale, f"{opname}.scale") + bias = self._to_onnx_proto(operation.bias, f"{opname}.bias") + mean = self._to_onnx_proto(operation.mean, f"{opname}.mean") + variance = self._to_onnx_proto(operation.variance, f"{opname}.variance") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name, scale.name, bias.name, mean.name, variance.name], + outputs=[opname], + epsilon=operation.epsilon, + momentum=operation.momentum, + name=opname, + ) + + return node + + def visit_Cast(self, operation: operations.Cast) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + to = operation.to + + node = onnx.helper.make_node( + op_type, inputs=[x.name], outputs=[opname], to=to, name=opname + ) + + return node + + def visit_Concat(self, operation: operations.Concat) -> onnx.NodeProto: + idx = self.op_counts["Concat"] = self.op_counts["Concat"] + 1 + opname = f"Concat_{idx}" + + inputs = [ + self._to_onnx_proto(x, f"{opname}.x{i}") for i, x in enumerate(operation.x) + ] + + node = onnx.helper.make_node( + "Concat", + inputs=[x.name for x in inputs], + outputs=[opname], + axis=operation.axis, + name=opname, + ) + + return node + + def visit_Conv(self, operation: operations.Conv) -> onnx.NodeProto: + idx = self.op_counts["Conv"] = self.op_counts["Conv"] + 1 + opname = f"Conv_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + w = self._to_onnx_proto(operation.w, f"{opname}.w") + inputs = [x.name, w.name] + if operation.b is not None: + b = self._to_onnx_proto(operation.b, f"{opname}.b") + inputs.append(b.name) + elif self.add_missing_optional_inputs: + b_ = np.zeros(w.shape[0], dtype=w.dtype) + b = self._to_onnx_proto(b_, f"{opname}.b") + inputs.append(b.name) + + node = onnx.helper.make_node( + "Conv", + inputs=inputs, + outputs=[opname], + kernel_shape=list(operation.kernel_shape), + strides=list(operation.strides), + dilations=list(operation.dilations), + group=operation.group, + pads=list(operation.pads), + name=opname, + ) + + return node + + def visit_ConvTranspose( + self, operation: operations.ConvTranspose + ) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + w = self._to_onnx_proto(operation.w, f"{opname}.w") + inputs = [x.name, w.name] + if operation.b is not None: + b = self._to_onnx_proto(operation.b, f"{opname}.b") + inputs.append(b.name) + elif self.add_missing_optional_inputs: + b_ = np.zeros(w.shape[1], dtype=w.dtype) + b = self._to_onnx_proto(b_, f"{opname}.b") + inputs.append(b.name) + + extra_attributes = {} + if operation.output_shape is not None: + extra_attributes["output_shape"] = list(operation.output_shape) + node = onnx.helper.make_node( + op_type, + inputs=inputs, + outputs=[opname], + auto_pad=operation.auto_pad, + dilations=list(operation.dilations), + group=operation.group, + kernel_shape=list(operation.kernel_shape), + output_padding=list(operation.output_padding), + pads=list(operation.pads), + strides=list(operation.strides), + name=opname, + **extra_attributes, + ) + + return node + + def visit_Div(self, operation: operations.Div) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + + node = onnx.helper.make_node( + op_type, + inputs=[a.name, b.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Dropout(self, operation: operations.Dropout) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + ratio = self._to_onnx_proto(operation.ratio, f"{opname}.ratio") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name, ratio.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Elu(self, operation: operations.Elu) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + alpha=operation.alpha, + outputs=[opname], + name=opname, + ) + + return node + + def visit_Expand(self, operation: operations.Expand) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + shape = self._to_onnx_proto(operation.shape, f"{opname}.shape") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name, shape.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Flatten(self, operation: operations.Flatten) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + outputs=[opname], + axis=operation.axis, + name=opname, + ) + + return node + + def visit_Gather(self, operation: operations.Gather) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + indices = self._to_onnx_proto(operation.indices, f"{opname}.indices") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name, indices.name], + outputs=[opname], + axis=operation.axis, + name=opname, + ) + + return node + + def visit_Gemm(self, operation: operations.Gemm) -> onnx.NodeProto: + idx = self.op_counts["Gemm"] = self.op_counts["Gemm"] + 1 + opname = f"Gemm_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + inputs = [a.name, b.name] + if operation.c is not None: + c = self._to_onnx_proto(operation.c, f"{opname}.c") + inputs.append(c.name) + elif self.add_missing_optional_inputs: + output_details = OperationGraph([operation]).output_details[0] + c_ = np.zeros(output_details.shape[1], dtype=output_details.dtype) + c = self._to_onnx_proto(c_, f"{opname}.c") + inputs.append(c.name) + + node = onnx.helper.make_node( + "Gemm", + inputs=inputs, + outputs=[opname], + alpha=operation.alpha, + beta=operation.beta, + transA=operation.transpose_a, + transB=operation.transpose_b, + name=opname, + ) + + return node + + def visit_GlobalAveragePool( + self, operation: operations.GlobalAveragePool + ) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Input(self, operation: operations.Input) -> onnx.ValueInfoProto: + idx = self.op_counts["Input"] = self.op_counts["Input"] + 1 + opname = f"Input_{idx}" + + shape = np.asarray(operation.shape).tolist() + if shape[0] < 0: + shape[0] = 1 + dtype = NUMPY_TO_ONNX_DTYPE[operation.dtype] + + node = onnx.helper.make_tensor_value_info(opname, dtype, shape) + self.inputs.append(node) + + return node + + def visit_MatMul(self, operation: operations.MatMul) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + + node = onnx.helper.make_node( + op_type, + inputs=[a.name, b.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_MaxPool(self, operation: operations.MaxPool) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + outputs=[opname], + kernel_shape=list(operation.kernel_shape), + ceil_mode=operation.ceil_mode, + strides=list(operation.strides), + dilations=list(operation.dilations), + pads=list(operation.pads), + storage_order=operation.storage_order, + name=opname, + ) + + return node + + def visit_Mul(self, operation: operations.Mul) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + + node = onnx.helper.make_node( + op_type, + inputs=[a.name, b.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_OutputSelect(self, operation: operations.OutputSelect) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + # if operation.index != 0: + # raise NotImplementedError( + # "Support for operations with multiple ouputs is not yet implemented." + # ) + + op = self._to_onnx_proto(operation.operation, f"{opname}.operation") + + node = onnx.helper.make_node( + "Identity", + # inputs=[op.name], + inputs=[op.outputs[operation.index]], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Relu(self, operation: operations.Relu) -> onnx.NodeProto: + idx = self.op_counts["Relu"] = self.op_counts["Relu"] + 1 + opname = f"Relu_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + "Relu", inputs=[x.name], outputs=[opname], name=opname + ) + + return node + + def visit_Reshape(self, operation: operations.Reshape) -> onnx.NodeProto: + idx = self.op_counts["Reshape"] = self.op_counts["Reshape"] + 1 + opname = f"Reshape_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + shape = self._to_onnx_proto(operation.shape, f"{opname}.shape") + + if operation.allowzero: + # TODO : need to use newer onnx opset version + raise ValueError("Reshape allowzero is not yet supported") + + node = onnx.helper.make_node( + "Reshape", + inputs=[x.name, shape.name], + # allowzero=operation.allowzero, + outputs=[opname], + name=opname, + ) + + return node + + def visit_Sigmoid(self, operation: operations.Sigmoid) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, inputs=[x.name], outputs=[opname], name=opname + ) + + return node + + def visit_Sub(self, operation: operations.Sub) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + + node = onnx.helper.make_node( + op_type, + inputs=[a.name, b.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Tanh(self, operation: operations.Tanh) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, inputs=[x.name], outputs=[opname], name=opname + ) + + return node + + def visit_Transpose(self, operation: operations.Transpose) -> onnx.NodeProto: + idx = self.op_counts["Transpose"] = self.op_counts["Transpose"] + 1 + opname = f"Transpose_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + "Transpose", + inputs=[x.name], + outputs=[opname], + name=opname, + perm=list(operation.permutation), + ) + + return node + + def visit_Split(self, operation: operations.Split) -> onnx.NodeProto: + op_type = str(operation) + # TODO: split attribute is optional. Edits to nn/parser/onnx.py required. + assert operation.split is not None + idx = self.op_counts["Split"] = self.op_counts["Split"] + 1 + opname = f"Split_{idx}" + outputs = [] + for i in range(len(operation.split)): + outputs.append(f"output_{i}") + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name, operation.split], + outputs=outputs, + name=opname, + axis=operation.axis, + ) + + return node diff --git a/dnnv/nn/converters/tensorflow.py b/dnnv/nn/converters/tensorflow.py index cab8fecc..eded3aec 100644 --- a/dnnv/nn/converters/tensorflow.py +++ b/dnnv/nn/converters/tensorflow.py @@ -1,874 +1,889 @@ -import numpy as np -import tensorflow as tf - -from ..graph import OperationGraph -from ..operations import Operation -from ..utils import ONNX_TO_TENSORFLOW_DTYPE -from ..visitors import OperationVisitor - - -class TensorflowConverterError(Exception): - pass - - -def convert(op_graph: OperationGraph): - converter = TensorflowConverter() - output_funcs = [] - for op in op_graph.output_operations: - output_funcs.append(converter.visit(op)) - - def func(*inputs, squeeze=True): - converter._cache.clear() - outputs = [] - for output_func in output_funcs: - output = output_func(*inputs) - if isinstance(output, tf.Tensor) and tf.executing_eagerly(): - output = output.numpy() - outputs.append(output) - if squeeze and len(outputs) == 1: - return outputs[0] - return tuple(outputs) - - return func - - -def _concretize(variables, inputs): - concrete_values = [] - for variable in variables: - if callable(variable): - concrete_values.append(variable(*inputs)) - else: - concrete_values.append(variable) - if len(concrete_values) == 1: - return concrete_values[0] - return concrete_values - - -class TensorflowConverter(OperationVisitor): - def __init__(self): - self.input_count = 0 - self.results = {} - self._cache = {} - - def _cached(self, func): - def wrapped_func(*args, **kwargs): - if func not in self._cache: - try: - self._cache[func] = func(*args, **kwargs) - except TensorflowConverterError: - raise - except Exception as e: - args_str = "" - if len(e.args) == 1: - args_str = e.args[0] - elif len(e.args) > 1: - args_str = str(e.args) - raise TensorflowConverterError( - f"{type(e).__name__}: {args_str}" - ).with_traceback(e.__traceback__) - return self._cache[func] - - return wrapped_func - - def visit(self, operation): - if operation not in self.results: - result = super().visit(operation) - self.results[operation] = result - return self.results[operation] - - def generic_visit(self, operation): - if not hasattr(self, "visit_%s" % operation.__class__.__name__): - raise ValueError( - "Tensorflow converter not implemented for operation type %s" - % operation.__class__.__name__ - ) - return super().generic_visit(operation) - - def visit_Add(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - - @self._cached - def add_func(*inputs): - a, b = _concretize([a_, b_], inputs) - result = tf.add(a, b) - return result - - return add_func - - def visit_Atan(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def atan_func(*inputs): - x = _concretize([x_], inputs) - result = tf.atan(x) - return result - - return atan_func - - def visit_AveragePool(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def avgpool_func(*inputs): - x = _concretize([x_], inputs) - if operation.ceil_mode: - # TODO : add support - raise ValueError( - "ceil_mode=True is not currently supported for AveragePool" - ) - if any(p != 0 for p in operation.pads) and not operation.count_include_pad: - # TODO : add support - raise ValueError( - "count_include_pad=False is not currently supported for AveragePool" - ) - kernel_shape = operation.kernel_shape - strides = operation.strides - num_pads = len(operation.pads) - pads = tuple( - zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) - ) - - x_ndim = int(tf.rank(x)) - x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) - padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) - result = tf.nn.pool( - padded_x, - kernel_shape, - pooling_type="AVG", - strides=strides, - padding="VALID", - ) - result_ndim = int(tf.rank(result)) - result = tf.transpose( - result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) - ) - return result - - return avgpool_func - - def visit_BatchNormalization(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def batchnorm_func(*inputs): - x = _concretize([x_], inputs) - scale = operation.scale - bias = operation.bias - mean = operation.mean - variance = operation.variance - epsilon = operation.epsilon - - x_ndim = int(tf.rank(x)) - x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) - result = tf.nn.batch_normalization( - x, - mean=mean, - variance=variance, - offset=bias, - scale=scale, - variance_epsilon=epsilon, - ) - result_ndim = int(tf.rank(result)) - result = tf.transpose( - result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) - ) - return result - - return batchnorm_func - - def visit_Cast(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def cast_func(*inputs): - x = _concretize([x_], inputs) - result = tf.cast(x, ONNX_TO_TENSORFLOW_DTYPE[operation.to]) - return result - - return cast_func - - def visit_Concat(self, operation): - tensors_ = [] - for x in operation.x: - if isinstance(x, Operation): - x = self.visit(x) - tensors_.append(x) - - @self._cached - def concat_func(*inputs): - tensors = x = _concretize(tensors_, inputs) - result = tf.concat(tensors, axis=operation.axis) - return result - - return concat_func - - def visit_Conv(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def conv_func(*inputs): - x = _concretize([x_], inputs) - if len(operation.kernel_shape) != 2: - raise NotImplementedError( - "Non 2d convolutions are not currently supported." - ) - weights = operation.w - if operation.b is not None: - bias = operation.b - else: - bias = np.zeros((weights.shape[0],), dtype=weights.dtype) - assert np.all(operation.dilations == 1) - assert np.all(operation.group == 1) - num_pads = len(operation.pads) - pads = tuple( - zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) - ) - - x_ndim = int(tf.rank(x)) - x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) - padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) - result = tf.nn.bias_add( - tf.nn.conv2d( - padded_x, - weights.transpose((2, 3, 1, 0)), - operation.strides, - padding="VALID", - ), - bias, - ) - result_ndim = int(tf.rank(result)) - result = tf.transpose( - result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) - ) - return result - - return conv_func - - def visit_ConvTranspose(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def convtranspose_func(*inputs): - x = _concretize([x_], inputs) - - if len(operation.kernel_shape) == 1: - conv_transpose = tf.nn.conv1d_transpose - elif len(operation.kernel_shape) == 2: - conv_transpose = tf.nn.conv2d_transpose - elif len(operation.kernel_shape) == 3: - conv_transpose = tf.nn.conv3d_transpose - else: - raise NotImplementedError( - f"{len(operation.kernel_shape)}d ConvTranspose operations are not currently supported." - ) - if ( - operation.auto_pad != "NOTSET" - or operation.auto_pad == "VALID" - and any(p != 0 for p in operation.pads) - ): - raise NotImplementedError( - f"Unsupported padding for ConvTranspose: {operation.auto_pad}" - ) - if np.any(operation.dilations != 1): - raise NotImplementedError( - f"Unsupported dilations for ConvTranspose: {operation.dilations}" - ) - - weights = operation.w - if operation.b is not None: - bias = operation.b - else: - bias = np.zeros((weights.shape[1],), dtype=weights.dtype) - assert np.all(operation.group == 1) - - num_pads = len(operation.pads) - pads = tuple( - zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) - ) - if any(p != 0 for p in operation.pads): - raise NotImplementedError( - "Non 0 pads are not currently supported for ConvTranspose" - ) - - output_shape = operation.output_shape - if output_shape is None: - input_shape = [int(d) for d in x.shape[2:]] - start_pads = operation.pads[: num_pads // 2] - end_pads = operation.pads[num_pads // 2 :] - output_shape = ( - [int(x.shape[0])] - + [ - ( - operation.strides[i] * (input_shape[i] - 1) - + operation.output_padding[i] - + ( - (operation.kernel_shape[i] - 1) * operation.dilations[i] - + 1 - ) - - start_pads[i] - - end_pads[i] - ) - for i in range(len(operation.kernel_shape)) - ] - + [weights.shape[1]] - ) - - x_ndim = int(tf.rank(x)) - x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) - padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) - weights_ndim = int(tf.rank(weights)) - result = tf.nn.bias_add( - conv_transpose( - padded_x, - weights.transpose(tuple(range(2, weights_ndim)) + (1, 0)), - output_shape, - strides=operation.strides, - padding="VALID", - dilations=operation.dilations, - ), - bias, - ) - result_ndim = int(tf.rank(result)) - result = tf.transpose( - result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) - ) - return result - - return convtranspose_func - - def visit_Div(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - - @self._cached - def div_func(*inputs): - a, b = _concretize([a_, b_], inputs) - result = tf.convert_to_tensor(tf.divide(a, b)) - return result - - return div_func - - def visit_Dropout(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def dropout_func(*inputs): - x = _concretize([x_], inputs) - return x, None - - return dropout_func - - def visit_Elu(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def elu_func(*inputs): - if operation.alpha != 1.0: - raise NotImplementedError( - "The tensorflow converter currently does not support ELU activations with alpha other than 1.0" - ) - x = _concretize([x_], inputs) - result = tf.nn.elu(x) - return result - - return elu_func - - def visit_Expand(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def expand_func(*inputs): - x = _concretize([x_], inputs) - shape = operation.shape - result = x * tf.ones(shape, x.dtype) - return result - - return expand_func - - def visit_Flatten(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def flatten_func(*inputs): - x = _concretize([x_], inputs) - axis = operation.axis - new_shape = (1, -1) if axis == 0 else (int(np.prod(x.shape[:axis])), -1) - result = tf.reshape(x, new_shape) - return result - - return flatten_func - - def visit_Gather(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - indices_ = operation.indices - if isinstance(indices_, Operation): - indices_ = self.visit(indices_) - - @self._cached - def gather_func(*inputs): - x, indices = _concretize([x_, indices_], inputs) - result = tf.gather(x, indices, axis=operation.axis) - return result - - return gather_func - - def visit_Gemm(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - c_ = operation.c - if isinstance(c_, Operation): - c_ = self.visit(c_) - - @self._cached - def gemm_func(*inputs): - a, b, c = _concretize([a_, b_, c_], inputs) - result = operation.alpha * tf.matmul( - a, - b, - transpose_a=operation.transpose_a, - transpose_b=operation.transpose_b, - ) - if c is not None: - result = result + operation.beta * c - return result - - return gemm_func - - def visit_GlobalAveragePool(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def globalavgpool_func(*inputs): - x = _concretize([x_], inputs) - - x = tf.transpose(x, (0, 2, 3, 1)) - result = tf.nn.pool( - x, x.shape[1:3], pooling_type="AVG", strides=(1, 1), padding="VALID" - ) - result = tf.transpose(result, (0, 3, 1, 2)) - return result - - return globalavgpool_func - - def visit_Identity(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def identity_func(*inputs): - x = _concretize([x_], inputs) - return tf.convert_to_tensor(x) - - return identity_func - - def visit_Input(self, operation): - input_idx = self.input_count - - @self._cached - def input_func(*inputs): - x = inputs[input_idx] - if any( - d1 != d2 and -1 not in (d1, d2) and None not in (d1, d2) - for d1, d2 in zip(operation.shape[1:], x.shape[1:]) - ): - raise ValueError( - "Incorrect input shape: %s != %s" % (operation.shape, x.shape) - ) - if x.dtype != operation.dtype: - raise TypeError( - "Incorrect type, %s, for input %d. Expected type %s." - % (x.dtype, input_idx, operation.dtype) - ) - return tf.convert_to_tensor(x) - - self.input_count += 1 - - return input_func - - def visit_LeakyRelu(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def leakyrelu_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.leaky_relu(x, alpha=operation.alpha) - return result - - return leakyrelu_func - - def visit_LogSoftmax(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def softmax_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.log_softmax(x, axis=operation.axis) - return result - - return softmax_func - - def visit_MatMul(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - - @self._cached - def matmul_func(*inputs): - a, b = _concretize([a_, b_], inputs) - - if len(a.shape) == 2 and len(b.shape) == 1: - result = tf.matmul(a, b[:, None])[:, 0] - elif len(a.shape) == 1 and len(b.shape) == 2: - result = tf.matmul(a[None], b)[0] - else: - result = tf.matmul(a, b) - return result - - return matmul_func - - def visit_MaxPool(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def maxpool_func(*inputs): - x = _concretize([x_], inputs) - - if operation.ceil_mode: - # TODO : add support - raise ValueError( - "ceil_mode=True is not currently supported for MaxPool" - ) - - kernel_shape = operation.kernel_shape - strides = operation.strides - num_pads = len(operation.pads) - pads = tuple( - zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) - ) - - x_ndim = int(tf.rank(x)) - x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) - padded_x = tf.pad( - x, ((0, 0),) + pads + ((0, 0),), constant_values=x.dtype.min - ) - result = tf.nn.pool( - padded_x, - kernel_shape, - pooling_type="MAX", - strides=strides, - dilations=operation.dilations, - padding="VALID", - ) - result_ndim = int(tf.rank(result)) - result = tf.transpose( - result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) - ) - return result - - return maxpool_func - - def visit_Mul(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - - @self._cached - def mul_func(*inputs): - a, b = _concretize([a_, b_], inputs) - result = tf.multiply(a, b) - return result - - return mul_func - - def visit_OutputSelect(self, operation): - x_ = self.visit(operation.operation) - - @self._cached - def output_select_func(*inputs): - x = _concretize([x_], inputs) - return x[operation.index] - - return output_select_func - - def visit_Pad(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def pad_func(*inputs): - x = _concretize([x_], inputs) - mode = operation.mode.upper() - if mode != "CONSTANT": - raise ValueError(f"{mode} padding is not currently supported") - num_pads = len(operation.pads) - pads = tuple( - zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) - ) - result = tf.pad(x, pads, mode=mode, constant_values=operation.value) - return result - - return pad_func - - def visit_Relu(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def relu_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.relu(x) - return result - - return relu_func - - def visit_Reshape(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - shape_ = operation.shape - if isinstance(shape_, Operation): - shape_ = self.visit(shape_) - - @self._cached - def reshape_func(*inputs): - x, shape = _concretize([x_, shape_], inputs) - if not operation.allowzero: - for i, d in enumerate(shape): - if d == 0: - shape[i] = x.shape[i] - result = tf.reshape(x, shape) - return result - - return reshape_func - - def visit_Resize(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - roi_ = operation.roi - if isinstance(roi_, Operation): - roi_ = self.visit(roi_) - scales_ = operation.scales - if isinstance(scales_, Operation): - scales_ = self.visit(scales_) - sizes_ = operation.sizes - if isinstance(sizes_, Operation): - sizes_ = self.visit(sizes_) - - @self._cached - def resize_func(*inputs): - x, roi, scales, sizes = _concretize([x_, roi_, scales_, sizes_], inputs) - assert operation.coordinate_transformation_mode in [ - "asymmetric", - "tf_crop_and_resize", - ] - assert operation.mode in ["nearest", "linear"] - assert operation.exclude_outside == 0 - if roi is None or roi.size == 0: - roi = np.array([[0, 0, 1, 1]]) - else: - assert operation.coordinate_transformation_mode == "tf_crop_and_resize" - assert roi.size == 8 and roi.ndim == 1 - roi = roi[None, [2, 3, 6, 7]] - if sizes is None or sizes.size == 0: - assert scales[0] == 1.0 and scales[1] == 1.0 - sizes = (scales * [int(d) for d in x.shape]).astype(int) - assert sizes.ndim == 1 and sizes.size == 4 - sizes = sizes[2:] - method = operation.mode - if method == "linear": - method = "bilinear" - result = tf.transpose(x, (0, 2, 3, 1)) - result = tf.image.crop_and_resize( - result, - boxes=roi, - box_indices=np.arange(int(x.shape[0])), - crop_size=sizes, - method=method, - extrapolation_value=operation.extrapolation_value, - ) - result = tf.transpose(result, (0, 3, 1, 2)) - return result - - return resize_func - - def visit_Shape(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def shape_func(*inputs): - x = _concretize([x_], inputs) - result = tf.shape(x) - return result - - return shape_func - - def visit_Sigmoid(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def sigmoid_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.sigmoid(x) - return result - - return sigmoid_func - - def visit_Sign(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def sign_func(*inputs): - x = _concretize([x_], inputs) - result = tf.math.sign(x) - return result - - return sign_func - - def visit_Softmax(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def softmax_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.softmax(x, axis=operation.axis) - return result - - return softmax_func - - def visit_Sub(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - - @self._cached - def sub_func(*inputs): - a, b = _concretize([a_, b_], inputs) - result = tf.subtract(a, b) - return result - - return sub_func - - def visit_Tanh(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def tanh_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.tanh(x) - return result - - return tanh_func - - def visit_Tile(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - repeats_ = operation.repeats - if isinstance(repeats_, Operation): - repeats_ = self.visit(repeats_) - - @self._cached - def tile_func(*inputs): - x, repeats = _concretize([x_, repeats_], inputs) - result = tf.tile(x, repeats) - return result - - return tile_func - - def visit_Transpose(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def transpose_func(*inputs): - x = _concretize([x_], inputs) - permutation = operation.permutation - result = tf.transpose(x, permutation) - return result - - return transpose_func - - def visit_Unsqueeze(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - axes_ = operation.axes - if isinstance(axes_, Operation): - axes_ = self.visit(axes_) - - @self._cached - def unsqueeze_func(*inputs): - x, axes = _concretize([x_, axes_], inputs) - for axis in sorted(axes): - x = tf.expand_dims(x, axis) - return x - - return unsqueeze_func +import numpy as np +import tensorflow as tf + +from ..graph import OperationGraph +from ..operations import Operation +from ..utils import ONNX_TO_TENSORFLOW_DTYPE +from ..visitors import OperationVisitor + + +class TensorflowConverterError(Exception): + pass + + +def convert(op_graph: OperationGraph): + converter = TensorflowConverter() + output_funcs = [] + for op in op_graph.output_operations: + output_funcs.append(converter.visit(op)) + + def func(*inputs, squeeze=True): + converter._cache.clear() + outputs = [] + for output_func in output_funcs: + output = output_func(*inputs) + if isinstance(output, tf.Tensor) and tf.executing_eagerly(): + output = output.numpy() + outputs.append(output) + if squeeze and len(outputs) == 1: + return outputs[0] + return tuple(outputs) + + return func + + +def _concretize(variables, inputs): + concrete_values = [] + for variable in variables: + if callable(variable): + concrete_values.append(variable(*inputs)) + else: + concrete_values.append(variable) + if len(concrete_values) == 1: + return concrete_values[0] + return concrete_values + + +class TensorflowConverter(OperationVisitor): + def __init__(self): + self.input_count = 0 + self.results = {} + self._cache = {} + + def _cached(self, func): + def wrapped_func(*args, **kwargs): + if func not in self._cache: + try: + self._cache[func] = func(*args, **kwargs) + except TensorflowConverterError: + raise + except Exception as e: + args_str = "" + if len(e.args) == 1: + args_str = e.args[0] + elif len(e.args) > 1: + args_str = str(e.args) + raise TensorflowConverterError( + f"{type(e).__name__}: {args_str}" + ).with_traceback(e.__traceback__) + return self._cache[func] + + return wrapped_func + + def visit(self, operation): + if operation not in self.results: + result = super().visit(operation) + self.results[operation] = result + return self.results[operation] + + def generic_visit(self, operation): + if not hasattr(self, "visit_%s" % operation.__class__.__name__): + raise ValueError( + "Tensorflow converter not implemented for operation type %s" + % operation.__class__.__name__ + ) + return super().generic_visit(operation) + + def visit_Add(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + + @self._cached + def add_func(*inputs): + a, b = _concretize([a_, b_], inputs) + result = tf.add(a, b) + return result + + return add_func + + def visit_Atan(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def atan_func(*inputs): + x = _concretize([x_], inputs) + result = tf.atan(x) + return result + + return atan_func + + def visit_AveragePool(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def avgpool_func(*inputs): + x = _concretize([x_], inputs) + if operation.ceil_mode: + # TODO : add support + raise ValueError( + "ceil_mode=True is not currently supported for AveragePool" + ) + if any(p != 0 for p in operation.pads) and not operation.count_include_pad: + # TODO : add support + raise ValueError( + "count_include_pad=False is not currently supported for AveragePool" + ) + kernel_shape = operation.kernel_shape + strides = operation.strides + num_pads = len(operation.pads) + pads = tuple( + zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) + ) + + x_ndim = int(tf.rank(x)) + x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) + padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) + result = tf.nn.pool( + padded_x, + kernel_shape, + pooling_type="AVG", + strides=strides, + padding="VALID", + ) + result_ndim = int(tf.rank(result)) + result = tf.transpose( + result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) + ) + return result + + return avgpool_func + + def visit_BatchNormalization(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def batchnorm_func(*inputs): + x = _concretize([x_], inputs) + scale = operation.scale + bias = operation.bias + mean = operation.mean + variance = operation.variance + epsilon = operation.epsilon + + x_ndim = int(tf.rank(x)) + x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) + result = tf.nn.batch_normalization( + x, + mean=mean, + variance=variance, + offset=bias, + scale=scale, + variance_epsilon=epsilon, + ) + result_ndim = int(tf.rank(result)) + result = tf.transpose( + result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) + ) + return result + + return batchnorm_func + + def visit_Cast(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def cast_func(*inputs): + x = _concretize([x_], inputs) + result = tf.cast(x, ONNX_TO_TENSORFLOW_DTYPE[operation.to]) + return result + + return cast_func + + def visit_Concat(self, operation): + tensors_ = [] + for x in operation.x: + if isinstance(x, Operation): + x = self.visit(x) + tensors_.append(x) + + @self._cached + def concat_func(*inputs): + tensors = x = _concretize(tensors_, inputs) + result = tf.concat(tensors, axis=operation.axis) + return result + + return concat_func + + def visit_Conv(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def conv_func(*inputs): + x = _concretize([x_], inputs) + if len(operation.kernel_shape) != 2: + raise NotImplementedError( + "Non 2d convolutions are not currently supported." + ) + weights = operation.w + if operation.b is not None: + bias = operation.b + else: + bias = np.zeros((weights.shape[0],), dtype=weights.dtype) + assert np.all(operation.dilations == 1) + # assert np.all(operation.group == 1) + num_pads = len(operation.pads) + pads = tuple( + zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) + ) + + x_ndim = int(tf.rank(x)) + x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) + padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) + result = tf.nn.bias_add( + tf.nn.conv2d( + padded_x, + weights.transpose((2, 3, 1, 0)), + operation.strides, + padding="VALID", + ), + bias, + ) + result_ndim = int(tf.rank(result)) + result = tf.transpose( + result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) + ) + return result + + return conv_func + + def visit_ConvTranspose(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def convtranspose_func(*inputs): + x = _concretize([x_], inputs) + + if len(operation.kernel_shape) == 1: + conv_transpose = tf.nn.conv1d_transpose + elif len(operation.kernel_shape) == 2: + conv_transpose = tf.nn.conv2d_transpose + elif len(operation.kernel_shape) == 3: + conv_transpose = tf.nn.conv3d_transpose + else: + raise NotImplementedError( + f"{len(operation.kernel_shape)}d ConvTranspose operations are not currently supported." + ) + if ( + operation.auto_pad != "NOTSET" + or operation.auto_pad == "VALID" + and any(p != 0 for p in operation.pads) + ): + raise NotImplementedError( + f"Unsupported padding for ConvTranspose: {operation.auto_pad}" + ) + if np.any(operation.dilations != 1): + raise NotImplementedError( + f"Unsupported dilations for ConvTranspose: {operation.dilations}" + ) + + weights = operation.w + if operation.b is not None: + bias = operation.b + else: + bias = np.zeros((weights.shape[1],), dtype=weights.dtype) + # assert np.all(operation.group == 1) + + num_pads = len(operation.pads) + pads = tuple( + zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) + ) + if any(p != 0 for p in operation.pads): + raise NotImplementedError( + "Non 0 pads are not currently supported for ConvTranspose" + ) + + output_shape = operation.output_shape + if output_shape is None: + input_shape = [int(d) for d in x.shape[2:]] + start_pads = operation.pads[: num_pads // 2] + end_pads = operation.pads[num_pads // 2 :] + output_shape = ( + [int(x.shape[0])] + + [ + ( + operation.strides[i] * (input_shape[i] - 1) + + operation.output_padding[i] + + ( + (operation.kernel_shape[i] - 1) * operation.dilations[i] + + 1 + ) + - start_pads[i] + - end_pads[i] + ) + for i in range(len(operation.kernel_shape)) + ] + + [weights.shape[1]] + ) + + x_ndim = int(tf.rank(x)) + x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) + padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) + weights_ndim = int(tf.rank(weights)) + result = tf.nn.bias_add( + conv_transpose( + padded_x, + weights.transpose(tuple(range(2, weights_ndim)) + (1, 0)), + output_shape, + strides=operation.strides, + padding="VALID", + dilations=operation.dilations, + ), + bias, + ) + result_ndim = int(tf.rank(result)) + result = tf.transpose( + result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) + ) + return result + + return convtranspose_func + + def visit_Div(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + + @self._cached + def div_func(*inputs): + a, b = _concretize([a_, b_], inputs) + result = tf.convert_to_tensor(tf.divide(a, b)) + return result + + return div_func + + def visit_Dropout(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def dropout_func(*inputs): + x = _concretize([x_], inputs) + return x, None + + return dropout_func + + def visit_Elu(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def elu_func(*inputs): + if operation.alpha != 1.0: + raise NotImplementedError( + "The tensorflow converter currently does not support ELU activations with alpha other than 1.0" + ) + x = _concretize([x_], inputs) + result = tf.nn.elu(x) + return result + + return elu_func + + def visit_Expand(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def expand_func(*inputs): + x = _concretize([x_], inputs) + shape = operation.shape + result = x * tf.ones(shape, x.dtype) + return result + + return expand_func + + def visit_Flatten(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def flatten_func(*inputs): + x = _concretize([x_], inputs) + axis = operation.axis + new_shape = (1, -1) if axis == 0 else (int(np.prod(x.shape[:axis])), -1) + result = tf.reshape(x, new_shape) + return result + + return flatten_func + + def visit_Gather(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + indices_ = operation.indices + if isinstance(indices_, Operation): + indices_ = self.visit(indices_) + + @self._cached + def gather_func(*inputs): + x, indices = _concretize([x_, indices_], inputs) + result = tf.gather(x, indices, axis=operation.axis) + return result + + return gather_func + + def visit_Gemm(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + c_ = operation.c + if isinstance(c_, Operation): + c_ = self.visit(c_) + + @self._cached + def gemm_func(*inputs): + a, b, c = _concretize([a_, b_, c_], inputs) + result = operation.alpha * tf.matmul( + a, + b, + transpose_a=operation.transpose_a, + transpose_b=operation.transpose_b, + ) + if c is not None: + result = result + operation.beta * c + return result + + return gemm_func + + def visit_GlobalAveragePool(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def globalavgpool_func(*inputs): + x = _concretize([x_], inputs) + + x = tf.transpose(x, (0, 2, 3, 1)) + result = tf.nn.pool( + x, x.shape[1:3], pooling_type="AVG", strides=(1, 1), padding="VALID" + ) + result = tf.transpose(result, (0, 3, 1, 2)) + return result + + return globalavgpool_func + + def visit_Identity(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def identity_func(*inputs): + x = _concretize([x_], inputs) + return tf.convert_to_tensor(x) + + return identity_func + + def visit_Input(self, operation): + input_idx = self.input_count + + @self._cached + def input_func(*inputs): + x = inputs[input_idx] + if any( + d1 != d2 and -1 not in (d1, d2) and None not in (d1, d2) + for d1, d2 in zip(operation.shape[1:], x.shape[1:]) + ): + raise ValueError( + "Incorrect input shape: %s != %s" % (operation.shape, x.shape) + ) + if x.dtype != operation.dtype: + raise TypeError( + "Incorrect type, %s, for input %d. Expected type %s." + % (x.dtype, input_idx, operation.dtype) + ) + return tf.convert_to_tensor(x) + + self.input_count += 1 + + return input_func + + def visit_LeakyRelu(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def leakyrelu_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.leaky_relu(x, alpha=operation.alpha) + return result + + return leakyrelu_func + + def visit_LogSoftmax(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def softmax_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.log_softmax(x, axis=operation.axis) + return result + + return softmax_func + + def visit_MatMul(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + + @self._cached + def matmul_func(*inputs): + a, b = _concretize([a_, b_], inputs) + + if len(a.shape) == 2 and len(b.shape) == 1: + result = tf.matmul(a, b[:, None])[:, 0] + elif len(a.shape) == 1 and len(b.shape) == 2: + result = tf.matmul(a[None], b)[0] + else: + result = tf.matmul(a, b) + return result + + return matmul_func + + def visit_MaxPool(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def maxpool_func(*inputs): + x = _concretize([x_], inputs) + + if operation.ceil_mode: + # TODO : add support + raise ValueError( + "ceil_mode=True is not currently supported for MaxPool" + ) + + kernel_shape = operation.kernel_shape + strides = operation.strides + num_pads = len(operation.pads) + pads = tuple( + zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) + ) + + x_ndim = int(tf.rank(x)) + x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) + padded_x = tf.pad( + x, ((0, 0),) + pads + ((0, 0),), constant_values=x.dtype.min + ) + result = tf.nn.pool( + padded_x, + kernel_shape, + pooling_type="MAX", + strides=strides, + dilations=operation.dilations, + padding="VALID", + ) + result_ndim = int(tf.rank(result)) + result = tf.transpose( + result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) + ) + return result + + return maxpool_func + + def visit_Mul(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + + @self._cached + def mul_func(*inputs): + a, b = _concretize([a_, b_], inputs) + result = tf.multiply(a, b) + return result + + return mul_func + + def visit_OutputSelect(self, operation): + x_ = self.visit(operation.operation) + + @self._cached + def output_select_func(*inputs): + x = _concretize([x_], inputs) + return x[operation.index] + + return output_select_func + + def visit_Pad(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def pad_func(*inputs): + x = _concretize([x_], inputs) + mode = operation.mode.upper() + if mode != "CONSTANT": + raise ValueError(f"{mode} padding is not currently supported") + num_pads = len(operation.pads) + pads = tuple( + zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) + ) + result = tf.pad(x, pads, mode=mode, constant_values=operation.value) + return result + + return pad_func + + def visit_Relu(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def relu_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.relu(x) + return result + + return relu_func + + def visit_Reshape(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + shape_ = operation.shape + if isinstance(shape_, Operation): + shape_ = self.visit(shape_) + + @self._cached + def reshape_func(*inputs): + x, shape = _concretize([x_, shape_], inputs) + if not operation.allowzero: + for i, d in enumerate(shape): + if d == 0: + shape[i] = x.shape[i] + result = tf.reshape(x, shape) + return result + + return reshape_func + + def visit_Resize(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + roi_ = operation.roi + if isinstance(roi_, Operation): + roi_ = self.visit(roi_) + scales_ = operation.scales + if isinstance(scales_, Operation): + scales_ = self.visit(scales_) + sizes_ = operation.sizes + if isinstance(sizes_, Operation): + sizes_ = self.visit(sizes_) + + @self._cached + def resize_func(*inputs): + x, roi, scales, sizes = _concretize([x_, roi_, scales_, sizes_], inputs) + assert operation.coordinate_transformation_mode in [ + "asymmetric", + "tf_crop_and_resize", + ] + assert operation.mode in ["nearest", "linear"] + assert operation.exclude_outside == 0 + if roi is None or roi.size == 0: + roi = np.array([[0, 0, 1, 1]]) + else: + assert operation.coordinate_transformation_mode == "tf_crop_and_resize" + assert roi.size == 8 and roi.ndim == 1 + roi = roi[None, [2, 3, 6, 7]] + if sizes is None or sizes.size == 0: + assert scales[0] == 1.0 and scales[1] == 1.0 + sizes = (scales * [int(d) for d in x.shape]).astype(int) + assert sizes.ndim == 1 and sizes.size == 4 + sizes = sizes[2:] + method = operation.mode + if method == "linear": + method = "bilinear" + result = tf.transpose(x, (0, 2, 3, 1)) + result = tf.image.crop_and_resize( + result, + boxes=roi, + box_indices=np.arange(int(x.shape[0])), + crop_size=sizes, + method=method, + extrapolation_value=operation.extrapolation_value, + ) + result = tf.transpose(result, (0, 3, 1, 2)) + return result + + return resize_func + + def visit_Shape(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def shape_func(*inputs): + x = _concretize([x_], inputs) + result = tf.shape(x) + return result + + return shape_func + + def visit_Sigmoid(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def sigmoid_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.sigmoid(x) + return result + + return sigmoid_func + + def visit_Sign(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def sign_func(*inputs): + x = _concretize([x_], inputs) + result = tf.math.sign(x) + return result + + return sign_func + + def visit_Softmax(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def softmax_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.softmax(x, axis=operation.axis) + return result + + return softmax_func + + def visit_Sub(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + + @self._cached + def sub_func(*inputs): + a, b = _concretize([a_, b_], inputs) + result = tf.subtract(a, b) + return result + + return sub_func + + def visit_Tanh(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def tanh_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.tanh(x) + return result + + return tanh_func + + def visit_Tile(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + repeats_ = operation.repeats + if isinstance(repeats_, Operation): + repeats_ = self.visit(repeats_) + + @self._cached + def tile_func(*inputs): + x, repeats = _concretize([x_, repeats_], inputs) + result = tf.tile(x, repeats) + return result + + return tile_func + + def visit_Transpose(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def transpose_func(*inputs): + x = _concretize([x_], inputs) + permutation = operation.permutation + result = tf.transpose(x, permutation) + return result + + return transpose_func + + def visit_Unsqueeze(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + axes_ = operation.axes + if isinstance(axes_, Operation): + axes_ = self.visit(axes_) + + @self._cached + def unsqueeze_func(*inputs): + x, axes = _concretize([x_, axes_], inputs) + for axis in sorted(axes): + x = tf.expand_dims(x, axis) + return x + + return unsqueeze_func + + def visit_Split(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + axis = operation.axis + split = operation.split + + @self._cached + def split_func(*inputs): + x = _concretize([x_], inputs) + x = tf.split(x, split, axis=axis) + return x + + return split_func diff --git a/dnnv/nn/transformers/simplifiers/squeeze_convs.py b/dnnv/nn/transformers/simplifiers/squeeze_convs.py index 1b2c15a8..27ca8193 100644 --- a/dnnv/nn/transformers/simplifiers/squeeze_convs.py +++ b/dnnv/nn/transformers/simplifiers/squeeze_convs.py @@ -1,42 +1,43 @@ -import numpy as np - -from copy import copy - -from .base import Simplifier -from ... import operations - - -class SqueezeConvs(Simplifier): - def is_diagonal(self, array): - i, j = array.shape - return ~np.any(array.reshape(-1)[:-1].reshape(i - 1, j + 1)[:, 1:]) - - def visit_Conv(self, operation: operations.Conv) -> operations.Conv: - if ( - isinstance(operation.x, operations.Conv) - and operation.x.w.shape[2] == 1 - and operation.x.w.shape[3] == 1 - and all(p == 0 for p in operation.pads) - and all(s == 1 for s in operation.x.strides) - and all(p == 0 for p in operation.x.pads) - and all(d == 1 for d in operation.x.dilations) - and operation.x.group == 1 - and self.is_diagonal(operation.x.w[:, :, 0, 0]) - ): - w = np.diag(operation.x.w[:, :, 0, 0]).reshape((1, -1, 1, 1)) - b = operation.x.b - - out_c, in_c, k_h, k_w = operation.w.shape - - weights = operation.w * np.tile(w, (out_c, 1, k_h, k_w)) - bias = operation.b + ( - operation.w * np.tile(b.reshape((1, -1, 1, 1)), (out_c, 1, k_h, k_w)) - ).sum(axis=(1, 2, 3)) - - op = copy(operation) - op.x = operation.x.x - op.w = weights - op.b = bias - - return op - return operation +import numpy as np + +from copy import copy + +from .base import Simplifier +from ... import operations + + +class SqueezeConvs(Simplifier): + def is_diagonal(self, array): + i, j = array.shape + # return ~np.any(array.reshape(-1)[:-1].reshape(i - 1, j + 1)[:, 1:]) + return i == j and ~np.any(array.reshape(-1)[:-1].reshape(i - 1, j + 1)[:, 1:]) + + def visit_Conv(self, operation: operations.Conv) -> operations.Conv: + if ( + isinstance(operation.x, operations.Conv) + and operation.x.w.shape[2] == 1 + and operation.x.w.shape[3] == 1 + and all(p == 0 for p in operation.pads) + and all(s == 1 for s in operation.x.strides) + and all(p == 0 for p in operation.x.pads) + and all(d == 1 for d in operation.x.dilations) + and operation.x.group == 1 + and self.is_diagonal(operation.x.w[:, :, 0, 0]) + ): + w = np.diag(operation.x.w[:, :, 0, 0]).reshape((1, -1, 1, 1)) + b = operation.x.b + + out_c, in_c, k_h, k_w = operation.w.shape + + weights = operation.w * np.tile(w, (out_c, 1, k_h, k_w)) + bias = operation.b + ( + operation.w * np.tile(b.reshape((1, -1, 1, 1)), (out_c, 1, k_h, k_w)) + ).sum(axis=(1, 2, 3)) + + op = copy(operation) + op.x = operation.x.x + op.w = weights + op.b = bias + + return op + return operation diff --git a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py new file mode 100644 index 00000000..62ba0192 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py @@ -0,0 +1,144 @@ +import numpy as np +import onnxruntime +import pytest + +from dnnv.nn.converters.onnx import * +from dnnv.nn.operations import * + +# Tests based on: +# https://github.com/onnx/onnx/blob/2ab133404afce34552aaccd86e7023e1fb9a60d2/onnx/test/shape_inference_test.py +# https://github.com/onnx/onnx/blob/2ab133404afce34552aaccd86e7023e1fb9a60d2/onnx/test/automatic_upgrade_test.py +# https://github.com/onnx/onnx/blob/35092895d9bf3592e58f4710d098f8131afef259/onnx/backend/test/case/node/split.py + + +def test_Split_export_1d() -> None: + input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + + op = Split(input, axis=0, split=[2, 2, 2]) + all_results = np.zeros((3, 2)) + for i in range(3): + outputselect = OutputSelect(op, i) + onnx_model = convert(OperationGraph([outputselect])) + results = onnxruntime.backend.run(onnx_model, []) + all_results[i] = results + + expected_outputs = [ + np.array([1.0, 2.0]).astype(np.float32), + np.array([3.0, 4.0]).astype(np.float32), + np.array([5.0, 6.0]).astype(np.float32), + ] + assert len(all_results) == 3 + assert np.allclose(all_results, expected_outputs) + + op = Split(input, axis=0, split=np.array([2, 4]).astype(np.int64)) + onnx_model = convert(OperationGraph([op])) + results = onnxruntime.backend.run(onnx_model, []) + + expected_outputs = [ + np.array([1.0, 2.0]).astype(np.float32), + np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32), + ] + assert len(results) == 2 + assert np.allclose(results, expected_outputs) + + +def test_Split_export_2d() -> None: + input = np.array( + [[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [7.0, 8.0, 9.0, 10.0, 11.0, 12.0]] + ).astype(np.float32) + + # node = onnx.helper.make_node( + # 'Split', + # inputs=['input'], + # outputs=['output_1', 'output_2'], + # axis=1 + # ) + op = Split(input, axis=1, split=[2, 2]) + all_results = [] + for i in range(2): + outputselect = OutputSelect(op, i) + onnx_model = convert(OperationGraph([outputselect])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results) + expected_outputs = [ + np.array([[1.0, 2.0, 3.0], [7.0, 8.0, 9.0]]).astype(np.float32), + np.array([[4.0, 5.0, 6.0], [10.0, 11.0, 12.0]]).astype(np.float32), + ] + + # expect(node, inputs=[input], outputs=[y for y in expected_outputs], name='test_split_equal_parts_2d') + for i in range(2): + assert all_results[i].shape == (2, 3) + assert np.allclose(all_results[i], expected_outputs[i]) + split = np.array([2, 4]).astype(np.int64) + # node = onnx.helper.make_node( + # 'Split', + # inputs=['input', 'split'], + # outputs=['output_1', 'output_2'], + # axis=1, + # ) + + expected_outputs = [ + np.array([[1.0, 2.0], [7.0, 8.0]]).astype(np.float32), + np.array([[3.0, 4.0, 5.0, 6.0], [9.0, 10.0, 11.0, 12.0]]).astype(np.float32), + ] + + # expect(node, inputs=[input, split], outputs=[y for y in expected_outputs], name='test_split_variable_parts_2d') + + +def test_Split_export_default_values() -> None: + input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + + # If axis is not specified, split is applied on default axis 0 + node = onnx.helper.make_node( + "Split", inputs=["input"], outputs=["output_1", "output_2", "output_3"] + ) + + expected_outputs = [ + np.array([1.0, 2.0]).astype(np.float32), + np.array([3.0, 4.0]).astype(np.float32), + np.array([5.0, 6.0]).astype(np.float32), + ] + expect( + node, + inputs=[input], + outputs=[y for y in expected_outputs], + name="test_split_equal_parts_default_axis", + ) + + split = np.array([2, 4]).astype(np.int64) + node = onnx.helper.make_node( + "Split", inputs=["input", "split"], outputs=["output_1", "output_2"] + ) + + expected_outputs = [ + np.array([1.0, 2.0]).astype(np.float32), + np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32), + ] + expect( + node, + inputs=[input, split], + outputs=[y for y in expected_outputs], + name="test_split_variable_parts_default_axis", + ) + + +def test_Split_export_zero_size_splits() -> None: + input = np.array([]).astype(np.float32) + + # Split emtpy tensor to tensors of size zero + split = np.array([0, 0, 0]).astype(np.int64) + node = onnx.helper.make_node( + "Split", inputs=["input", "split"], outputs=["output_1", "output_2", "output_3"] + ) + + expected_outputs = [ + np.array([]).astype(np.float32), + np.array([]).astype(np.float32), + np.array([]).astype(np.float32), + ] + expect( + node, + inputs=[input, split], + outputs=[y for y in expected_outputs], + name="test_split_zero_size_splits", + ) diff --git a/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py new file mode 100644 index 00000000..6bbac5c9 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py @@ -0,0 +1,134 @@ +import numpy as np +import pytest + +from dnnv.nn.converters.tensorflow import * +from dnnv.nn.operations import * + + +def test_Reshape(): + original_shape = [0, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([3, 4, 0], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape, allowzero=True) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + op = Reshape( + Input((0, 3, 4), np.dtype(np.float32)), + Input((3,), np.dtype(np.int64)), + allowzero=True, + ) + tf_op = TensorflowConverter().visit(op) + result = tf_op(data, new_shape).numpy() + assert np.allclose(result, y) + + +def test_Reshape_reordered_all_dims(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([4, 2, 3], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_reordered_last_dims(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, 4, 3], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_reduced_dims(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, 12], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_extended_dims(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, 3, 2, 2], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_one_dim(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([24], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_negative_dim(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, -1, 2], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_negative_extended_dims(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([-1, 2, 3, 4], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_zero_dim(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, 0, 4, 1], dtype=np.int64) + y = np.reshape(data, [2, 3, 4, 1]) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_zero_and_negative_dim(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, 0, 1, -1], dtype=np.int64) + y = np.reshape(data, [2, 3, 1, -1]) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) From 00afb929de2644e602660e6fb8e1c23888e5adba Mon Sep 17 00:00:00 2001 From: Meriel von Stein Date: Mon, 9 May 2022 11:57:01 -0400 Subject: [PATCH 03/18] Normalize line endings --- README.md | 234 +-- dnnv/nn/converters/onnx.py | 1244 ++++++------ dnnv/nn/converters/tensorflow.py | 1778 ++++++++--------- .../transformers/simplifiers/squeeze_convs.py | 86 +- docs/make.bat | 70 +- .../test_converters/test_onnx/test_Split.py | 288 +-- .../test_tensorflow/test_Split.py | 268 +-- 7 files changed, 1984 insertions(+), 1984 deletions(-) diff --git a/README.md b/README.md index dbd9fb62..4d88f0b2 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,117 @@ -# Deep Neural Network Verification - -A framework for verification and analysis of deep neural networks. You can read an overview of DNNV in our CAV 2021 paper [*DNNV: A Framework for Deep Neural Network Verification*](https://arxiv.org/abs/2105.12841), or watch our presentation on [YouTube](https://youtu.be/GhXlONbvx1Y). - -## Getting Started - -For detailed instructions on installing and using DNNV, see our [documentation](https://dnnv.readthedocs.io/en/stable/). - -### Installation - -DNNV requires python >=3.7,<3.10, and has been tested on linux. To install the latest stable version run: - -```bash -$ pip install dnnv -``` - -or - -```bash -$ pip install git+https://github.com/dlshriver/DNNV.git@main -``` - -We recommend installing DNNV into a [python virtual environment](https://docs.python.org/3/tutorial/venv.html). - -Install any of the supported verifiers ([Reluplex](https://github.com/guykatzz/ReluplexCav2017), [planet](https://github.com/progirep/planet), [MIPVerify.jl](https://github.com/vtjeng/MIPVerify.jl), [Neurify](https://github.com/tcwangshiqi-columbia/Neurify), [ERAN](https://github.com/eth-sri/eran), [BaB](https://github.com/oval-group/PLNN-verification), [marabou](https://github.com/NeuralNetworkVerification/Marabou), [nnenum](https://github.com/stanleybak/nnenum), [verinet](https://vas.doc.ic.ac.uk/software/neural/)): - -```bash -$ dnnv_manage install reluplex planet mipverify neurify eran bab marabou nnenum verinet -``` - -*Several verifiers make use of the [Gurobi solver](https://www.gurobi.com/).* This should be installed automatically, but requires a license to be manually activated and available on the host machine. Academic licenses can be obtained for free from the [Gurobi website](https://user.gurobi.com/download/licenses/free-academic). - -> After installing a verifier that requires Gurobi, the grbgetkey command can be found at `.venv/opt/gurobi912/linux64/bin/grbgetkey`. - -#### Source Installation - -First create and activate a python virtual environment. - -```bash -$ python -m venv .venv -$ . .venv/bin/activate -``` - -Then run the following commands to clone DNNV and install it into the virtual environment: - -```bash -$ git clone https://github.com/dlshriver/DNNV.git -$ cd DNNV -$ pip install . -``` - -Verifiers can then be installed using the `dnnv_manage` tool as described above. - -**Make sure that the project environment is activated** when using dnnv or the dnnv_manage tools. - -#### Docker Installation - -We provide a docker image with DNNV and all non-Gurobi dependent verifiers. To obtain and use the latest pre-built image of the main branch, run: - -```bash -$ docker pull dlshriver/dnnv:latest -$ docker run --rm -it dlshriver/dnnv:latest -(.venv) dnnv@hostname:~$ dnnv -h -``` - -The latest version of the develop branch is available as `dlshriver/dnnv:develop`, and tagged releases are available as `dlshriver/dnnv:vX.X.X` where `vX.X.X` is the desired version number. - -The docker image can also be built using the provided Dockerfile. The provided build file will install DNNV with all of the verifiers that do not require Gurobi. To build and run the docker image, run: - -```bash -$ docker build . -t dlshriver/dnnv -$ docker run --rm -it dlshriver/dnnv -(.venv) dnnv@hostname:~$ dnnv -h -``` - -### Usage - -Properties are specified in our Python-embedded DSL, [DNNP](https://dnnv.readthedocs.io/en/latest/usage/specifying_properties.html). A property specification can import python modules, and define variables. The only required component is the property expression, which must appear at the end of the file. An example of a local robustness property is shown below. - -```python -from dnnv.properties import * - -N = Network("N") -x = Image("path/to/image") -epsilon = Parameter("epsilon", float, default=1.0) - -Forall( - x_, - Implies( - ((x - epsilon) < x_ < (x + epsilon)), - argmax(N(x_)) == argmax(N(x))), - ), -) -``` - -To check whether property holds for some network using the ERAN verifier, run: - -```bash -$ dnnv property.dnnp --network N network.onnx --eran -``` - -Additionally, if the property defines parameters, using the `Parameter` keyword, they can be specified on the command line using the option `--prop.PARAMETER_NAME`, where `PARAMETER_NAME` is the name of the parameter. For the property defined above, a value for `epsilon` can be provided with a command line option as follows: - -```bash -$ dnnv property.dnnp --network N network.onnx --eran --prop.epsilon=2.0 -``` - -To save any counter-example found by the verifier, use the option `--save-violation /path/to/array.npy` when running DNNV. This will save any violation found as a numpy array at the path specified, which is useful for viewing counter-examples to properties and enables additional debugging and analysis later. - -### Example Problems - -We have made several DNN verification benchmarks available in DNNP+ONNX format in [dlshriver/dnnv-benchmarks](https://github.com/dlshriver/dnnv-benchmarks). -This repo includes the [ACAS Xu](https://github.com/dlshriver/dnnv-benchmarks/tree/main/benchmarks/ACAS_Xu) benchmark, ready to run with DNNV! - -## Acknowledgements - -This material is based in part upon work supported by the National Science Foundation under grant number 1900676 and 2019239. +# Deep Neural Network Verification + +A framework for verification and analysis of deep neural networks. You can read an overview of DNNV in our CAV 2021 paper [*DNNV: A Framework for Deep Neural Network Verification*](https://arxiv.org/abs/2105.12841), or watch our presentation on [YouTube](https://youtu.be/GhXlONbvx1Y). + +## Getting Started + +For detailed instructions on installing and using DNNV, see our [documentation](https://dnnv.readthedocs.io/en/stable/). + +### Installation + +DNNV requires python >=3.7,<3.10, and has been tested on linux. To install the latest stable version run: + +```bash +$ pip install dnnv +``` + +or + +```bash +$ pip install git+https://github.com/dlshriver/DNNV.git@main +``` + +We recommend installing DNNV into a [python virtual environment](https://docs.python.org/3/tutorial/venv.html). + +Install any of the supported verifiers ([Reluplex](https://github.com/guykatzz/ReluplexCav2017), [planet](https://github.com/progirep/planet), [MIPVerify.jl](https://github.com/vtjeng/MIPVerify.jl), [Neurify](https://github.com/tcwangshiqi-columbia/Neurify), [ERAN](https://github.com/eth-sri/eran), [BaB](https://github.com/oval-group/PLNN-verification), [marabou](https://github.com/NeuralNetworkVerification/Marabou), [nnenum](https://github.com/stanleybak/nnenum), [verinet](https://vas.doc.ic.ac.uk/software/neural/)): + +```bash +$ dnnv_manage install reluplex planet mipverify neurify eran bab marabou nnenum verinet +``` + +*Several verifiers make use of the [Gurobi solver](https://www.gurobi.com/).* This should be installed automatically, but requires a license to be manually activated and available on the host machine. Academic licenses can be obtained for free from the [Gurobi website](https://user.gurobi.com/download/licenses/free-academic). + +> After installing a verifier that requires Gurobi, the grbgetkey command can be found at `.venv/opt/gurobi912/linux64/bin/grbgetkey`. + +#### Source Installation + +First create and activate a python virtual environment. + +```bash +$ python -m venv .venv +$ . .venv/bin/activate +``` + +Then run the following commands to clone DNNV and install it into the virtual environment: + +```bash +$ git clone https://github.com/dlshriver/DNNV.git +$ cd DNNV +$ pip install . +``` + +Verifiers can then be installed using the `dnnv_manage` tool as described above. + +**Make sure that the project environment is activated** when using dnnv or the dnnv_manage tools. + +#### Docker Installation + +We provide a docker image with DNNV and all non-Gurobi dependent verifiers. To obtain and use the latest pre-built image of the main branch, run: + +```bash +$ docker pull dlshriver/dnnv:latest +$ docker run --rm -it dlshriver/dnnv:latest +(.venv) dnnv@hostname:~$ dnnv -h +``` + +The latest version of the develop branch is available as `dlshriver/dnnv:develop`, and tagged releases are available as `dlshriver/dnnv:vX.X.X` where `vX.X.X` is the desired version number. + +The docker image can also be built using the provided Dockerfile. The provided build file will install DNNV with all of the verifiers that do not require Gurobi. To build and run the docker image, run: + +```bash +$ docker build . -t dlshriver/dnnv +$ docker run --rm -it dlshriver/dnnv +(.venv) dnnv@hostname:~$ dnnv -h +``` + +### Usage + +Properties are specified in our Python-embedded DSL, [DNNP](https://dnnv.readthedocs.io/en/latest/usage/specifying_properties.html). A property specification can import python modules, and define variables. The only required component is the property expression, which must appear at the end of the file. An example of a local robustness property is shown below. + +```python +from dnnv.properties import * + +N = Network("N") +x = Image("path/to/image") +epsilon = Parameter("epsilon", float, default=1.0) + +Forall( + x_, + Implies( + ((x - epsilon) < x_ < (x + epsilon)), + argmax(N(x_)) == argmax(N(x))), + ), +) +``` + +To check whether property holds for some network using the ERAN verifier, run: + +```bash +$ dnnv property.dnnp --network N network.onnx --eran +``` + +Additionally, if the property defines parameters, using the `Parameter` keyword, they can be specified on the command line using the option `--prop.PARAMETER_NAME`, where `PARAMETER_NAME` is the name of the parameter. For the property defined above, a value for `epsilon` can be provided with a command line option as follows: + +```bash +$ dnnv property.dnnp --network N network.onnx --eran --prop.epsilon=2.0 +``` + +To save any counter-example found by the verifier, use the option `--save-violation /path/to/array.npy` when running DNNV. This will save any violation found as a numpy array at the path specified, which is useful for viewing counter-examples to properties and enables additional debugging and analysis later. + +### Example Problems + +We have made several DNN verification benchmarks available in DNNP+ONNX format in [dlshriver/dnnv-benchmarks](https://github.com/dlshriver/dnnv-benchmarks). +This repo includes the [ACAS Xu](https://github.com/dlshriver/dnnv-benchmarks/tree/main/benchmarks/ACAS_Xu) benchmark, ready to run with DNNV! + +## Acknowledgements + +This material is based in part upon work supported by the National Science Foundation under grant number 1900676 and 2019239. diff --git a/dnnv/nn/converters/onnx.py b/dnnv/nn/converters/onnx.py index 7fecc50d..f7c0637c 100644 --- a/dnnv/nn/converters/onnx.py +++ b/dnnv/nn/converters/onnx.py @@ -1,622 +1,622 @@ -from dnnv.nn.operations.base import OutputSelect -import numpy as np -import onnx - -from collections import defaultdict -from typing import Any, Dict, List, Union - -from .. import operations -from ..graph import OperationGraph -from ..operations import Operation -from ..utils import NUMPY_TO_ONNX_DTYPE -from ..visitors import OperationVisitor - - -def convert(op_graph: OperationGraph, *, add_missing_optional_inputs=False): - converter = OnnxConverter( - op_graph, add_missing_optional_inputs=add_missing_optional_inputs - ) - model = converter.convert() - - return model - - -class OnnxConverter(OperationVisitor): - def __init__(self, op_graph: OperationGraph, add_missing_optional_inputs=False): - self.op_graph = op_graph - self.inputs: List[onnx.ValueInfoProto] = [] - self.outputs: List[onnx.ValueInfoProto] = [] - self.initializer: List[onnx.TensorProto] = [] - self.visited: Dict[Operation, onnx.NodeProto] = {} - self.op_counts: Dict[str, int] = defaultdict(int) - self.add_missing_optional_inputs = add_missing_optional_inputs - - def convert(self, name="onnx_model") -> onnx.ModelProto: - output_details = ( - self.op_graph.output_details - ) # TODO: don't rely on tensorflow converter - for op, (shape, dtype) in zip(self.op_graph.output_operations, output_details): - output_op = self.visit(op) - node = onnx.helper.make_tensor_value_info( - output_op.name, NUMPY_TO_ONNX_DTYPE[dtype], shape - ) - self.outputs.append(node) - - nodes = [n for n in self.visited.values() if isinstance(n, onnx.NodeProto)] - graph_def = onnx.helper.make_graph( - nodes, - name, - self.inputs, - self.outputs, - initializer=self.initializer, - ) - # TODO : make opset configurable - model_def = onnx.helper.make_model( - graph_def, - producer_name="dnnv", - ir_version=7, - opset_imports=[onnx.helper.make_opsetid("", 13)], - ) - model_def = onnx.shape_inference.infer_shapes(model_def) - onnx.checker.check_model(model_def, full_check=True) - return model_def - - def visit(self, operation: Operation) -> Union[onnx.NodeProto, onnx.ValueInfoProto]: - if operation not in self.visited: - result = super().visit(operation) - self.visited[operation] = result - return self.visited[operation] - - def generic_visit(self, operation: Operation): - if not hasattr(self, "visit_%s" % operation.__class__.__name__): - raise ValueError( - f"ONNX converter not implemented for operation type {type(operation).__name__}" - ) - return super().generic_visit(operation) - - def _to_onnx_proto( - self, value: Any, opname: str - ) -> Union[onnx.NodeProto, onnx.TensorProto, onnx.ValueInfoProto]: - if isinstance(value, Operation): - return self.visit(value) - elif isinstance(value, np.ndarray): - tensor_proto = onnx.numpy_helper.from_array(value, name=opname) - self.initializer.append(tensor_proto) - return tensor_proto - elif isinstance(value, (int, float)): - tensor_proto = onnx.numpy_helper.from_array( - np.array(value, dtype=f"{type(value).__name__}32"), name=opname - ) - self.initializer.append(tensor_proto) - return tensor_proto - raise ValueError(f"Unknown type for operand of {opname}: {type(value)}") - - def visit_Add(self, operation: operations.Add) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - - node = onnx.helper.make_node( - op_type, - inputs=[a.name, b.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Atan(self, operation: operations.Atan) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, inputs=[x.name], outputs=[opname], name=opname - ) - - return node - - def visit_AveragePool(self, operation: operations.AveragePool) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name], - outputs=[opname], - kernel_shape=list(operation.kernel_shape), - ceil_mode=operation.ceil_mode, - count_include_pad=operation.count_include_pad, - strides=list(operation.strides), - pads=list(operation.pads), - name=opname, - ) - - return node - - def visit_BatchNormalization( - self, operation: operations.BatchNormalization - ) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - scale = self._to_onnx_proto(operation.scale, f"{opname}.scale") - bias = self._to_onnx_proto(operation.bias, f"{opname}.bias") - mean = self._to_onnx_proto(operation.mean, f"{opname}.mean") - variance = self._to_onnx_proto(operation.variance, f"{opname}.variance") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name, scale.name, bias.name, mean.name, variance.name], - outputs=[opname], - epsilon=operation.epsilon, - momentum=operation.momentum, - name=opname, - ) - - return node - - def visit_Cast(self, operation: operations.Cast) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - to = operation.to - - node = onnx.helper.make_node( - op_type, inputs=[x.name], outputs=[opname], to=to, name=opname - ) - - return node - - def visit_Concat(self, operation: operations.Concat) -> onnx.NodeProto: - idx = self.op_counts["Concat"] = self.op_counts["Concat"] + 1 - opname = f"Concat_{idx}" - - inputs = [ - self._to_onnx_proto(x, f"{opname}.x{i}") for i, x in enumerate(operation.x) - ] - - node = onnx.helper.make_node( - "Concat", - inputs=[x.name for x in inputs], - outputs=[opname], - axis=operation.axis, - name=opname, - ) - - return node - - def visit_Conv(self, operation: operations.Conv) -> onnx.NodeProto: - idx = self.op_counts["Conv"] = self.op_counts["Conv"] + 1 - opname = f"Conv_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - w = self._to_onnx_proto(operation.w, f"{opname}.w") - inputs = [x.name, w.name] - if operation.b is not None: - b = self._to_onnx_proto(operation.b, f"{opname}.b") - inputs.append(b.name) - elif self.add_missing_optional_inputs: - b_ = np.zeros(w.shape[0], dtype=w.dtype) - b = self._to_onnx_proto(b_, f"{opname}.b") - inputs.append(b.name) - - node = onnx.helper.make_node( - "Conv", - inputs=inputs, - outputs=[opname], - kernel_shape=list(operation.kernel_shape), - strides=list(operation.strides), - dilations=list(operation.dilations), - group=operation.group, - pads=list(operation.pads), - name=opname, - ) - - return node - - def visit_ConvTranspose( - self, operation: operations.ConvTranspose - ) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - w = self._to_onnx_proto(operation.w, f"{opname}.w") - inputs = [x.name, w.name] - if operation.b is not None: - b = self._to_onnx_proto(operation.b, f"{opname}.b") - inputs.append(b.name) - elif self.add_missing_optional_inputs: - b_ = np.zeros(w.shape[1], dtype=w.dtype) - b = self._to_onnx_proto(b_, f"{opname}.b") - inputs.append(b.name) - - extra_attributes = {} - if operation.output_shape is not None: - extra_attributes["output_shape"] = list(operation.output_shape) - node = onnx.helper.make_node( - op_type, - inputs=inputs, - outputs=[opname], - auto_pad=operation.auto_pad, - dilations=list(operation.dilations), - group=operation.group, - kernel_shape=list(operation.kernel_shape), - output_padding=list(operation.output_padding), - pads=list(operation.pads), - strides=list(operation.strides), - name=opname, - **extra_attributes, - ) - - return node - - def visit_Div(self, operation: operations.Div) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - - node = onnx.helper.make_node( - op_type, - inputs=[a.name, b.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Dropout(self, operation: operations.Dropout) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - ratio = self._to_onnx_proto(operation.ratio, f"{opname}.ratio") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name, ratio.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Elu(self, operation: operations.Elu) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name], - alpha=operation.alpha, - outputs=[opname], - name=opname, - ) - - return node - - def visit_Expand(self, operation: operations.Expand) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - shape = self._to_onnx_proto(operation.shape, f"{opname}.shape") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name, shape.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Flatten(self, operation: operations.Flatten) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name], - outputs=[opname], - axis=operation.axis, - name=opname, - ) - - return node - - def visit_Gather(self, operation: operations.Gather) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - indices = self._to_onnx_proto(operation.indices, f"{opname}.indices") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name, indices.name], - outputs=[opname], - axis=operation.axis, - name=opname, - ) - - return node - - def visit_Gemm(self, operation: operations.Gemm) -> onnx.NodeProto: - idx = self.op_counts["Gemm"] = self.op_counts["Gemm"] + 1 - opname = f"Gemm_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - inputs = [a.name, b.name] - if operation.c is not None: - c = self._to_onnx_proto(operation.c, f"{opname}.c") - inputs.append(c.name) - elif self.add_missing_optional_inputs: - output_details = OperationGraph([operation]).output_details[0] - c_ = np.zeros(output_details.shape[1], dtype=output_details.dtype) - c = self._to_onnx_proto(c_, f"{opname}.c") - inputs.append(c.name) - - node = onnx.helper.make_node( - "Gemm", - inputs=inputs, - outputs=[opname], - alpha=operation.alpha, - beta=operation.beta, - transA=operation.transpose_a, - transB=operation.transpose_b, - name=opname, - ) - - return node - - def visit_GlobalAveragePool( - self, operation: operations.GlobalAveragePool - ) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Input(self, operation: operations.Input) -> onnx.ValueInfoProto: - idx = self.op_counts["Input"] = self.op_counts["Input"] + 1 - opname = f"Input_{idx}" - - shape = np.asarray(operation.shape).tolist() - if shape[0] < 0: - shape[0] = 1 - dtype = NUMPY_TO_ONNX_DTYPE[operation.dtype] - - node = onnx.helper.make_tensor_value_info(opname, dtype, shape) - self.inputs.append(node) - - return node - - def visit_MatMul(self, operation: operations.MatMul) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - - node = onnx.helper.make_node( - op_type, - inputs=[a.name, b.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_MaxPool(self, operation: operations.MaxPool) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name], - outputs=[opname], - kernel_shape=list(operation.kernel_shape), - ceil_mode=operation.ceil_mode, - strides=list(operation.strides), - dilations=list(operation.dilations), - pads=list(operation.pads), - storage_order=operation.storage_order, - name=opname, - ) - - return node - - def visit_Mul(self, operation: operations.Mul) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - - node = onnx.helper.make_node( - op_type, - inputs=[a.name, b.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_OutputSelect(self, operation: operations.OutputSelect) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - # if operation.index != 0: - # raise NotImplementedError( - # "Support for operations with multiple ouputs is not yet implemented." - # ) - - op = self._to_onnx_proto(operation.operation, f"{opname}.operation") - - node = onnx.helper.make_node( - "Identity", - # inputs=[op.name], - inputs=[op.outputs[operation.index]], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Relu(self, operation: operations.Relu) -> onnx.NodeProto: - idx = self.op_counts["Relu"] = self.op_counts["Relu"] + 1 - opname = f"Relu_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - "Relu", inputs=[x.name], outputs=[opname], name=opname - ) - - return node - - def visit_Reshape(self, operation: operations.Reshape) -> onnx.NodeProto: - idx = self.op_counts["Reshape"] = self.op_counts["Reshape"] + 1 - opname = f"Reshape_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - shape = self._to_onnx_proto(operation.shape, f"{opname}.shape") - - if operation.allowzero: - # TODO : need to use newer onnx opset version - raise ValueError("Reshape allowzero is not yet supported") - - node = onnx.helper.make_node( - "Reshape", - inputs=[x.name, shape.name], - # allowzero=operation.allowzero, - outputs=[opname], - name=opname, - ) - - return node - - def visit_Sigmoid(self, operation: operations.Sigmoid) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, inputs=[x.name], outputs=[opname], name=opname - ) - - return node - - def visit_Sub(self, operation: operations.Sub) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - a = self._to_onnx_proto(operation.a, f"{opname}.a") - b = self._to_onnx_proto(operation.b, f"{opname}.b") - - node = onnx.helper.make_node( - op_type, - inputs=[a.name, b.name], - outputs=[opname], - name=opname, - ) - - return node - - def visit_Tanh(self, operation: operations.Tanh) -> onnx.NodeProto: - op_type = str(operation) - idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 - opname = f"{op_type}_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, inputs=[x.name], outputs=[opname], name=opname - ) - - return node - - def visit_Transpose(self, operation: operations.Transpose) -> onnx.NodeProto: - idx = self.op_counts["Transpose"] = self.op_counts["Transpose"] + 1 - opname = f"Transpose_{idx}" - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - "Transpose", - inputs=[x.name], - outputs=[opname], - name=opname, - perm=list(operation.permutation), - ) - - return node - - def visit_Split(self, operation: operations.Split) -> onnx.NodeProto: - op_type = str(operation) - # TODO: split attribute is optional. Edits to nn/parser/onnx.py required. - assert operation.split is not None - idx = self.op_counts["Split"] = self.op_counts["Split"] + 1 - opname = f"Split_{idx}" - outputs = [] - for i in range(len(operation.split)): - outputs.append(f"output_{i}") - - x = self._to_onnx_proto(operation.x, f"{opname}.x") - - node = onnx.helper.make_node( - op_type, - inputs=[x.name, operation.split], - outputs=outputs, - name=opname, - axis=operation.axis, - ) - - return node +from dnnv.nn.operations.base import OutputSelect +import numpy as np +import onnx + +from collections import defaultdict +from typing import Any, Dict, List, Union + +from .. import operations +from ..graph import OperationGraph +from ..operations import Operation +from ..utils import NUMPY_TO_ONNX_DTYPE +from ..visitors import OperationVisitor + + +def convert(op_graph: OperationGraph, *, add_missing_optional_inputs=False): + converter = OnnxConverter( + op_graph, add_missing_optional_inputs=add_missing_optional_inputs + ) + model = converter.convert() + + return model + + +class OnnxConverter(OperationVisitor): + def __init__(self, op_graph: OperationGraph, add_missing_optional_inputs=False): + self.op_graph = op_graph + self.inputs: List[onnx.ValueInfoProto] = [] + self.outputs: List[onnx.ValueInfoProto] = [] + self.initializer: List[onnx.TensorProto] = [] + self.visited: Dict[Operation, onnx.NodeProto] = {} + self.op_counts: Dict[str, int] = defaultdict(int) + self.add_missing_optional_inputs = add_missing_optional_inputs + + def convert(self, name="onnx_model") -> onnx.ModelProto: + output_details = ( + self.op_graph.output_details + ) # TODO: don't rely on tensorflow converter + for op, (shape, dtype) in zip(self.op_graph.output_operations, output_details): + output_op = self.visit(op) + node = onnx.helper.make_tensor_value_info( + output_op.name, NUMPY_TO_ONNX_DTYPE[dtype], shape + ) + self.outputs.append(node) + + nodes = [n for n in self.visited.values() if isinstance(n, onnx.NodeProto)] + graph_def = onnx.helper.make_graph( + nodes, + name, + self.inputs, + self.outputs, + initializer=self.initializer, + ) + # TODO : make opset configurable + model_def = onnx.helper.make_model( + graph_def, + producer_name="dnnv", + ir_version=7, + opset_imports=[onnx.helper.make_opsetid("", 13)], + ) + model_def = onnx.shape_inference.infer_shapes(model_def) + onnx.checker.check_model(model_def, full_check=True) + return model_def + + def visit(self, operation: Operation) -> Union[onnx.NodeProto, onnx.ValueInfoProto]: + if operation not in self.visited: + result = super().visit(operation) + self.visited[operation] = result + return self.visited[operation] + + def generic_visit(self, operation: Operation): + if not hasattr(self, "visit_%s" % operation.__class__.__name__): + raise ValueError( + f"ONNX converter not implemented for operation type {type(operation).__name__}" + ) + return super().generic_visit(operation) + + def _to_onnx_proto( + self, value: Any, opname: str + ) -> Union[onnx.NodeProto, onnx.TensorProto, onnx.ValueInfoProto]: + if isinstance(value, Operation): + return self.visit(value) + elif isinstance(value, np.ndarray): + tensor_proto = onnx.numpy_helper.from_array(value, name=opname) + self.initializer.append(tensor_proto) + return tensor_proto + elif isinstance(value, (int, float)): + tensor_proto = onnx.numpy_helper.from_array( + np.array(value, dtype=f"{type(value).__name__}32"), name=opname + ) + self.initializer.append(tensor_proto) + return tensor_proto + raise ValueError(f"Unknown type for operand of {opname}: {type(value)}") + + def visit_Add(self, operation: operations.Add) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + + node = onnx.helper.make_node( + op_type, + inputs=[a.name, b.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Atan(self, operation: operations.Atan) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, inputs=[x.name], outputs=[opname], name=opname + ) + + return node + + def visit_AveragePool(self, operation: operations.AveragePool) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + outputs=[opname], + kernel_shape=list(operation.kernel_shape), + ceil_mode=operation.ceil_mode, + count_include_pad=operation.count_include_pad, + strides=list(operation.strides), + pads=list(operation.pads), + name=opname, + ) + + return node + + def visit_BatchNormalization( + self, operation: operations.BatchNormalization + ) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + scale = self._to_onnx_proto(operation.scale, f"{opname}.scale") + bias = self._to_onnx_proto(operation.bias, f"{opname}.bias") + mean = self._to_onnx_proto(operation.mean, f"{opname}.mean") + variance = self._to_onnx_proto(operation.variance, f"{opname}.variance") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name, scale.name, bias.name, mean.name, variance.name], + outputs=[opname], + epsilon=operation.epsilon, + momentum=operation.momentum, + name=opname, + ) + + return node + + def visit_Cast(self, operation: operations.Cast) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + to = operation.to + + node = onnx.helper.make_node( + op_type, inputs=[x.name], outputs=[opname], to=to, name=opname + ) + + return node + + def visit_Concat(self, operation: operations.Concat) -> onnx.NodeProto: + idx = self.op_counts["Concat"] = self.op_counts["Concat"] + 1 + opname = f"Concat_{idx}" + + inputs = [ + self._to_onnx_proto(x, f"{opname}.x{i}") for i, x in enumerate(operation.x) + ] + + node = onnx.helper.make_node( + "Concat", + inputs=[x.name for x in inputs], + outputs=[opname], + axis=operation.axis, + name=opname, + ) + + return node + + def visit_Conv(self, operation: operations.Conv) -> onnx.NodeProto: + idx = self.op_counts["Conv"] = self.op_counts["Conv"] + 1 + opname = f"Conv_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + w = self._to_onnx_proto(operation.w, f"{opname}.w") + inputs = [x.name, w.name] + if operation.b is not None: + b = self._to_onnx_proto(operation.b, f"{opname}.b") + inputs.append(b.name) + elif self.add_missing_optional_inputs: + b_ = np.zeros(w.shape[0], dtype=w.dtype) + b = self._to_onnx_proto(b_, f"{opname}.b") + inputs.append(b.name) + + node = onnx.helper.make_node( + "Conv", + inputs=inputs, + outputs=[opname], + kernel_shape=list(operation.kernel_shape), + strides=list(operation.strides), + dilations=list(operation.dilations), + group=operation.group, + pads=list(operation.pads), + name=opname, + ) + + return node + + def visit_ConvTranspose( + self, operation: operations.ConvTranspose + ) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + w = self._to_onnx_proto(operation.w, f"{opname}.w") + inputs = [x.name, w.name] + if operation.b is not None: + b = self._to_onnx_proto(operation.b, f"{opname}.b") + inputs.append(b.name) + elif self.add_missing_optional_inputs: + b_ = np.zeros(w.shape[1], dtype=w.dtype) + b = self._to_onnx_proto(b_, f"{opname}.b") + inputs.append(b.name) + + extra_attributes = {} + if operation.output_shape is not None: + extra_attributes["output_shape"] = list(operation.output_shape) + node = onnx.helper.make_node( + op_type, + inputs=inputs, + outputs=[opname], + auto_pad=operation.auto_pad, + dilations=list(operation.dilations), + group=operation.group, + kernel_shape=list(operation.kernel_shape), + output_padding=list(operation.output_padding), + pads=list(operation.pads), + strides=list(operation.strides), + name=opname, + **extra_attributes, + ) + + return node + + def visit_Div(self, operation: operations.Div) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + + node = onnx.helper.make_node( + op_type, + inputs=[a.name, b.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Dropout(self, operation: operations.Dropout) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + ratio = self._to_onnx_proto(operation.ratio, f"{opname}.ratio") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name, ratio.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Elu(self, operation: operations.Elu) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + alpha=operation.alpha, + outputs=[opname], + name=opname, + ) + + return node + + def visit_Expand(self, operation: operations.Expand) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + shape = self._to_onnx_proto(operation.shape, f"{opname}.shape") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name, shape.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Flatten(self, operation: operations.Flatten) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + outputs=[opname], + axis=operation.axis, + name=opname, + ) + + return node + + def visit_Gather(self, operation: operations.Gather) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + indices = self._to_onnx_proto(operation.indices, f"{opname}.indices") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name, indices.name], + outputs=[opname], + axis=operation.axis, + name=opname, + ) + + return node + + def visit_Gemm(self, operation: operations.Gemm) -> onnx.NodeProto: + idx = self.op_counts["Gemm"] = self.op_counts["Gemm"] + 1 + opname = f"Gemm_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + inputs = [a.name, b.name] + if operation.c is not None: + c = self._to_onnx_proto(operation.c, f"{opname}.c") + inputs.append(c.name) + elif self.add_missing_optional_inputs: + output_details = OperationGraph([operation]).output_details[0] + c_ = np.zeros(output_details.shape[1], dtype=output_details.dtype) + c = self._to_onnx_proto(c_, f"{opname}.c") + inputs.append(c.name) + + node = onnx.helper.make_node( + "Gemm", + inputs=inputs, + outputs=[opname], + alpha=operation.alpha, + beta=operation.beta, + transA=operation.transpose_a, + transB=operation.transpose_b, + name=opname, + ) + + return node + + def visit_GlobalAveragePool( + self, operation: operations.GlobalAveragePool + ) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Input(self, operation: operations.Input) -> onnx.ValueInfoProto: + idx = self.op_counts["Input"] = self.op_counts["Input"] + 1 + opname = f"Input_{idx}" + + shape = np.asarray(operation.shape).tolist() + if shape[0] < 0: + shape[0] = 1 + dtype = NUMPY_TO_ONNX_DTYPE[operation.dtype] + + node = onnx.helper.make_tensor_value_info(opname, dtype, shape) + self.inputs.append(node) + + return node + + def visit_MatMul(self, operation: operations.MatMul) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + + node = onnx.helper.make_node( + op_type, + inputs=[a.name, b.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_MaxPool(self, operation: operations.MaxPool) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + outputs=[opname], + kernel_shape=list(operation.kernel_shape), + ceil_mode=operation.ceil_mode, + strides=list(operation.strides), + dilations=list(operation.dilations), + pads=list(operation.pads), + storage_order=operation.storage_order, + name=opname, + ) + + return node + + def visit_Mul(self, operation: operations.Mul) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + + node = onnx.helper.make_node( + op_type, + inputs=[a.name, b.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_OutputSelect(self, operation: operations.OutputSelect) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + # if operation.index != 0: + # raise NotImplementedError( + # "Support for operations with multiple ouputs is not yet implemented." + # ) + + op = self._to_onnx_proto(operation.operation, f"{opname}.operation") + + node = onnx.helper.make_node( + "Identity", + # inputs=[op.name], + inputs=[op.outputs[operation.index]], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Relu(self, operation: operations.Relu) -> onnx.NodeProto: + idx = self.op_counts["Relu"] = self.op_counts["Relu"] + 1 + opname = f"Relu_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + "Relu", inputs=[x.name], outputs=[opname], name=opname + ) + + return node + + def visit_Reshape(self, operation: operations.Reshape) -> onnx.NodeProto: + idx = self.op_counts["Reshape"] = self.op_counts["Reshape"] + 1 + opname = f"Reshape_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + shape = self._to_onnx_proto(operation.shape, f"{opname}.shape") + + if operation.allowzero: + # TODO : need to use newer onnx opset version + raise ValueError("Reshape allowzero is not yet supported") + + node = onnx.helper.make_node( + "Reshape", + inputs=[x.name, shape.name], + # allowzero=operation.allowzero, + outputs=[opname], + name=opname, + ) + + return node + + def visit_Sigmoid(self, operation: operations.Sigmoid) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, inputs=[x.name], outputs=[opname], name=opname + ) + + return node + + def visit_Sub(self, operation: operations.Sub) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + a = self._to_onnx_proto(operation.a, f"{opname}.a") + b = self._to_onnx_proto(operation.b, f"{opname}.b") + + node = onnx.helper.make_node( + op_type, + inputs=[a.name, b.name], + outputs=[opname], + name=opname, + ) + + return node + + def visit_Tanh(self, operation: operations.Tanh) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, inputs=[x.name], outputs=[opname], name=opname + ) + + return node + + def visit_Transpose(self, operation: operations.Transpose) -> onnx.NodeProto: + idx = self.op_counts["Transpose"] = self.op_counts["Transpose"] + 1 + opname = f"Transpose_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + "Transpose", + inputs=[x.name], + outputs=[opname], + name=opname, + perm=list(operation.permutation), + ) + + return node + + def visit_Split(self, operation: operations.Split) -> onnx.NodeProto: + op_type = str(operation) + # TODO: split attribute is optional. Edits to nn/parser/onnx.py required. + assert operation.split is not None + idx = self.op_counts["Split"] = self.op_counts["Split"] + 1 + opname = f"Split_{idx}" + outputs = [] + for i in range(len(operation.split)): + outputs.append(f"output_{i}") + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + + node = onnx.helper.make_node( + op_type, + inputs=[x.name, operation.split], + outputs=outputs, + name=opname, + axis=operation.axis, + ) + + return node diff --git a/dnnv/nn/converters/tensorflow.py b/dnnv/nn/converters/tensorflow.py index eded3aec..e5dba2f3 100644 --- a/dnnv/nn/converters/tensorflow.py +++ b/dnnv/nn/converters/tensorflow.py @@ -1,889 +1,889 @@ -import numpy as np -import tensorflow as tf - -from ..graph import OperationGraph -from ..operations import Operation -from ..utils import ONNX_TO_TENSORFLOW_DTYPE -from ..visitors import OperationVisitor - - -class TensorflowConverterError(Exception): - pass - - -def convert(op_graph: OperationGraph): - converter = TensorflowConverter() - output_funcs = [] - for op in op_graph.output_operations: - output_funcs.append(converter.visit(op)) - - def func(*inputs, squeeze=True): - converter._cache.clear() - outputs = [] - for output_func in output_funcs: - output = output_func(*inputs) - if isinstance(output, tf.Tensor) and tf.executing_eagerly(): - output = output.numpy() - outputs.append(output) - if squeeze and len(outputs) == 1: - return outputs[0] - return tuple(outputs) - - return func - - -def _concretize(variables, inputs): - concrete_values = [] - for variable in variables: - if callable(variable): - concrete_values.append(variable(*inputs)) - else: - concrete_values.append(variable) - if len(concrete_values) == 1: - return concrete_values[0] - return concrete_values - - -class TensorflowConverter(OperationVisitor): - def __init__(self): - self.input_count = 0 - self.results = {} - self._cache = {} - - def _cached(self, func): - def wrapped_func(*args, **kwargs): - if func not in self._cache: - try: - self._cache[func] = func(*args, **kwargs) - except TensorflowConverterError: - raise - except Exception as e: - args_str = "" - if len(e.args) == 1: - args_str = e.args[0] - elif len(e.args) > 1: - args_str = str(e.args) - raise TensorflowConverterError( - f"{type(e).__name__}: {args_str}" - ).with_traceback(e.__traceback__) - return self._cache[func] - - return wrapped_func - - def visit(self, operation): - if operation not in self.results: - result = super().visit(operation) - self.results[operation] = result - return self.results[operation] - - def generic_visit(self, operation): - if not hasattr(self, "visit_%s" % operation.__class__.__name__): - raise ValueError( - "Tensorflow converter not implemented for operation type %s" - % operation.__class__.__name__ - ) - return super().generic_visit(operation) - - def visit_Add(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - - @self._cached - def add_func(*inputs): - a, b = _concretize([a_, b_], inputs) - result = tf.add(a, b) - return result - - return add_func - - def visit_Atan(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def atan_func(*inputs): - x = _concretize([x_], inputs) - result = tf.atan(x) - return result - - return atan_func - - def visit_AveragePool(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def avgpool_func(*inputs): - x = _concretize([x_], inputs) - if operation.ceil_mode: - # TODO : add support - raise ValueError( - "ceil_mode=True is not currently supported for AveragePool" - ) - if any(p != 0 for p in operation.pads) and not operation.count_include_pad: - # TODO : add support - raise ValueError( - "count_include_pad=False is not currently supported for AveragePool" - ) - kernel_shape = operation.kernel_shape - strides = operation.strides - num_pads = len(operation.pads) - pads = tuple( - zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) - ) - - x_ndim = int(tf.rank(x)) - x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) - padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) - result = tf.nn.pool( - padded_x, - kernel_shape, - pooling_type="AVG", - strides=strides, - padding="VALID", - ) - result_ndim = int(tf.rank(result)) - result = tf.transpose( - result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) - ) - return result - - return avgpool_func - - def visit_BatchNormalization(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def batchnorm_func(*inputs): - x = _concretize([x_], inputs) - scale = operation.scale - bias = operation.bias - mean = operation.mean - variance = operation.variance - epsilon = operation.epsilon - - x_ndim = int(tf.rank(x)) - x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) - result = tf.nn.batch_normalization( - x, - mean=mean, - variance=variance, - offset=bias, - scale=scale, - variance_epsilon=epsilon, - ) - result_ndim = int(tf.rank(result)) - result = tf.transpose( - result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) - ) - return result - - return batchnorm_func - - def visit_Cast(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def cast_func(*inputs): - x = _concretize([x_], inputs) - result = tf.cast(x, ONNX_TO_TENSORFLOW_DTYPE[operation.to]) - return result - - return cast_func - - def visit_Concat(self, operation): - tensors_ = [] - for x in operation.x: - if isinstance(x, Operation): - x = self.visit(x) - tensors_.append(x) - - @self._cached - def concat_func(*inputs): - tensors = x = _concretize(tensors_, inputs) - result = tf.concat(tensors, axis=operation.axis) - return result - - return concat_func - - def visit_Conv(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def conv_func(*inputs): - x = _concretize([x_], inputs) - if len(operation.kernel_shape) != 2: - raise NotImplementedError( - "Non 2d convolutions are not currently supported." - ) - weights = operation.w - if operation.b is not None: - bias = operation.b - else: - bias = np.zeros((weights.shape[0],), dtype=weights.dtype) - assert np.all(operation.dilations == 1) - # assert np.all(operation.group == 1) - num_pads = len(operation.pads) - pads = tuple( - zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) - ) - - x_ndim = int(tf.rank(x)) - x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) - padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) - result = tf.nn.bias_add( - tf.nn.conv2d( - padded_x, - weights.transpose((2, 3, 1, 0)), - operation.strides, - padding="VALID", - ), - bias, - ) - result_ndim = int(tf.rank(result)) - result = tf.transpose( - result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) - ) - return result - - return conv_func - - def visit_ConvTranspose(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def convtranspose_func(*inputs): - x = _concretize([x_], inputs) - - if len(operation.kernel_shape) == 1: - conv_transpose = tf.nn.conv1d_transpose - elif len(operation.kernel_shape) == 2: - conv_transpose = tf.nn.conv2d_transpose - elif len(operation.kernel_shape) == 3: - conv_transpose = tf.nn.conv3d_transpose - else: - raise NotImplementedError( - f"{len(operation.kernel_shape)}d ConvTranspose operations are not currently supported." - ) - if ( - operation.auto_pad != "NOTSET" - or operation.auto_pad == "VALID" - and any(p != 0 for p in operation.pads) - ): - raise NotImplementedError( - f"Unsupported padding for ConvTranspose: {operation.auto_pad}" - ) - if np.any(operation.dilations != 1): - raise NotImplementedError( - f"Unsupported dilations for ConvTranspose: {operation.dilations}" - ) - - weights = operation.w - if operation.b is not None: - bias = operation.b - else: - bias = np.zeros((weights.shape[1],), dtype=weights.dtype) - # assert np.all(operation.group == 1) - - num_pads = len(operation.pads) - pads = tuple( - zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) - ) - if any(p != 0 for p in operation.pads): - raise NotImplementedError( - "Non 0 pads are not currently supported for ConvTranspose" - ) - - output_shape = operation.output_shape - if output_shape is None: - input_shape = [int(d) for d in x.shape[2:]] - start_pads = operation.pads[: num_pads // 2] - end_pads = operation.pads[num_pads // 2 :] - output_shape = ( - [int(x.shape[0])] - + [ - ( - operation.strides[i] * (input_shape[i] - 1) - + operation.output_padding[i] - + ( - (operation.kernel_shape[i] - 1) * operation.dilations[i] - + 1 - ) - - start_pads[i] - - end_pads[i] - ) - for i in range(len(operation.kernel_shape)) - ] - + [weights.shape[1]] - ) - - x_ndim = int(tf.rank(x)) - x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) - padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) - weights_ndim = int(tf.rank(weights)) - result = tf.nn.bias_add( - conv_transpose( - padded_x, - weights.transpose(tuple(range(2, weights_ndim)) + (1, 0)), - output_shape, - strides=operation.strides, - padding="VALID", - dilations=operation.dilations, - ), - bias, - ) - result_ndim = int(tf.rank(result)) - result = tf.transpose( - result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) - ) - return result - - return convtranspose_func - - def visit_Div(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - - @self._cached - def div_func(*inputs): - a, b = _concretize([a_, b_], inputs) - result = tf.convert_to_tensor(tf.divide(a, b)) - return result - - return div_func - - def visit_Dropout(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def dropout_func(*inputs): - x = _concretize([x_], inputs) - return x, None - - return dropout_func - - def visit_Elu(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def elu_func(*inputs): - if operation.alpha != 1.0: - raise NotImplementedError( - "The tensorflow converter currently does not support ELU activations with alpha other than 1.0" - ) - x = _concretize([x_], inputs) - result = tf.nn.elu(x) - return result - - return elu_func - - def visit_Expand(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def expand_func(*inputs): - x = _concretize([x_], inputs) - shape = operation.shape - result = x * tf.ones(shape, x.dtype) - return result - - return expand_func - - def visit_Flatten(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def flatten_func(*inputs): - x = _concretize([x_], inputs) - axis = operation.axis - new_shape = (1, -1) if axis == 0 else (int(np.prod(x.shape[:axis])), -1) - result = tf.reshape(x, new_shape) - return result - - return flatten_func - - def visit_Gather(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - indices_ = operation.indices - if isinstance(indices_, Operation): - indices_ = self.visit(indices_) - - @self._cached - def gather_func(*inputs): - x, indices = _concretize([x_, indices_], inputs) - result = tf.gather(x, indices, axis=operation.axis) - return result - - return gather_func - - def visit_Gemm(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - c_ = operation.c - if isinstance(c_, Operation): - c_ = self.visit(c_) - - @self._cached - def gemm_func(*inputs): - a, b, c = _concretize([a_, b_, c_], inputs) - result = operation.alpha * tf.matmul( - a, - b, - transpose_a=operation.transpose_a, - transpose_b=operation.transpose_b, - ) - if c is not None: - result = result + operation.beta * c - return result - - return gemm_func - - def visit_GlobalAveragePool(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def globalavgpool_func(*inputs): - x = _concretize([x_], inputs) - - x = tf.transpose(x, (0, 2, 3, 1)) - result = tf.nn.pool( - x, x.shape[1:3], pooling_type="AVG", strides=(1, 1), padding="VALID" - ) - result = tf.transpose(result, (0, 3, 1, 2)) - return result - - return globalavgpool_func - - def visit_Identity(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def identity_func(*inputs): - x = _concretize([x_], inputs) - return tf.convert_to_tensor(x) - - return identity_func - - def visit_Input(self, operation): - input_idx = self.input_count - - @self._cached - def input_func(*inputs): - x = inputs[input_idx] - if any( - d1 != d2 and -1 not in (d1, d2) and None not in (d1, d2) - for d1, d2 in zip(operation.shape[1:], x.shape[1:]) - ): - raise ValueError( - "Incorrect input shape: %s != %s" % (operation.shape, x.shape) - ) - if x.dtype != operation.dtype: - raise TypeError( - "Incorrect type, %s, for input %d. Expected type %s." - % (x.dtype, input_idx, operation.dtype) - ) - return tf.convert_to_tensor(x) - - self.input_count += 1 - - return input_func - - def visit_LeakyRelu(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def leakyrelu_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.leaky_relu(x, alpha=operation.alpha) - return result - - return leakyrelu_func - - def visit_LogSoftmax(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def softmax_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.log_softmax(x, axis=operation.axis) - return result - - return softmax_func - - def visit_MatMul(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - - @self._cached - def matmul_func(*inputs): - a, b = _concretize([a_, b_], inputs) - - if len(a.shape) == 2 and len(b.shape) == 1: - result = tf.matmul(a, b[:, None])[:, 0] - elif len(a.shape) == 1 and len(b.shape) == 2: - result = tf.matmul(a[None], b)[0] - else: - result = tf.matmul(a, b) - return result - - return matmul_func - - def visit_MaxPool(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def maxpool_func(*inputs): - x = _concretize([x_], inputs) - - if operation.ceil_mode: - # TODO : add support - raise ValueError( - "ceil_mode=True is not currently supported for MaxPool" - ) - - kernel_shape = operation.kernel_shape - strides = operation.strides - num_pads = len(operation.pads) - pads = tuple( - zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) - ) - - x_ndim = int(tf.rank(x)) - x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) - padded_x = tf.pad( - x, ((0, 0),) + pads + ((0, 0),), constant_values=x.dtype.min - ) - result = tf.nn.pool( - padded_x, - kernel_shape, - pooling_type="MAX", - strides=strides, - dilations=operation.dilations, - padding="VALID", - ) - result_ndim = int(tf.rank(result)) - result = tf.transpose( - result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) - ) - return result - - return maxpool_func - - def visit_Mul(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - - @self._cached - def mul_func(*inputs): - a, b = _concretize([a_, b_], inputs) - result = tf.multiply(a, b) - return result - - return mul_func - - def visit_OutputSelect(self, operation): - x_ = self.visit(operation.operation) - - @self._cached - def output_select_func(*inputs): - x = _concretize([x_], inputs) - return x[operation.index] - - return output_select_func - - def visit_Pad(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def pad_func(*inputs): - x = _concretize([x_], inputs) - mode = operation.mode.upper() - if mode != "CONSTANT": - raise ValueError(f"{mode} padding is not currently supported") - num_pads = len(operation.pads) - pads = tuple( - zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) - ) - result = tf.pad(x, pads, mode=mode, constant_values=operation.value) - return result - - return pad_func - - def visit_Relu(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def relu_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.relu(x) - return result - - return relu_func - - def visit_Reshape(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - shape_ = operation.shape - if isinstance(shape_, Operation): - shape_ = self.visit(shape_) - - @self._cached - def reshape_func(*inputs): - x, shape = _concretize([x_, shape_], inputs) - if not operation.allowzero: - for i, d in enumerate(shape): - if d == 0: - shape[i] = x.shape[i] - result = tf.reshape(x, shape) - return result - - return reshape_func - - def visit_Resize(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - roi_ = operation.roi - if isinstance(roi_, Operation): - roi_ = self.visit(roi_) - scales_ = operation.scales - if isinstance(scales_, Operation): - scales_ = self.visit(scales_) - sizes_ = operation.sizes - if isinstance(sizes_, Operation): - sizes_ = self.visit(sizes_) - - @self._cached - def resize_func(*inputs): - x, roi, scales, sizes = _concretize([x_, roi_, scales_, sizes_], inputs) - assert operation.coordinate_transformation_mode in [ - "asymmetric", - "tf_crop_and_resize", - ] - assert operation.mode in ["nearest", "linear"] - assert operation.exclude_outside == 0 - if roi is None or roi.size == 0: - roi = np.array([[0, 0, 1, 1]]) - else: - assert operation.coordinate_transformation_mode == "tf_crop_and_resize" - assert roi.size == 8 and roi.ndim == 1 - roi = roi[None, [2, 3, 6, 7]] - if sizes is None or sizes.size == 0: - assert scales[0] == 1.0 and scales[1] == 1.0 - sizes = (scales * [int(d) for d in x.shape]).astype(int) - assert sizes.ndim == 1 and sizes.size == 4 - sizes = sizes[2:] - method = operation.mode - if method == "linear": - method = "bilinear" - result = tf.transpose(x, (0, 2, 3, 1)) - result = tf.image.crop_and_resize( - result, - boxes=roi, - box_indices=np.arange(int(x.shape[0])), - crop_size=sizes, - method=method, - extrapolation_value=operation.extrapolation_value, - ) - result = tf.transpose(result, (0, 3, 1, 2)) - return result - - return resize_func - - def visit_Shape(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def shape_func(*inputs): - x = _concretize([x_], inputs) - result = tf.shape(x) - return result - - return shape_func - - def visit_Sigmoid(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def sigmoid_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.sigmoid(x) - return result - - return sigmoid_func - - def visit_Sign(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def sign_func(*inputs): - x = _concretize([x_], inputs) - result = tf.math.sign(x) - return result - - return sign_func - - def visit_Softmax(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def softmax_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.softmax(x, axis=operation.axis) - return result - - return softmax_func - - def visit_Sub(self, operation): - a_ = operation.a - if isinstance(a_, Operation): - a_ = self.visit(a_) - b_ = operation.b - if isinstance(b_, Operation): - b_ = self.visit(b_) - - @self._cached - def sub_func(*inputs): - a, b = _concretize([a_, b_], inputs) - result = tf.subtract(a, b) - return result - - return sub_func - - def visit_Tanh(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def tanh_func(*inputs): - x = _concretize([x_], inputs) - result = tf.nn.tanh(x) - return result - - return tanh_func - - def visit_Tile(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - repeats_ = operation.repeats - if isinstance(repeats_, Operation): - repeats_ = self.visit(repeats_) - - @self._cached - def tile_func(*inputs): - x, repeats = _concretize([x_, repeats_], inputs) - result = tf.tile(x, repeats) - return result - - return tile_func - - def visit_Transpose(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - - @self._cached - def transpose_func(*inputs): - x = _concretize([x_], inputs) - permutation = operation.permutation - result = tf.transpose(x, permutation) - return result - - return transpose_func - - def visit_Unsqueeze(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - axes_ = operation.axes - if isinstance(axes_, Operation): - axes_ = self.visit(axes_) - - @self._cached - def unsqueeze_func(*inputs): - x, axes = _concretize([x_, axes_], inputs) - for axis in sorted(axes): - x = tf.expand_dims(x, axis) - return x - - return unsqueeze_func - - def visit_Split(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - axis = operation.axis - split = operation.split - - @self._cached - def split_func(*inputs): - x = _concretize([x_], inputs) - x = tf.split(x, split, axis=axis) - return x - - return split_func +import numpy as np +import tensorflow as tf + +from ..graph import OperationGraph +from ..operations import Operation +from ..utils import ONNX_TO_TENSORFLOW_DTYPE +from ..visitors import OperationVisitor + + +class TensorflowConverterError(Exception): + pass + + +def convert(op_graph: OperationGraph): + converter = TensorflowConverter() + output_funcs = [] + for op in op_graph.output_operations: + output_funcs.append(converter.visit(op)) + + def func(*inputs, squeeze=True): + converter._cache.clear() + outputs = [] + for output_func in output_funcs: + output = output_func(*inputs) + if isinstance(output, tf.Tensor) and tf.executing_eagerly(): + output = output.numpy() + outputs.append(output) + if squeeze and len(outputs) == 1: + return outputs[0] + return tuple(outputs) + + return func + + +def _concretize(variables, inputs): + concrete_values = [] + for variable in variables: + if callable(variable): + concrete_values.append(variable(*inputs)) + else: + concrete_values.append(variable) + if len(concrete_values) == 1: + return concrete_values[0] + return concrete_values + + +class TensorflowConverter(OperationVisitor): + def __init__(self): + self.input_count = 0 + self.results = {} + self._cache = {} + + def _cached(self, func): + def wrapped_func(*args, **kwargs): + if func not in self._cache: + try: + self._cache[func] = func(*args, **kwargs) + except TensorflowConverterError: + raise + except Exception as e: + args_str = "" + if len(e.args) == 1: + args_str = e.args[0] + elif len(e.args) > 1: + args_str = str(e.args) + raise TensorflowConverterError( + f"{type(e).__name__}: {args_str}" + ).with_traceback(e.__traceback__) + return self._cache[func] + + return wrapped_func + + def visit(self, operation): + if operation not in self.results: + result = super().visit(operation) + self.results[operation] = result + return self.results[operation] + + def generic_visit(self, operation): + if not hasattr(self, "visit_%s" % operation.__class__.__name__): + raise ValueError( + "Tensorflow converter not implemented for operation type %s" + % operation.__class__.__name__ + ) + return super().generic_visit(operation) + + def visit_Add(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + + @self._cached + def add_func(*inputs): + a, b = _concretize([a_, b_], inputs) + result = tf.add(a, b) + return result + + return add_func + + def visit_Atan(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def atan_func(*inputs): + x = _concretize([x_], inputs) + result = tf.atan(x) + return result + + return atan_func + + def visit_AveragePool(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def avgpool_func(*inputs): + x = _concretize([x_], inputs) + if operation.ceil_mode: + # TODO : add support + raise ValueError( + "ceil_mode=True is not currently supported for AveragePool" + ) + if any(p != 0 for p in operation.pads) and not operation.count_include_pad: + # TODO : add support + raise ValueError( + "count_include_pad=False is not currently supported for AveragePool" + ) + kernel_shape = operation.kernel_shape + strides = operation.strides + num_pads = len(operation.pads) + pads = tuple( + zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) + ) + + x_ndim = int(tf.rank(x)) + x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) + padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) + result = tf.nn.pool( + padded_x, + kernel_shape, + pooling_type="AVG", + strides=strides, + padding="VALID", + ) + result_ndim = int(tf.rank(result)) + result = tf.transpose( + result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) + ) + return result + + return avgpool_func + + def visit_BatchNormalization(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def batchnorm_func(*inputs): + x = _concretize([x_], inputs) + scale = operation.scale + bias = operation.bias + mean = operation.mean + variance = operation.variance + epsilon = operation.epsilon + + x_ndim = int(tf.rank(x)) + x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) + result = tf.nn.batch_normalization( + x, + mean=mean, + variance=variance, + offset=bias, + scale=scale, + variance_epsilon=epsilon, + ) + result_ndim = int(tf.rank(result)) + result = tf.transpose( + result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) + ) + return result + + return batchnorm_func + + def visit_Cast(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def cast_func(*inputs): + x = _concretize([x_], inputs) + result = tf.cast(x, ONNX_TO_TENSORFLOW_DTYPE[operation.to]) + return result + + return cast_func + + def visit_Concat(self, operation): + tensors_ = [] + for x in operation.x: + if isinstance(x, Operation): + x = self.visit(x) + tensors_.append(x) + + @self._cached + def concat_func(*inputs): + tensors = x = _concretize(tensors_, inputs) + result = tf.concat(tensors, axis=operation.axis) + return result + + return concat_func + + def visit_Conv(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def conv_func(*inputs): + x = _concretize([x_], inputs) + if len(operation.kernel_shape) != 2: + raise NotImplementedError( + "Non 2d convolutions are not currently supported." + ) + weights = operation.w + if operation.b is not None: + bias = operation.b + else: + bias = np.zeros((weights.shape[0],), dtype=weights.dtype) + assert np.all(operation.dilations == 1) + # assert np.all(operation.group == 1) + num_pads = len(operation.pads) + pads = tuple( + zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) + ) + + x_ndim = int(tf.rank(x)) + x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) + padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) + result = tf.nn.bias_add( + tf.nn.conv2d( + padded_x, + weights.transpose((2, 3, 1, 0)), + operation.strides, + padding="VALID", + ), + bias, + ) + result_ndim = int(tf.rank(result)) + result = tf.transpose( + result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) + ) + return result + + return conv_func + + def visit_ConvTranspose(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def convtranspose_func(*inputs): + x = _concretize([x_], inputs) + + if len(operation.kernel_shape) == 1: + conv_transpose = tf.nn.conv1d_transpose + elif len(operation.kernel_shape) == 2: + conv_transpose = tf.nn.conv2d_transpose + elif len(operation.kernel_shape) == 3: + conv_transpose = tf.nn.conv3d_transpose + else: + raise NotImplementedError( + f"{len(operation.kernel_shape)}d ConvTranspose operations are not currently supported." + ) + if ( + operation.auto_pad != "NOTSET" + or operation.auto_pad == "VALID" + and any(p != 0 for p in operation.pads) + ): + raise NotImplementedError( + f"Unsupported padding for ConvTranspose: {operation.auto_pad}" + ) + if np.any(operation.dilations != 1): + raise NotImplementedError( + f"Unsupported dilations for ConvTranspose: {operation.dilations}" + ) + + weights = operation.w + if operation.b is not None: + bias = operation.b + else: + bias = np.zeros((weights.shape[1],), dtype=weights.dtype) + # assert np.all(operation.group == 1) + + num_pads = len(operation.pads) + pads = tuple( + zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) + ) + if any(p != 0 for p in operation.pads): + raise NotImplementedError( + "Non 0 pads are not currently supported for ConvTranspose" + ) + + output_shape = operation.output_shape + if output_shape is None: + input_shape = [int(d) for d in x.shape[2:]] + start_pads = operation.pads[: num_pads // 2] + end_pads = operation.pads[num_pads // 2 :] + output_shape = ( + [int(x.shape[0])] + + [ + ( + operation.strides[i] * (input_shape[i] - 1) + + operation.output_padding[i] + + ( + (operation.kernel_shape[i] - 1) * operation.dilations[i] + + 1 + ) + - start_pads[i] + - end_pads[i] + ) + for i in range(len(operation.kernel_shape)) + ] + + [weights.shape[1]] + ) + + x_ndim = int(tf.rank(x)) + x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) + padded_x = tf.pad(x, ((0, 0),) + pads + ((0, 0),)) + weights_ndim = int(tf.rank(weights)) + result = tf.nn.bias_add( + conv_transpose( + padded_x, + weights.transpose(tuple(range(2, weights_ndim)) + (1, 0)), + output_shape, + strides=operation.strides, + padding="VALID", + dilations=operation.dilations, + ), + bias, + ) + result_ndim = int(tf.rank(result)) + result = tf.transpose( + result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) + ) + return result + + return convtranspose_func + + def visit_Div(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + + @self._cached + def div_func(*inputs): + a, b = _concretize([a_, b_], inputs) + result = tf.convert_to_tensor(tf.divide(a, b)) + return result + + return div_func + + def visit_Dropout(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def dropout_func(*inputs): + x = _concretize([x_], inputs) + return x, None + + return dropout_func + + def visit_Elu(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def elu_func(*inputs): + if operation.alpha != 1.0: + raise NotImplementedError( + "The tensorflow converter currently does not support ELU activations with alpha other than 1.0" + ) + x = _concretize([x_], inputs) + result = tf.nn.elu(x) + return result + + return elu_func + + def visit_Expand(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def expand_func(*inputs): + x = _concretize([x_], inputs) + shape = operation.shape + result = x * tf.ones(shape, x.dtype) + return result + + return expand_func + + def visit_Flatten(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def flatten_func(*inputs): + x = _concretize([x_], inputs) + axis = operation.axis + new_shape = (1, -1) if axis == 0 else (int(np.prod(x.shape[:axis])), -1) + result = tf.reshape(x, new_shape) + return result + + return flatten_func + + def visit_Gather(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + indices_ = operation.indices + if isinstance(indices_, Operation): + indices_ = self.visit(indices_) + + @self._cached + def gather_func(*inputs): + x, indices = _concretize([x_, indices_], inputs) + result = tf.gather(x, indices, axis=operation.axis) + return result + + return gather_func + + def visit_Gemm(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + c_ = operation.c + if isinstance(c_, Operation): + c_ = self.visit(c_) + + @self._cached + def gemm_func(*inputs): + a, b, c = _concretize([a_, b_, c_], inputs) + result = operation.alpha * tf.matmul( + a, + b, + transpose_a=operation.transpose_a, + transpose_b=operation.transpose_b, + ) + if c is not None: + result = result + operation.beta * c + return result + + return gemm_func + + def visit_GlobalAveragePool(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def globalavgpool_func(*inputs): + x = _concretize([x_], inputs) + + x = tf.transpose(x, (0, 2, 3, 1)) + result = tf.nn.pool( + x, x.shape[1:3], pooling_type="AVG", strides=(1, 1), padding="VALID" + ) + result = tf.transpose(result, (0, 3, 1, 2)) + return result + + return globalavgpool_func + + def visit_Identity(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def identity_func(*inputs): + x = _concretize([x_], inputs) + return tf.convert_to_tensor(x) + + return identity_func + + def visit_Input(self, operation): + input_idx = self.input_count + + @self._cached + def input_func(*inputs): + x = inputs[input_idx] + if any( + d1 != d2 and -1 not in (d1, d2) and None not in (d1, d2) + for d1, d2 in zip(operation.shape[1:], x.shape[1:]) + ): + raise ValueError( + "Incorrect input shape: %s != %s" % (operation.shape, x.shape) + ) + if x.dtype != operation.dtype: + raise TypeError( + "Incorrect type, %s, for input %d. Expected type %s." + % (x.dtype, input_idx, operation.dtype) + ) + return tf.convert_to_tensor(x) + + self.input_count += 1 + + return input_func + + def visit_LeakyRelu(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def leakyrelu_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.leaky_relu(x, alpha=operation.alpha) + return result + + return leakyrelu_func + + def visit_LogSoftmax(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def softmax_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.log_softmax(x, axis=operation.axis) + return result + + return softmax_func + + def visit_MatMul(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + + @self._cached + def matmul_func(*inputs): + a, b = _concretize([a_, b_], inputs) + + if len(a.shape) == 2 and len(b.shape) == 1: + result = tf.matmul(a, b[:, None])[:, 0] + elif len(a.shape) == 1 and len(b.shape) == 2: + result = tf.matmul(a[None], b)[0] + else: + result = tf.matmul(a, b) + return result + + return matmul_func + + def visit_MaxPool(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def maxpool_func(*inputs): + x = _concretize([x_], inputs) + + if operation.ceil_mode: + # TODO : add support + raise ValueError( + "ceil_mode=True is not currently supported for MaxPool" + ) + + kernel_shape = operation.kernel_shape + strides = operation.strides + num_pads = len(operation.pads) + pads = tuple( + zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) + ) + + x_ndim = int(tf.rank(x)) + x = tf.transpose(x, (0,) + tuple(range(2, x_ndim)) + (1,)) + padded_x = tf.pad( + x, ((0, 0),) + pads + ((0, 0),), constant_values=x.dtype.min + ) + result = tf.nn.pool( + padded_x, + kernel_shape, + pooling_type="MAX", + strides=strides, + dilations=operation.dilations, + padding="VALID", + ) + result_ndim = int(tf.rank(result)) + result = tf.transpose( + result, (0, result_ndim - 1) + tuple(range(1, result_ndim - 1)) + ) + return result + + return maxpool_func + + def visit_Mul(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + + @self._cached + def mul_func(*inputs): + a, b = _concretize([a_, b_], inputs) + result = tf.multiply(a, b) + return result + + return mul_func + + def visit_OutputSelect(self, operation): + x_ = self.visit(operation.operation) + + @self._cached + def output_select_func(*inputs): + x = _concretize([x_], inputs) + return x[operation.index] + + return output_select_func + + def visit_Pad(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def pad_func(*inputs): + x = _concretize([x_], inputs) + mode = operation.mode.upper() + if mode != "CONSTANT": + raise ValueError(f"{mode} padding is not currently supported") + num_pads = len(operation.pads) + pads = tuple( + zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) + ) + result = tf.pad(x, pads, mode=mode, constant_values=operation.value) + return result + + return pad_func + + def visit_Relu(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def relu_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.relu(x) + return result + + return relu_func + + def visit_Reshape(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + shape_ = operation.shape + if isinstance(shape_, Operation): + shape_ = self.visit(shape_) + + @self._cached + def reshape_func(*inputs): + x, shape = _concretize([x_, shape_], inputs) + if not operation.allowzero: + for i, d in enumerate(shape): + if d == 0: + shape[i] = x.shape[i] + result = tf.reshape(x, shape) + return result + + return reshape_func + + def visit_Resize(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + roi_ = operation.roi + if isinstance(roi_, Operation): + roi_ = self.visit(roi_) + scales_ = operation.scales + if isinstance(scales_, Operation): + scales_ = self.visit(scales_) + sizes_ = operation.sizes + if isinstance(sizes_, Operation): + sizes_ = self.visit(sizes_) + + @self._cached + def resize_func(*inputs): + x, roi, scales, sizes = _concretize([x_, roi_, scales_, sizes_], inputs) + assert operation.coordinate_transformation_mode in [ + "asymmetric", + "tf_crop_and_resize", + ] + assert operation.mode in ["nearest", "linear"] + assert operation.exclude_outside == 0 + if roi is None or roi.size == 0: + roi = np.array([[0, 0, 1, 1]]) + else: + assert operation.coordinate_transformation_mode == "tf_crop_and_resize" + assert roi.size == 8 and roi.ndim == 1 + roi = roi[None, [2, 3, 6, 7]] + if sizes is None or sizes.size == 0: + assert scales[0] == 1.0 and scales[1] == 1.0 + sizes = (scales * [int(d) for d in x.shape]).astype(int) + assert sizes.ndim == 1 and sizes.size == 4 + sizes = sizes[2:] + method = operation.mode + if method == "linear": + method = "bilinear" + result = tf.transpose(x, (0, 2, 3, 1)) + result = tf.image.crop_and_resize( + result, + boxes=roi, + box_indices=np.arange(int(x.shape[0])), + crop_size=sizes, + method=method, + extrapolation_value=operation.extrapolation_value, + ) + result = tf.transpose(result, (0, 3, 1, 2)) + return result + + return resize_func + + def visit_Shape(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def shape_func(*inputs): + x = _concretize([x_], inputs) + result = tf.shape(x) + return result + + return shape_func + + def visit_Sigmoid(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def sigmoid_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.sigmoid(x) + return result + + return sigmoid_func + + def visit_Sign(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def sign_func(*inputs): + x = _concretize([x_], inputs) + result = tf.math.sign(x) + return result + + return sign_func + + def visit_Softmax(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def softmax_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.softmax(x, axis=operation.axis) + return result + + return softmax_func + + def visit_Sub(self, operation): + a_ = operation.a + if isinstance(a_, Operation): + a_ = self.visit(a_) + b_ = operation.b + if isinstance(b_, Operation): + b_ = self.visit(b_) + + @self._cached + def sub_func(*inputs): + a, b = _concretize([a_, b_], inputs) + result = tf.subtract(a, b) + return result + + return sub_func + + def visit_Tanh(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def tanh_func(*inputs): + x = _concretize([x_], inputs) + result = tf.nn.tanh(x) + return result + + return tanh_func + + def visit_Tile(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + repeats_ = operation.repeats + if isinstance(repeats_, Operation): + repeats_ = self.visit(repeats_) + + @self._cached + def tile_func(*inputs): + x, repeats = _concretize([x_, repeats_], inputs) + result = tf.tile(x, repeats) + return result + + return tile_func + + def visit_Transpose(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + + @self._cached + def transpose_func(*inputs): + x = _concretize([x_], inputs) + permutation = operation.permutation + result = tf.transpose(x, permutation) + return result + + return transpose_func + + def visit_Unsqueeze(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + axes_ = operation.axes + if isinstance(axes_, Operation): + axes_ = self.visit(axes_) + + @self._cached + def unsqueeze_func(*inputs): + x, axes = _concretize([x_, axes_], inputs) + for axis in sorted(axes): + x = tf.expand_dims(x, axis) + return x + + return unsqueeze_func + + def visit_Split(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + axis = operation.axis + split = operation.split + + @self._cached + def split_func(*inputs): + x = _concretize([x_], inputs) + x = tf.split(x, split, axis=axis) + return x + + return split_func diff --git a/dnnv/nn/transformers/simplifiers/squeeze_convs.py b/dnnv/nn/transformers/simplifiers/squeeze_convs.py index 27ca8193..dbb61847 100644 --- a/dnnv/nn/transformers/simplifiers/squeeze_convs.py +++ b/dnnv/nn/transformers/simplifiers/squeeze_convs.py @@ -1,43 +1,43 @@ -import numpy as np - -from copy import copy - -from .base import Simplifier -from ... import operations - - -class SqueezeConvs(Simplifier): - def is_diagonal(self, array): - i, j = array.shape - # return ~np.any(array.reshape(-1)[:-1].reshape(i - 1, j + 1)[:, 1:]) - return i == j and ~np.any(array.reshape(-1)[:-1].reshape(i - 1, j + 1)[:, 1:]) - - def visit_Conv(self, operation: operations.Conv) -> operations.Conv: - if ( - isinstance(operation.x, operations.Conv) - and operation.x.w.shape[2] == 1 - and operation.x.w.shape[3] == 1 - and all(p == 0 for p in operation.pads) - and all(s == 1 for s in operation.x.strides) - and all(p == 0 for p in operation.x.pads) - and all(d == 1 for d in operation.x.dilations) - and operation.x.group == 1 - and self.is_diagonal(operation.x.w[:, :, 0, 0]) - ): - w = np.diag(operation.x.w[:, :, 0, 0]).reshape((1, -1, 1, 1)) - b = operation.x.b - - out_c, in_c, k_h, k_w = operation.w.shape - - weights = operation.w * np.tile(w, (out_c, 1, k_h, k_w)) - bias = operation.b + ( - operation.w * np.tile(b.reshape((1, -1, 1, 1)), (out_c, 1, k_h, k_w)) - ).sum(axis=(1, 2, 3)) - - op = copy(operation) - op.x = operation.x.x - op.w = weights - op.b = bias - - return op - return operation +import numpy as np + +from copy import copy + +from .base import Simplifier +from ... import operations + + +class SqueezeConvs(Simplifier): + def is_diagonal(self, array): + i, j = array.shape + # return ~np.any(array.reshape(-1)[:-1].reshape(i - 1, j + 1)[:, 1:]) + return i == j and ~np.any(array.reshape(-1)[:-1].reshape(i - 1, j + 1)[:, 1:]) + + def visit_Conv(self, operation: operations.Conv) -> operations.Conv: + if ( + isinstance(operation.x, operations.Conv) + and operation.x.w.shape[2] == 1 + and operation.x.w.shape[3] == 1 + and all(p == 0 for p in operation.pads) + and all(s == 1 for s in operation.x.strides) + and all(p == 0 for p in operation.x.pads) + and all(d == 1 for d in operation.x.dilations) + and operation.x.group == 1 + and self.is_diagonal(operation.x.w[:, :, 0, 0]) + ): + w = np.diag(operation.x.w[:, :, 0, 0]).reshape((1, -1, 1, 1)) + b = operation.x.b + + out_c, in_c, k_h, k_w = operation.w.shape + + weights = operation.w * np.tile(w, (out_c, 1, k_h, k_w)) + bias = operation.b + ( + operation.w * np.tile(b.reshape((1, -1, 1, 1)), (out_c, 1, k_h, k_w)) + ).sum(axis=(1, 2, 3)) + + op = copy(operation) + op.x = operation.x.x + op.w = weights + op.b = bias + + return op + return operation diff --git a/docs/make.bat b/docs/make.bat index 2119f510..922152e9 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,35 +1,35 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py index 62ba0192..87f0afb2 100644 --- a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py +++ b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py @@ -1,144 +1,144 @@ -import numpy as np -import onnxruntime -import pytest - -from dnnv.nn.converters.onnx import * -from dnnv.nn.operations import * - -# Tests based on: -# https://github.com/onnx/onnx/blob/2ab133404afce34552aaccd86e7023e1fb9a60d2/onnx/test/shape_inference_test.py -# https://github.com/onnx/onnx/blob/2ab133404afce34552aaccd86e7023e1fb9a60d2/onnx/test/automatic_upgrade_test.py -# https://github.com/onnx/onnx/blob/35092895d9bf3592e58f4710d098f8131afef259/onnx/backend/test/case/node/split.py - - -def test_Split_export_1d() -> None: - input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) - - op = Split(input, axis=0, split=[2, 2, 2]) - all_results = np.zeros((3, 2)) - for i in range(3): - outputselect = OutputSelect(op, i) - onnx_model = convert(OperationGraph([outputselect])) - results = onnxruntime.backend.run(onnx_model, []) - all_results[i] = results - - expected_outputs = [ - np.array([1.0, 2.0]).astype(np.float32), - np.array([3.0, 4.0]).astype(np.float32), - np.array([5.0, 6.0]).astype(np.float32), - ] - assert len(all_results) == 3 - assert np.allclose(all_results, expected_outputs) - - op = Split(input, axis=0, split=np.array([2, 4]).astype(np.int64)) - onnx_model = convert(OperationGraph([op])) - results = onnxruntime.backend.run(onnx_model, []) - - expected_outputs = [ - np.array([1.0, 2.0]).astype(np.float32), - np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32), - ] - assert len(results) == 2 - assert np.allclose(results, expected_outputs) - - -def test_Split_export_2d() -> None: - input = np.array( - [[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [7.0, 8.0, 9.0, 10.0, 11.0, 12.0]] - ).astype(np.float32) - - # node = onnx.helper.make_node( - # 'Split', - # inputs=['input'], - # outputs=['output_1', 'output_2'], - # axis=1 - # ) - op = Split(input, axis=1, split=[2, 2]) - all_results = [] - for i in range(2): - outputselect = OutputSelect(op, i) - onnx_model = convert(OperationGraph([outputselect])) - results = onnxruntime.backend.run(onnx_model, []) - all_results.append(results) - expected_outputs = [ - np.array([[1.0, 2.0, 3.0], [7.0, 8.0, 9.0]]).astype(np.float32), - np.array([[4.0, 5.0, 6.0], [10.0, 11.0, 12.0]]).astype(np.float32), - ] - - # expect(node, inputs=[input], outputs=[y for y in expected_outputs], name='test_split_equal_parts_2d') - for i in range(2): - assert all_results[i].shape == (2, 3) - assert np.allclose(all_results[i], expected_outputs[i]) - split = np.array([2, 4]).astype(np.int64) - # node = onnx.helper.make_node( - # 'Split', - # inputs=['input', 'split'], - # outputs=['output_1', 'output_2'], - # axis=1, - # ) - - expected_outputs = [ - np.array([[1.0, 2.0], [7.0, 8.0]]).astype(np.float32), - np.array([[3.0, 4.0, 5.0, 6.0], [9.0, 10.0, 11.0, 12.0]]).astype(np.float32), - ] - - # expect(node, inputs=[input, split], outputs=[y for y in expected_outputs], name='test_split_variable_parts_2d') - - -def test_Split_export_default_values() -> None: - input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) - - # If axis is not specified, split is applied on default axis 0 - node = onnx.helper.make_node( - "Split", inputs=["input"], outputs=["output_1", "output_2", "output_3"] - ) - - expected_outputs = [ - np.array([1.0, 2.0]).astype(np.float32), - np.array([3.0, 4.0]).astype(np.float32), - np.array([5.0, 6.0]).astype(np.float32), - ] - expect( - node, - inputs=[input], - outputs=[y for y in expected_outputs], - name="test_split_equal_parts_default_axis", - ) - - split = np.array([2, 4]).astype(np.int64) - node = onnx.helper.make_node( - "Split", inputs=["input", "split"], outputs=["output_1", "output_2"] - ) - - expected_outputs = [ - np.array([1.0, 2.0]).astype(np.float32), - np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32), - ] - expect( - node, - inputs=[input, split], - outputs=[y for y in expected_outputs], - name="test_split_variable_parts_default_axis", - ) - - -def test_Split_export_zero_size_splits() -> None: - input = np.array([]).astype(np.float32) - - # Split emtpy tensor to tensors of size zero - split = np.array([0, 0, 0]).astype(np.int64) - node = onnx.helper.make_node( - "Split", inputs=["input", "split"], outputs=["output_1", "output_2", "output_3"] - ) - - expected_outputs = [ - np.array([]).astype(np.float32), - np.array([]).astype(np.float32), - np.array([]).astype(np.float32), - ] - expect( - node, - inputs=[input, split], - outputs=[y for y in expected_outputs], - name="test_split_zero_size_splits", - ) +import numpy as np +import onnxruntime +import pytest + +from dnnv.nn.converters.onnx import * +from dnnv.nn.operations import * + +# Tests based on: +# https://github.com/onnx/onnx/blob/2ab133404afce34552aaccd86e7023e1fb9a60d2/onnx/test/shape_inference_test.py +# https://github.com/onnx/onnx/blob/2ab133404afce34552aaccd86e7023e1fb9a60d2/onnx/test/automatic_upgrade_test.py +# https://github.com/onnx/onnx/blob/35092895d9bf3592e58f4710d098f8131afef259/onnx/backend/test/case/node/split.py + + +def test_Split_export_1d() -> None: + input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + + op = Split(input, axis=0, split=[2, 2, 2]) + all_results = np.zeros((3, 2)) + for i in range(3): + outputselect = OutputSelect(op, i) + onnx_model = convert(OperationGraph([outputselect])) + results = onnxruntime.backend.run(onnx_model, []) + all_results[i] = results + + expected_outputs = [ + np.array([1.0, 2.0]).astype(np.float32), + np.array([3.0, 4.0]).astype(np.float32), + np.array([5.0, 6.0]).astype(np.float32), + ] + assert len(all_results) == 3 + assert np.allclose(all_results, expected_outputs) + + op = Split(input, axis=0, split=np.array([2, 4]).astype(np.int64)) + onnx_model = convert(OperationGraph([op])) + results = onnxruntime.backend.run(onnx_model, []) + + expected_outputs = [ + np.array([1.0, 2.0]).astype(np.float32), + np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32), + ] + assert len(results) == 2 + assert np.allclose(results, expected_outputs) + + +def test_Split_export_2d() -> None: + input = np.array( + [[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [7.0, 8.0, 9.0, 10.0, 11.0, 12.0]] + ).astype(np.float32) + + # node = onnx.helper.make_node( + # 'Split', + # inputs=['input'], + # outputs=['output_1', 'output_2'], + # axis=1 + # ) + op = Split(input, axis=1, split=[2, 2]) + all_results = [] + for i in range(2): + outputselect = OutputSelect(op, i) + onnx_model = convert(OperationGraph([outputselect])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results) + expected_outputs = [ + np.array([[1.0, 2.0, 3.0], [7.0, 8.0, 9.0]]).astype(np.float32), + np.array([[4.0, 5.0, 6.0], [10.0, 11.0, 12.0]]).astype(np.float32), + ] + + # expect(node, inputs=[input], outputs=[y for y in expected_outputs], name='test_split_equal_parts_2d') + for i in range(2): + assert all_results[i].shape == (2, 3) + assert np.allclose(all_results[i], expected_outputs[i]) + split = np.array([2, 4]).astype(np.int64) + # node = onnx.helper.make_node( + # 'Split', + # inputs=['input', 'split'], + # outputs=['output_1', 'output_2'], + # axis=1, + # ) + + expected_outputs = [ + np.array([[1.0, 2.0], [7.0, 8.0]]).astype(np.float32), + np.array([[3.0, 4.0, 5.0, 6.0], [9.0, 10.0, 11.0, 12.0]]).astype(np.float32), + ] + + # expect(node, inputs=[input, split], outputs=[y for y in expected_outputs], name='test_split_variable_parts_2d') + + +def test_Split_export_default_values() -> None: + input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + + # If axis is not specified, split is applied on default axis 0 + node = onnx.helper.make_node( + "Split", inputs=["input"], outputs=["output_1", "output_2", "output_3"] + ) + + expected_outputs = [ + np.array([1.0, 2.0]).astype(np.float32), + np.array([3.0, 4.0]).astype(np.float32), + np.array([5.0, 6.0]).astype(np.float32), + ] + expect( + node, + inputs=[input], + outputs=[y for y in expected_outputs], + name="test_split_equal_parts_default_axis", + ) + + split = np.array([2, 4]).astype(np.int64) + node = onnx.helper.make_node( + "Split", inputs=["input", "split"], outputs=["output_1", "output_2"] + ) + + expected_outputs = [ + np.array([1.0, 2.0]).astype(np.float32), + np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32), + ] + expect( + node, + inputs=[input, split], + outputs=[y for y in expected_outputs], + name="test_split_variable_parts_default_axis", + ) + + +def test_Split_export_zero_size_splits() -> None: + input = np.array([]).astype(np.float32) + + # Split emtpy tensor to tensors of size zero + split = np.array([0, 0, 0]).astype(np.int64) + node = onnx.helper.make_node( + "Split", inputs=["input", "split"], outputs=["output_1", "output_2", "output_3"] + ) + + expected_outputs = [ + np.array([]).astype(np.float32), + np.array([]).astype(np.float32), + np.array([]).astype(np.float32), + ] + expect( + node, + inputs=[input, split], + outputs=[y for y in expected_outputs], + name="test_split_zero_size_splits", + ) diff --git a/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py index 6bbac5c9..aa094a88 100644 --- a/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py +++ b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py @@ -1,134 +1,134 @@ -import numpy as np -import pytest - -from dnnv.nn.converters.tensorflow import * -from dnnv.nn.operations import * - - -def test_Reshape(): - original_shape = [0, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([3, 4, 0], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape, allowzero=True) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - op = Reshape( - Input((0, 3, 4), np.dtype(np.float32)), - Input((3,), np.dtype(np.int64)), - allowzero=True, - ) - tf_op = TensorflowConverter().visit(op) - result = tf_op(data, new_shape).numpy() - assert np.allclose(result, y) - - -def test_Reshape_reordered_all_dims(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([4, 2, 3], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_reordered_last_dims(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, 4, 3], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_reduced_dims(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, 12], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_extended_dims(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, 3, 2, 2], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_one_dim(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([24], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_negative_dim(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, -1, 2], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_negative_extended_dims(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([-1, 2, 3, 4], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_zero_dim(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, 0, 4, 1], dtype=np.int64) - y = np.reshape(data, [2, 3, 4, 1]) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_zero_and_negative_dim(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, 0, 1, -1], dtype=np.int64) - y = np.reshape(data, [2, 3, 1, -1]) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) +import numpy as np +import pytest + +from dnnv.nn.converters.tensorflow import * +from dnnv.nn.operations import * + + +def test_Reshape(): + original_shape = [0, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([3, 4, 0], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape, allowzero=True) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + op = Reshape( + Input((0, 3, 4), np.dtype(np.float32)), + Input((3,), np.dtype(np.int64)), + allowzero=True, + ) + tf_op = TensorflowConverter().visit(op) + result = tf_op(data, new_shape).numpy() + assert np.allclose(result, y) + + +def test_Reshape_reordered_all_dims(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([4, 2, 3], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_reordered_last_dims(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, 4, 3], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_reduced_dims(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, 12], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_extended_dims(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, 3, 2, 2], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_one_dim(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([24], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_negative_dim(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, -1, 2], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_negative_extended_dims(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([-1, 2, 3, 4], dtype=np.int64) + y = np.reshape(data, new_shape) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_zero_dim(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, 0, 4, 1], dtype=np.int64) + y = np.reshape(data, [2, 3, 4, 1]) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) + + +def test_Reshape_zero_and_negative_dim(): + original_shape = [2, 3, 4] + data = np.random.random_sample(original_shape).astype(np.float32) + new_shape = np.array([2, 0, 1, -1], dtype=np.int64) + y = np.reshape(data, [2, 3, 1, -1]) + + op = Reshape(data, new_shape) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.allclose(result, y) From 9f9c9755381334ec8bc3cb49f0a464987120670a Mon Sep 17 00:00:00 2001 From: Meriel von Stein Date: Fri, 24 Jun 2022 11:02:01 -0400 Subject: [PATCH 04/18] tests for Split --- README.md | 234 +++++++++--------- dnnv/nn/converters/onnx.py | 10 +- dnnv/nn/converters/tensorflow.py | 8 +- dnnv/nn/operations/tensor.py | 10 +- docs/make.bat | 70 +++--- .../test_converters/test_onnx/test_Split.py | 175 ++++++------- .../test_tensorflow/test_Split.py | 172 +++++-------- 7 files changed, 300 insertions(+), 379 deletions(-) diff --git a/README.md b/README.md index 4d88f0b2..dbd9fb62 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,117 @@ -# Deep Neural Network Verification - -A framework for verification and analysis of deep neural networks. You can read an overview of DNNV in our CAV 2021 paper [*DNNV: A Framework for Deep Neural Network Verification*](https://arxiv.org/abs/2105.12841), or watch our presentation on [YouTube](https://youtu.be/GhXlONbvx1Y). - -## Getting Started - -For detailed instructions on installing and using DNNV, see our [documentation](https://dnnv.readthedocs.io/en/stable/). - -### Installation - -DNNV requires python >=3.7,<3.10, and has been tested on linux. To install the latest stable version run: - -```bash -$ pip install dnnv -``` - -or - -```bash -$ pip install git+https://github.com/dlshriver/DNNV.git@main -``` - -We recommend installing DNNV into a [python virtual environment](https://docs.python.org/3/tutorial/venv.html). - -Install any of the supported verifiers ([Reluplex](https://github.com/guykatzz/ReluplexCav2017), [planet](https://github.com/progirep/planet), [MIPVerify.jl](https://github.com/vtjeng/MIPVerify.jl), [Neurify](https://github.com/tcwangshiqi-columbia/Neurify), [ERAN](https://github.com/eth-sri/eran), [BaB](https://github.com/oval-group/PLNN-verification), [marabou](https://github.com/NeuralNetworkVerification/Marabou), [nnenum](https://github.com/stanleybak/nnenum), [verinet](https://vas.doc.ic.ac.uk/software/neural/)): - -```bash -$ dnnv_manage install reluplex planet mipverify neurify eran bab marabou nnenum verinet -``` - -*Several verifiers make use of the [Gurobi solver](https://www.gurobi.com/).* This should be installed automatically, but requires a license to be manually activated and available on the host machine. Academic licenses can be obtained for free from the [Gurobi website](https://user.gurobi.com/download/licenses/free-academic). - -> After installing a verifier that requires Gurobi, the grbgetkey command can be found at `.venv/opt/gurobi912/linux64/bin/grbgetkey`. - -#### Source Installation - -First create and activate a python virtual environment. - -```bash -$ python -m venv .venv -$ . .venv/bin/activate -``` - -Then run the following commands to clone DNNV and install it into the virtual environment: - -```bash -$ git clone https://github.com/dlshriver/DNNV.git -$ cd DNNV -$ pip install . -``` - -Verifiers can then be installed using the `dnnv_manage` tool as described above. - -**Make sure that the project environment is activated** when using dnnv or the dnnv_manage tools. - -#### Docker Installation - -We provide a docker image with DNNV and all non-Gurobi dependent verifiers. To obtain and use the latest pre-built image of the main branch, run: - -```bash -$ docker pull dlshriver/dnnv:latest -$ docker run --rm -it dlshriver/dnnv:latest -(.venv) dnnv@hostname:~$ dnnv -h -``` - -The latest version of the develop branch is available as `dlshriver/dnnv:develop`, and tagged releases are available as `dlshriver/dnnv:vX.X.X` where `vX.X.X` is the desired version number. - -The docker image can also be built using the provided Dockerfile. The provided build file will install DNNV with all of the verifiers that do not require Gurobi. To build and run the docker image, run: - -```bash -$ docker build . -t dlshriver/dnnv -$ docker run --rm -it dlshriver/dnnv -(.venv) dnnv@hostname:~$ dnnv -h -``` - -### Usage - -Properties are specified in our Python-embedded DSL, [DNNP](https://dnnv.readthedocs.io/en/latest/usage/specifying_properties.html). A property specification can import python modules, and define variables. The only required component is the property expression, which must appear at the end of the file. An example of a local robustness property is shown below. - -```python -from dnnv.properties import * - -N = Network("N") -x = Image("path/to/image") -epsilon = Parameter("epsilon", float, default=1.0) - -Forall( - x_, - Implies( - ((x - epsilon) < x_ < (x + epsilon)), - argmax(N(x_)) == argmax(N(x))), - ), -) -``` - -To check whether property holds for some network using the ERAN verifier, run: - -```bash -$ dnnv property.dnnp --network N network.onnx --eran -``` - -Additionally, if the property defines parameters, using the `Parameter` keyword, they can be specified on the command line using the option `--prop.PARAMETER_NAME`, where `PARAMETER_NAME` is the name of the parameter. For the property defined above, a value for `epsilon` can be provided with a command line option as follows: - -```bash -$ dnnv property.dnnp --network N network.onnx --eran --prop.epsilon=2.0 -``` - -To save any counter-example found by the verifier, use the option `--save-violation /path/to/array.npy` when running DNNV. This will save any violation found as a numpy array at the path specified, which is useful for viewing counter-examples to properties and enables additional debugging and analysis later. - -### Example Problems - -We have made several DNN verification benchmarks available in DNNP+ONNX format in [dlshriver/dnnv-benchmarks](https://github.com/dlshriver/dnnv-benchmarks). -This repo includes the [ACAS Xu](https://github.com/dlshriver/dnnv-benchmarks/tree/main/benchmarks/ACAS_Xu) benchmark, ready to run with DNNV! - -## Acknowledgements - -This material is based in part upon work supported by the National Science Foundation under grant number 1900676 and 2019239. +# Deep Neural Network Verification + +A framework for verification and analysis of deep neural networks. You can read an overview of DNNV in our CAV 2021 paper [*DNNV: A Framework for Deep Neural Network Verification*](https://arxiv.org/abs/2105.12841), or watch our presentation on [YouTube](https://youtu.be/GhXlONbvx1Y). + +## Getting Started + +For detailed instructions on installing and using DNNV, see our [documentation](https://dnnv.readthedocs.io/en/stable/). + +### Installation + +DNNV requires python >=3.7,<3.10, and has been tested on linux. To install the latest stable version run: + +```bash +$ pip install dnnv +``` + +or + +```bash +$ pip install git+https://github.com/dlshriver/DNNV.git@main +``` + +We recommend installing DNNV into a [python virtual environment](https://docs.python.org/3/tutorial/venv.html). + +Install any of the supported verifiers ([Reluplex](https://github.com/guykatzz/ReluplexCav2017), [planet](https://github.com/progirep/planet), [MIPVerify.jl](https://github.com/vtjeng/MIPVerify.jl), [Neurify](https://github.com/tcwangshiqi-columbia/Neurify), [ERAN](https://github.com/eth-sri/eran), [BaB](https://github.com/oval-group/PLNN-verification), [marabou](https://github.com/NeuralNetworkVerification/Marabou), [nnenum](https://github.com/stanleybak/nnenum), [verinet](https://vas.doc.ic.ac.uk/software/neural/)): + +```bash +$ dnnv_manage install reluplex planet mipverify neurify eran bab marabou nnenum verinet +``` + +*Several verifiers make use of the [Gurobi solver](https://www.gurobi.com/).* This should be installed automatically, but requires a license to be manually activated and available on the host machine. Academic licenses can be obtained for free from the [Gurobi website](https://user.gurobi.com/download/licenses/free-academic). + +> After installing a verifier that requires Gurobi, the grbgetkey command can be found at `.venv/opt/gurobi912/linux64/bin/grbgetkey`. + +#### Source Installation + +First create and activate a python virtual environment. + +```bash +$ python -m venv .venv +$ . .venv/bin/activate +``` + +Then run the following commands to clone DNNV and install it into the virtual environment: + +```bash +$ git clone https://github.com/dlshriver/DNNV.git +$ cd DNNV +$ pip install . +``` + +Verifiers can then be installed using the `dnnv_manage` tool as described above. + +**Make sure that the project environment is activated** when using dnnv or the dnnv_manage tools. + +#### Docker Installation + +We provide a docker image with DNNV and all non-Gurobi dependent verifiers. To obtain and use the latest pre-built image of the main branch, run: + +```bash +$ docker pull dlshriver/dnnv:latest +$ docker run --rm -it dlshriver/dnnv:latest +(.venv) dnnv@hostname:~$ dnnv -h +``` + +The latest version of the develop branch is available as `dlshriver/dnnv:develop`, and tagged releases are available as `dlshriver/dnnv:vX.X.X` where `vX.X.X` is the desired version number. + +The docker image can also be built using the provided Dockerfile. The provided build file will install DNNV with all of the verifiers that do not require Gurobi. To build and run the docker image, run: + +```bash +$ docker build . -t dlshriver/dnnv +$ docker run --rm -it dlshriver/dnnv +(.venv) dnnv@hostname:~$ dnnv -h +``` + +### Usage + +Properties are specified in our Python-embedded DSL, [DNNP](https://dnnv.readthedocs.io/en/latest/usage/specifying_properties.html). A property specification can import python modules, and define variables. The only required component is the property expression, which must appear at the end of the file. An example of a local robustness property is shown below. + +```python +from dnnv.properties import * + +N = Network("N") +x = Image("path/to/image") +epsilon = Parameter("epsilon", float, default=1.0) + +Forall( + x_, + Implies( + ((x - epsilon) < x_ < (x + epsilon)), + argmax(N(x_)) == argmax(N(x))), + ), +) +``` + +To check whether property holds for some network using the ERAN verifier, run: + +```bash +$ dnnv property.dnnp --network N network.onnx --eran +``` + +Additionally, if the property defines parameters, using the `Parameter` keyword, they can be specified on the command line using the option `--prop.PARAMETER_NAME`, where `PARAMETER_NAME` is the name of the parameter. For the property defined above, a value for `epsilon` can be provided with a command line option as follows: + +```bash +$ dnnv property.dnnp --network N network.onnx --eran --prop.epsilon=2.0 +``` + +To save any counter-example found by the verifier, use the option `--save-violation /path/to/array.npy` when running DNNV. This will save any violation found as a numpy array at the path specified, which is useful for viewing counter-examples to properties and enables additional debugging and analysis later. + +### Example Problems + +We have made several DNN verification benchmarks available in DNNP+ONNX format in [dlshriver/dnnv-benchmarks](https://github.com/dlshriver/dnnv-benchmarks). +This repo includes the [ACAS Xu](https://github.com/dlshriver/dnnv-benchmarks/tree/main/benchmarks/ACAS_Xu) benchmark, ready to run with DNNV! + +## Acknowledgements + +This material is based in part upon work supported by the National Science Foundation under grant number 1900676 and 2019239. diff --git a/dnnv/nn/converters/onnx.py b/dnnv/nn/converters/onnx.py index f7c0637c..85a456d2 100644 --- a/dnnv/nn/converters/onnx.py +++ b/dnnv/nn/converters/onnx.py @@ -499,8 +499,7 @@ def visit_OutputSelect(self, operation: operations.OutputSelect) -> onnx.NodePro node = onnx.helper.make_node( "Identity", - # inputs=[op.name], - inputs=[op.outputs[operation.index]], + inputs=[op.output[operation.index]], outputs=[opname], name=opname, ) @@ -608,12 +607,13 @@ def visit_Split(self, operation: operations.Split) -> onnx.NodeProto: outputs = [] for i in range(len(operation.split)): outputs.append(f"output_{i}") - + outputs = np.array(outputs) x = self._to_onnx_proto(operation.x, f"{opname}.x") - + split = self._to_onnx_proto(operation.split, f"{opname}.split") + print(f"{[x.name, split.name]}") node = onnx.helper.make_node( op_type, - inputs=[x.name, operation.split], + inputs=[x.name, split.name], outputs=outputs, name=opname, axis=operation.axis, diff --git a/dnnv/nn/converters/tensorflow.py b/dnnv/nn/converters/tensorflow.py index e5dba2f3..b3a189b3 100644 --- a/dnnv/nn/converters/tensorflow.py +++ b/dnnv/nn/converters/tensorflow.py @@ -877,13 +877,15 @@ def visit_Split(self, operation): x_ = operation.x if isinstance(x_, Operation): x_ = self.visit(x_) + split_ = operation.split + if isinstance(split_, Operation): + split_ = self.visit(split_) axis = operation.axis - split = operation.split @self._cached def split_func(*inputs): - x = _concretize([x_], inputs) - x = tf.split(x, split, axis=axis) + x, split = _concretize([x_, split_], inputs) + x = tf.split(x, tf.convert_to_tensor(split), axis=axis) return x return split_func diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index 3c82d593..9e6f30bf 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -222,7 +222,7 @@ def from_onnx(cls, onnx_node, *inputs): class Split(Operation): - def __init__(self, x, axis, split, *, name: Optional[str] = None): + def __init__(self, x, split=None, *, axis=0, name: Optional[str] = None): super().__init__(name=name) self.x = x self.axis = axis @@ -231,10 +231,8 @@ def __init__(self, x, axis, split, *, name: Optional[str] = None): @classmethod def from_onnx(cls, onnx_node, *inputs): attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} - axis = attributes.get("axis") - split = attributes.get("split") - split = tuple(split) - return cls(*inputs, axis=axis, split=split, name=onnx_node.name) + axis = attributes.get("axis", 0) + return cls(*inputs, axis=axis, name=onnx_node.name) __all__ = [ @@ -248,8 +246,8 @@ def from_onnx(cls, onnx_node, *inputs): "Reshape", "Resize", "Shape", + "Split", "Tile", "Transpose", "Unsqueeze", - "Split", ] diff --git a/docs/make.bat b/docs/make.bat index 922152e9..2119f510 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,35 +1,35 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py index 87f0afb2..7b0e827a 100644 --- a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py +++ b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py @@ -1,5 +1,5 @@ import numpy as np -import onnxruntime +import onnxruntime.backend import pytest from dnnv.nn.converters.onnx import * @@ -13,33 +13,29 @@ def test_Split_export_1d() -> None: input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) - - op = Split(input, axis=0, split=[2, 2, 2]) - all_results = np.zeros((3, 2)) + op = Split(input, axis=0, split=np.array([2, 2, 2])) + all_results = [] for i in range(3): - outputselect = OutputSelect(op, i) - onnx_model = convert(OperationGraph([outputselect])) + onnx_model = convert(OperationGraph([OutputSelect(op, i)])) results = onnxruntime.backend.run(onnx_model, []) - all_results[i] = results - - expected_outputs = [ - np.array([1.0, 2.0]).astype(np.float32), - np.array([3.0, 4.0]).astype(np.float32), - np.array([5.0, 6.0]).astype(np.float32), - ] + all_results.append(results[0]) + all_results = np.array(all_results) + expected_outputs = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]).astype(np.float32) assert len(all_results) == 3 - assert np.allclose(all_results, expected_outputs) + assert np.array_equiv(all_results, expected_outputs) op = Split(input, axis=0, split=np.array([2, 4]).astype(np.int64)) - onnx_model = convert(OperationGraph([op])) - results = onnxruntime.backend.run(onnx_model, []) - - expected_outputs = [ - np.array([1.0, 2.0]).astype(np.float32), - np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32), - ] - assert len(results) == 2 - assert np.allclose(results, expected_outputs) + all_results = [] + for i in range(2): + onnx_model = convert(OperationGraph([OutputSelect(op, i)])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + all_results = np.array(all_results) + assert len(all_results) == 2 + assert np.array_equiv(all_results[0], np.array([1.0, 2.0]).astype(np.float32)) + assert np.array_equiv( + all_results[1], np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32) + ) def test_Split_export_2d() -> None: @@ -47,98 +43,77 @@ def test_Split_export_2d() -> None: [[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [7.0, 8.0, 9.0, 10.0, 11.0, 12.0]] ).astype(np.float32) - # node = onnx.helper.make_node( - # 'Split', - # inputs=['input'], - # outputs=['output_1', 'output_2'], - # axis=1 - # ) - op = Split(input, axis=1, split=[2, 2]) + op = Split(input, axis=1, split=np.array([3, 3])) all_results = [] for i in range(2): outputselect = OutputSelect(op, i) onnx_model = convert(OperationGraph([outputselect])) results = onnxruntime.backend.run(onnx_model, []) - all_results.append(results) - expected_outputs = [ - np.array([[1.0, 2.0, 3.0], [7.0, 8.0, 9.0]]).astype(np.float32), - np.array([[4.0, 5.0, 6.0], [10.0, 11.0, 12.0]]).astype(np.float32), - ] - - # expect(node, inputs=[input], outputs=[y for y in expected_outputs], name='test_split_equal_parts_2d') + print(f"{results=}") + all_results.append(results[0]) + expected_outputs = np.array( + [[[1.0, 2.0, 3.0], [7.0, 8.0, 9.0]], [[4.0, 5.0, 6.0], [10.0, 11.0, 12.0]]] + ).astype(np.float32) for i in range(2): assert all_results[i].shape == (2, 3) - assert np.allclose(all_results[i], expected_outputs[i]) - split = np.array([2, 4]).astype(np.int64) - # node = onnx.helper.make_node( - # 'Split', - # inputs=['input', 'split'], - # outputs=['output_1', 'output_2'], - # axis=1, - # ) - - expected_outputs = [ - np.array([[1.0, 2.0], [7.0, 8.0]]).astype(np.float32), - np.array([[3.0, 4.0, 5.0, 6.0], [9.0, 10.0, 11.0, 12.0]]).astype(np.float32), - ] + assert np.array_equiv(all_results[i], expected_outputs[i]) - # expect(node, inputs=[input, split], outputs=[y for y in expected_outputs], name='test_split_variable_parts_2d') + op = Split(input, axis=1, split=np.array([2, 4])) + all_results = [] + for i in range(2): + outputselect = OutputSelect(op, i) + onnx_model = convert(OperationGraph([outputselect])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + expected_outputs1 = np.array([[1.0, 2.0], [7.0, 8.0]]).astype(np.float32) + expected_outputs2 = np.array( + [[3.0, 4.0, 5.0, 6.0], [9.0, 10.0, 11.0, 12.0]] + ).astype(np.float32) + assert all_results[0].shape == (2, 2) + assert np.array_equiv(all_results[0], expected_outputs1) + assert all_results[1].shape == (2, 4) + assert np.array_equiv(all_results[1], expected_outputs2) def test_Split_export_default_values() -> None: input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + op = Split(input, split=np.array([2, 2, 2])) + all_results = [] + for i in range(3): + onnx_model = convert(OperationGraph([OutputSelect(op, i)])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + all_results = np.array(all_results) + expected_outputs = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]).astype(np.float32) + assert len(all_results) == 3 + assert all_results.shape == expected_outputs.shape + assert np.array_equiv(all_results, expected_outputs) - # If axis is not specified, split is applied on default axis 0 - node = onnx.helper.make_node( - "Split", inputs=["input"], outputs=["output_1", "output_2", "output_3"] - ) - - expected_outputs = [ - np.array([1.0, 2.0]).astype(np.float32), - np.array([3.0, 4.0]).astype(np.float32), - np.array([5.0, 6.0]).astype(np.float32), - ] - expect( - node, - inputs=[input], - outputs=[y for y in expected_outputs], - name="test_split_equal_parts_default_axis", - ) - - split = np.array([2, 4]).astype(np.int64) - node = onnx.helper.make_node( - "Split", inputs=["input", "split"], outputs=["output_1", "output_2"] - ) - - expected_outputs = [ - np.array([1.0, 2.0]).astype(np.float32), - np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32), - ] - expect( - node, - inputs=[input, split], - outputs=[y for y in expected_outputs], - name="test_split_variable_parts_default_axis", + op = Split(input, split=np.array([2, 4])) + all_results = [] + for i in range(2): + onnx_model = convert(OperationGraph([OutputSelect(op, i)])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + assert len(all_results) == 2 + assert len(all_results[0]) == 2 + assert len(all_results[1]) == 4 + assert np.array_equiv(all_results[0], np.array([1.0, 2.0]).astype(np.float32)) + assert np.array_equiv( + all_results[1], np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32) ) def test_Split_export_zero_size_splits() -> None: - input = np.array([]).astype(np.float32) - # Split emtpy tensor to tensors of size zero - split = np.array([0, 0, 0]).astype(np.int64) - node = onnx.helper.make_node( - "Split", inputs=["input", "split"], outputs=["output_1", "output_2", "output_3"] - ) - - expected_outputs = [ - np.array([]).astype(np.float32), - np.array([]).astype(np.float32), - np.array([]).astype(np.float32), - ] - expect( - node, - inputs=[input, split], - outputs=[y for y in expected_outputs], - name="test_split_zero_size_splits", - ) + input = np.array([]).astype(np.float32) + op = Split(input, split=np.array([0, 0, 0])) + all_results = [] + for i in range(3): + onnx_model = convert(OperationGraph([OutputSelect(op, i)])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + all_results = np.array(all_results) + expected_outputs = np.array([[], [], []]).astype(np.float32) + assert all_results.shape == (3, 0) + assert np.array_equiv(all_results, expected_outputs) diff --git a/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py index aa094a88..29644628 100644 --- a/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py +++ b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py @@ -5,130 +5,76 @@ from dnnv.nn.operations import * -def test_Reshape(): - original_shape = [0, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([3, 4, 0], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape, allowzero=True) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - op = Reshape( - Input((0, 3, 4), np.dtype(np.float32)), - Input((3,), np.dtype(np.int64)), - allowzero=True, - ) - tf_op = TensorflowConverter().visit(op) - result = tf_op(data, new_shape).numpy() - assert np.allclose(result, y) - - -def test_Reshape_reordered_all_dims(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([4, 2, 3], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_reordered_last_dims(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, 4, 3], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) - tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_reduced_dims(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, 12], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) +def test_Split_export_1d() -> None: + input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + op = Split(input, axis=0, split=np.array([2, 2, 2])) tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_extended_dims(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, 3, 2, 2], dtype=np.int64) - y = np.reshape(data, new_shape) + result_ = tf_op() + expected_outputs = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]).astype(np.float32) + assert len(result_) == 3 + assert np.array_equiv(result_, expected_outputs) - op = Reshape(data, new_shape) + op = Split(input, axis=0, split=np.array([2, 4]).astype(np.int64)) tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) + result_ = tf_op() + assert len(result_) == 2 + assert np.array_equiv(result_[0], np.array([1.0, 2.0]).astype(np.float32)) + assert np.array_equiv(result_[1], np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32)) -def test_Reshape_one_dim(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([24], dtype=np.int64) - y = np.reshape(data, new_shape) +def test_Split_export_2d() -> None: + input = np.array( + [[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [7.0, 8.0, 9.0, 10.0, 11.0, 12.0]] + ).astype(np.float32) - op = Reshape(data, new_shape) + op = Split(input, axis=1, split=np.array([3, 3])) tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_negative_dim(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, -1, 2], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) + result_ = tf_op() + expected_outputs = np.array( + [[[1.0, 2.0, 3.0], [7.0, 8.0, 9.0]], [[4.0, 5.0, 6.0], [10.0, 11.0, 12.0]]] + ).astype(np.float32) + for i in range(2): + assert result_[i].shape == (2, 3) + assert np.array_equiv(result_[i], expected_outputs[i]) + + op = Split(input, axis=1, split=np.array([2, 4])) tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - - -def test_Reshape_negative_extended_dims(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([-1, 2, 3, 4], dtype=np.int64) - y = np.reshape(data, new_shape) - - op = Reshape(data, new_shape) + result_ = tf_op() + expected_outputs1 = np.array([[1.0, 2.0], [7.0, 8.0]]).astype(np.float32) + expected_outputs2 = np.array( + [[3.0, 4.0, 5.0, 6.0], [9.0, 10.0, 11.0, 12.0]] + ).astype(np.float32) + assert result_[0].shape == (2, 2) + assert np.array_equiv(result_[0], expected_outputs1) + assert result_[1].shape == (2, 4) + assert np.array_equiv(result_[1], expected_outputs2) + + +def test_Split_export_default_values() -> None: + input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + op = Split(input, split=np.array([2, 2, 2])) tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) + result_ = tf_op() + expected_outputs = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]).astype(np.float32) + assert len(result_) == 3 + assert np.array(result_).shape == expected_outputs.shape + assert np.array_equiv(np.array(result_), expected_outputs) - -def test_Reshape_zero_dim(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, 0, 4, 1], dtype=np.int64) - y = np.reshape(data, [2, 3, 4, 1]) - - op = Reshape(data, new_shape) + op = Split(input, split=np.array([2, 4])) tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) - + result_ = tf_op() + assert len(result_) == 2 + assert len(result_[0]) == 2 + assert len(result_[1]) == 4 + assert np.array_equiv(result_[0], np.array([1.0, 2.0]).astype(np.float32)) + assert np.array_equiv(result_[1], np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32)) -def test_Reshape_zero_and_negative_dim(): - original_shape = [2, 3, 4] - data = np.random.random_sample(original_shape).astype(np.float32) - new_shape = np.array([2, 0, 1, -1], dtype=np.int64) - y = np.reshape(data, [2, 3, 1, -1]) - op = Reshape(data, new_shape) +def test_Split_export_zero_size_splits() -> None: + input = np.array([]).astype(np.float32) + op = Split(input, split=np.array([0, 0, 0])) tf_op = TensorflowConverter().visit(op) - result = tf_op().numpy() - assert np.allclose(result, y) + result_ = tf_op() + expected_outputs = np.array([[], [], []]).astype(np.float32) + assert np.array(result_).shape == (3, 0) + assert np.array_equiv(np.array(result_), expected_outputs) From 771f94699103054fe15cc3f59fcc85725f97cdf8 Mon Sep 17 00:00:00 2001 From: Meriel von Stein Date: Fri, 24 Jun 2022 11:36:02 -0400 Subject: [PATCH 05/18] added printvisitor for Split --- dnnv/nn/converters/onnx.py | 1 - dnnv/nn/visitors.py | 8 ++++++++ .../test_nn/test_converters/test_onnx/test_Split.py | 6 ------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dnnv/nn/converters/onnx.py b/dnnv/nn/converters/onnx.py index 85a456d2..1136c2dc 100644 --- a/dnnv/nn/converters/onnx.py +++ b/dnnv/nn/converters/onnx.py @@ -610,7 +610,6 @@ def visit_Split(self, operation: operations.Split) -> onnx.NodeProto: outputs = np.array(outputs) x = self._to_onnx_proto(operation.x, f"{opname}.x") split = self._to_onnx_proto(operation.split, f"{opname}.split") - print(f"{[x.name, split.name]}") node = onnx.helper.make_node( op_type, inputs=[x.name, split.name], diff --git a/dnnv/nn/visitors.py b/dnnv/nn/visitors.py index a302c9bd..f8be2c74 100644 --- a/dnnv/nn/visitors.py +++ b/dnnv/nn/visitors.py @@ -345,6 +345,14 @@ def visit_Softmax(self, operation: operations.Softmax) -> None: self.print_op_id(operation) print("Softmax(%s, axis=%s)" % (self.get_op_id(operation.x), operation.axis)) + def visit_Split(self, operation: operations.Sub) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print( + "Split(%s, axis=%s, split=%s)" + % (self.get_op_id(operation.x), operation.axis, operation.split) + ) + def visit_Sub(self, operation: operations.Sub) -> None: self.generic_visit(operation) self.print_op_id(operation) diff --git a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py index 7b0e827a..800ca9d9 100644 --- a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py +++ b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py @@ -5,11 +5,6 @@ from dnnv.nn.converters.onnx import * from dnnv.nn.operations import * -# Tests based on: -# https://github.com/onnx/onnx/blob/2ab133404afce34552aaccd86e7023e1fb9a60d2/onnx/test/shape_inference_test.py -# https://github.com/onnx/onnx/blob/2ab133404afce34552aaccd86e7023e1fb9a60d2/onnx/test/automatic_upgrade_test.py -# https://github.com/onnx/onnx/blob/35092895d9bf3592e58f4710d098f8131afef259/onnx/backend/test/case/node/split.py - def test_Split_export_1d() -> None: input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) @@ -49,7 +44,6 @@ def test_Split_export_2d() -> None: outputselect = OutputSelect(op, i) onnx_model = convert(OperationGraph([outputselect])) results = onnxruntime.backend.run(onnx_model, []) - print(f"{results=}") all_results.append(results[0]) expected_outputs = np.array( [[[1.0, 2.0, 3.0], [7.0, 8.0, 9.0]], [[4.0, 5.0, 6.0], [10.0, 11.0, 12.0]]] From 229c42602b8f8f2cd01303a3fa96e0f4f24d5fe3 Mon Sep 17 00:00:00 2001 From: Meriel von Stein Date: Sun, 10 Apr 2022 19:06:51 -0400 Subject: [PATCH 06/18] added Split operator added tests for Split Normalize line endings tests for Split added printvisitor for Split --- dnnv/nn/converters/onnx.py | 32 ++++- dnnv/nn/converters/tensorflow.py | 21 +++- dnnv/nn/operations/tensor.py | 15 +++ .../transformers/simplifiers/squeeze_convs.py | 3 +- dnnv/nn/visitors.py | 8 ++ .../test_converters/test_onnx/test_Split.py | 113 ++++++++++++++++++ .../test_tensorflow/test_Split.py | 80 +++++++++++++ 7 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py create mode 100644 tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py diff --git a/dnnv/nn/converters/onnx.py b/dnnv/nn/converters/onnx.py index f20bb4a2..1136c2dc 100644 --- a/dnnv/nn/converters/onnx.py +++ b/dnnv/nn/converters/onnx.py @@ -490,16 +490,16 @@ def visit_OutputSelect(self, operation: operations.OutputSelect) -> onnx.NodePro idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 opname = f"{op_type}_{idx}" - if operation.index != 0: - raise NotImplementedError( - "Support for operations with multiple ouputs is not yet implemented." - ) + # if operation.index != 0: + # raise NotImplementedError( + # "Support for operations with multiple ouputs is not yet implemented." + # ) op = self._to_onnx_proto(operation.operation, f"{opname}.operation") node = onnx.helper.make_node( "Identity", - inputs=[op.name], + inputs=[op.output[operation.index]], outputs=[opname], name=opname, ) @@ -597,3 +597,25 @@ def visit_Transpose(self, operation: operations.Transpose) -> onnx.NodeProto: ) return node + + def visit_Split(self, operation: operations.Split) -> onnx.NodeProto: + op_type = str(operation) + # TODO: split attribute is optional. Edits to nn/parser/onnx.py required. + assert operation.split is not None + idx = self.op_counts["Split"] = self.op_counts["Split"] + 1 + opname = f"Split_{idx}" + outputs = [] + for i in range(len(operation.split)): + outputs.append(f"output_{i}") + outputs = np.array(outputs) + x = self._to_onnx_proto(operation.x, f"{opname}.x") + split = self._to_onnx_proto(operation.split, f"{opname}.split") + node = onnx.helper.make_node( + op_type, + inputs=[x.name, split.name], + outputs=outputs, + name=opname, + axis=operation.axis, + ) + + return node diff --git a/dnnv/nn/converters/tensorflow.py b/dnnv/nn/converters/tensorflow.py index cab8fecc..b3a189b3 100644 --- a/dnnv/nn/converters/tensorflow.py +++ b/dnnv/nn/converters/tensorflow.py @@ -234,7 +234,7 @@ def conv_func(*inputs): else: bias = np.zeros((weights.shape[0],), dtype=weights.dtype) assert np.all(operation.dilations == 1) - assert np.all(operation.group == 1) + # assert np.all(operation.group == 1) num_pads = len(operation.pads) pads = tuple( zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) @@ -297,7 +297,7 @@ def convtranspose_func(*inputs): bias = operation.b else: bias = np.zeros((weights.shape[1],), dtype=weights.dtype) - assert np.all(operation.group == 1) + # assert np.all(operation.group == 1) num_pads = len(operation.pads) pads = tuple( @@ -872,3 +872,20 @@ def unsqueeze_func(*inputs): return x return unsqueeze_func + + def visit_Split(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + split_ = operation.split + if isinstance(split_, Operation): + split_ = self.visit(split_) + axis = operation.axis + + @self._cached + def split_func(*inputs): + x, split = _concretize([x_, split_], inputs) + x = tf.split(x, tf.convert_to_tensor(split), axis=axis) + return x + + return split_func diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index a9a4cf31..9e6f30bf 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -221,6 +221,20 @@ def from_onnx(cls, onnx_node, *inputs): return cls(*inputs, axes=axes, name=onnx_node.name) +class Split(Operation): + def __init__(self, x, split=None, *, axis=0, name: Optional[str] = None): + super().__init__(name=name) + self.x = x + self.axis = axis + self.split = split + + @classmethod + def from_onnx(cls, onnx_node, *inputs): + attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} + axis = attributes.get("axis", 0) + return cls(*inputs, axis=axis, name=onnx_node.name) + + __all__ = [ "Cast", "Concat", @@ -232,6 +246,7 @@ def from_onnx(cls, onnx_node, *inputs): "Reshape", "Resize", "Shape", + "Split", "Tile", "Transpose", "Unsqueeze", diff --git a/dnnv/nn/transformers/simplifiers/squeeze_convs.py b/dnnv/nn/transformers/simplifiers/squeeze_convs.py index 1b2c15a8..dbb61847 100644 --- a/dnnv/nn/transformers/simplifiers/squeeze_convs.py +++ b/dnnv/nn/transformers/simplifiers/squeeze_convs.py @@ -9,7 +9,8 @@ class SqueezeConvs(Simplifier): def is_diagonal(self, array): i, j = array.shape - return ~np.any(array.reshape(-1)[:-1].reshape(i - 1, j + 1)[:, 1:]) + # return ~np.any(array.reshape(-1)[:-1].reshape(i - 1, j + 1)[:, 1:]) + return i == j and ~np.any(array.reshape(-1)[:-1].reshape(i - 1, j + 1)[:, 1:]) def visit_Conv(self, operation: operations.Conv) -> operations.Conv: if ( diff --git a/dnnv/nn/visitors.py b/dnnv/nn/visitors.py index a302c9bd..f8be2c74 100644 --- a/dnnv/nn/visitors.py +++ b/dnnv/nn/visitors.py @@ -345,6 +345,14 @@ def visit_Softmax(self, operation: operations.Softmax) -> None: self.print_op_id(operation) print("Softmax(%s, axis=%s)" % (self.get_op_id(operation.x), operation.axis)) + def visit_Split(self, operation: operations.Sub) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print( + "Split(%s, axis=%s, split=%s)" + % (self.get_op_id(operation.x), operation.axis, operation.split) + ) + def visit_Sub(self, operation: operations.Sub) -> None: self.generic_visit(operation) self.print_op_id(operation) diff --git a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py new file mode 100644 index 00000000..800ca9d9 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Split.py @@ -0,0 +1,113 @@ +import numpy as np +import onnxruntime.backend +import pytest + +from dnnv.nn.converters.onnx import * +from dnnv.nn.operations import * + + +def test_Split_export_1d() -> None: + input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + op = Split(input, axis=0, split=np.array([2, 2, 2])) + all_results = [] + for i in range(3): + onnx_model = convert(OperationGraph([OutputSelect(op, i)])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + all_results = np.array(all_results) + expected_outputs = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]).astype(np.float32) + assert len(all_results) == 3 + assert np.array_equiv(all_results, expected_outputs) + + op = Split(input, axis=0, split=np.array([2, 4]).astype(np.int64)) + all_results = [] + for i in range(2): + onnx_model = convert(OperationGraph([OutputSelect(op, i)])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + all_results = np.array(all_results) + assert len(all_results) == 2 + assert np.array_equiv(all_results[0], np.array([1.0, 2.0]).astype(np.float32)) + assert np.array_equiv( + all_results[1], np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32) + ) + + +def test_Split_export_2d() -> None: + input = np.array( + [[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [7.0, 8.0, 9.0, 10.0, 11.0, 12.0]] + ).astype(np.float32) + + op = Split(input, axis=1, split=np.array([3, 3])) + all_results = [] + for i in range(2): + outputselect = OutputSelect(op, i) + onnx_model = convert(OperationGraph([outputselect])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + expected_outputs = np.array( + [[[1.0, 2.0, 3.0], [7.0, 8.0, 9.0]], [[4.0, 5.0, 6.0], [10.0, 11.0, 12.0]]] + ).astype(np.float32) + for i in range(2): + assert all_results[i].shape == (2, 3) + assert np.array_equiv(all_results[i], expected_outputs[i]) + + op = Split(input, axis=1, split=np.array([2, 4])) + all_results = [] + for i in range(2): + outputselect = OutputSelect(op, i) + onnx_model = convert(OperationGraph([outputselect])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + expected_outputs1 = np.array([[1.0, 2.0], [7.0, 8.0]]).astype(np.float32) + expected_outputs2 = np.array( + [[3.0, 4.0, 5.0, 6.0], [9.0, 10.0, 11.0, 12.0]] + ).astype(np.float32) + assert all_results[0].shape == (2, 2) + assert np.array_equiv(all_results[0], expected_outputs1) + assert all_results[1].shape == (2, 4) + assert np.array_equiv(all_results[1], expected_outputs2) + + +def test_Split_export_default_values() -> None: + input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + op = Split(input, split=np.array([2, 2, 2])) + all_results = [] + for i in range(3): + onnx_model = convert(OperationGraph([OutputSelect(op, i)])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + all_results = np.array(all_results) + expected_outputs = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]).astype(np.float32) + assert len(all_results) == 3 + assert all_results.shape == expected_outputs.shape + assert np.array_equiv(all_results, expected_outputs) + + op = Split(input, split=np.array([2, 4])) + all_results = [] + for i in range(2): + onnx_model = convert(OperationGraph([OutputSelect(op, i)])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + assert len(all_results) == 2 + assert len(all_results[0]) == 2 + assert len(all_results[1]) == 4 + assert np.array_equiv(all_results[0], np.array([1.0, 2.0]).astype(np.float32)) + assert np.array_equiv( + all_results[1], np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32) + ) + + +def test_Split_export_zero_size_splits() -> None: + # Split emtpy tensor to tensors of size zero + input = np.array([]).astype(np.float32) + op = Split(input, split=np.array([0, 0, 0])) + all_results = [] + for i in range(3): + onnx_model = convert(OperationGraph([OutputSelect(op, i)])) + results = onnxruntime.backend.run(onnx_model, []) + all_results.append(results[0]) + all_results = np.array(all_results) + expected_outputs = np.array([[], [], []]).astype(np.float32) + assert all_results.shape == (3, 0) + assert np.array_equiv(all_results, expected_outputs) diff --git a/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py new file mode 100644 index 00000000..29644628 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Split.py @@ -0,0 +1,80 @@ +import numpy as np +import pytest + +from dnnv.nn.converters.tensorflow import * +from dnnv.nn.operations import * + + +def test_Split_export_1d() -> None: + input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + op = Split(input, axis=0, split=np.array([2, 2, 2])) + tf_op = TensorflowConverter().visit(op) + result_ = tf_op() + expected_outputs = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]).astype(np.float32) + assert len(result_) == 3 + assert np.array_equiv(result_, expected_outputs) + + op = Split(input, axis=0, split=np.array([2, 4]).astype(np.int64)) + tf_op = TensorflowConverter().visit(op) + result_ = tf_op() + assert len(result_) == 2 + assert np.array_equiv(result_[0], np.array([1.0, 2.0]).astype(np.float32)) + assert np.array_equiv(result_[1], np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32)) + + +def test_Split_export_2d() -> None: + input = np.array( + [[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [7.0, 8.0, 9.0, 10.0, 11.0, 12.0]] + ).astype(np.float32) + + op = Split(input, axis=1, split=np.array([3, 3])) + tf_op = TensorflowConverter().visit(op) + result_ = tf_op() + expected_outputs = np.array( + [[[1.0, 2.0, 3.0], [7.0, 8.0, 9.0]], [[4.0, 5.0, 6.0], [10.0, 11.0, 12.0]]] + ).astype(np.float32) + for i in range(2): + assert result_[i].shape == (2, 3) + assert np.array_equiv(result_[i], expected_outputs[i]) + + op = Split(input, axis=1, split=np.array([2, 4])) + tf_op = TensorflowConverter().visit(op) + result_ = tf_op() + expected_outputs1 = np.array([[1.0, 2.0], [7.0, 8.0]]).astype(np.float32) + expected_outputs2 = np.array( + [[3.0, 4.0, 5.0, 6.0], [9.0, 10.0, 11.0, 12.0]] + ).astype(np.float32) + assert result_[0].shape == (2, 2) + assert np.array_equiv(result_[0], expected_outputs1) + assert result_[1].shape == (2, 4) + assert np.array_equiv(result_[1], expected_outputs2) + + +def test_Split_export_default_values() -> None: + input = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).astype(np.float32) + op = Split(input, split=np.array([2, 2, 2])) + tf_op = TensorflowConverter().visit(op) + result_ = tf_op() + expected_outputs = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]).astype(np.float32) + assert len(result_) == 3 + assert np.array(result_).shape == expected_outputs.shape + assert np.array_equiv(np.array(result_), expected_outputs) + + op = Split(input, split=np.array([2, 4])) + tf_op = TensorflowConverter().visit(op) + result_ = tf_op() + assert len(result_) == 2 + assert len(result_[0]) == 2 + assert len(result_[1]) == 4 + assert np.array_equiv(result_[0], np.array([1.0, 2.0]).astype(np.float32)) + assert np.array_equiv(result_[1], np.array([3.0, 4.0, 5.0, 6.0]).astype(np.float32)) + + +def test_Split_export_zero_size_splits() -> None: + input = np.array([]).astype(np.float32) + op = Split(input, split=np.array([0, 0, 0])) + tf_op = TensorflowConverter().visit(op) + result_ = tf_op() + expected_outputs = np.array([[], [], []]).astype(np.float32) + assert np.array(result_).shape == (3, 0) + assert np.array_equiv(np.array(result_), expected_outputs) From 6452e5d44bb5fac8c31366f7d07de02c3fd52a01 Mon Sep 17 00:00:00 2001 From: Meriel von Stein Date: Fri, 24 Jun 2022 13:28:19 -0400 Subject: [PATCH 07/18] compatability for split as input or attribute based on changes since ONNX 11 --- dnnv/nn/operations/tensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index 9e6f30bf..8f40b556 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -232,6 +232,10 @@ def __init__(self, x, split=None, *, axis=0, name: Optional[str] = None): def from_onnx(cls, onnx_node, *inputs): attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} axis = attributes.get("axis", 0) + if len(inputs) < 2: + # TODO: split is an input past version 11 (?) + split = attributes.get("split") + return cls(*inputs, axis=axis, split=split, name=onnx_node.name) return cls(*inputs, axis=axis, name=onnx_node.name) From 45fb31c489d01dd9a9689d71d32386c89d21787c Mon Sep 17 00:00:00 2001 From: Meriel von Stein Date: Fri, 24 Jun 2022 15:51:32 -0400 Subject: [PATCH 08/18] style and remove comments --- dnnv/nn/converters/onnx.py | 49 ++++++++++++++------------------ dnnv/nn/converters/tensorflow.py | 36 +++++++++++------------ dnnv/nn/operations/tensor.py | 36 +++++++++++------------ 3 files changed, 57 insertions(+), 64 deletions(-) diff --git a/dnnv/nn/converters/onnx.py b/dnnv/nn/converters/onnx.py index 1136c2dc..dc1ef182 100644 --- a/dnnv/nn/converters/onnx.py +++ b/dnnv/nn/converters/onnx.py @@ -490,11 +490,6 @@ def visit_OutputSelect(self, operation: operations.OutputSelect) -> onnx.NodePro idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 opname = f"{op_type}_{idx}" - # if operation.index != 0: - # raise NotImplementedError( - # "Support for operations with multiple ouputs is not yet implemented." - # ) - op = self._to_onnx_proto(operation.operation, f"{opname}.operation") node = onnx.helper.make_node( @@ -552,6 +547,28 @@ def visit_Sigmoid(self, operation: operations.Sigmoid) -> onnx.NodeProto: return node + def visit_Split(self, operation: operations.Split) -> onnx.NodeProto: + op_type = str(operation) + # TODO: split attribute is optional. Edits to nn/parser/onnx.py required. + assert operation.split is not None + idx = self.op_counts["Split"] = self.op_counts["Split"] + 1 + opname = f"Split_{idx}" + outputs = [] + for i in range(len(operation.split)): + outputs.append(f"output_{i}") + outputs = np.array(outputs) + x = self._to_onnx_proto(operation.x, f"{opname}.x") + split = self._to_onnx_proto(operation.split, f"{opname}.split") + node = onnx.helper.make_node( + op_type, + inputs=[x.name, split.name], + outputs=outputs, + name=opname, + axis=operation.axis, + ) + + return node + def visit_Sub(self, operation: operations.Sub) -> onnx.NodeProto: op_type = str(operation) idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 @@ -597,25 +614,3 @@ def visit_Transpose(self, operation: operations.Transpose) -> onnx.NodeProto: ) return node - - def visit_Split(self, operation: operations.Split) -> onnx.NodeProto: - op_type = str(operation) - # TODO: split attribute is optional. Edits to nn/parser/onnx.py required. - assert operation.split is not None - idx = self.op_counts["Split"] = self.op_counts["Split"] + 1 - opname = f"Split_{idx}" - outputs = [] - for i in range(len(operation.split)): - outputs.append(f"output_{i}") - outputs = np.array(outputs) - x = self._to_onnx_proto(operation.x, f"{opname}.x") - split = self._to_onnx_proto(operation.split, f"{opname}.split") - node = onnx.helper.make_node( - op_type, - inputs=[x.name, split.name], - outputs=outputs, - name=opname, - axis=operation.axis, - ) - - return node diff --git a/dnnv/nn/converters/tensorflow.py b/dnnv/nn/converters/tensorflow.py index b3a189b3..2c024e42 100644 --- a/dnnv/nn/converters/tensorflow.py +++ b/dnnv/nn/converters/tensorflow.py @@ -234,7 +234,6 @@ def conv_func(*inputs): else: bias = np.zeros((weights.shape[0],), dtype=weights.dtype) assert np.all(operation.dilations == 1) - # assert np.all(operation.group == 1) num_pads = len(operation.pads) pads = tuple( zip(operation.pads[: num_pads // 2], operation.pads[num_pads // 2 :]) @@ -297,7 +296,6 @@ def convtranspose_func(*inputs): bias = operation.b else: bias = np.zeros((weights.shape[1],), dtype=weights.dtype) - # assert np.all(operation.group == 1) num_pads = len(operation.pads) pads = tuple( @@ -797,6 +795,23 @@ def softmax_func(*inputs): return softmax_func + def visit_Split(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + split_ = operation.split + if isinstance(split_, Operation): + split_ = self.visit(split_) + axis = operation.axis + + @self._cached + def split_func(*inputs): + x, split = _concretize([x_, split_], inputs) + x = tf.split(x, tf.convert_to_tensor(split), axis=axis) + return x + + return split_func + def visit_Sub(self, operation): a_ = operation.a if isinstance(a_, Operation): @@ -872,20 +887,3 @@ def unsqueeze_func(*inputs): return x return unsqueeze_func - - def visit_Split(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - split_ = operation.split - if isinstance(split_, Operation): - split_ = self.visit(split_) - axis = operation.axis - - @self._cached - def split_func(*inputs): - x, split = _concretize([x_, split_], inputs) - x = tf.split(x, tf.convert_to_tensor(split), axis=axis) - return x - - return split_func diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index 8f40b556..7c69f9ea 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -176,6 +176,24 @@ def from_onnx(cls, onnx_node, *inputs): return cls(*inputs, name=onnx_node.name) +class Split(Operation): + def __init__(self, x, split=None, *, axis=0, name: Optional[str] = None): + super().__init__(name=name) + self.x = x + self.axis = axis + self.split = split + + @classmethod + def from_onnx(cls, onnx_node, *inputs): + attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} + axis = attributes.get("axis", 0) + if len(inputs) < 2: + # TODO: split is an input past version 11 (?) + split = attributes.get("split") + return cls(*inputs, axis=axis, split=split, name=onnx_node.name) + return cls(*inputs, axis=axis, name=onnx_node.name) + + class Tile(Operation): def __init__(self, x, repeats, *, name: Optional[str] = None): super().__init__(name=name) @@ -221,24 +239,6 @@ def from_onnx(cls, onnx_node, *inputs): return cls(*inputs, axes=axes, name=onnx_node.name) -class Split(Operation): - def __init__(self, x, split=None, *, axis=0, name: Optional[str] = None): - super().__init__(name=name) - self.x = x - self.axis = axis - self.split = split - - @classmethod - def from_onnx(cls, onnx_node, *inputs): - attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} - axis = attributes.get("axis", 0) - if len(inputs) < 2: - # TODO: split is an input past version 11 (?) - split = attributes.get("split") - return cls(*inputs, axis=axis, split=split, name=onnx_node.name) - return cls(*inputs, axis=axis, name=onnx_node.name) - - __all__ = [ "Cast", "Concat", From e1138e1c2223cd4727c90b736f7ca77fe91a957c Mon Sep 17 00:00:00 2001 From: Felipe Date: Wed, 29 Jun 2022 10:32:59 -0400 Subject: [PATCH 09/18] Added operations --- dnnv/nn/converters/tensorflow.py | 124 +++++++++++++++++- dnnv/nn/operations/tensor.py | 89 ++++++++++++- dnnv/nn/visitors.py | 57 ++++++++ .../transformers/propagate_constants.py | 4 + 4 files changed, 267 insertions(+), 7 deletions(-) diff --git a/dnnv/nn/converters/tensorflow.py b/dnnv/nn/converters/tensorflow.py index e5dba2f3..1892cc0b 100644 --- a/dnnv/nn/converters/tensorflow.py +++ b/dnnv/nn/converters/tensorflow.py @@ -1,6 +1,7 @@ import numpy as np import tensorflow as tf +from .. import operations from ..graph import OperationGraph from ..operations import Operation from ..utils import ONNX_TO_TENSORFLOW_DTYPE @@ -211,6 +212,7 @@ def visit_Concat(self, operation): @self._cached def concat_func(*inputs): tensors = x = _concretize(tensors_, inputs) + # tensors = _concretize(tensors_, inputs) result = tf.concat(tensors, axis=operation.axis) return result @@ -220,20 +222,25 @@ def visit_Conv(self, operation): x_ = operation.x if isinstance(x_, Operation): x_ = self.visit(x_) + w_ = operation.w + if isinstance(w_, Operation): + w_ = self.visit(w_) @self._cached def conv_func(*inputs): - x = _concretize([x_], inputs) + x, weights = _concretize([x_, w_], inputs) if len(operation.kernel_shape) != 2: raise NotImplementedError( "Non 2d convolutions are not currently supported." ) - weights = operation.w + if not isinstance(weights,np.ndarray): + weights = np.array(weights) + # weights = operation.w if operation.b is not None: bias = operation.b else: bias = np.zeros((weights.shape[0],), dtype=weights.dtype) - assert np.all(operation.dilations == 1) + # assert np.all(operation.dilations == 1) # assert np.all(operation.group == 1) num_pads = len(operation.pads) pads = tuple( @@ -249,6 +256,7 @@ def conv_func(*inputs): weights.transpose((2, 3, 1, 0)), operation.strides, padding="VALID", + dilations=operation.dilations ), bias, ) @@ -403,11 +411,14 @@ def visit_Expand(self, operation): x_ = operation.x if isinstance(x_, Operation): x_ = self.visit(x_) + shape_ = operation.shape + if isinstance(shape_, Operation): + shape_ = self.visit(shape_) @self._cached def expand_func(*inputs): - x = _concretize([x_], inputs) - shape = operation.shape + x, shape = _concretize([x_, shape_], inputs) + # shape = operation.shape result = x * tf.ones(shape, x.dtype) return result @@ -714,6 +725,7 @@ def resize_func(*inputs): assert operation.coordinate_transformation_mode in [ "asymmetric", "tf_crop_and_resize", + "align_corners" ] assert operation.mode in ["nearest", "linear"] assert operation.exclude_outside == 0 @@ -724,9 +736,11 @@ def resize_func(*inputs): assert roi.size == 8 and roi.ndim == 1 roi = roi[None, [2, 3, 6, 7]] if sizes is None or sizes.size == 0: + # if sizes is None or sizes.shape == 0: assert scales[0] == 1.0 and scales[1] == 1.0 sizes = (scales * [int(d) for d in x.shape]).astype(int) assert sizes.ndim == 1 and sizes.size == 4 + # assert sizes.ndim == 1 and sizes.shape == 4 sizes = sizes[2:] method = operation.mode if method == "linear": @@ -887,3 +901,103 @@ def split_func(*inputs): return x return split_func + + def visit_ReduceL2(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + axes = operation.axes + keepdims = operation.keepdims + + @self._cached + def reduceL2_func(*inputs): + x = _concretize([x_], inputs) + x = tf.norm(x, ord=2, axis=axes, keepdims=keepdims) + return x + + return reduceL2_func + + def visit_Clip(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + _min = operation.min + _max = operation.max + + @self._cached + def clip_func(*inputs): + x = _concretize([x_], inputs) + x = tf.clip_by_value(x, _min, _max) + return x + + return clip_func + + def visit_Squeeze(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + axes = operation.axes + + @self._cached + def squeeze_func(*inputs): + x = _concretize([x_], inputs) + x = tf.squeeze(x, axis=axes) + return x + + return squeeze_func + + def visit_Upsample(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + scales = operation.scales + mode = operation.mode + + @self._cached + def upsample_func(*inputs): + x = _concretize([x_], inputs) + # x = tf.keras.layers.UpSampling2D(size=scales[-2:], interpolation=mode, data_format="channels_first")(x) + scaled_dim = [int(sd) for sd in x.shape[2:] * scales[2:]] + # xr = tf.reshape(x, [x.shape[0],x.shape[2],x.shape[3],x.shape[1]]) + xr = tf.transpose(x, perm=[0,2,3,1]) + xr = tf.image.resize(xr, scaled_dim, method="nearest") + # xr = tf.reshape(xr, [xr.shape[0],xr.shape[3],xr.shape[1],xr.shape[2]]) + xr = tf.transpose(xr, perm=[0,3,1,2]) + return xr + + return upsample_func + + def visit_Slice(self, operation: operations.Slice): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + starts_ = operation.starts + if isinstance(starts_, Operation): + starts_ = self.visit(starts_) + ends_ = operation.ends + if isinstance(ends_, Operation): + ends_ = self.visit(ends_) + axes_ = operation.axes + if isinstance(axes_, Operation): + axes_ = self.visit(axes_) + steps_ = operation.steps + if isinstance(steps_, Operation): + steps_ = self.visit(steps_) + + @self._cached + def slice_func(*inputs): + x, starts, ends, axes, steps = _concretize( + [x_, starts_, ends_, axes_, steps_], inputs + ) + n = x.ndim + slices = [slice(None) for _ in range(n)] + if axes is None: + axes = range(n) + if steps is None: + steps = [1 for _ in range(n)] + for i, axis in enumerate(axes): + slices[axis] = slice(starts[i], ends[i], steps[i]) + result = x[tuple(slices)] + return result + + return slice_func \ No newline at end of file diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index 3c82d593..40e36966 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -15,8 +15,8 @@ def __init__(self, x, to, *, name: Optional[str] = None): @classmethod def from_onnx(cls, onnx_node, *inputs): attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} - axis = attributes.get("to") - return cls(inputs, axis=axis, name=onnx_node.name) + to = attributes.get("to") + return cls(*inputs, to=to, name=onnx_node.name) class Concat(Operation): @@ -130,6 +130,8 @@ def __init__( name: Optional[str] = None ): super().__init__(name=name) + # assert scales.size != 0 or sizes.size != 0 + # assert scales.size == 0 or sizes.size == 0 assert scales.size != 0 or sizes.size != 0 assert scales.size == 0 or sizes.size == 0 self.x = x @@ -237,6 +239,84 @@ def from_onnx(cls, onnx_node, *inputs): return cls(*inputs, axis=axis, split=split, name=onnx_node.name) +class ReduceL2(Operation): + def __init__(self, x, axes, keepdims, *, name: Optional[str] = None): + super().__init__(name=name) + self.x = x + if isinstance(axes, int): + self.axes = axes + elif len(axes) == 1: + self.axes = int(axes[0]) + elif len(axes) > 1: + self.axes = tuple(axes) + self.keepdims = keepdims + + @classmethod + def from_onnx(cls, onnx_node, *inputs): + attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} + axes = attributes.get("axes") + keepdims = attributes.get("keepdims") + return cls(*inputs, axes=axes, keepdims=keepdims, name=onnx_node.name) + + +class Clip(Operation): + def __init__(self, x, min, max, *, name: Optional[str] = None): + super().__init__(name=name) + self.x = x + self.min = min if min is not None else -np.inf + self.max = max if max is not None else +np.inf + + @classmethod + def from_onnx(cls, onnx_node, *inputs): + attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} + _min = attributes.get("min") + _max = attributes.get("max") + return cls(*inputs, min=_min, max=_max, name=onnx_node.name) + + +class Squeeze(Operation): + def __init__(self, x, axes, *, name: Optional[str] = None): + super().__init__(name=name) + self.x = x + self.axes = axes + + @classmethod + def from_onnx(cls, onnx_node, *inputs): + attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} + axes = attributes.get("axes") + return cls(*inputs, axes=axes, name=onnx_node.name) + + +class Upsample(Operation): + def __init__(self, x, scales, mode, *, name: Optional[str] = None): + super().__init__(name=name) + self.x = x + self.scales = scales + self.mode = mode + + @classmethod + def from_onnx(cls, onnx_node, *inputs): + attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} + mode = attributes.get("mode") + # scales = attributes.get("scales") + return cls(*inputs, mode=mode, name=onnx_node.name) + +class Slice(Operation): + def __init__( + self, x, starts, ends, axes=None, steps=None, *, name: Optional[str] = None + ): + super().__init__(name=name) + self.x = x + self.starts = starts + self.ends = ends + self.axes = axes + self.steps = steps + + @classmethod + def from_onnx(cls, onnx_node, *inputs): + attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} + return cls(*inputs, name=onnx_node.name) + __all__ = [ "Cast", "Concat", @@ -252,4 +332,9 @@ def from_onnx(cls, onnx_node, *inputs): "Transpose", "Unsqueeze", "Split", + "ReduceL2", + "Clip", + "Squeeze", + "Upsample", + "Slice" ] diff --git a/dnnv/nn/visitors.py b/dnnv/nn/visitors.py index a302c9bd..fc369cb8 100644 --- a/dnnv/nn/visitors.py +++ b/dnnv/nn/visitors.py @@ -308,6 +308,8 @@ def visit_Resize(self, operation: operations.Resize) -> None: inputs.append(f"roi={operation.roi.tolist()}") if operation.scales.size > 0: inputs.append(f"scales={operation.scales.tolist()}") + # if operation.sizes.size > 0: + # inputs.append(f"sizes={operation.sizes.tolist()}") if operation.sizes.size > 0: inputs.append(f"sizes={operation.sizes.tolist()}") inputs_str = ", ".join(inputs) @@ -375,6 +377,61 @@ def visit_Unsqueeze(self, operation: operations.Unsqueeze) -> None: self.print_op_id(operation) print("Unsqueeze(%s, axes=%s)" % (self.get_op_id(operation.x), operation.axes)) + def visit_Split(self, operation: operations.Split) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print( + "Split(%s, axis=%s, split=%s)" + % (self.get_op_id(operation.x), operation.axis, operation.split) + ) + + def visit_ReduceL2(self, operation: operations.ReduceL2) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print( + "ReduceL2(%s, axes=%s, keepdims=%s)" + % (self.get_op_id(operation.x), operation.axes, operation.keepdims) + ) + + def visit_Clip(self, operation: operations.Clip) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print( + "Clip(%s, min=%s, max=%s)" + % (self.get_op_id(operation.x), operation.min, operation.max) + ) + + def visit_Squeeze(self, operation: operations.Squeeze) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print("Squeeze(%s, axes=%s)" % (self.get_op_id(operation.x), operation.axes)) + + def visit_Upsample(self, operation: operations.Upsample) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print("Upsample(%s, scales=%s)" % (self.get_op_id(operation.x), operation.scales)) + + def visit_Slice(self, operation: operations.Slice) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + axes = ( + f", axes={self.get_op_id(operation.axes)}" + if operation.axes is not None + else "" + ) + steps = ( + f", steps={self.get_op_id(operation.steps)}" + if operation.steps is not None + else "" + ) + print( + "Slice(" + f"{self.get_op_id(operation.x)}, " + f"{self.get_op_id(operation.starts)}, " + f"{self.get_op_id(operation.ends)}" + f"{axes}{steps}" + ")" + ) __all__ = [ "OperationVisitor", diff --git a/dnnv/properties/transformers/propagate_constants.py b/dnnv/properties/transformers/propagate_constants.py index 6618fd2e..3c5fc807 100644 --- a/dnnv/properties/transformers/propagate_constants.py +++ b/dnnv/properties/transformers/propagate_constants.py @@ -4,6 +4,8 @@ from typing import Union +from dnnv.nn.graph import OperationGraph + from ..expressions import ( ArithmeticExpression, AssociativeExpression, @@ -98,6 +100,8 @@ def visit_Call(self, expression: Call) -> Expression: result = expression.value if isinstance(result, Expression): return result.propagate_constants() + if isinstance(result, OperationGraph): + return Network(str(expression)).concretize(result) return Constant(result) return Call(function, args, kwargs) From 4a2dce66ad094f883fc653486d14a137b848e74d Mon Sep 17 00:00:00 2001 From: Felipe Date: Thu, 30 Jun 2022 09:43:01 -0400 Subject: [PATCH 10/18] Reordered operations --- dnnv/nn/converters/tensorflow.py | 105 +++++++++++++++---------------- dnnv/nn/visitors.py | 85 +++++++++---------------- 2 files changed, 77 insertions(+), 113 deletions(-) diff --git a/dnnv/nn/converters/tensorflow.py b/dnnv/nn/converters/tensorflow.py index e615b77e..44139db3 100644 --- a/dnnv/nn/converters/tensorflow.py +++ b/dnnv/nn/converters/tensorflow.py @@ -208,6 +208,21 @@ def cast_func(*inputs): return cast_func + def visit_Clip(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + _min = operation.min + _max = operation.max + + @self._cached + def clip_func(*inputs): + x = _concretize([x_], inputs) + x = tf.clip_by_value(x, _min, _max) + return x + + return clip_func + def visit_Concat(self, operation): tensors_ = [] for x in operation.x: @@ -238,7 +253,7 @@ def conv_func(*inputs): raise NotImplementedError( "Non 2d convolutions are not currently supported." ) - if not isinstance(weights,np.ndarray): + if not isinstance(weights, np.ndarray): weights = np.array(weights) if operation.b is not None: bias = operation.b @@ -262,7 +277,7 @@ def conv_func(*inputs): weights.transpose((2, 3, 1, 0)), operation.strides, padding="VALID", - dilations=operation.dilations + dilations=operation.dilations, ), bias, ) @@ -429,7 +444,6 @@ def visit_Expand(self, operation): @self._cached def expand_func(*inputs): x, shape = _concretize([x_, shape_], inputs) - # shape = operation.shape result = x * tf.ones(shape, x.dtype) return result @@ -693,6 +707,21 @@ def pad_func(*inputs): return pad_func + def visit_ReduceL2(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + axes = operation.axes + keepdims = operation.keepdims + + @self._cached + def reduceL2_func(*inputs): + x = _concretize([x_], inputs) + x = tf.norm(x, ord=2, axis=axes, keepdims=keepdims) + return x + + return reduceL2_func + def visit_Relu(self, operation): x_ = operation.x if isinstance(x_, Operation): @@ -746,7 +775,7 @@ def resize_func(*inputs): assert operation.coordinate_transformation_mode in [ "asymmetric", "tf_crop_and_resize", - "align_corners" + "align_corners", ] assert operation.mode in ["nearest", "linear"] assert operation.exclude_outside == 0 @@ -757,11 +786,9 @@ def resize_func(*inputs): assert roi.size == 8 and roi.ndim == 1 roi = roi[None, [2, 3, 6, 7]] if sizes is None or sizes.size == 0: - # if sizes is None or sizes.shape == 0: assert scales[0] == 1.0 and scales[1] == 1.0 sizes = (scales * [int(d) for d in x.shape]).astype(int) assert sizes.ndim == 1 and sizes.size == 4 - # assert sizes.ndim == 1 and sizes.shape == 4 sizes = sizes[2:] method = operation.mode if method == "linear": @@ -884,6 +911,20 @@ def split_func(*inputs): return split_func + def visit_Squeeze(self, operation): + x_ = operation.x + if isinstance(x_, Operation): + x_ = self.visit(x_) + axes = operation.axes + + @self._cached + def squeeze_func(*inputs): + x = _concretize([x_], inputs) + x = tf.squeeze(x, axis=axes) + return x + + return squeeze_func + def visit_Sub(self, operation): a_ = operation.a if isinstance(a_, Operation): @@ -960,67 +1001,19 @@ def unsqueeze_func(*inputs): return unsqueeze_func - def visit_ReduceL2(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - axes = operation.axes - keepdims = operation.keepdims - - @self._cached - def reduceL2_func(*inputs): - x = _concretize([x_], inputs) - x = tf.norm(x, ord=2, axis=axes, keepdims=keepdims) - return x - - return reduceL2_func - - def visit_Clip(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - _min = operation.min - _max = operation.max - - @self._cached - def clip_func(*inputs): - x = _concretize([x_], inputs) - x = tf.clip_by_value(x, _min, _max) - return x - - return clip_func - - def visit_Squeeze(self, operation): - x_ = operation.x - if isinstance(x_, Operation): - x_ = self.visit(x_) - axes = operation.axes - - @self._cached - def squeeze_func(*inputs): - x = _concretize([x_], inputs) - x = tf.squeeze(x, axis=axes) - return x - - return squeeze_func - def visit_Upsample(self, operation): x_ = operation.x if isinstance(x_, Operation): x_ = self.visit(x_) scales = operation.scales - mode = operation.mode @self._cached def upsample_func(*inputs): x = _concretize([x_], inputs) - # x = tf.keras.layers.UpSampling2D(size=scales[-2:], interpolation=mode, data_format="channels_first")(x) scaled_dim = [int(sd) for sd in x.shape[2:] * scales[2:]] - # xr = tf.reshape(x, [x.shape[0],x.shape[2],x.shape[3],x.shape[1]]) - xr = tf.transpose(x, perm=[0,2,3,1]) + xr = tf.transpose(x, perm=[0, 2, 3, 1]) xr = tf.image.resize(xr, scaled_dim, method="nearest") - # xr = tf.reshape(xr, [xr.shape[0],xr.shape[3],xr.shape[1],xr.shape[2]]) - xr = tf.transpose(xr, perm=[0,3,1,2]) + xr = tf.transpose(xr, perm=[0, 3, 1, 2]) return xr return upsample_func diff --git a/dnnv/nn/visitors.py b/dnnv/nn/visitors.py index c48606c9..0c984389 100644 --- a/dnnv/nn/visitors.py +++ b/dnnv/nn/visitors.py @@ -132,6 +132,14 @@ def visit_Cast(self, operation: operations.Cast) -> None: self.print_op_id(operation) print(f"Cast({self.get_op_id(operation.x)}, to={operation.to})") + def visit_Clip(self, operation: operations.Clip) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print( + "Clip(%s, min=%s, max=%s)" + % (self.get_op_id(operation.x), operation.min, operation.max) + ) + def visit_Concat(self, operation: operations.Concat) -> None: self.generic_visit(operation) self.print_op_id(operation) @@ -268,6 +276,14 @@ def visit_Pad(self, operation: operations.Pad) -> None: self.print_op_id(operation) print(f"Pad({self.get_op_id(operation.x)}, pads={operation.pads})") + def visit_ReduceL2(self, operation: operations.ReduceL2) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print( + "ReduceL2(%s, axes=%s, keepdims=%s)" + % (self.get_op_id(operation.x), operation.axes, operation.keepdims) + ) + def visit_Relu(self, operation: operations.Relu) -> None: self.generic_visit(operation) self.print_op_id(operation) @@ -288,8 +304,6 @@ def visit_Resize(self, operation: operations.Resize) -> None: inputs.append(f"roi={operation.roi.tolist()}") if operation.scales.size > 0: inputs.append(f"scales={operation.scales.tolist()}") - # if operation.sizes.size > 0: - # inputs.append(f"sizes={operation.sizes.tolist()}") if operation.sizes.size > 0: inputs.append(f"sizes={operation.sizes.tolist()}") inputs_str = ", ".join(inputs) @@ -314,6 +328,11 @@ def visit_Shape(self, operation: operations.Shape) -> None: self.print_op_id(operation) print(f"Shape({self.get_op_id(operation.x)})") + def visit_Sign(self, operation: operations.Sign) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print(f"Sign({self.get_op_id(operation.x)})") + def visit_Sigmoid(self, operation: operations.Sigmoid) -> None: self.generic_visit(operation) self.print_op_id(operation) @@ -341,11 +360,6 @@ def visit_Slice(self, operation: operations.Slice) -> None: ")" ) - def visit_Sign(self, operation: operations.Sign) -> None: - self.generic_visit(operation) - self.print_op_id(operation) - print(f"Sign({self.get_op_id(operation.x)})") - def visit_Softmax(self, operation: operations.Softmax) -> None: self.generic_visit(operation) self.print_op_id(operation) @@ -359,6 +373,11 @@ def visit_Split(self, operation: operations.Sub) -> None: % (self.get_op_id(operation.x), operation.axis, operation.split) ) + def visit_Squeeze(self, operation: operations.Squeeze) -> None: + self.generic_visit(operation) + self.print_op_id(operation) + print("Squeeze(%s, axes=%s)" % (self.get_op_id(operation.x), operation.axes)) + def visit_Sub(self, operation: operations.Sub) -> None: self.generic_visit(operation) self.print_op_id(operation) @@ -387,62 +406,14 @@ def visit_Unsqueeze(self, operation: operations.Unsqueeze) -> None: self.print_op_id(operation) print(f"Unsqueeze({self.get_op_id(operation.x)}, axes={operation.axes})") - def visit_Split(self, operation: operations.Split) -> None: - self.generic_visit(operation) - self.print_op_id(operation) - print( - "Split(%s, axis=%s, split=%s)" - % (self.get_op_id(operation.x), operation.axis, operation.split) - ) - - def visit_ReduceL2(self, operation: operations.ReduceL2) -> None: - self.generic_visit(operation) - self.print_op_id(operation) - print( - "ReduceL2(%s, axes=%s, keepdims=%s)" - % (self.get_op_id(operation.x), operation.axes, operation.keepdims) - ) - - def visit_Clip(self, operation: operations.Clip) -> None: - self.generic_visit(operation) - self.print_op_id(operation) - print( - "Clip(%s, min=%s, max=%s)" - % (self.get_op_id(operation.x), operation.min, operation.max) - ) - - def visit_Squeeze(self, operation: operations.Squeeze) -> None: - self.generic_visit(operation) - self.print_op_id(operation) - print("Squeeze(%s, axes=%s)" % (self.get_op_id(operation.x), operation.axes)) - def visit_Upsample(self, operation: operations.Upsample) -> None: self.generic_visit(operation) self.print_op_id(operation) - print("Upsample(%s, scales=%s)" % (self.get_op_id(operation.x), operation.scales)) - - def visit_Slice(self, operation: operations.Slice) -> None: - self.generic_visit(operation) - self.print_op_id(operation) - axes = ( - f", axes={self.get_op_id(operation.axes)}" - if operation.axes is not None - else "" - ) - steps = ( - f", steps={self.get_op_id(operation.steps)}" - if operation.steps is not None - else "" - ) print( - "Slice(" - f"{self.get_op_id(operation.x)}, " - f"{self.get_op_id(operation.starts)}, " - f"{self.get_op_id(operation.ends)}" - f"{axes}{steps}" - ")" + "Upsample(%s, scales=%s)" % (self.get_op_id(operation.x), operation.scales) ) + __all__ = [ "OperationVisitor", "GetInputDetails", From cb70aa92f896ce206c506348482bed4de7fd26de Mon Sep 17 00:00:00 2001 From: Felipe Date: Thu, 30 Jun 2022 16:33:31 -0400 Subject: [PATCH 11/18] Added onnx operations and tests --- dnnv/nn/converters/onnx.py | 86 +++++++++++- dnnv/nn/converters/tensorflow.py | 3 + dnnv/nn/operations/tensor.py | 38 +++--- .../test_converters/test_onnx/test_Clip.py | 105 ++++++++++++++ .../test_onnx/test_ReduceL2.py | 128 ++++++++++++++++++ .../test_converters/test_onnx/test_Squeeze.py | 27 ++++ .../test_onnx/test_Upsample.py | 40 ++++++ .../test_tensorflow/test_Clip.py | 104 ++++++++++++++ .../test_tensorflow/test_ReduceL2.py | 123 +++++++++++++++++ .../test_tensorflow/test_Squeeze.py | 26 ++++ .../test_tensorflow/test_Upsample.py | 37 +++++ .../test_visitors/test_PrintVisitor.py | 58 ++++++++ 12 files changed, 756 insertions(+), 19 deletions(-) create mode 100644 tests/unit_tests/test_nn/test_converters/test_onnx/test_Clip.py create mode 100644 tests/unit_tests/test_nn/test_converters/test_onnx/test_ReduceL2.py create mode 100644 tests/unit_tests/test_nn/test_converters/test_onnx/test_Squeeze.py create mode 100644 tests/unit_tests/test_nn/test_converters/test_onnx/test_Upsample.py create mode 100644 tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Clip.py create mode 100644 tests/unit_tests/test_nn/test_converters/test_tensorflow/test_ReduceL2.py create mode 100644 tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Squeeze.py create mode 100644 tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Upsample.py diff --git a/dnnv/nn/converters/onnx.py b/dnnv/nn/converters/onnx.py index 39d73c53..48f4c9e5 100644 --- a/dnnv/nn/converters/onnx.py +++ b/dnnv/nn/converters/onnx.py @@ -11,6 +11,10 @@ from ..visitors import OperationVisitor +class OnnxConverterError(Exception): + pass + + def convert(op_graph: OperationGraph, *, add_missing_optional_inputs=False): converter = OnnxConverter( op_graph, add_missing_optional_inputs=add_missing_optional_inputs @@ -79,7 +83,7 @@ def _to_onnx_proto( ) -> Union[onnx.NodeProto, onnx.TensorProto, onnx.ValueInfoProto]: if isinstance(value, Operation): return self.visit(value) - if isinstance(value, np.ndarray): + if isinstance(value, (np.ndarray, np.number)): tensor_proto = onnx.numpy_helper.from_array(value, name=opname) self.initializer.append(tensor_proto) return tensor_proto @@ -184,6 +188,29 @@ def visit_Cast(self, operation: operations.Cast) -> onnx.NodeProto: return node + def visit_Clip(self, operation: operations.Clip) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + inputs = [x.name] + if operation.min is not None: + min = self._to_onnx_proto(operation.min, f"{opname}.min") + inputs.extend([min.name]) + if operation.max is not None: + max = self._to_onnx_proto(operation.max, f"{opname}.max") + inputs.extend([max.name]) + + node = onnx.helper.make_node( + op_type, + inputs=inputs, + outputs=[opname], + name=opname, + ) + + return node + def visit_Concat(self, operation: operations.Concat) -> onnx.NodeProto: idx = self.op_counts["Concat"] = self.op_counts["Concat"] + 1 opname = f"Concat_{idx}" @@ -508,6 +535,26 @@ def visit_OutputSelect(self, operation: operations.OutputSelect) -> onnx.NodePro return node + def visit_ReduceL2(self, operation: operations.ReduceL2) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + axes = operation.axes + keepdims = operation.keepdims + + node = onnx.helper.make_node( + op_type, + inputs=[x.name], + axes=axes, + keepdims=keepdims, + outputs=[opname], + name=opname, + ) + + return node + def visit_Relu(self, operation: operations.Relu) -> onnx.NodeProto: idx = self.op_counts["Relu"] = self.op_counts["Relu"] + 1 opname = f"Relu_{idx}" @@ -576,6 +623,25 @@ def visit_Split(self, operation: operations.Split) -> onnx.NodeProto: return node + def visit_Squeeze(self, operation: operations.Squeeze) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + inputs = [x.name] + if operation.axes is not None: + axes = self._to_onnx_proto(operation.axes, f"{opname}.axes") + if axes.data_type != onnx.TensorProto.INT64: + raise OnnxConverterError("Squeeze axes should be int64.") + inputs.extend([axes.name]) + + node = onnx.helper.make_node( + op_type, inputs=inputs, outputs=[opname], name=opname + ) + + return node + def visit_Slice(self, operation: operations.Slice) -> onnx.NodeProto: op_type = str(operation) idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 @@ -648,3 +714,21 @@ def visit_Transpose(self, operation: operations.Transpose) -> onnx.NodeProto: ) return node + + def visit_Upsample(self, operation: operations.Upsample) -> onnx.NodeProto: + op_type = str(operation) + idx = self.op_counts[op_type] = self.op_counts[op_type] + 1 + opname = f"{op_type}_{idx}" + + x = self._to_onnx_proto(operation.x, f"{opname}.x") + inputs = [x.name] + if operation.scales is not None: + scales = self._to_onnx_proto(operation.scales, f"{opname}.scales") + inputs.extend([scales.name]) + mode = operation.mode + + node = onnx.helper.make_node( + op_type, inputs=inputs, outputs=[opname], name=opname, mode=mode + ) + + return node diff --git a/dnnv/nn/converters/tensorflow.py b/dnnv/nn/converters/tensorflow.py index 44139db3..551fb447 100644 --- a/dnnv/nn/converters/tensorflow.py +++ b/dnnv/nn/converters/tensorflow.py @@ -916,6 +916,8 @@ def visit_Squeeze(self, operation): if isinstance(x_, Operation): x_ = self.visit(x_) axes = operation.axes + if isinstance(axes, np.ndarray): + axes = axes.tolist() @self._cached def squeeze_func(*inputs): @@ -1006,6 +1008,7 @@ def visit_Upsample(self, operation): if isinstance(x_, Operation): x_ = self.visit(x_) scales = operation.scales + mode = operation.mode @self._cached def upsample_func(*inputs): diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index fd61ab86..4aaf829e 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -19,6 +19,21 @@ def from_onnx(cls, onnx_node, *inputs): return cls(*inputs, to=to, name=onnx_node.name) +class Clip(Operation): + def __init__(self, x, min, max, *, name: Optional[str] = None): + super().__init__(name=name) + self.x = x + self.min = min if min is not None else -np.inf + self.max = max if max is not None else +np.inf + + @classmethod + def from_onnx(cls, onnx_node, *inputs): + attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} + _min = attributes.get("min") + _max = attributes.get("max") + return cls(*inputs, min=_min, max=_max, name=onnx_node.name) + + class Concat(Operation): def __init__(self, x, axis, *, name: Optional[str] = None): super().__init__(name=name) @@ -262,7 +277,9 @@ class ReduceL2(Operation): def __init__(self, x, axes, keepdims, *, name: Optional[str] = None): super().__init__(name=name) self.x = x - if isinstance(axes, int): + if axes is None: + self.axes = None + elif isinstance(axes, int): self.axes = axes elif len(axes) == 1: self.axes = int(axes[0]) @@ -278,21 +295,6 @@ def from_onnx(cls, onnx_node, *inputs): return cls(*inputs, axes=axes, keepdims=keepdims, name=onnx_node.name) -class Clip(Operation): - def __init__(self, x, min, max, *, name: Optional[str] = None): - super().__init__(name=name) - self.x = x - self.min = min if min is not None else -np.inf - self.max = max if max is not None else +np.inf - - @classmethod - def from_onnx(cls, onnx_node, *inputs): - attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} - _min = attributes.get("min") - _max = attributes.get("max") - return cls(*inputs, min=_min, max=_max, name=onnx_node.name) - - class Squeeze(Operation): def __init__(self, x, axes, *, name: Optional[str] = None): super().__init__(name=name) @@ -317,9 +319,9 @@ def __init__(self, x, scales, mode, *, name: Optional[str] = None): def from_onnx(cls, onnx_node, *inputs): attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} mode = attributes.get("mode") - # scales = attributes.get("scales") return cls(*inputs, mode=mode, name=onnx_node.name) + __all__ = [ "Cast", "Concat", @@ -339,5 +341,5 @@ def from_onnx(cls, onnx_node, *inputs): "ReduceL2", "Clip", "Squeeze", - "Upsample" + "Upsample", ] diff --git a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Clip.py b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Clip.py new file mode 100644 index 00000000..4ad67fa1 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Clip.py @@ -0,0 +1,105 @@ +import numpy as np +import onnxruntime.backend + +from dnnv.nn.converters.onnx import * +from dnnv.nn.operations import * + + +def test_Clip(): + x = np.array([-2, 0, 2]).astype(np.float32) + min_val = np.float32(-1) + max_val = np.float32(1) + y = np.clip(x, min_val, max_val) # expected output [-1., 0., 1.] + + op = Clip(x, min=min_val, max=max_val) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, min_val, max_val]) + assert np.all(output == y) + + x = np.random.randn(3, 4, 5).astype(np.float32) + y = np.clip(x, min_val, max_val) + + op = Clip(x, min=min_val, max=max_val) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, min_val, max_val]) + assert np.all(output == y) + + min_val = np.float32(-5) + max_val = np.float32(5) + x = np.array([-1, 0, 1]).astype(np.float32) + y = np.array([-1, 0, 1]).astype(np.float32) + + op = Clip(x, min=min_val, max=max_val) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, min_val, max_val]) + assert np.all(output == y) + + x = np.array([-6, 0, 6]).astype(np.float32) + y = np.array([-5, 0, 5]).astype(np.float32) + + op = Clip(x, min=min_val, max=max_val) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, min_val, max_val]) + assert np.all(output == y) + + x = np.array([-1, 0, 6]).astype(np.float32) + y = np.array([-1, 0, 5]).astype(np.float32) + + op = Clip(x, min=min_val, max=max_val) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, min_val, max_val]) + assert np.all(output == y) + + +def test_Clip_Default(): + min_val = np.float32(0) + max_val = np.inf + x = np.random.randn(3, 4, 5).astype(np.float32) + y = np.clip(x, min_val, max_val) + + op = Clip(x, min=min_val, max=max_val) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, min_val, max_val]) + assert np.all(output == y) + + min_val = -np.inf + max_val = np.float32(0) + x = np.random.randn(3, 4, 5).astype(np.float32) + y = np.clip(x, min_val, max_val) + + op = Clip(x, min=min_val, max=max_val) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, min_val, max_val]) + assert np.all(output == y) + + min_val = None + max_val = None + x = np.array([-1, 0, 1]).astype(np.float32) + y = np.array([-1, 0, 1]).astype(np.float32) + + op = Clip(x, min=min_val, max=max_val) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, min_val, max_val]) + assert np.all(output == y) + + +def test_Clip_Default_int8(): + min_val = np.int8(0) + max_val = np.int8(np.iinfo(np.int8).max) + x = np.random.randn(3, 4, 5).astype(np.int8) + y = np.clip(x, min_val, max_val) + + op = Clip(x, min=min_val, max=max_val) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, min_val, max_val]) + assert np.all(output == y) + + min_val = np.int8(np.iinfo(np.int8).min) + max_val = np.int8(0) + x = np.random.randn(3, 4, 5).astype(np.int8) + y = np.clip(x, min_val, max_val) + + op = Clip(x, min=min_val, max=max_val) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, min_val, max_val]) + assert np.all(output == y) diff --git a/tests/unit_tests/test_nn/test_converters/test_onnx/test_ReduceL2.py b/tests/unit_tests/test_nn/test_converters/test_onnx/test_ReduceL2.py new file mode 100644 index 00000000..61fb2910 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_onnx/test_ReduceL2.py @@ -0,0 +1,128 @@ +import numpy as np +import onnxruntime.backend +import pytest + +from dnnv.nn.converters.onnx import * +from dnnv.nn.operations import * + + +def test_ReduceL2(): + shape = [3, 2, 2] + axes = None + keepdims = 1 + + data = np.reshape(np.arange(1, np.prod(shape) + 1, dtype=np.float32), shape) + # [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]], [[9., 10.], [11., 12.]]] + reduced = np.sqrt(np.sum(a=np.square(data), axis=axes, keepdims=keepdims == 1)) + # [[[25.49509757]]] + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [data]) + assert np.all(output == reduced) + + np.random.seed(0) + data = np.random.uniform(-10, 10, shape).astype(np.float32) + reduced = np.sqrt(np.sum(a=np.square(data), axis=axes, keepdims=keepdims == 1)) + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [data]) + assert np.all(output == reduced) + + +@pytest.mark.xfail +def test_ReduceL2_do_not_keep_dims(): + shape = [3, 2, 2] + axes = [2] + keepdims = 0 + + data = np.reshape(np.arange(1, np.prod(shape) + 1, dtype=np.float32), shape) + # [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]], [[9., 10.], [11., 12.]]] + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + # [[2.23606798, 5.], + # [7.81024968, 10.63014581], + # [13.45362405, 16.2788206]] + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [data]) + assert np.all(output == reduced) + + np.random.seed(0) + data = np.random.uniform(-10, 10, shape).astype(np.float32) + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [data]) + assert np.all(output == reduced) + + +@pytest.mark.xfail +def test_ReduceL2_keep_dims(): + shape = [3, 2, 2] + axes = [2] + keepdims = 1 + + data = np.reshape(np.arange(1, np.prod(shape) + 1, dtype=np.float32), shape) + # print(data) + # [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]], [[9., 10.], [11., 12.]]] + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + # [[[2.23606798], [5.]] + # [[7.81024968], [10.63014581]] + # [[13.45362405], [16.2788206 ]]] + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [data]) + assert np.all(output == reduced) + + np.random.seed(0) + data = np.random.uniform(-10, 10, shape).astype(np.float32) + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [data]) + assert np.all(output == reduced) + + +@pytest.mark.xfail +def test_ReduceL2_negative_axes_keepdims(): + shape = [3, 2, 2] + axes = [-1] + keepdims = 1 + + data = np.reshape(np.arange(1, np.prod(shape) + 1, dtype=np.float32), shape) + # [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]], [[9., 10.], [11., 12.]]] + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + # [[[2.23606798], [5.]] + # [[7.81024968], [10.63014581]] + # [[13.45362405], [16.2788206 ]]] + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [data]) + assert np.all(output == reduced) + + np.random.seed(0) + data = np.random.uniform(-10, 10, shape).astype(np.float32) + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [data]) + assert np.all(output == reduced) diff --git a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Squeeze.py b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Squeeze.py new file mode 100644 index 00000000..85eea5d7 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Squeeze.py @@ -0,0 +1,27 @@ +import numpy as np +import onnxruntime.backend + +from dnnv.nn.converters.onnx import * +from dnnv.nn.operations import * + + +def test_Squeeze(): + x = np.random.randn(1, 3, 4, 5).astype(np.float32) + axes = np.array([0], dtype=np.int64) + y = np.squeeze(x, axis=0) + + op = Squeeze(x, axes=axes) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, []) + assert np.all(output == y) + + +def test_Squeeze_negative_axes(): + x = np.random.randn(1, 3, 1, 5).astype(np.float32) + axes = np.array([-2], dtype=np.int64) + y = np.squeeze(x, axis=-2) + + op = Squeeze(x, axes=axes) + onnx_model = convert(OperationGraph([op])) + output = onnxruntime.backend.run(onnx_model, [x, axes]) + assert np.all(output == y) diff --git a/tests/unit_tests/test_nn/test_converters/test_onnx/test_Upsample.py b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Upsample.py new file mode 100644 index 00000000..333a5353 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_onnx/test_Upsample.py @@ -0,0 +1,40 @@ +import numpy as np +import onnxruntime.backend +import pytest + +from dnnv.nn.converters.onnx import * +from dnnv.nn.operations import * + + +@pytest.mark.xfail +def test_Upsample(): + data = np.array( + [ + [ + [ + [1, 2], + [3, 4], + ] + ] + ], + dtype=np.float32, + ) + scales = np.array([1.0, 1.0, 2.0, 3.0], dtype=np.float32) + output = np.array( + [ + [ + [ + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [3, 3, 3, 4, 4, 4], + [3, 3, 3, 4, 4, 4], + ] + ] + ], + dtype=np.float32, + ) + + op = Upsample(data, scales=scales, mode="nearest") + onnx_model = convert(OperationGraph([op])) + result = onnxruntime.backend.run(onnx_model, [data, scales], mode="nearest") + assert np.all(result == output) diff --git a/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Clip.py b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Clip.py new file mode 100644 index 00000000..2f9cdc06 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Clip.py @@ -0,0 +1,104 @@ +import numpy as np + +from dnnv.nn.converters.tensorflow import * +from dnnv.nn.operations import * + + +def test_Clip(): + x = np.array([-2, 0, 2]).astype(np.float32) + min_val = np.float32(-1) + max_val = np.float32(1) + y = np.clip(x, min_val, max_val) # expected output [-1., 0., 1.] + + op = Clip(x, min=min_val, max=max_val) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) + + x = np.random.randn(3, 4, 5).astype(np.float32) + y = np.clip(x, min_val, max_val) + + op = Clip(x, min=min_val, max=max_val) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) + + min_val = np.float32(-5) + max_val = np.float32(5) + x = np.array([-1, 0, 1]).astype(np.float32) + y = np.array([-1, 0, 1]).astype(np.float32) + + op = Clip(x, min=min_val, max=max_val) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) + + x = np.array([-6, 0, 6]).astype(np.float32) + y = np.array([-5, 0, 5]).astype(np.float32) + + op = Clip(x, min=min_val, max=max_val) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) + + x = np.array([-1, 0, 6]).astype(np.float32) + y = np.array([-1, 0, 5]).astype(np.float32) + + op = Clip(x, min=min_val, max=max_val) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) + + +def test_Clip_Default(): + min_val = np.float32(0) + max_val = np.inf + x = np.random.randn(3, 4, 5).astype(np.float32) + y = np.clip(x, min_val, max_val) + + op = Clip(x, min=min_val, max=max_val) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) + + min_val = -np.inf + max_val = np.float32(0) + x = np.random.randn(3, 4, 5).astype(np.float32) + y = np.clip(x, min_val, max_val) + + op = Clip(x, min=min_val, max=max_val) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) + + min_val = None + max_val = None + x = np.array([-1, 0, 1]).astype(np.float32) + y = np.array([-1, 0, 1]).astype(np.float32) + + op = Clip(x, min=min_val, max=max_val) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) + + +def test_Clip_Default_int8(): + min_val = np.int8(0) + max_val = np.iinfo(np.int8).max + x = np.random.randn(3, 4, 5).astype(np.int8) + y = np.clip(x, min_val, max_val) + + op = Clip(x, min=min_val, max=max_val) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) + + min_val = np.iinfo(np.int8).min + max_val = np.int8(0) + x = np.random.randn(3, 4, 5).astype(np.int8) + y = np.clip(x, min_val, max_val) + + op = Clip(x, min=min_val, max=max_val) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) diff --git a/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_ReduceL2.py b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_ReduceL2.py new file mode 100644 index 00000000..72c13672 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_ReduceL2.py @@ -0,0 +1,123 @@ +import numpy as np + +from dnnv.nn.converters.tensorflow import * +from dnnv.nn.operations import * + + +def test_ReduceL2(): + shape = [3, 2, 2] + axes = None + keepdims = 1 + + data = np.reshape(np.arange(1, np.prod(shape) + 1, dtype=np.float32), shape) + # [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]], [[9., 10.], [11., 12.]]] + reduced = np.sqrt(np.sum(a=np.square(data), axis=axes, keepdims=keepdims == 1)) + # [[[25.49509757]]] + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == reduced) + + np.random.seed(0) + data = np.random.uniform(-10, 10, shape).astype(np.float32) + reduced = np.sqrt(np.sum(a=np.square(data), axis=axes, keepdims=keepdims == 1)) + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == reduced) + + +def test_ReduceL2_do_not_keep_dims(): + shape = [3, 2, 2] + axes = [2] + keepdims = 0 + + data = np.reshape(np.arange(1, np.prod(shape) + 1, dtype=np.float32), shape) + # [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]], [[9., 10.], [11., 12.]]] + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + # [[2.23606798, 5.], + # [7.81024968, 10.63014581], + # [13.45362405, 16.2788206]] + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == reduced) + + np.random.seed(0) + data = np.random.uniform(-10, 10, shape).astype(np.float32) + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == reduced) + + +def test_ReduceL2_keep_dims(): + shape = [3, 2, 2] + axes = [2] + keepdims = 1 + + data = np.reshape(np.arange(1, np.prod(shape) + 1, dtype=np.float32), shape) + # print(data) + # [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]], [[9., 10.], [11., 12.]]] + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + # [[[2.23606798], [5.]] + # [[7.81024968], [10.63014581]] + # [[13.45362405], [16.2788206 ]]] + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == reduced) + + np.random.seed(0) + data = np.random.uniform(-10, 10, shape).astype(np.float32) + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == reduced) + + +def test_ReduceL2_negative_axes_keepdims(): + shape = [3, 2, 2] + axes = [-1] + keepdims = 1 + + data = np.reshape(np.arange(1, np.prod(shape) + 1, dtype=np.float32), shape) + # [[[1., 2.], [3., 4.]], [[5., 6.], [7., 8.]], [[9., 10.], [11., 12.]]] + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + # [[[2.23606798], [5.]] + # [[7.81024968], [10.63014581]] + # [[13.45362405], [16.2788206 ]]] + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == reduced) + + np.random.seed(0) + data = np.random.uniform(-10, 10, shape).astype(np.float32) + reduced = np.sqrt( + np.sum(a=np.square(data), axis=tuple(axes), keepdims=keepdims == 1) + ) + + op = ReduceL2(data, axes=axes, keepdims=keepdims) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == reduced) diff --git a/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Squeeze.py b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Squeeze.py new file mode 100644 index 00000000..a9bd097f --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Squeeze.py @@ -0,0 +1,26 @@ +import numpy as np + +from dnnv.nn.converters.tensorflow import * +from dnnv.nn.operations import * + + +def test_Squeeze(): + x = np.random.randn(1, 3, 4, 5).astype(np.float32) + axes = 0 + y = np.squeeze(x, axis=axes) + + op = Squeeze(x, axes=axes) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) + + +def test_Squeeze_negative_axes(): + x = np.random.randn(1, 3, 1, 5).astype(np.float32) + axes = -2 + y = np.squeeze(x, axis=axes) + + op = Squeeze(x, axes=axes) + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == y) diff --git a/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Upsample.py b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Upsample.py new file mode 100644 index 00000000..5a43c1f6 --- /dev/null +++ b/tests/unit_tests/test_nn/test_converters/test_tensorflow/test_Upsample.py @@ -0,0 +1,37 @@ +import numpy as np + +from dnnv.nn.converters.tensorflow import * +from dnnv.nn.operations import * + + +def test_Upsample(): + data = np.array( + [ + [ + [ + [1, 2], + [3, 4], + ] + ] + ], + dtype=np.float32, + ) + scales = np.array([1.0, 1.0, 2.0, 3.0], dtype=np.float32) + output = np.array( + [ + [ + [ + [1, 1, 1, 2, 2, 2], + [1, 1, 1, 2, 2, 2], + [3, 3, 3, 4, 4, 4], + [3, 3, 3, 4, 4, 4], + ] + ] + ], + dtype=np.float32, + ) + + op = Upsample(data, scales=scales, mode="nearest") + tf_op = TensorflowConverter().visit(op) + result = tf_op().numpy() + assert np.all(result == output) diff --git a/tests/unit_tests/test_nn/test_visitors/test_PrintVisitor.py b/tests/unit_tests/test_nn/test_visitors/test_PrintVisitor.py index e6abc9c2..6afedf22 100644 --- a/tests/unit_tests/test_nn/test_visitors/test_PrintVisitor.py +++ b/tests/unit_tests/test_nn/test_visitors/test_PrintVisitor.py @@ -94,6 +94,18 @@ def test_Cast(capsys): assert captured.out == expected_output +def test_Clip(capsys): + input_op = Input((1, 5), np.dtype(np.float32)) + clip_op = Clip(input_op, min=0, max=1) + PrintVisitor().visit(clip_op) + captured = capsys.readouterr() + expected_output = """\ +Input_0 : Input((1, 5), dtype=float32) +Clip_0 : Clip(Input_0, min=0, max=1) +""" + assert captured.out == expected_output + + def test_Concat(capsys): input_op = Input((1, 5), np.dtype(np.float32)) concat_op = Concat([input_op, input_op], axis=1) @@ -431,6 +443,28 @@ def test_Pad(capsys): assert captured.out == expected_output +def test_ReduceL2(capsys): + input_op = Input((1, 5), np.dtype(np.float32)) + reduceL2_op = ReduceL2(input_op, axes=[1], keepdims=1) + PrintVisitor().visit(reduceL2_op) + captured = capsys.readouterr() + expected_output = """\ +Input_0 : Input((1, 5), dtype=float32) +ReduceL2_0 : ReduceL2(Input_0, axes=1, keepdims=1) +""" + assert captured.out == expected_output + + input_op = Input((1, 5), np.dtype(np.float32)) + reduceL2_op = ReduceL2(input_op, axes=[1, 2], keepdims=1) + PrintVisitor().visit(reduceL2_op) + captured = capsys.readouterr() + expected_output = """\ +Input_0 : Input((1, 5), dtype=float32) +ReduceL2_0 : ReduceL2(Input_0, axes=(1, 2), keepdims=1) +""" + assert captured.out == expected_output + + def test_Relu(capsys): input_op = Input((1, 5), np.dtype(np.float32)) relu_op = Relu(input_op) @@ -601,6 +635,30 @@ def test_Softmax(capsys): assert captured.out == expected_output +def test_Squeeze(capsys): + input_op = Input((1, 5), np.dtype(np.float32)) + squeeze_op = Squeeze(input_op, axes=0) + PrintVisitor().visit(squeeze_op) + captured = capsys.readouterr() + expected_output = """\ +Input_0 : Input((1, 5), dtype=float32) +Squeeze_0 : Squeeze(Input_0, axes=0) +""" + assert captured.out == expected_output + + +def test_Upsample(capsys): + input_op = Input((1, 5), np.dtype(np.float32)) + upsample_op = Upsample(input_op, scales=2, mode="nearest") + PrintVisitor().visit(upsample_op) + captured = capsys.readouterr() + expected_output = """\ +Input_0 : Input((1, 5), dtype=float32) +Upsample_0 : Upsample(Input_0, scales=2) +""" + assert captured.out == expected_output + + def test_Sub(capsys): input_op = Input((1, 5), np.dtype(np.float32)) sub_op = Sub(input_op, np.float32(2)) From 965383eac07df56fcf9d5b57fc2efe845bf187e1 Mon Sep 17 00:00:00 2001 From: Felipe Date: Thu, 7 Jul 2022 11:00:44 -0400 Subject: [PATCH 12/18] Added protobuf version in verinet installation --- dnnv/_manage/linux/verifiers/verinet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dnnv/_manage/linux/verifiers/verinet.py b/dnnv/_manage/linux/verifiers/verinet.py index fc305167..251f5287 100644 --- a/dnnv/_manage/linux/verifiers/verinet.py +++ b/dnnv/_manage/linux/verifiers/verinet.py @@ -123,6 +123,7 @@ def run(self, env: Environment, dependency: Dependency): "pip install" ' "numba>=0.50,<0.60"' ' "onnx>=1.8,<1.11"' + ' "protobuf<=3.20"' ' "torch>=1.8,<1.9"' ' "torchvision>=0.9,<0.10"' ), From df3ae4330190d3f0533b97751313749f9751af5b Mon Sep 17 00:00:00 2001 From: Felipe Toledo Date: Thu, 7 Jul 2022 12:51:53 -0400 Subject: [PATCH 13/18] Update dnnv/nn/operations/tensor.py Co-authored-by: David Shriver --- dnnv/nn/operations/tensor.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index 4aaf829e..c4c361c2 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -277,14 +277,7 @@ class ReduceL2(Operation): def __init__(self, x, axes, keepdims, *, name: Optional[str] = None): super().__init__(name=name) self.x = x - if axes is None: - self.axes = None - elif isinstance(axes, int): - self.axes = axes - elif len(axes) == 1: - self.axes = int(axes[0]) - elif len(axes) > 1: - self.axes = tuple(axes) + self.axes = axes self.keepdims = keepdims @classmethod From a17c656daaac6914d8b558485207a9e45095482d Mon Sep 17 00:00:00 2001 From: Felipe Toledo Date: Thu, 7 Jul 2022 12:52:09 -0400 Subject: [PATCH 14/18] Update dnnv/nn/operations/tensor.py Co-authored-by: David Shriver --- dnnv/nn/operations/tensor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index c4c361c2..b77f74ba 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -29,9 +29,11 @@ def __init__(self, x, min, max, *, name: Optional[str] = None): @classmethod def from_onnx(cls, onnx_node, *inputs): attributes = {a.name: as_numpy(a) for a in onnx_node.attribute} - _min = attributes.get("min") - _max = attributes.get("max") - return cls(*inputs, min=_min, max=_max, name=onnx_node.name) + if len(inputs) == 1: + _min = attributes.get("min") + _max = attributes.get("max") + return cls(*inputs, min=_min, max=_max, name=onnx_node.name) + return cls(*inputs, name=onnx_node.name) class Concat(Operation): From d5ddd004a9fd24b2683c19ba20c3225273efeb89 Mon Sep 17 00:00:00 2001 From: Felipe Toledo Date: Thu, 7 Jul 2022 12:52:18 -0400 Subject: [PATCH 15/18] Update dnnv/nn/operations/tensor.py Co-authored-by: David Shriver --- dnnv/nn/operations/tensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index b77f74ba..fcf1dca7 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -20,7 +20,7 @@ def from_onnx(cls, onnx_node, *inputs): class Clip(Operation): - def __init__(self, x, min, max, *, name: Optional[str] = None): + def __init__(self, x, min=None, max=None, *, name: Optional[str] = None): super().__init__(name=name) self.x = x self.min = min if min is not None else -np.inf From 2603863d35853238647a02e0fcf9650e6234146b Mon Sep 17 00:00:00 2001 From: Felipe Toledo Date: Thu, 7 Jul 2022 12:52:27 -0400 Subject: [PATCH 16/18] Update dnnv/nn/operations/tensor.py Co-authored-by: David Shriver --- dnnv/nn/operations/tensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index fcf1dca7..6768f26c 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -23,8 +23,8 @@ class Clip(Operation): def __init__(self, x, min=None, max=None, *, name: Optional[str] = None): super().__init__(name=name) self.x = x - self.min = min if min is not None else -np.inf - self.max = max if max is not None else +np.inf + self.min = min + self.max = max @classmethod def from_onnx(cls, onnx_node, *inputs): From d42d28bb08abdf74dc9c4ac9da1f9a4680fe47fc Mon Sep 17 00:00:00 2001 From: Felipe Toledo Date: Thu, 7 Jul 2022 12:52:42 -0400 Subject: [PATCH 17/18] Update dnnv/nn/operations/tensor.py Co-authored-by: David Shriver --- dnnv/nn/operations/tensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dnnv/nn/operations/tensor.py b/dnnv/nn/operations/tensor.py index 6768f26c..f87d7ae6 100644 --- a/dnnv/nn/operations/tensor.py +++ b/dnnv/nn/operations/tensor.py @@ -147,8 +147,6 @@ def __init__( name: Optional[str] = None ): super().__init__(name=name) - # assert scales.size != 0 or sizes.size != 0 - # assert scales.size == 0 or sizes.size == 0 assert scales.size != 0 or sizes.size != 0 assert scales.size == 0 or sizes.size == 0 self.x = x From a88191e69d46aff7168377f98e6f75fd74299c17 Mon Sep 17 00:00:00 2001 From: Felipe Date: Tue, 6 Sep 2022 08:17:44 -0400 Subject: [PATCH 18/18] Updated nnenum installation --- dnnv/_manage/linux/verifiers/nnenum.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/dnnv/_manage/linux/verifiers/nnenum.py b/dnnv/_manage/linux/verifiers/nnenum.py index 9e868876..fae14f5a 100644 --- a/dnnv/_manage/linux/verifiers/nnenum.py +++ b/dnnv/_manage/linux/verifiers/nnenum.py @@ -105,14 +105,15 @@ def run(self, env: Environment, dependency: Dependency): "pip install --upgrade pip", ( "pip install" - ' "numpy>=1.19,<1.22"' - ' "onnx>=1.8,<1.11"' - ' "onnxruntime>=1.7,<1.11"' - ' "scipy>=1.4.1<1.8"' - ' "threadpoolctl==2.1.0"' - ' "skl2onnx==1.7.0"' - ' "swiglpk"' - ' "termcolor"' + ' "numpy>=1.21,<1.24"' + ' "onnx>=1.10,<1.12"' + ' "onnxruntime>=1.10,<1.13"' + ' "scipy>=1.7<1.10"' + ' "threadpoolctl==3.1.0"' + ' "skl2onnx==1.12"' + ' "swiglpk~=5.0"' + ' "termcolor==1.1.0"' + ' "packaging==21.3"' ' "protobuf<=3.20"' ), f"cd {cache_dir}",