Skip to content

Commit

Permalink
Use pytest_cases to simplify parametrization of decomon inputs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nhuet authored and ducoffeM committed Jan 11, 2024
1 parent 5bb1dba commit bcb66d4
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 157 deletions.
68 changes: 65 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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",
)
53 changes: 16 additions & 37 deletions tests/test_backward_compute_output_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,68 +35,47 @@
(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):
layer = layer_class(mode=mode, dc_decomp=dc_decomp, **layer_kwargs)
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)
Expand Down
65 changes: 18 additions & 47 deletions tests/test_backward_layer_wo_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit bcb66d4

Please sign in to comment.