From b0e84499b48e7ac22b8a9048ffca3c902dac3811 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 23 Aug 2023 12:05:59 +0200 Subject: [PATCH 1/6] ENH: automatic model output naming --- otbtf/model.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/otbtf/model.py b/otbtf/model.py index 9958510b..58bb6ba1 100644 --- a/otbtf/model.py +++ b/otbtf/model.py @@ -34,7 +34,7 @@ TensorsDict = Dict[str, Tensor] class ModelBase(abc.ABC): """ - Base class for all models + Base class for all fully convolutional models """ def __init__( @@ -178,24 +178,33 @@ class ModelBase(abc.ABC): a dict of post-processed model outputs """ - - # Add extra outputs for inference + # Dict of extra outputs for inference extra_outputs = {} for out_key, out_tensor in outputs.items(): for crop in self.inference_cropping: extra_output_key = cropped_tensor_name(out_key, crop) - extra_output_name = cropped_tensor_name( + layer_name = cropped_tensor_name( out_tensor._keras_history.layer.name, crop ) - logging.info( - "Adding extra output for tensor %s with crop %s (%s)", - out_key, crop, extra_output_name - ) - cropped = out_tensor[:, crop:-crop, crop:-crop, :] - identity = tf.keras.layers.Activation( - 'linear', name=extra_output_name - ) - extra_outputs[extra_output_key] = identity(cropped) + + def _put_cropped_output(tgt_name: str): + """ + Puts a new cropped tensor from a named op in extra outputs. + """ + logging.info( + "Adding extra output for tensor %s with crop %s: %s", + out_key, crop, extra_output_name + ) + eye_op = tf.keras.layers.Activation("linear", name=name) + new_out = eye_op(out_tensor[:, crop:-crop, crop:-crop, :]) + extra_outputs[f"postproc_{tgt_name}"] = new_out + + # extra output named after layer + _put_cropped_output(layer_name) + + # extra output named after output key + if layer_name != extra_output_key: + _put_cropped_output(extra_output_key) return extra_outputs -- GitLab From ad43f87d70f24c37b3fc9e444a10417f827e8b05 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 23 Aug 2023 13:27:48 +0200 Subject: [PATCH 2/6] TEST: add testing for extra outputs --- test/api_unittest.py | 137 ++++++++++++++++++++----------------------- 1 file changed, 64 insertions(+), 73 deletions(-) diff --git a/test/api_unittest.py b/test/api_unittest.py index 2fe3fe38..d16ea934 100644 --- a/test/api_unittest.py +++ b/test/api_unittest.py @@ -8,7 +8,7 @@ from otbtf.examples.tensorflow_v2x.fcnn import create_tfrecords from otbtf.examples.tensorflow_v2x.fcnn import train_from_patchesimages from otbtf.examples.tensorflow_v2x.fcnn import train_from_tfrecords from otbtf.examples.tensorflow_v2x.fcnn.fcnn_model import INPUT_NAME, \ - OUTPUT_SOFTMAX_NAME + OUTPUT_SOFTMAX_NAME, OUTPUT_ARGMAX_NAME, TARGET_NAME from otbtf.model import cropped_tensor_name from test_utils import resolve_paths, files_exist, run_command_and_compare @@ -39,42 +39,75 @@ class APITest(unittest.TestCase): '$TMPDIR/model_from_pimg/variables/variables.index' ])) - @pytest.mark.order(2) + @pytest.mark.order(2, 5) def test_model_inference1(self): - self.assertTrue( - run_command_and_compare( - command= + def _make_command(out_name, rfield, efield): + return ( "otbcli_TensorflowModelServe " "-source1.il $DATADIR/fake_spot6.jp2 " - "-source1.rfieldx 64 " - "-source1.rfieldy 64 " + f"-source1.rfieldx {rfield} " + f"-source1.rfieldy {rfield} " f"-source1.placeholder {INPUT_NAME} " "-model.dir $TMPDIR/model_from_pimg " "-model.fullyconv on " - f"-output.names {cropped_tensor_name(OUTPUT_SOFTMAX_NAME, 16)} " - "-output.efieldx 32 " - "-output.efieldy 32 " - "-out \"$TMPDIR/classif_model4_softmax.tif?&gdal:co:compress=deflate\" uint8", - to_compare_dict={ - "$DATADIR/classif_model4_softmax.tif": "$TMPDIR/classif_model4_softmax.tif"}, - tol=INFERENCE_MAE_TOL)) - self.assertTrue( - run_command_and_compare( - command= - "otbcli_TensorflowModelServe " - "-source1.il $DATADIR/fake_spot6.jp2 " - "-source1.rfieldx 128 " - "-source1.rfieldy 128 " - f"-source1.placeholder {INPUT_NAME} " - "-model.dir $TMPDIR/model_from_pimg " - "-model.fullyconv on " - f"-output.names {cropped_tensor_name(OUTPUT_SOFTMAX_NAME, 32)} " - "-output.efieldx 64 " - "-output.efieldy 64 " - "-out \"$TMPDIR/classif_model4_softmax.tif?&gdal:co:compress=deflate\" uint8", - to_compare_dict={ - "$DATADIR/classif_model4_softmax.tif": "$TMPDIR/classif_model4_softmax.tif"}, - tol=INFERENCE_MAE_TOL)) + f"-output.names {out_name} " + f"-output.efieldx {efield} " + f"-output.efieldy {efield} " + "-out \"$TMPDIR/classif_model4_softmax.tif?&gdal:co:compress=deflate\" uint8" + ) + + command_crop16_softmax = _make_command( + out_name=cropped_tensor_name(OUTPUT_SOFTMAX_NAME, 16), + rfield=64, + efield=32 + ) + command_crop32_softmax = _make_command( + out_name=cropped_tensor_name(OUTPUT_SOFTMAX_NAME, 32), + rfield=128, + efield=64 + ) + command_crop16_softmax2 = _make_command( + out_name=cropped_tensor_name(TARGET_NAME, 16), + rfield=64, + efield=32 + ) + command_crop32_softmax2 = _make_command( + out_name=cropped_tensor_name(TARGET_NAME, 32), + rfield=128, + efield=64 + ) + command_crop16_argmax = _make_command( + out_name=cropped_tensor_name(OUTPUT_ARGMAX_NAME, 16), + rfield=64, + efield=32 + ) + command_crop32_argmax = _make_command( + out_name=cropped_tensor_name(OUTPUT_ARGMAX_NAME, 32), + rfield=128, + efield=64 + ) + + def _test_compare(command): + self.assertTrue( + run_command_and_compare( + command=command, + to_compare_dict={ + "$DATADIR/classif_model4_softmax.tif": + "$TMPDIR/classif_model4_softmax.tif" + }, + tol=INFERENCE_MAE_TOL)) + + # softmax (from layer name) + _test_compare(command_crop16_softmax) + _test_compare(command_crop32_softmax) + + # softmax (from target key i.e. model output name) + _test_compare(command_crop16_softmax2) + _test_compare(command_crop32_softmax2) + + # argmax + self.assertTrue(run_command_and_test_exist(command_crop16_argmax)) + self.assertTrue(run_command_and_test_exist(command_crop32_argmax)) @pytest.mark.order(3) def test_create_tfrecords(self): @@ -116,48 +149,6 @@ class APITest(unittest.TestCase): '$TMPDIR/model_from_tfrecs/variables/variables.index' ])) - @pytest.mark.order(5) - def test_model_inference2(self): - self.assertTrue( - run_command_and_compare( - command= - "otbcli_TensorflowModelServe " - "-source1.il $DATADIR/fake_spot6.jp2 " - "-source1.rfieldx 64 " - "-source1.rfieldy 64 " - f"-source1.placeholder {INPUT_NAME} " - "-model.dir $TMPDIR/model_from_pimg " - "-model.fullyconv on " - f"-output.names {cropped_tensor_name(OUTPUT_SOFTMAX_NAME, 16)} " - "-output.efieldx 32 " - "-output.efieldy 32 " - "-out \"$TMPDIR/classif_model4_softmax.tif?&gdal:co:compress=deflate\" uint8", - to_compare_dict={ - "$DATADIR/classif_model4_softmax.tif": - "$TMPDIR/classif_model4_softmax.tif" - }, - tol=INFERENCE_MAE_TOL)) - - self.assertTrue( - run_command_and_compare( - command= - "otbcli_TensorflowModelServe " - "-source1.il $DATADIR/fake_spot6.jp2 " - "-source1.rfieldx 128 " - "-source1.rfieldy 128 " - f"-source1.placeholder {INPUT_NAME} " - "-model.dir $TMPDIR/model_from_pimg " - "-model.fullyconv on " - f"-output.names {cropped_tensor_name(OUTPUT_SOFTMAX_NAME, 32)} " - "-output.efieldx 64 " - "-output.efieldy 64 " - "-out \"$TMPDIR/classif_model4_softmax.tif?&gdal:co:compress=deflate\" uint8", - to_compare_dict={ - "$DATADIR/classif_model4_softmax.tif": - "$TMPDIR/classif_model4_softmax.tif" - }, - tol=INFERENCE_MAE_TOL)) - if __name__ == '__main__': unittest.main() -- GitLab From 313ab1a6289aa5b36df2e176b42df266bd5b9154 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 23 Aug 2023 13:28:17 +0200 Subject: [PATCH 3/6] ENH: auto append extra outputs --- otbtf/model.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/otbtf/model.py b/otbtf/model.py index 58bb6ba1..1c648de2 100644 --- a/otbtf/model.py +++ b/otbtf/model.py @@ -181,30 +181,32 @@ class ModelBase(abc.ABC): # Dict of extra outputs for inference extra_outputs = {} for out_key, out_tensor in outputs.items(): - for crop in self.inference_cropping: - extra_output_key = cropped_tensor_name(out_key, crop) - layer_name = cropped_tensor_name( - out_tensor._keras_history.layer.name, crop + layer_name = out_tensor._keras_history.layer.name + # extra output named after layer + srcs_names = [layer_name] + if layer_name == out_key: + logging.warning( + "Output \"%s\" already exist from layer of the same " + "name. Skipping extra-outputs creation. If you have " + "any doubt, you can use the following command to see " + "available model outputs: " + "`saved_model_cli show --dir your_model_dir --all`", + layer_name ) - - def _put_cropped_output(tgt_name: str): - """ - Puts a new cropped tensor from a named op in extra outputs. - """ + else: + # extra output named after output key + srcs_names += [out_key] + # Now for all accepted src_name, we create extra outputs + for crop in self.inference_cropping: + for src_name in srcs_names: + tgt_name = cropped_tensor_name(src_name, crop) logging.info( "Adding extra output for tensor %s with crop %s: %s", - out_key, crop, extra_output_name + out_key, crop, tgt_name ) - eye_op = tf.keras.layers.Activation("linear", name=name) - new_out = eye_op(out_tensor[:, crop:-crop, crop:-crop, :]) - extra_outputs[f"postproc_{tgt_name}"] = new_out - - # extra output named after layer - _put_cropped_output(layer_name) - - # extra output named after output key - if layer_name != extra_output_key: - _put_cropped_output(extra_output_key) + eye = tf.keras.layers.Activation("linear", name=tgt_name) + extra_out = eye(out_tensor[:, crop:-crop, crop:-crop, :]) + extra_outputs[f"postproc_{tgt_name}"] = extra_out return extra_outputs -- GitLab From b884d7527b7c5e92e2206f51155e67113cb591b7 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 23 Aug 2023 13:28:36 +0200 Subject: [PATCH 4/6] ADD: argmax output in the toy model --- .../tensorflow_v2x/fcnn/fcnn_model.py | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/otbtf/examples/tensorflow_v2x/fcnn/fcnn_model.py b/otbtf/examples/tensorflow_v2x/fcnn/fcnn_model.py index 44285d92..4f2377ae 100644 --- a/otbtf/examples/tensorflow_v2x/fcnn/fcnn_model.py +++ b/otbtf/examples/tensorflow_v2x/fcnn/fcnn_model.py @@ -5,6 +5,7 @@ import logging import tensorflow as tf +import otbtf.layers from otbtf.model import ModelBase logging.basicConfig( @@ -23,8 +24,9 @@ INPUT_NAME = "input_xs" # Name of the output in the `FCNNModel` instance TARGET_NAME = "predictions" -# Name (prefix) of the output node in the SavedModel -OUTPUT_SOFTMAX_NAME = "predictions_softmax_tensor" +# Name (prefix) of the output nodes in the SavedModel +OUTPUT_SOFTMAX_NAME = "predictions_softmax" +OUTPUT_ARGMAX_NAME = "predictions_argmax" class FCNNModel(ModelBase): @@ -115,15 +117,22 @@ class FCNNModel(ModelBase): # from `MyModel.get_output()`. They are two identifiers with a # different purpose: # - the output layer name is used only at inference time, to identify - # the output tensor from which generate the output image, + # the output tensor from which generate the output image (e.g. + # OUTPUT_SOFTMAX_NAME), # - the output tensor key identifies the output tensors, mainly to # fit the targets to model outputs during training process, but it # can also be used to access the tensors as tf/keras objects, for # instance to display previews images in TensorBoard. + # For convenience, since OTBTF 4.3.0, the post-processed outputs are + # automatically added after the output tensor keys. This avoids lazy + # users to explicitly name the last layers. When the name is already + # took by the layer, the post-processed outputs creating is skipped. softmax_op = tf.keras.layers.Softmax(name=OUTPUT_SOFTMAX_NAME) predictions = softmax_op(out_tconv4) + argmax_op = otbtf.layers.Argmax() + labels = argmax_op(predictions) - return {TARGET_NAME: predictions} + return {TARGET_NAME: predictions, OUTPUT_ARGMAX_NAME: labels} def dataset_preprocessing_fn(examples: dict): @@ -146,9 +155,9 @@ def dataset_preprocessing_fn(examples: dict): """ return { INPUT_NAME: examples["input_xs_patches"], - TARGET_NAME: tf.one_hot( - tf.squeeze(tf.cast(examples["labels_patches"], tf.int32), axis=-1), - depth=N_CLASSES + TARGET_NAME: otbtf.ops.one_hot( + labels=examples["labels_patches"], + nb_classes=N_CLASSES ) } @@ -173,12 +182,21 @@ def train(params, ds_train, ds_valid, ds_test): model = FCNNModel(dataset_element_spec=ds_train.element_spec) # Compile the model + # Here using a `dict` to explicitly name the outputs over which the + # losses/metrics are computed is a good practice. model.compile( - loss=tf.keras.losses.CategoricalCrossentropy(), + loss={ + TARGET_NAME: tf.keras.losses.CategoricalCrossentropy() + }, optimizer=tf.keras.optimizers.Adam( learning_rate=params.learning_rate ), - metrics=[tf.keras.metrics.Precision(), tf.keras.metrics.Recall()] + metrics={ + TARGET_NAME: [ + tf.keras.metrics.Precision(class_id=1), + tf.keras.metrics.Recall(class_id=1) + ] + } ) # Summarize the model (in CLI) -- GitLab From d031d80a615fc58e02edae06388620f257716d1a Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 23 Aug 2023 13:53:22 +0200 Subject: [PATCH 5/6] TEST: add testing for extra outputs --- test/api_unittest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/api_unittest.py b/test/api_unittest.py index d16ea934..73b57b7d 100644 --- a/test/api_unittest.py +++ b/test/api_unittest.py @@ -10,7 +10,8 @@ from otbtf.examples.tensorflow_v2x.fcnn import train_from_tfrecords from otbtf.examples.tensorflow_v2x.fcnn.fcnn_model import INPUT_NAME, \ OUTPUT_SOFTMAX_NAME, OUTPUT_ARGMAX_NAME, TARGET_NAME from otbtf.model import cropped_tensor_name -from test_utils import resolve_paths, files_exist, run_command_and_compare +from test_utils import resolve_paths, files_exist, run_command_and_compare, \ + run_command_and_test_exist INFERENCE_MAE_TOL = 10.0 # Dummy value: we don't really care of the mae value but rather the image size etc -- GitLab From beafa1b1c612fe96a2ac7c46204e48681e22f521 Mon Sep 17 00:00:00 2001 From: Remi Cresson <remi.cresson@inrae.fr> Date: Wed, 23 Aug 2023 14:27:22 +0200 Subject: [PATCH 6/6] TEST: add testing for extra outputs --- test/api_unittest.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/test/api_unittest.py b/test/api_unittest.py index 73b57b7d..aa2f51b3 100644 --- a/test/api_unittest.py +++ b/test/api_unittest.py @@ -89,14 +89,24 @@ class APITest(unittest.TestCase): ) def _test_compare(command): - self.assertTrue( + self.assertTrue( run_command_and_compare( command=command, to_compare_dict={ "$DATADIR/classif_model4_softmax.tif": "$TMPDIR/classif_model4_softmax.tif" }, - tol=INFERENCE_MAE_TOL)) + tol=INFERENCE_MAE_TOL + ) + ) + + def _test_exist(command): + self.assertTrue( + run_command_and_test_exist( + command=command, + file_list=["$TMPDIR/classif_model4_softmax.tif"] + ) + ) # softmax (from layer name) _test_compare(command_crop16_softmax) @@ -107,8 +117,8 @@ class APITest(unittest.TestCase): _test_compare(command_crop32_softmax2) # argmax - self.assertTrue(run_command_and_test_exist(command_crop16_argmax)) - self.assertTrue(run_command_and_test_exist(command_crop32_argmax)) + _test_exist(command_crop16_argmax) + _test_exist(command_crop32_argmax) @pytest.mark.order(3) def test_create_tfrecords(self): -- GitLab