From bcb66d48d9bbb5232403a6c67ab98b87a65bc3a4 Mon Sep 17 00:00:00 2001 From: Nolwen Date: Fri, 15 Dec 2023 16:09:11 +0100 Subject: [PATCH] Use pytest_cases to simplify parametrization of decomon inputs We use pytest_cases to get union of fixture and thus define a single fixture to generate all kind of inputs for decomon/backwad layers: - 1d - multid - images These inputs are parametrized via several parameters (dc_decomp, mode, n, odd, data_format) depending of the kind of inputs. These well managed by the fixture union. We can also limit the cases by setting the value of mode, dc_decomp, ... in a parameter of a given test. We make use of this to test backward layers on all values of n, odd, ... without having to explicitely list all cases. --- tests/conftest.py | 68 ++++++++++++++- tests/test_backward_compute_output_shape.py | 53 ++++-------- tests/test_backward_layer_wo_merge.py | 65 ++++---------- tests/test_decomon_compute_output_shape.py | 93 +++++---------------- tox.ini | 1 + 5 files changed, 123 insertions(+), 157 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e3c9aa6c..84cb3a1b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ ) from keras.models import Model, Sequential from numpy.testing import assert_almost_equal +from pytest_cases import fixture, fixture_union, param_fixture from decomon.core import ForwardMode, Slope from decomon.keras_utils import ( @@ -95,9 +96,7 @@ def activation(request): return request.param -@pytest.fixture(params=["channels_last", "channels_first"]) -def data_format(request): - return request.param +data_format = param_fixture(argname="data_format", argvalues=["channels_last", "channels_first"]) @pytest.fixture(params=[0, 1]) @@ -1607,3 +1606,66 @@ def toy_model(request, helpers): def toy_model_1d(request, helpers): model_name = request.param return helpers.toy_model(model_name, dtype=keras_config.floatx()) + + +@fixture() +def decomon_inputs_1d(n, mode, dc_decomp, helpers): + # tensors inputs + inputs = helpers.get_tensor_decomposition_1d_box(dc_decomp=dc_decomp) + inputs_for_mode = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs, mode=mode, dc_decomp=dc_decomp) + input_ref = helpers.get_input_ref_from_full_inputs(inputs) + + # numpy inputs + inputs_ = helpers.get_standard_values_1d_box(n=n, dc_decomp=dc_decomp) + inputs_for_mode_ = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs_, mode=mode, dc_decomp=dc_decomp) + input_ref_ = helpers.get_input_ref_from_full_inputs(inputs_) + + # inputs metadata worth to pass to the layer + inputs_metadata = dict() + + return inputs, inputs_for_mode, input_ref, inputs_, inputs_for_mode_, input_ref_, inputs_metadata + + +@fixture() +def decomon_inputs_multid(odd, mode, dc_decomp, helpers): + # tensors inputs + inputs = helpers.get_tensor_decomposition_multid_box(odd=odd, dc_decomp=dc_decomp) + inputs_for_mode = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs, mode=mode, dc_decomp=dc_decomp) + input_ref = helpers.get_input_ref_from_full_inputs(inputs) + + # numpy inputs + inputs_ = helpers.get_standard_values_multid_box(odd=odd, dc_decomp=dc_decomp) + inputs_for_mode_ = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs_, mode=mode, dc_decomp=dc_decomp) + input_ref_ = helpers.get_input_ref_from_full_inputs(inputs_) + + # inputs metadata worth to pass to the layer + inputs_metadata = dict() + + return inputs, inputs_for_mode, input_ref, inputs_, inputs_for_mode_, input_ref_, inputs_metadata + + +@fixture() +def decomon_inputs_images(data_format, mode, dc_decomp, helpers): + odd, m_0, m_1 = 0, 0, 1 + + # tensors inputs + inputs = helpers.get_tensor_decomposition_images_box(data_format=data_format, odd=odd, dc_decomp=dc_decomp) + inputs_for_mode = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs, mode=mode, dc_decomp=dc_decomp) + input_ref = helpers.get_input_ref_from_full_inputs(inputs) + + # numpy inputs + inputs_ = helpers.get_standard_values_images_box(data_format=data_format, odd=odd, dc_decomp=dc_decomp) + inputs_for_mode_ = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs_, mode=mode, dc_decomp=dc_decomp) + input_ref_ = helpers.get_input_ref_from_full_inputs(inputs_) + + # inputs metadata worth to pass to the layer + inputs_metadata = dict(data_format=data_format) + + return inputs, inputs_for_mode, input_ref, inputs_, inputs_for_mode_, input_ref_, inputs_metadata + + +decomon_inputs = fixture_union( + "decomon_inputs", + [decomon_inputs_1d, decomon_inputs_multid, decomon_inputs_images], + unpack_into="inputs, inputs_for_mode, input_ref, inputs_, inputs_for_mode_, input_ref_, inputs_metadata", +) diff --git a/tests/test_backward_compute_output_shape.py b/tests/test_backward_compute_output_shape.py index 6db504dd..9b367229 100644 --- a/tests/test_backward_compute_output_shape.py +++ b/tests/test_backward_compute_output_shape.py @@ -35,60 +35,39 @@ (DecomonActivation, dict(activation="relu")), (Activation, dict(activation="linear")), (Activation, dict(activation="relu")), - (DecomonConv2D, dict(filters=10, kernel_size=(3, 3), data_format="channels_last")), + (DecomonConv2D, dict(filters=10, kernel_size=(3, 3))), (DecomonReshape, dict(target_shape=(1, -1, 1))), (Reshape, dict(target_shape=(1, -1, 1))), (DecomonGroupSort2, dict()), ], ) -@pytest.mark.parametrize( - "kerastensor_inputs_fn_name, kerastensor_inputs_kwargs, np_inputs_fn_name, np_inputs_kwargs", - [ - ("get_tensor_decomposition_1d_box", dict(), "get_standard_values_1d_box", dict(n=0)), - ("get_tensor_decomposition_multid_box", dict(odd=0), "get_standard_values_multid_box", dict(odd=0)), - ( - "get_tensor_decomposition_images_box", - dict(odd=0, data_format="channels_last"), - "get_standard_values_images_box", - dict(odd=0, data_format="channels_last"), - ), - ], -) +@pytest.mark.parametrize("dc_decomp", [False]) # limit dc_decomp +@pytest.mark.parametrize("n", [0]) # limit 1d cases +@pytest.mark.parametrize("odd", [0]) # limit multid cases +@pytest.mark.parametrize("data_format", ["channels_last"]) # limit images cases def test_compute_output_shape( helpers, mode, + dc_decomp, layer_class, layer_kwargs, - kerastensor_inputs_fn_name, - kerastensor_inputs_kwargs, - np_inputs_fn_name, - np_inputs_kwargs, + inputs_for_mode, # decomon inputs: symbolic tensors + input_ref, # keras input: symbolic tensor + inputs_for_mode_, # decomon inputs: numpy arrays + inputs_metadata, # inputs metadata: data_format, ... ): - dc_decomp = False + # skip nonsensical combinations if (layer_class == DecomonBatchNormalization or layer_class == DecomonMaxPooling2D) and dc_decomp: pytest.skip(f"{layer_class} with dc_decomp=True not yet implemented.") - if ( - layer_class == DecomonConv2D or layer_class == DecomonMaxPooling2D - ) and kerastensor_inputs_fn_name != "get_tensor_decomposition_images_box": + if (layer_class == DecomonConv2D or layer_class == DecomonMaxPooling2D) and len(input_ref.shape) < 4: pytest.skip(f"{layer_class} applies only on image-like inputs.") if layer_class == DecomonGroupSort2: if not deel_lip_available: pytest.skip("deel-lip is not available") - # contruct inputs functions - kerastensor_inputs_fn = getattr(helpers, kerastensor_inputs_fn_name) - kerastensor_inputs_kwargs["dc_decomp"] = dc_decomp - np_inputs_fn = getattr(helpers, np_inputs_fn_name) - np_inputs_kwargs["dc_decomp"] = dc_decomp - - # tensors inputs - inputs = kerastensor_inputs_fn(**kerastensor_inputs_kwargs) - inputs_for_mode = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs, mode=mode, dc_decomp=dc_decomp) - inputs_ref = helpers.get_input_ref_from_full_inputs(inputs) - - # numpy inputs - inputs_ = np_inputs_fn(**np_inputs_kwargs) - inputs_for_mode_ = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs_, mode=mode, dc_decomp=dc_decomp) + # add data_format for convolution and maxpooling + if layer_class in (DecomonConv2D, DecomonMaxPooling2D): + layer_kwargs["data_format"] = inputs_metadata["data_format"] # construct and build original layer (decomon or keras) if issubclass(layer_class, DecomonLayer): @@ -96,7 +75,7 @@ def test_compute_output_shape( layer(inputs_for_mode) else: # keras layer layer = layer_class(**layer_kwargs) - layer(inputs_ref) + layer(input_ref) # get backward layer backward_layer = to_backward(layer, mode=mode) diff --git a/tests/test_backward_layer_wo_merge.py b/tests/test_backward_layer_wo_merge.py index dd2711fb..90807e7c 100644 --- a/tests/test_backward_layer_wo_merge.py +++ b/tests/test_backward_layer_wo_merge.py @@ -42,72 +42,43 @@ (Permute, dict(dims=(2, 1, 3))), ], ) -@pytest.mark.parametrize( - "kerastensor_inputs_fn_name, kerastensor_inputs_kwargs, np_inputs_fn_name, np_inputs_kwargs", - [ - ("get_tensor_decomposition_1d_box", dict(), "get_standard_values_1d_box", dict(n=0)), - ("get_tensor_decomposition_multid_box", dict(odd=0), "get_standard_values_multid_box", dict(odd=0)), - ( - "get_tensor_decomposition_images_box", - dict(odd=0, data_format="channels_last"), - "get_standard_values_images_box", - dict(odd=0, data_format="channels_last"), - ), - ], -) @pytest.mark.parametrize("randomize_weights", [False, True]) +@pytest.mark.parametrize("floatx", [32]) # fix floatx +@pytest.mark.parametrize("data_format", ["channels_last"]) # limit images cases +@pytest.mark.parametrize("dc_decomp", [False]) # limit dc_decomp def test_backward_layer( helpers, mode, layer_class, layer_kwargs, - kerastensor_inputs_fn_name, - kerastensor_inputs_kwargs, - np_inputs_fn_name, - np_inputs_kwargs, randomize_weights, + decimal, + dc_decomp, + inputs_for_mode, # decomon inputs: symbolic tensors + inputs, # decomon inputs: symbolic tensors + input_ref, # keras input: symbolic tensor + inputs_, # decomon inputs: numpy arrays + input_ref_, # keras input: numpy array + inputs_metadata, # inputs metadata: data_format, ... ): # skip nonsensical combinations if ( layer_class is BatchNormalization and "axis" in layer_kwargs and layer_kwargs["axis"] > 1 - and kerastensor_inputs_fn_name != "get_tensor_decomposition_images_box" + and len(input_ref.shape) < 4 ): pytest.skip("batchnormalization on axis>1 possible only for image-like data") - if layer_class in (Conv2D, MaxPooling2D) and kerastensor_inputs_fn_name != "get_tensor_decomposition_images_box": + if layer_class in (Conv2D, MaxPooling2D) and len(input_ref.shape) < 4: pytest.skip("convolution and maxpooling2d possible only for image-like data") - if ( - layer_class is Permute - and len(layer_kwargs["dims"]) < 3 - and kerastensor_inputs_fn_name == "get_tensor_decomposition_images_box" - ): + if layer_class is Permute and len(layer_kwargs["dims"]) < 3 and len(input_ref.shape) >= 4: pytest.skip("1d permutation not possible for image-like data") - if ( - layer_class is Permute - and len(layer_kwargs["dims"]) == 3 - and kerastensor_inputs_fn_name != "get_tensor_decomposition_images_box" - ): + if layer_class is Permute and len(layer_kwargs["dims"]) == 3 and len(input_ref.shape) < 4: pytest.skip("3d permutation possible only for image-like data") - dc_decomp = False - decimal = 4 - - # contruct inputs functions - kerastensor_inputs_fn = getattr(helpers, kerastensor_inputs_fn_name) - kerastensor_inputs_kwargs["dc_decomp"] = dc_decomp - np_inputs_fn = getattr(helpers, np_inputs_fn_name) - np_inputs_kwargs["dc_decomp"] = dc_decomp - - # tensors inputs - inputs = kerastensor_inputs_fn(**kerastensor_inputs_kwargs) - inputs_for_mode = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs, mode=mode, dc_decomp=dc_decomp) - input_ref = helpers.get_input_ref_from_full_inputs(inputs) - - # numpy inputs - inputs_ = np_inputs_fn(**np_inputs_kwargs) - inputs_for_mode_ = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs_, mode=mode, dc_decomp=dc_decomp) - input_ref_ = helpers.get_input_ref_from_full_inputs(inputs_) + # add data_format for convolution and maxpooling + if layer_class in (Conv2D,): + layer_kwargs["data_format"] = inputs_metadata["data_format"] # construct and build original layer (keras) layer = layer_class(**layer_kwargs) diff --git a/tests/test_decomon_compute_output_shape.py b/tests/test_decomon_compute_output_shape.py index d5cbd621..ab192fe1 100644 --- a/tests/test_decomon_compute_output_shape.py +++ b/tests/test_decomon_compute_output_shape.py @@ -27,59 +27,38 @@ (DecomonFlatten, dict()), (DecomonBatchNormalization, dict(center=True, scale=True)), (DecomonBatchNormalization, dict(center=False, scale=False)), - (DecomonConv2D, dict(filters=10, kernel_size=(3, 3), data_format="channels_last")), + (DecomonConv2D, dict(filters=10, kernel_size=(3, 3))), (DecomonMaxPooling2D, dict(pool_size=(2, 2), strides=(2, 2), padding="valid")), (DecomonReshape, dict(target_shape=(1, -1, 1))), (DecomonGroupSort2, dict()), ], ) -@pytest.mark.parametrize( - "kerastensor_inputs_fn_name, kerastensor_inputs_kwargs, np_inputs_fn_name, np_inputs_kwargs", - [ - ("get_tensor_decomposition_1d_box", dict(), "get_standard_values_1d_box", dict(n=0)), - ("get_tensor_decomposition_multid_box", dict(odd=0), "get_standard_values_multid_box", dict(odd=0)), - ( - "get_tensor_decomposition_images_box", - dict(odd=0, data_format="channels_last"), - "get_standard_values_images_box", - dict(odd=0, data_format="channels_last"), - ), - ], -) +@pytest.mark.parametrize("n", [0]) # limit 1d cases +@pytest.mark.parametrize("odd", [0]) # limit multid cases +@pytest.mark.parametrize("data_format", ["channels_last"]) # limit images cases def test_compute_output_shape( helpers, mode, dc_decomp, layer_class, layer_kwargs, - kerastensor_inputs_fn_name, - kerastensor_inputs_kwargs, - np_inputs_fn_name, - np_inputs_kwargs, + inputs_for_mode, # decomon inputs: symbolic tensors + input_ref, # keras input: symbolic tensor + inputs_for_mode_, # decomon inputs: numpy arrays + inputs_metadata, # inputs metadata: data_format, ... ): + # skip nonsensical combinations if (layer_class == DecomonBatchNormalization or layer_class == DecomonMaxPooling2D) and dc_decomp: pytest.skip(f"{layer_class} with dc_decomp=True not yet implemented.") - if ( - layer_class == DecomonConv2D or layer_class == DecomonMaxPooling2D - ) and kerastensor_inputs_fn_name != "get_tensor_decomposition_images_box": + if (layer_class == DecomonConv2D or layer_class == DecomonMaxPooling2D) and len(input_ref.shape) < 4: pytest.skip(f"{layer_class} applies only on image-like inputs.") if layer_class == DecomonGroupSort2: if not deel_lip_available: pytest.skip("deel-lip is not available") - # contruct inputs functions - kerastensor_inputs_fn = getattr(helpers, kerastensor_inputs_fn_name) - kerastensor_inputs_kwargs["dc_decomp"] = dc_decomp - np_inputs_fn = getattr(helpers, np_inputs_fn_name) - np_inputs_kwargs["dc_decomp"] = dc_decomp - - # tensors inputs - inputs = kerastensor_inputs_fn(**kerastensor_inputs_kwargs) - inputs_for_mode = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs, mode=mode, dc_decomp=dc_decomp) - - # numpy inputs - inputs_ = np_inputs_fn(**np_inputs_kwargs) - inputs_for_mode_ = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs_, mode=mode, dc_decomp=dc_decomp) + # add data_format for convolution and maxpooling + if layer_class in (DecomonConv2D, DecomonMaxPooling2D): + layer_kwargs["data_format"] = inputs_metadata["data_format"] # construct layer layer = layer_class(mode=mode, dc_decomp=dc_decomp, **layer_kwargs) @@ -103,63 +82,37 @@ def test_compute_output_shape( (DecomonConcatenate, dict()), ], ) -@pytest.mark.parametrize( - "kerastensor_inputs_fn_name, kerastensor_inputs_kwargs, np_inputs_fn_name, np_inputs_kwargs", - [ - ("get_tensor_decomposition_1d_box", dict(), "get_standard_values_1d_box", dict(n=0)), - ("get_tensor_decomposition_multid_box", dict(odd=0), "get_standard_values_multid_box", dict(odd=0)), - ( - "get_tensor_decomposition_images_box", - dict(odd=0, data_format="channels_last"), - "get_standard_values_images_box", - dict(odd=0, data_format="channels_last"), - ), - ], -) +@pytest.mark.parametrize("n", [0]) # limit 1d cases +@pytest.mark.parametrize("odd", [0]) # limit multid cases +@pytest.mark.parametrize("data_format", ["channels_last"]) # limit images cases def test_merge_layers_compute_output_shape( helpers, mode, dc_decomp, layer_class, layer_kwargs, - kerastensor_inputs_fn_name, - kerastensor_inputs_kwargs, - np_inputs_fn_name, - np_inputs_kwargs, + inputs_for_mode, # decomon inputs: symbolic tensors + inputs_for_mode_, # decomon inputs: numpy arrays ): if dc_decomp: pytest.skip(f"{layer_class} with dc_decomp=True not yet implemented.") - # contruct inputs functions - kerastensor_inputs_fn = getattr(helpers, kerastensor_inputs_fn_name) - kerastensor_inputs_kwargs["dc_decomp"] = dc_decomp - np_inputs_fn = getattr(helpers, np_inputs_fn_name) - np_inputs_kwargs["dc_decomp"] = dc_decomp - # tensors inputs - inputs_1 = kerastensor_inputs_fn(**kerastensor_inputs_kwargs) - inputs_for_mode_1 = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs_1, mode=mode, dc_decomp=dc_decomp) - inputs_2 = kerastensor_inputs_fn(**kerastensor_inputs_kwargs) - inputs_for_mode_2 = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs_2, mode=mode, dc_decomp=dc_decomp) - inputs_for_mode = inputs_for_mode_1 + inputs_for_mode_2 + concatenated_inputs_for_mode = inputs_for_mode + inputs_for_mode # numpy inputs - inputs_1_ = np_inputs_fn(**np_inputs_kwargs) - inputs_for_mode_1_ = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs_1_, mode=mode, dc_decomp=dc_decomp) - inputs_2_ = np_inputs_fn(**np_inputs_kwargs) - inputs_for_mode_2_ = helpers.get_inputs_for_mode_from_full_inputs(inputs=inputs_2_, mode=mode, dc_decomp=dc_decomp) - inputs_for_mode_ = inputs_for_mode_1_ + inputs_for_mode_2_ + concatenated_inputs_for_mode_ = inputs_for_mode_ + inputs_for_mode_ # construct layer layer = layer_class(mode=mode, dc_decomp=dc_decomp, **layer_kwargs) # check symbolic tensor output shapes - inputshapes = [i.shape for i in inputs_for_mode] + inputshapes = [i.shape for i in concatenated_inputs_for_mode] outputshapes = layer.compute_output_shape(inputshapes) - outputs = layer(inputs_for_mode) + outputs = layer(concatenated_inputs_for_mode) assert [o.shape for o in outputs] == outputshapes # check output shapes for concrete call - outputs_ = layer(inputs_for_mode_) + outputs_ = layer(concatenated_inputs_for_mode_) # compare without batch sizes assert [tuple(o.shape)[1:] for o in outputs_] == [s[1:] for s in outputshapes] diff --git a/tox.ini b/tox.ini index 5267200d..4ea9e607 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ platform = linux: linux win: win32 deps = pytest + pytest-cases !nodeellip: deel-lip py39-linux-tf-nodeellip: pytest-cov tf: tensorflow>=2.15 # backend for keras 3