From 46fcb2885a61169149ee13e42670617278858426 Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Sat, 11 Nov 2023 01:29:13 +0000 Subject: [PATCH 01/14] Adding support for explicit padding in 2D layers --- src/omlt/io/onnx_parser.py | 22 +++++++++++----------- src/omlt/neuralnet/layer.py | 33 ++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/omlt/io/onnx_parser.py b/src/omlt/io/onnx_parser.py index 511261c0..713db274 100644 --- a/src/omlt/io/onnx_parser.py +++ b/src/omlt/io/onnx_parser.py @@ -368,16 +368,17 @@ def _consume_conv_nodes(self, node, next_nodes): raise ValueError( f"{node} has multiple groups ({attr['group']}). This is not supported." ) - if "pads" in attr and np.any(attr["pads"]): - raise ValueError( - f"{node} has non-zero pads ({attr['pads']}). This is not supported." - ) + if "pads" in attr: + pads = attr["pads"] + else: + pads = None # generate new nodes for the node output - padding = 0 + + padding = [pads[i] + pads[i + len(node.input)] for i in range(len(node.input))] output_size = [out_channels] - for w, k, s in zip(input_output_size[1:], kernel_shape, strides): - new_w = int((w - k + 2 * padding) / s) + 1 + for w, k, s, p in zip(input_output_size[1:], kernel_shape, strides, padding): + new_w = int((w - k + p) / s) + 1 output_size.append(new_w) activation = "linear" @@ -401,6 +402,7 @@ def _consume_conv_nodes(self, node, next_nodes): output_size, strides, weights, + pads=pads, activation=activation, input_index_mapper=transformer, ) @@ -467,6 +469,7 @@ def _consume_pool_nodes(self, node, next_nodes): kernel_depth = attr["kernel_shape"][0] kernel_shape = attr["kernel_shape"][1:] strides = attr["strides"] if "strides" in attr else [1] * len(kernel_shape) + pads = attr["pads"] if "pads" in attr else None # check only kernel shape, stride, storage order are set # everything else is not supported @@ -474,10 +477,6 @@ def _consume_pool_nodes(self, node, next_nodes): raise ValueError( f"{node.name} has non-identity dilations ({attr['dilations']}). This is not supported." ) - if "pads" in attr and np.any(attr["pads"]): - raise ValueError( - f"{node.name} has non-zero pads ({attr['pads']}). This is not supported." - ) if ("auto_pad" in attr) and (attr["auto_pad"] != "NOTSET"): raise ValueError( f"{node.name} has autopad set ({attr['auto_pad']}). This is not supported." @@ -519,6 +518,7 @@ def _consume_pool_nodes(self, node, next_nodes): pool_func_name, tuple(kernel_shape), kernel_depth, + pads=pads, activation=activation, input_index_mapper=transformer, ) diff --git a/src/omlt/neuralnet/layer.py b/src/omlt/neuralnet/layer.py index d7f7fa89..956da4bf 100644 --- a/src/omlt/neuralnet/layer.py +++ b/src/omlt/neuralnet/layer.py @@ -225,6 +225,8 @@ class Layer2D(Layer): the size of the output. strides : matrix-like stride of the kernel. + pads : matrix-like + Padding for the kernel. Given as [left, bottom, right, top] activation : str or None activation function name input_index_mapper : IndexMapper or None @@ -237,6 +239,7 @@ def __init__( output_size, strides, *, + pads=None, activation=None, input_index_mapper=None, ): @@ -247,12 +250,21 @@ def __init__( input_index_mapper=input_index_mapper, ) self.__strides = strides + if pads is None: + self.__pads = [0, 0, 0, 0] + else: + self.__pads = pads @property def strides(self): """Return the stride of the layer""" return self.__strides + @property + def pads(self): + """Return the padding of the layer""" + return self.__pads + @property def kernel_shape(self): """Return the shape of the kernel""" @@ -280,12 +292,14 @@ def kernel_index_with_input_indexes(self, out_d, out_r, out_c): kernel_d = self.kernel_depth [kernel_r, kernel_c] = self.kernel_shape [rows_stride, cols_stride] = self.__strides + [pads_row, pads_col] = self.__pads[:1] start_in_d = 0 - start_in_r = out_r * rows_stride - start_in_c = out_c * cols_stride - mapper = lambda x: x - if self.input_index_mapper is not None: - mapper = self.input_index_mapper + start_in_r = out_r * rows_stride - pads_row + start_in_c = out_c * cols_stride - pads_col + # Defined but never used: + # mapper = lambda x: x + # if self.input_index_mapper is not None: + # mapper = self.input_index_mapper for k_d in range(kernel_d): for k_r in range(kernel_r): @@ -299,6 +313,7 @@ def kernel_index_with_input_indexes(self, out_d, out_r, out_c): # even though we loop over ALL kernel indexes. if not all( input_index[i] < self.input_size[i] + and input_index[i] >= 0 for i in range(len(input_index)) ): continue @@ -345,6 +360,8 @@ class PoolingLayer2D(Layer2D): the size of the output. strides : matrix-like stride of the kernel. + pads : matrix-like + Padding for the kernel. Given as [left, bottom, right, top] pool_func : str name of function used to pool values in a kernel to a single value. transpose : bool @@ -367,6 +384,7 @@ def __init__( kernel_shape, kernel_depth, *, + pads=None, activation=None, input_index_mapper=None, ): @@ -374,6 +392,7 @@ def __init__( input_size, output_size, strides, + pads=pads, activation=activation, input_index_mapper=input_index_mapper, ) @@ -421,6 +440,8 @@ class ConvLayer2D(Layer2D): stride of the cross-correlation kernel. kernel : matrix-like the cross-correlation kernel. + pads : matrix-like + Padding for the kernel. Given as [left, bottom, right, top] activation : str or None activation function name input_index_mapper : IndexMapper or None @@ -434,6 +455,7 @@ def __init__( strides, kernel, *, + pads=None, activation=None, input_index_mapper=None, ): @@ -441,6 +463,7 @@ def __init__( input_size, output_size, strides, + pads=pads, activation=activation, input_index_mapper=input_index_mapper, ) From abf7a4dc1c68e1867adef011a75a6968957e72ee Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:19:34 +0000 Subject: [PATCH 02/14] Fixed parsing of padded conv layers. --- src/omlt/io/onnx_parser.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/omlt/io/onnx_parser.py b/src/omlt/io/onnx_parser.py index 713db274..5a15b8b0 100644 --- a/src/omlt/io/onnx_parser.py +++ b/src/omlt/io/onnx_parser.py @@ -371,11 +371,12 @@ def _consume_conv_nodes(self, node, next_nodes): if "pads" in attr: pads = attr["pads"] else: - pads = None + pads = 2*(len(input_output_size)-1)*[0] # generate new nodes for the node output - - padding = [pads[i] + pads[i + len(node.input)] for i in range(len(node.input))] + padding = [ + pads[i] + pads[i + len(input_output_size)-1] + for i in range(len(input_output_size)-1)] output_size = [out_channels] for w, k, s, p in zip(input_output_size[1:], kernel_shape, strides, padding): new_w = int((w - k + p) / s) + 1 From 6d0cf77e9fff49f986401072cbbf9fdcefe4fcfe Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Fri, 17 Nov 2023 00:51:53 +0000 Subject: [PATCH 03/14] Including dilations for 2D layers --- src/omlt/io/onnx_parser.py | 30 ++++++++++-------- src/omlt/neuralnet/layer.py | 61 +++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/omlt/io/onnx_parser.py b/src/omlt/io/onnx_parser.py index 5a15b8b0..3ddb8005 100644 --- a/src/omlt/io/onnx_parser.py +++ b/src/omlt/io/onnx_parser.py @@ -359,24 +359,29 @@ def _consume_conv_nodes(self, node, next_nodes): f"Input/output size ({input_output_size}) first dimension must match input weights channels ({in_channels})." ) + # TODO: need to check pads and dilations also have correct dimensions. Also should + # add support for autopad. + if "pads" in attr: + pads = attr["pads"] + else: + pads = 2*(len(input_output_size)-1)*[0] + + if "dilations" in attr: + dilations = attr["dilations"] + else: + dilations = (len(input_output_size)-1)*[1] + # Other attributes are not supported - if "dilations" in attr and attr["dilations"] != [1, 1]: - raise ValueError( - f"{node} has non-identity dilations ({attr['dilations']}). This is not supported." - ) if attr["group"] != 1: raise ValueError( f"{node} has multiple groups ({attr['group']}). This is not supported." ) - if "pads" in attr: - pads = attr["pads"] - else: - pads = 2*(len(input_output_size)-1)*[0] # generate new nodes for the node output padding = [ pads[i] + pads[i + len(input_output_size)-1] - for i in range(len(input_output_size)-1)] + for i in range(len(input_output_size)-1) + ] output_size = [out_channels] for w, k, s, p in zip(input_output_size[1:], kernel_shape, strides, padding): new_w = int((w - k + p) / s) + 1 @@ -404,6 +409,7 @@ def _consume_conv_nodes(self, node, next_nodes): strides, weights, pads=pads, + dilations=dilations, activation=activation, input_index_mapper=transformer, ) @@ -471,13 +477,10 @@ def _consume_pool_nodes(self, node, next_nodes): kernel_shape = attr["kernel_shape"][1:] strides = attr["strides"] if "strides" in attr else [1] * len(kernel_shape) pads = attr["pads"] if "pads" in attr else None + dilations = attr["dilations"] if "dilations" in attr else None # check only kernel shape, stride, storage order are set # everything else is not supported - if "dilations" in attr and attr["dilations"] != [1, 1]: - raise ValueError( - f"{node.name} has non-identity dilations ({attr['dilations']}). This is not supported." - ) if ("auto_pad" in attr) and (attr["auto_pad"] != "NOTSET"): raise ValueError( f"{node.name} has autopad set ({attr['auto_pad']}). This is not supported." @@ -520,6 +523,7 @@ def _consume_pool_nodes(self, node, next_nodes): tuple(kernel_shape), kernel_depth, pads=pads, + dilations=dilations, activation=activation, input_index_mapper=transformer, ) diff --git a/src/omlt/neuralnet/layer.py b/src/omlt/neuralnet/layer.py index 956da4bf..f38fc747 100644 --- a/src/omlt/neuralnet/layer.py +++ b/src/omlt/neuralnet/layer.py @@ -227,6 +227,8 @@ class Layer2D(Layer): stride of the kernel. pads : matrix-like Padding for the kernel. Given as [left, bottom, right, top] + dilations : matrix-like + Dilations of the kernel activation : str or None activation function name input_index_mapper : IndexMapper or None @@ -240,6 +242,7 @@ def __init__( strides, *, pads=None, + dilations=None, activation=None, input_index_mapper=None, ): @@ -254,6 +257,10 @@ def __init__( self.__pads = [0, 0, 0, 0] else: self.__pads = pads + if dilations is None: + self.__dilations = [1, 1] + else: + self.__dilations = dilations @property def strides(self): @@ -275,6 +282,20 @@ def kernel_depth(self): """Return the depth of the kernel""" raise NotImplementedError() + @property + def dilations(self): + """Return the kernel dilation of the layer""" + return self.__dilations + + @property + def dilated_kernel_shape(self): + """Return the shape of the kernel after dilation""" + dilated_dims = [ + self.dilations[i]*(self.kernel_shape[i]-1) + 1 + for i in range(len(self.kernel_shape)) + ] + return tuple(dilated_dims) + def kernel_index_with_input_indexes(self, out_d, out_r, out_c): """ Returns an iterator over the index within the kernel and input index @@ -290,9 +311,9 @@ def kernel_index_with_input_indexes(self, out_d, out_r, out_c): the output column. """ kernel_d = self.kernel_depth - [kernel_r, kernel_c] = self.kernel_shape + [kernel_r, kernel_c] = self.dilated_kernel_shape [rows_stride, cols_stride] = self.__strides - [pads_row, pads_col] = self.__pads[:1] + [pads_row, pads_col] = self.__pads[:2] start_in_d = 0 start_in_r = out_r * rows_stride - pads_row start_in_c = out_c * cols_stride - pads_col @@ -362,6 +383,8 @@ class PoolingLayer2D(Layer2D): stride of the kernel. pads : matrix-like Padding for the kernel. Given as [left, bottom, right, top] + dilations : matrix-like + Dilations of the kernel pool_func : str name of function used to pool values in a kernel to a single value. transpose : bool @@ -385,6 +408,7 @@ def __init__( kernel_depth, *, pads=None, + dilations=None, activation=None, input_index_mapper=None, ): @@ -393,6 +417,7 @@ def __init__( output_size, strides, pads=pads, + dilations=dilations, activation=activation, input_index_mapper=input_index_mapper, ) @@ -442,6 +467,8 @@ class ConvLayer2D(Layer2D): the cross-correlation kernel. pads : matrix-like Padding for the kernel. Given as [left, bottom, right, top] + dilations : matrix-like + Dilations of the kernel activation : str or None activation function name input_index_mapper : IndexMapper or None @@ -456,6 +483,7 @@ def __init__( kernel, *, pads=None, + dilations=None, activation=None, input_index_mapper=None, ): @@ -464,10 +492,34 @@ def __init__( output_size, strides, pads=pads, + dilations=dilations, activation=activation, input_index_mapper=input_index_mapper, ) self.__kernel = kernel + if self.dilations != [1, 1]: + dilate_rows = np.hstack([ + np.hstack([ + np.hstack([ + kernel[:, :, i, :].reshape(( + kernel.shape[0], kernel.shape[1], 1, kernel.shape[3])), + np.zeros(( + kernel.shape[0], kernel.shape[1], self.dilations[0] - 1, kernel.shape[3]))]) + for i in range(kernel.shape[2]-1)]), + kernel[:, :, -1, :].reshape((kernel.shape[0], kernel.shape[1], 1, kernel.shape[3])) + ]) + dilate_kernel = np.dstack([ + np.dstack([ + np.dstack([ + dilate_rows[:, :, :, i].reshape(( + dilate_rows.shape[0], dilate_rows.shape[1], dilate_rows.shape[2], 1)), + np.zeros((dilate_rows.shape[0], dilate_rows.shape[1], dilate_rows.shape[2], self.dilations[1] - 1))]) + for i in range(dilate_rows.shape[3]-1)]), + dilate_rows[:, :, :, -1].reshape((dilate_rows.shape[0], dilate_rows.shape[1], dilate_rows.shape[2], 1)) + ]) + self.__dilated_kernel = dilate_kernel + else: + self.__dilated_kernel = kernel def kernel_with_input_indexes(self, out_d, out_r, out_c): """ @@ -504,6 +556,11 @@ def kernel(self): """Return the cross-correlation kernel""" return self.__kernel + @property + def dilated_kernel(self): + """Return the dilated cross-correlation kernel""" + return self.__dilated_kernel + def __str__(self): return f"ConvLayer(input_size={self.input_size}, output_size={self.output_size}, strides={self.strides}, kernel_shape={self.kernel_shape})" From 668f192e433c4219fb5aa35dda8cf1700e429e28 Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Fri, 17 Nov 2023 01:26:29 +0000 Subject: [PATCH 04/14] Linting for dilations pt 1 --- src/omlt/io/onnx_parser.py | 8 ++-- src/omlt/neuralnet/layer.py | 88 +++++++++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 26 deletions(-) diff --git a/src/omlt/io/onnx_parser.py b/src/omlt/io/onnx_parser.py index 3ddb8005..1d1a3d6e 100644 --- a/src/omlt/io/onnx_parser.py +++ b/src/omlt/io/onnx_parser.py @@ -364,12 +364,12 @@ def _consume_conv_nodes(self, node, next_nodes): if "pads" in attr: pads = attr["pads"] else: - pads = 2*(len(input_output_size)-1)*[0] + pads = 2 * (len(input_output_size) - 1) * [0] if "dilations" in attr: dilations = attr["dilations"] else: - dilations = (len(input_output_size)-1)*[1] + dilations = (len(input_output_size) - 1) * [1] # Other attributes are not supported if attr["group"] != 1: @@ -379,8 +379,8 @@ def _consume_conv_nodes(self, node, next_nodes): # generate new nodes for the node output padding = [ - pads[i] + pads[i + len(input_output_size)-1] - for i in range(len(input_output_size)-1) + pads[i] + pads[i + len(input_output_size) - 1] + for i in range(len(input_output_size) - 1) ] output_size = [out_channels] for w, k, s, p in zip(input_output_size[1:], kernel_shape, strides, padding): diff --git a/src/omlt/neuralnet/layer.py b/src/omlt/neuralnet/layer.py index f38fc747..9cd0a039 100644 --- a/src/omlt/neuralnet/layer.py +++ b/src/omlt/neuralnet/layer.py @@ -291,7 +291,7 @@ def dilations(self): def dilated_kernel_shape(self): """Return the shape of the kernel after dilation""" dilated_dims = [ - self.dilations[i]*(self.kernel_shape[i]-1) + 1 + self.dilations[i] * (self.kernel_shape[i] - 1) + 1 for i in range(len(self.kernel_shape)) ] return tuple(dilated_dims) @@ -333,8 +333,7 @@ def kernel_index_with_input_indexes(self, out_d, out_r, out_c): # as this could require using a partial kernel # even though we loop over ALL kernel indexes. if not all( - input_index[i] < self.input_size[i] - and input_index[i] >= 0 + input_index[i] < self.input_size[i] and input_index[i] >= 0 for i in range(len(input_index)) ): continue @@ -498,25 +497,70 @@ def __init__( ) self.__kernel = kernel if self.dilations != [1, 1]: - dilate_rows = np.hstack([ - np.hstack([ - np.hstack([ - kernel[:, :, i, :].reshape(( - kernel.shape[0], kernel.shape[1], 1, kernel.shape[3])), - np.zeros(( - kernel.shape[0], kernel.shape[1], self.dilations[0] - 1, kernel.shape[3]))]) - for i in range(kernel.shape[2]-1)]), - kernel[:, :, -1, :].reshape((kernel.shape[0], kernel.shape[1], 1, kernel.shape[3])) - ]) - dilate_kernel = np.dstack([ - np.dstack([ - np.dstack([ - dilate_rows[:, :, :, i].reshape(( - dilate_rows.shape[0], dilate_rows.shape[1], dilate_rows.shape[2], 1)), - np.zeros((dilate_rows.shape[0], dilate_rows.shape[1], dilate_rows.shape[2], self.dilations[1] - 1))]) - for i in range(dilate_rows.shape[3]-1)]), - dilate_rows[:, :, :, -1].reshape((dilate_rows.shape[0], dilate_rows.shape[1], dilate_rows.shape[2], 1)) - ]) + dilate_rows = np.hstack( + [ + np.hstack( + [ + np.hstack( + [ + kernel[:, :, i, :].reshape( + ( + kernel.shape[0], + kernel.shape[1], + 1, + kernel.shape[3] + ) + ), + np.zeros( + ( + kernel.shape[0], + kernel.shape[1], + self.dilations[0] - 1, + kernel.shape[3] + ) + ) + ] + ) + for i in range(kernel.shape[2] - 1) + ] + ), + kernel[:, :, -1, :].reshape( + (kernel.shape[0], kernel.shape[1], 1, kernel.shape[3]) + ), + ] + ) + dilate_kernel = np.dstack( + [ + np.dstack( + [ + np.dstack( + [ + dilate_rows[:, :, :, i].reshape( + ( + dilate_rows.shape[0], + dilate_rows.shape[1], + dilate_rows.shape[2], + 1 + ) + ), + np.zeros( + ( + dilate_rows.shape[0], + dilate_rows.shape[1], + dilate_rows.shape[2], + self.dilations[1] - 1 + ) + ) + ] + ) + for i in range(dilate_rows.shape[3]-1) + ] + ), + dilate_rows[:, :, :, -1].reshape( + (dilate_rows.shape[0], dilate_rows.shape[1], dilate_rows.shape[2], 1) + ), + ] + ) self.__dilated_kernel = dilate_kernel else: self.__dilated_kernel = kernel From 4a83a22586292aae15b0df554993dd367363c451 Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Fri, 17 Nov 2023 01:26:29 +0000 Subject: [PATCH 05/14] Linting for dilations pt 1 --- src/omlt/neuralnet/layer.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/omlt/neuralnet/layer.py b/src/omlt/neuralnet/layer.py index 9cd0a039..8deaa1d3 100644 --- a/src/omlt/neuralnet/layer.py +++ b/src/omlt/neuralnet/layer.py @@ -508,7 +508,7 @@ def __init__( kernel.shape[0], kernel.shape[1], 1, - kernel.shape[3] + kernel.shape[3], ) ), np.zeros( @@ -516,9 +516,9 @@ def __init__( kernel.shape[0], kernel.shape[1], self.dilations[0] - 1, - kernel.shape[3] + kernel.shape[3], ) - ) + ), ] ) for i in range(kernel.shape[2] - 1) @@ -540,7 +540,7 @@ def __init__( dilate_rows.shape[0], dilate_rows.shape[1], dilate_rows.shape[2], - 1 + 1, ) ), np.zeros( @@ -548,16 +548,21 @@ def __init__( dilate_rows.shape[0], dilate_rows.shape[1], dilate_rows.shape[2], - self.dilations[1] - 1 + self.dilations[1] - 1, ) - ) + ), ] ) - for i in range(dilate_rows.shape[3]-1) + for i in range(dilate_rows.shape[3] - 1) ] ), dilate_rows[:, :, :, -1].reshape( - (dilate_rows.shape[0], dilate_rows.shape[1], dilate_rows.shape[2], 1) + ( + dilate_rows.shape[0], + dilate_rows.shape[1], + dilate_rows.shape[2], + 1, + ) ), ] ) From f47907ca0a513dbd03fead1916352d9c3c015927 Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Fri, 5 Jan 2024 07:58:40 +0000 Subject: [PATCH 06/14] tests for padding parsing --- src/omlt/io/onnx_parser.py | 24 ++++++++++-------- tests/io/test_onnx_parser.py | 49 ++++++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/omlt/io/onnx_parser.py b/src/omlt/io/onnx_parser.py index 1d1a3d6e..d11e90e0 100644 --- a/src/omlt/io/onnx_parser.py +++ b/src/omlt/io/onnx_parser.py @@ -176,13 +176,15 @@ def _visit_node(self, node, next_nodes): def _consume_dense_nodes(self, node, next_nodes): """Starting from a MatMul node, consume nodes to form a dense Ax + b node.""" + # This should only be called when we know we have a starting MatMul node. This + # error indicates a bug in the function calling this one. if node.op_type != "MatMul": raise ValueError( - f"{node.name} is a {node.op_type} node, only MatMul nodes can be used as starting points for consumption." + f"{node.name} is a {node.op_type} node, but the method for parsing MatMul nodes was invoked." ) if len(node.input) != 2: raise ValueError( - f"{node.name} input has {len(node.input)} dimensions, only nodes with 2 input dimensions can be used as starting points for consumption." + f"{node.name} input has {len(node.input)} dimensions, but the parser requires the starting node to have 2 input dimensions." ) [in_0, in_1] = list(node.input) @@ -200,7 +202,7 @@ def _consume_dense_nodes(self, node, next_nodes): raise TypeError(f"Expected a node next, got a {type_} instead.") if node.op_type != "Add": raise ValueError( - f"The first node to be consumed, {node.name}, is a {node.op_type} node. Only Add nodes are supported." + f"The next node to be parsed, {node.name}, is a {node.op_type} node. Only Add nodes are supported." ) # extract biases @@ -255,11 +257,11 @@ def _consume_gemm_dense_nodes(self, node, next_nodes): """Starting from a Gemm node, consume nodes to form a dense aAB + bC node.""" if node.op_type != "Gemm": raise ValueError( - f"{node.name} is a {node.op_type} node, only Gemm nodes can be used as starting points for consumption." + f"{node.name} is a {node.op_type} node, but the method for parsing Gemm nodes was invoked." ) if len(node.input) != 3: raise ValueError( - f"{node.name} input has {len(node.input)} dimensions, only nodes with 3 input dimensions can be used as starting points for consumption." + f"{node.name} input has {len(node.input)} dimensions, but the parser requires the starting node to have 3 input dimensions." ) attr = _collect_attributes(node) @@ -310,11 +312,11 @@ def _consume_conv_nodes(self, node, next_nodes): """ if node.op_type != "Conv": raise ValueError( - f"{node.name} is a {node.op_type} node, only Conv nodes can be used as starting points for consumption." + f"{node.name} is a {node.op_type} node, but the method for parsing Conv nodes was invoked." ) if len(node.input) not in [2, 3]: raise ValueError( - f"{node.name} input has {len(node.input)} dimensions, only nodes with 2 or 3 input dimensions can be used as starting points for consumption." + f"{node.name} input has {len(node.input)} dimensions, but the parser requires the starting node to have 2 or 3 input dimensions." ) if len(node.input) == 2: @@ -422,11 +424,11 @@ def _consume_reshape_nodes(self, node, next_nodes): """Parse a Reshape node.""" if node.op_type != "Reshape": raise ValueError( - f"{node.name} is a {node.op_type} node, only Reshape nodes can be used as starting points for consumption." + f"{node.name} is a {node.op_type} node, but the method for parsing Reshape nodes was invoked." ) if len(node.input) != 2: raise ValueError( - f"{node.name} input has {len(node.input)} dimensions, only nodes with 2 input dimensions can be used as starting points for consumption." + f"{node.name} input has {len(node.input)} dimensions, but the parser requires the starting node to have 2 input dimensions." ) [in_0, in_1] = list(node.input) input_layer = self._node_map[in_0] @@ -443,7 +445,7 @@ def _consume_pool_nodes(self, node, next_nodes): """ if node.op_type not in _POOLING_OP_TYPES: raise ValueError( - f"{node.name} is a {node.op_type} node, only MaxPool nodes can be used as starting points for consumption." + f"{node.name} is a {node.op_type} node, but the method for parsing MaxPool nodes was invoked." ) pool_func_name = "max" @@ -454,7 +456,7 @@ def _consume_pool_nodes(self, node, next_nodes): ) if len(node.input) != 1: raise ValueError( - f"{node.name} input has {len(node.input)} dimensions, only nodes with 1 input dimension can be used as starting points for consumption." + f"{node.name} input has {len(node.input)} dimensions, but the parser requires the starting node to have 1 input dimension." ) input_layer, transformer = self._node_input_and_transformer(node.input[0]) diff --git a/tests/io/test_onnx_parser.py b/tests/io/test_onnx_parser.py index 763b282c..afbf2a89 100644 --- a/tests/io/test_onnx_parser.py +++ b/tests/io/test_onnx_parser.py @@ -1,6 +1,8 @@ +from pathlib import Path import pytest from omlt.dependencies import onnx, onnx_available +from tests.conftest import _Datadir if onnx_available: from omlt.io.onnx import load_onnx_neural_network @@ -8,7 +10,7 @@ @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_linear_131(datadir): +def test_linear_131(datadir: _Datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -20,7 +22,7 @@ def test_linear_131(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_linear_131_relu(datadir): +def test_linear_131_relu(datadir: _Datadir): model = onnx.load(datadir.file("keras_linear_131_relu.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -32,7 +34,7 @@ def test_linear_131_relu(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_linear_131_sigmoid(datadir): +def test_linear_131_sigmoid(datadir: _Datadir): model = onnx.load(datadir.file("keras_linear_131_sigmoid.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -44,7 +46,7 @@ def test_linear_131_sigmoid(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_gemm(datadir): +def test_gemm(datadir: _Datadir): model = onnx.load(datadir.file("gemm.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -58,7 +60,7 @@ def test_gemm(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_gemm_transB(datadir): +def test_gemm_transB(datadir: _Datadir): model = onnx.load(datadir.file("gemm_not_transB.onnx")) model_transB = onnx.load(datadir.file("gemm_transB.onnx")) net = load_onnx_neural_network(model) @@ -74,7 +76,7 @@ def test_gemm_transB(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_conv(datadir): +def test_conv(datadir: _Datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -85,9 +87,24 @@ def test_conv(datadir): assert layers[1].strides == [1, 1] assert layers[1].kernel_shape == (2, 2) +@pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") +def test_conv_dilations(datadir: _Datadir): + model = onnx.load(datadir.file("convx1_gemmx1.onnx")) + for attr in model.graph.node[0].attribute: + if attr.name == "dilations": + attr.ints.clear() + attr.ints.extend([2,2]) + if attr.name == "pads": + attr.ints.clear() + attr.ints.extend([1,2,1,0]) + model.graph.node[1].attribute[0].t.raw_data = numpy_helper.from_array(np.array([-1,128])).raw_data + net = load_onnx_neural_network(model) + layers = list(net.layers) + assert layers[1].dilations == [2,2] + assert layers[1].pads == [1,2,1,0] @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_maxpool(datadir): +def test_maxpool(datadir: _Datadir): model = onnx.load(datadir.file("maxpool_2d.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -109,7 +126,7 @@ def test_maxpool(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_input_tensor_invalid_dims(datadir): +def test_input_tensor_invalid_dims(datadir: _Datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) model.graph.input[0].type.tensor_type.shape.dim[1].dim_value = 0 parser = NetworkParser() @@ -120,7 +137,7 @@ def test_input_tensor_invalid_dims(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_no_input_layers(datadir): +def test_no_input_layers(datadir: _Datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) model.graph.input.remove(model.graph.input[0]) parser = NetworkParser() @@ -131,7 +148,7 @@ def test_no_input_layers(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_node_no_inputs(datadir): +def test_node_no_inputs(datadir: _Datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) while len(model.graph.node[0].input) > 0: model.graph.node[0].input.pop() @@ -143,7 +160,7 @@ def test_node_no_inputs(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_wrong_node_type(datadir): +def test_consume_wrong_node_type(datadir: _Datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) @@ -190,7 +207,7 @@ def test_consume_wrong_node_type(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_dense_wrong_dims(datadir): +def test_consume_dense_wrong_dims(datadir: _Datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) @@ -208,7 +225,7 @@ def test_consume_dense_wrong_dims(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_gemm_wrong_dims(datadir): +def test_consume_gemm_wrong_dims(datadir: _Datadir): model = onnx.load(datadir.file("gemm.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) @@ -222,7 +239,7 @@ def test_consume_gemm_wrong_dims(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_conv_wrong_dims(datadir): +def test_consume_conv_wrong_dims(datadir: _Datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) @@ -236,7 +253,7 @@ def test_consume_conv_wrong_dims(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_reshape_wrong_dims(datadir): +def test_consume_reshape_wrong_dims(datadir: _Datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) @@ -250,7 +267,7 @@ def test_consume_reshape_wrong_dims(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_maxpool_wrong_dims(datadir): +def test_consume_maxpool_wrong_dims(datadir: _Datadir): model = onnx.load(datadir.file("maxpool_2d.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) From 8639bd44f1fb828d3e692235c44f9d74440ec1b8 Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Mon, 8 Jan 2024 05:23:45 +0000 Subject: [PATCH 07/14] Fixing broken annotations --- tests/io/test_onnx_parser.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/tests/io/test_onnx_parser.py b/tests/io/test_onnx_parser.py index afbf2a89..86241614 100644 --- a/tests/io/test_onnx_parser.py +++ b/tests/io/test_onnx_parser.py @@ -1,8 +1,6 @@ -from pathlib import Path import pytest from omlt.dependencies import onnx, onnx_available -from tests.conftest import _Datadir if onnx_available: from omlt.io.onnx import load_onnx_neural_network @@ -10,7 +8,7 @@ @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_linear_131(datadir: _Datadir): +def test_linear_131(datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -22,7 +20,7 @@ def test_linear_131(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_linear_131_relu(datadir: _Datadir): +def test_linear_131_relu(datadir): model = onnx.load(datadir.file("keras_linear_131_relu.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -34,7 +32,7 @@ def test_linear_131_relu(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_linear_131_sigmoid(datadir: _Datadir): +def test_linear_131_sigmoid(datadir): model = onnx.load(datadir.file("keras_linear_131_sigmoid.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -46,7 +44,7 @@ def test_linear_131_sigmoid(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_gemm(datadir: _Datadir): +def test_gemm(datadir): model = onnx.load(datadir.file("gemm.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -60,7 +58,7 @@ def test_gemm(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_gemm_transB(datadir: _Datadir): +def test_gemm_transB(datadir): model = onnx.load(datadir.file("gemm_not_transB.onnx")) model_transB = onnx.load(datadir.file("gemm_transB.onnx")) net = load_onnx_neural_network(model) @@ -76,7 +74,7 @@ def test_gemm_transB(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_conv(datadir: _Datadir): +def test_conv(datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -88,7 +86,7 @@ def test_conv(datadir: _Datadir): assert layers[1].kernel_shape == (2, 2) @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_conv_dilations(datadir: _Datadir): +def test_conv_dilations(datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) for attr in model.graph.node[0].attribute: if attr.name == "dilations": @@ -104,7 +102,7 @@ def test_conv_dilations(datadir: _Datadir): assert layers[1].pads == [1,2,1,0] @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_maxpool(datadir: _Datadir): +def test_maxpool(datadir): model = onnx.load(datadir.file("maxpool_2d.onnx")) net = load_onnx_neural_network(model) layers = list(net.layers) @@ -126,7 +124,7 @@ def test_maxpool(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_input_tensor_invalid_dims(datadir: _Datadir): +def test_input_tensor_invalid_dims(datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) model.graph.input[0].type.tensor_type.shape.dim[1].dim_value = 0 parser = NetworkParser() @@ -137,7 +135,7 @@ def test_input_tensor_invalid_dims(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_no_input_layers(datadir: _Datadir): +def test_no_input_layers(datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) model.graph.input.remove(model.graph.input[0]) parser = NetworkParser() @@ -148,7 +146,7 @@ def test_no_input_layers(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_node_no_inputs(datadir: _Datadir): +def test_node_no_inputs(datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) while len(model.graph.node[0].input) > 0: model.graph.node[0].input.pop() @@ -160,7 +158,7 @@ def test_node_no_inputs(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_wrong_node_type(datadir: _Datadir): +def test_consume_wrong_node_type(datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) @@ -207,7 +205,7 @@ def test_consume_wrong_node_type(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_dense_wrong_dims(datadir: _Datadir): +def test_consume_dense_wrong_dims(datadir): model = onnx.load(datadir.file("keras_linear_131.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) @@ -225,7 +223,7 @@ def test_consume_dense_wrong_dims(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_gemm_wrong_dims(datadir: _Datadir): +def test_consume_gemm_wrong_dims(datadir): model = onnx.load(datadir.file("gemm.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) @@ -239,7 +237,7 @@ def test_consume_gemm_wrong_dims(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_conv_wrong_dims(datadir: _Datadir): +def test_consume_conv_wrong_dims(datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) @@ -253,7 +251,7 @@ def test_consume_conv_wrong_dims(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_reshape_wrong_dims(datadir: _Datadir): +def test_consume_reshape_wrong_dims(datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) @@ -267,7 +265,7 @@ def test_consume_reshape_wrong_dims(datadir: _Datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") -def test_consume_maxpool_wrong_dims(datadir: _Datadir): +def test_consume_maxpool_wrong_dims(datadir): model = onnx.load(datadir.file("maxpool_2d.onnx")) parser = NetworkParser() parser.parse_network(model.graph, None, None) From 623aaf688d97458868a965d616378477c408435e Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Mon, 8 Jan 2024 05:45:55 +0000 Subject: [PATCH 08/14] Updated tests for error messages --- tests/io/test_onnx_parser.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/tests/io/test_onnx_parser.py b/tests/io/test_onnx_parser.py index 86241614..5d9562b0 100644 --- a/tests/io/test_onnx_parser.py +++ b/tests/io/test_onnx_parser.py @@ -5,6 +5,8 @@ if onnx_available: from omlt.io.onnx import load_onnx_neural_network from omlt.io.onnx_parser import NetworkParser + from onnx import numpy_helper + from numpy import ndarray @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") @@ -85,21 +87,24 @@ def test_conv(datadir): assert layers[1].strides == [1, 1] assert layers[1].kernel_shape == (2, 2) + @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") def test_conv_dilations(datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) for attr in model.graph.node[0].attribute: if attr.name == "dilations": attr.ints.clear() - attr.ints.extend([2,2]) + attr.ints.extend([2, 2]) if attr.name == "pads": attr.ints.clear() - attr.ints.extend([1,2,1,0]) - model.graph.node[1].attribute[0].t.raw_data = numpy_helper.from_array(np.array([-1,128])).raw_data + attr.ints.extend([1, 2, 1, 0]) + model.graph.node[1].attribute[0].t.raw_data = \ + numpy_helper.from_array(ndarray([-1, 128])).raw_data net = load_onnx_neural_network(model) layers = list(net.layers) - assert layers[1].dilations == [2,2] - assert layers[1].pads == [1,2,1,0] + assert layers[1].dilations == [2, 2] + assert layers[1].pads == [1, 2, 1, 0] + @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") def test_maxpool(datadir): @@ -168,7 +173,7 @@ def test_consume_wrong_node_type(datadir): parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/BiasAdd"][1], parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/BiasAdd"][2], ) - expected_msg_dense = "StatefulPartitionedCall/keras_linear_131/dense/BiasAdd is a Add node, only MatMul nodes can be used as starting points for consumption." + expected_msg_dense = "StatefulPartitionedCall/keras_linear_131/dense/BiasAdd is a Add node, but the method for parsing MatMul nodes was invoked." assert str(excinfo.value) == expected_msg_dense with pytest.raises(ValueError) as excinfo: @@ -176,7 +181,7 @@ def test_consume_wrong_node_type(datadir): parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/BiasAdd"][1], parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/BiasAdd"][2], ) - expected_msg_gemm = "StatefulPartitionedCall/keras_linear_131/dense/BiasAdd is a Add node, only Gemm nodes can be used as starting points for consumption." + expected_msg_gemm = "StatefulPartitionedCall/keras_linear_131/dense/BiasAdd is a Add node, but the method for parsing Gemm nodes was invoked." assert str(excinfo.value) == expected_msg_gemm with pytest.raises(ValueError) as excinfo: @@ -184,7 +189,7 @@ def test_consume_wrong_node_type(datadir): parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/BiasAdd"][1], parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/BiasAdd"][2], ) - expected_msg_conv = "StatefulPartitionedCall/keras_linear_131/dense/BiasAdd is a Add node, only Conv nodes can be used as starting points for consumption." + expected_msg_conv = "StatefulPartitionedCall/keras_linear_131/dense/BiasAdd is a Add node, but the method for parsing Conv nodes was invoked." assert str(excinfo.value) == expected_msg_conv with pytest.raises(ValueError) as excinfo: @@ -192,7 +197,7 @@ def test_consume_wrong_node_type(datadir): parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/BiasAdd"][1], parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/BiasAdd"][2], ) - expected_msg_reshape = "StatefulPartitionedCall/keras_linear_131/dense/BiasAdd is a Add node, only Reshape nodes can be used as starting points for consumption." + expected_msg_reshape = "StatefulPartitionedCall/keras_linear_131/dense/BiasAdd is a Add node, but the method for parsing Reshape nodes was invoked." assert str(excinfo.value) == expected_msg_reshape with pytest.raises(ValueError) as excinfo: @@ -200,7 +205,7 @@ def test_consume_wrong_node_type(datadir): parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/BiasAdd"][1], parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/BiasAdd"][2], ) - expected_msg_pool = """StatefulPartitionedCall/keras_linear_131/dense/BiasAdd is a Add node, only MaxPool nodes can be used as starting points for consumption.""" + expected_msg_pool = """StatefulPartitionedCall/keras_linear_131/dense/BiasAdd is a Add node, but the method for parsing MaxPool nodes was invoked.""" assert str(excinfo.value) == expected_msg_pool @@ -218,7 +223,7 @@ def test_consume_dense_wrong_dims(datadir): parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/MatMul"][1], parser._nodes["StatefulPartitionedCall/keras_linear_131/dense/MatMul"][2], ) - expected_msg_dense = "StatefulPartitionedCall/keras_linear_131/dense/MatMul input has 3 dimensions, only nodes with 2 input dimensions can be used as starting points for consumption." + expected_msg_dense = "StatefulPartitionedCall/keras_linear_131/dense/MatMul input has 3 dimensions, but the parser requires the starting node to have 2 input dimensions." assert str(excinfo.value) == expected_msg_dense @@ -232,7 +237,7 @@ def test_consume_gemm_wrong_dims(datadir): parser._consume_gemm_dense_nodes( parser._nodes["Gemm_0"][1], parser._nodes["Gemm_0"][2] ) - expected_msg_gemm = "Gemm_0 input has 4 dimensions, only nodes with 3 input dimensions can be used as starting points for consumption." + expected_msg_gemm = "Gemm_0 input has 4 dimensions, but the parser requires the starting node to have 3 input dimensions." assert str(excinfo.value) == expected_msg_gemm @@ -246,7 +251,7 @@ def test_consume_conv_wrong_dims(datadir): parser._consume_conv_nodes( parser._nodes["Conv_0"][1], parser._nodes["Conv_0"][2] ) - expected_msg_conv = "Conv_0 input has 4 dimensions, only nodes with 2 or 3 input dimensions can be used as starting points for consumption." + expected_msg_conv = "Conv_0 input has 4 dimensions, but the parser requires the starting node to have 2 or 3 input dimensions." assert str(excinfo.value) == expected_msg_conv @@ -260,7 +265,7 @@ def test_consume_reshape_wrong_dims(datadir): parser._consume_reshape_nodes( parser._nodes["Reshape_2"][1], parser._nodes["Reshape_2"][2] ) - expected_msg_reshape = """Reshape_2 input has 3 dimensions, only nodes with 2 input dimensions can be used as starting points for consumption.""" + expected_msg_reshape = """Reshape_2 input has 3 dimensions, but the parser requires the starting node to have 2 input dimensions.""" assert str(excinfo.value) == expected_msg_reshape @@ -272,5 +277,5 @@ def test_consume_maxpool_wrong_dims(datadir): parser._nodes["node1"][1].input.append("abcd") with pytest.raises(ValueError) as excinfo: parser._consume_pool_nodes(parser._nodes["node1"][1], parser._nodes["node1"][2]) - expected_msg_maxpool = """node1 input has 2 dimensions, only nodes with 1 input dimension can be used as starting points for consumption.""" + expected_msg_maxpool = """node1 input has 2 dimensions, but the parser requires the starting node to have 1 input dimension.""" assert str(excinfo.value) == expected_msg_maxpool From b9603e36337cb01e701e29cef8264aa265b2661a Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Mon, 8 Jan 2024 06:26:25 +0000 Subject: [PATCH 09/14] Fixing pads tests --- tests/io/test_onnx_parser.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/io/test_onnx_parser.py b/tests/io/test_onnx_parser.py index 5d9562b0..33c06ea1 100644 --- a/tests/io/test_onnx_parser.py +++ b/tests/io/test_onnx_parser.py @@ -6,7 +6,7 @@ from omlt.io.onnx import load_onnx_neural_network from omlt.io.onnx_parser import NetworkParser from onnx import numpy_helper - from numpy import ndarray + from numpy import array @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") @@ -93,13 +93,14 @@ def test_conv_dilations(datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) for attr in model.graph.node[0].attribute: if attr.name == "dilations": - attr.ints.clear() + del attr.ints[:] attr.ints.extend([2, 2]) if attr.name == "pads": - attr.ints.clear() + del attr.ints[:] attr.ints.extend([1, 2, 1, 0]) - model.graph.node[1].attribute[0].t.raw_data = \ - numpy_helper.from_array(ndarray([-1, 128])).raw_data + model.graph.node[1].attribute[0].t.raw_data = numpy_helper.from_array( + array([-1, 128]) + ).raw_data net = load_onnx_neural_network(model) layers = list(net.layers) assert layers[1].dilations == [2, 2] From 8bc6e2f00c995e3fd66861d594c8c164970cadaf Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Mon, 8 Jan 2024 07:25:23 +0000 Subject: [PATCH 10/14] Testing kernel dilation and fixing shapes --- src/omlt/neuralnet/layer.py | 5 +++++ tests/io/test_onnx_parser.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/omlt/neuralnet/layer.py b/src/omlt/neuralnet/layer.py index 8deaa1d3..663d7bdd 100644 --- a/src/omlt/neuralnet/layer.py +++ b/src/omlt/neuralnet/layer.py @@ -565,6 +565,11 @@ def __init__( ) ), ] + ).reshape( + kernel.shape[0], + kernel.shape[1], + kernel.shape[2] + self.dilations[1] - 1, + kernel.shape[3] + self.dilations[1] - 1 ) self.__dilated_kernel = dilate_kernel else: diff --git a/tests/io/test_onnx_parser.py b/tests/io/test_onnx_parser.py index 33c06ea1..dfdb5874 100644 --- a/tests/io/test_onnx_parser.py +++ b/tests/io/test_onnx_parser.py @@ -78,6 +78,9 @@ def test_gemm_transB(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") def test_conv(datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) + for attr in model.graph.node[0].attribute: + if attr.name == "dilations": + del attr net = load_onnx_neural_network(model) layers = list(net.layers) assert len(layers) == 4 @@ -86,6 +89,8 @@ def test_conv(datadir): assert layers[3].activation == "relu" assert layers[1].strides == [1, 1] assert layers[1].kernel_shape == (2, 2) + assert layers[1].dilations == [1, 1] + assert layers[1].pads == [0, 0, 0, 0] @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") @@ -104,6 +109,16 @@ def test_conv_dilations(datadir): net = load_onnx_neural_network(model) layers = list(net.layers) assert layers[1].dilations == [2, 2] + assert layers[1].dilated_kernel[0][0] == array( + [[-0.00886667, 0, 0.18750042], + [0, 0, 0], + [-0.11404419, 0, -0.02588665]] + ) + assert layers[1].dilated_kernel[1][0] == array( + [[-0.07554907, 0, -0.05939162], + [0, 0, 0], + [0.2217437, 0, 0.14637864]] + ) assert layers[1].pads == [1, 2, 1, 0] From 8be2198b1d3eff416e9fbf0fb309f8b390fec6a3 Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Mon, 8 Jan 2024 07:52:50 +0000 Subject: [PATCH 11/14] really fixing those dilation tests --- tests/io/test_onnx_parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/io/test_onnx_parser.py b/tests/io/test_onnx_parser.py index dfdb5874..511c6aaf 100644 --- a/tests/io/test_onnx_parser.py +++ b/tests/io/test_onnx_parser.py @@ -109,16 +109,16 @@ def test_conv_dilations(datadir): net = load_onnx_neural_network(model) layers = list(net.layers) assert layers[1].dilations == [2, 2] - assert layers[1].dilated_kernel[0][0] == array( + assert (layers[1].dilated_kernel[0][0].round(8) == array( [[-0.00886667, 0, 0.18750042], [0, 0, 0], [-0.11404419, 0, -0.02588665]] - ) - assert layers[1].dilated_kernel[1][0] == array( + )).all() + assert (layers[1].dilated_kernel[1][0].round(8) == array( [[-0.07554907, 0, -0.05939162], [0, 0, 0], [0.2217437, 0, 0.14637864]] - ) + )).all() assert layers[1].pads == [1, 2, 1, 0] From 66d28cda7475fd3f725bdd66fe3ff80d0fe8f7ca Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Mon, 8 Jan 2024 08:09:53 +0000 Subject: [PATCH 12/14] Linting --- src/omlt/neuralnet/layer.py | 2 +- tests/io/test_onnx_parser.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/omlt/neuralnet/layer.py b/src/omlt/neuralnet/layer.py index 663d7bdd..52d93f11 100644 --- a/src/omlt/neuralnet/layer.py +++ b/src/omlt/neuralnet/layer.py @@ -569,7 +569,7 @@ def __init__( kernel.shape[0], kernel.shape[1], kernel.shape[2] + self.dilations[1] - 1, - kernel.shape[3] + self.dilations[1] - 1 + kernel.shape[3] + self.dilations[1] - 1, ) self.__dilated_kernel = dilate_kernel else: diff --git a/tests/io/test_onnx_parser.py b/tests/io/test_onnx_parser.py index 511c6aaf..1289f3f1 100644 --- a/tests/io/test_onnx_parser.py +++ b/tests/io/test_onnx_parser.py @@ -109,16 +109,16 @@ def test_conv_dilations(datadir): net = load_onnx_neural_network(model) layers = list(net.layers) assert layers[1].dilations == [2, 2] - assert (layers[1].dilated_kernel[0][0].round(8) == array( - [[-0.00886667, 0, 0.18750042], - [0, 0, 0], - [-0.11404419, 0, -0.02588665]] - )).all() - assert (layers[1].dilated_kernel[1][0].round(8) == array( - [[-0.07554907, 0, -0.05939162], - [0, 0, 0], - [0.2217437, 0, 0.14637864]] - )).all() + assert ( + layers[1].dilated_kernel[0][0].round(8) + == array( + [[-0.00886667, 0, 0.18750042], [0, 0, 0], [-0.11404419, 0, -0.02588665]] + ) + ).all() + assert ( + layers[1].dilated_kernel[1][0].round(8) + == array([[-0.07554907, 0, -0.05939162], [0, 0, 0], [0.2217437, 0, 0.14637864]]) + ).all() assert layers[1].pads == [1, 2, 1, 0] From 0d405299b85848b1f88c8d51eebcb6066081db88 Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:36:44 +0000 Subject: [PATCH 13/14] Coverage for non-dilation case --- tests/io/test_onnx_parser.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/io/test_onnx_parser.py b/tests/io/test_onnx_parser.py index 1289f3f1..a0f67448 100644 --- a/tests/io/test_onnx_parser.py +++ b/tests/io/test_onnx_parser.py @@ -78,9 +78,8 @@ def test_gemm_transB(datadir): @pytest.mark.skipif(not onnx_available, reason="Need ONNX for this test") def test_conv(datadir): model = onnx.load(datadir.file("convx1_gemmx1.onnx")) - for attr in model.graph.node[0].attribute: - if attr.name == "dilations": - del attr + del model.graph.node[0].attribute[0] + del model.graph.node[0].attribute[2] net = load_onnx_neural_network(model) layers = list(net.layers) assert len(layers) == 4 From e17df8a28c8016e9e4c3e05f832c9cea51dfdafe Mon Sep 17 00:00:00 2001 From: Jeremy Sadler <53983960+jezsadler@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:42:44 +0000 Subject: [PATCH 14/14] Addressing some issues with dilations --- src/omlt/io/onnx_parser.py | 2 +- src/omlt/neuralnet/layer.py | 89 +++++++------------------------------ 2 files changed, 16 insertions(+), 75 deletions(-) diff --git a/src/omlt/io/onnx_parser.py b/src/omlt/io/onnx_parser.py index d11e90e0..224aae5a 100644 --- a/src/omlt/io/onnx_parser.py +++ b/src/omlt/io/onnx_parser.py @@ -475,7 +475,7 @@ def _consume_pool_nodes(self, node, next_nodes): in_channels = input_output_size[0] attr = _collect_attributes(node) - kernel_depth = attr["kernel_shape"][0] + kernel_depth = in_channels kernel_shape = attr["kernel_shape"][1:] strides = attr["strides"] if "strides" in attr else [1] * len(kernel_shape) pads = attr["pads"] if "pads" in attr else None diff --git a/src/omlt/neuralnet/layer.py b/src/omlt/neuralnet/layer.py index e915b16e..3feb9e59 100644 --- a/src/omlt/neuralnet/layer.py +++ b/src/omlt/neuralnet/layer.py @@ -674,81 +674,22 @@ def __init__( ) self.__kernel = kernel if self.dilations != [1, 1]: - dilate_rows = np.hstack( - [ - np.hstack( - [ - np.hstack( - [ - kernel[:, :, i, :].reshape( - ( - kernel.shape[0], - kernel.shape[1], - 1, - kernel.shape[3], - ) - ), - np.zeros( - ( - kernel.shape[0], - kernel.shape[1], - self.dilations[0] - 1, - kernel.shape[3], - ) - ), - ] - ) - for i in range(kernel.shape[2] - 1) - ] - ), - kernel[:, :, -1, :].reshape( - (kernel.shape[0], kernel.shape[1], 1, kernel.shape[3]) - ), - ] - ) - dilate_kernel = np.dstack( - [ - np.dstack( - [ - np.dstack( - [ - dilate_rows[:, :, :, i].reshape( - ( - dilate_rows.shape[0], - dilate_rows.shape[1], - dilate_rows.shape[2], - 1, - ) - ), - np.zeros( - ( - dilate_rows.shape[0], - dilate_rows.shape[1], - dilate_rows.shape[2], - self.dilations[1] - 1, - ) - ), - ] - ) - for i in range(dilate_rows.shape[3] - 1) - ] - ), - dilate_rows[:, :, :, -1].reshape( - ( - dilate_rows.shape[0], - dilate_rows.shape[1], - dilate_rows.shape[2], - 1, - ) - ), - ] - ).reshape( - kernel.shape[0], - kernel.shape[1], - kernel.shape[2] + self.dilations[1] - 1, - kernel.shape[3] + self.dilations[1] - 1, + dilated = np.zeros( + ( + kernel.shape[0], + kernel.shape[1], + (kernel.shape[2] - 1) * dilations[0] + 1, + (kernel.shape[3] - 1) * dilations[1] + 1, + ) ) - self.__dilated_kernel = dilate_kernel + for i in range(kernel.shape[0]): + for j in range(kernel.shape[1]): + for k in range(kernel.shape[2]): + for l in range(kernel.shape[3]): + dilated[i, j, k * dilations[0], l * dilations[1]] = kernel[ + i, j, k, l + ] + self.__dilated_kernel = dilated else: self.__dilated_kernel = kernel