From 191a591c240d6847d3a8a69e958e892cc3a16876 Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Wed, 20 Aug 2025 13:13:02 +0530 Subject: [PATCH 1/6] [tmva][sofie] New Keras Parser (from PrasannaKasar GSOC student 2025) New Keras parser - added support for LayerNorm, BatchNorm ND, ELU layers and added tests for them. Imported Keras within the required functions. Created new CMakeLists.txt file for the keras parser. Made changes in the pythonization CMake file to build the keras parser files removed get_keras_version function call from tmva __init__ file. Replaced import keras_version with get_keras_version and called it in necessary files Removed SOFIE Keras Parser CMakeLists.txt file from the Pythonization directory. Used import numpy statements within the parser functions to avoid slowing down the import of ROOT. Added print statements to display the TensorFlow Keras version used in CI Correctly inject RModelParser_Keras class into Python interfaces --- .../pythonizations/python/ROOT/_facade.py | 1 + .../ROOT/_pythonization/_tmva/__init__.py | 18 + .../_tmva/_sofie/_parser/_keras/__init__.py | 5 + .../_keras/generate_keras_functional.py | 208 +++++++ .../_keras/generate_keras_sequential.py | 205 +++++++ .../_sofie/_parser/_keras/layers/__init__.py | 0 .../_sofie/_parser/_keras/layers/batchnorm.py | 55 ++ .../_sofie/_parser/_keras/layers/binary.py | 23 + .../_sofie/_parser/_keras/layers/concat.py | 17 + .../_sofie/_parser/_keras/layers/conv.py | 73 +++ .../_sofie/_parser/_keras/layers/dense.py | 37 ++ .../_tmva/_sofie/_parser/_keras/layers/elu.py | 35 ++ .../_sofie/_parser/_keras/layers/flatten.py | 36 ++ .../_sofie/_parser/_keras/layers/identity.py | 15 + .../_sofie/_parser/_keras/layers/layernorm.py | 62 ++ .../_parser/_keras/layers/leaky_relu.py | 44 ++ .../_sofie/_parser/_keras/layers/permute.py | 36 ++ .../_sofie/_parser/_keras/layers/pooling.py | 78 +++ .../_sofie/_parser/_keras/layers/relu.py | 30 + .../_sofie/_parser/_keras/layers/reshape.py | 34 ++ .../_tmva/_sofie/_parser/_keras/layers/rnn.py | 92 +++ .../_sofie/_parser/_keras/layers/selu.py | 31 + .../_sofie/_parser/_keras/layers/sigmoid.py | 31 + .../_sofie/_parser/_keras/layers/softmax.py | 32 ++ .../_sofie/_parser/_keras/layers/swish.py | 31 + .../_sofie/_parser/_keras/layers/tanh.py | 31 + .../_tmva/_sofie/_parser/_keras/parser.py | 542 ++++++++++++++++++ .../_parser/_keras/parser_test_function.py | 96 ++++ .../pyroot/pythonizations/test/CMakeLists.txt | 7 + .../pythonizations/test/sofie_keras_parser.py | 74 +++ tmva/pymva/inc/TMVA/MethodPyKeras.h | 4 +- .../inc/TMVA/ROperator_LayerNormalization.hxx | 77 +++ tmva/sofie/inc/TMVA/ROperator_Reshape.hxx | 1 + .../inc/TMVA/RModelParser_Keras.h | 4 +- 34 files changed, 2061 insertions(+), 4 deletions(-) create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/__init__.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py create mode 100644 bindings/pyroot/pythonizations/test/sofie_keras_parser.py diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index 4695e2b74a972..310e6df3ad1f3 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -412,6 +412,7 @@ def TMVA(self): from ._pythonization import _tmva # noqa: F401 ns = self._fallback_getattr("TMVA") + setattr(ns.Experimental.SOFIE, "RModelParser_Keras", _tmva.RModelParser_Keras) hasRDF = "dataframe" in self.gROOT.GetConfigFeatures() if hasRDF: try: diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py index e271f232326e3..c130f55003bc7 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py @@ -37,6 +37,24 @@ def inject_rbatchgenerator(ns): return ns +from ._gnn import RModel_GNN, RModel_GraphIndependent +from ._sofie._parser._keras.parser import RModelParser_Keras + +hasRDF = "dataframe" in cppyy.gbl.ROOT.GetROOT().GetConfigFeatures() +if hasRDF: + from ._rtensor import ( + get_array_interface, + add_array_interface_property, + RTensorGetitem, + pythonize_rtensor, + _AsRTensor, + ) + +# this should be available only when xgboost is there ? +# We probably don't need a protection here since the code is run only when there is xgboost +from ._tree_inference import SaveXGBoost + + # list of python classes that are used to pythonize TMVA classes python_classes = [Factory, DataLoader, CrossValidation] diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py new file mode 100644 index 0000000000000..5f48c83e89aa1 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py @@ -0,0 +1,5 @@ +def get_keras_version() -> str: + + import keras + + return keras.__version__ \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py new file mode 100644 index 0000000000000..8a433e751c6bc --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py @@ -0,0 +1,208 @@ +def generate_keras_functional(dst_dir): + + from keras import models, layers + import numpy as np + + # Helper training function + def train_and_save(model, name): + # Handle multiple inputs dynamically + if isinstance(model.input_shape, list): + x_train = [np.random.rand(32, *shape[1:]) for shape in model.input_shape] + else: + x_train = np.random.rand(32, *model.input_shape[1:]) + y_train = np.random.rand(32, *model.output_shape[1:]) + + model.compile(optimizer='adam', loss='mean_squared_error', metrics=['mae']) + model.fit(x_train, y_train, epochs=1, verbose=0) + model.save(f"{dst_dir}/Functional_{name}_test.h5") + + # Activation Functions + for act in ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']: + inp = layers.Input(shape=(10,)) + out = layers.Activation(act)(inp) + model = models.Model(inp, out) + train_and_save(model, f"Activation_layer_{act.capitalize()}") + # Along with these, Keras allows explicit delcaration of activation layers such as: + # [ELU, ReLU, LeakyReLU, Softmax] + + # Add + in1 = layers.Input(shape=(8,)) + in2 = layers.Input(shape=(8,)) + out = layers.Add()([in1, in2]) + model = models.Model([in1, in2], out) + train_and_save(model, "Add") + + # AveragePooling2D channels_first + inp = layers.Input(shape=(3, 8, 8)) + out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "AveragePooling2D_channels_first") + + # AveragePooling2D channels_last + inp = layers.Input(shape=(8, 8, 3)) + out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "AveragePooling2D_channels_last") + + # BatchNorm + inp = layers.Input(shape=(10, 3, 5)) + out = layers.BatchNormalization(axis=2)(inp) + model = models.Model(inp, out) + train_and_save(model, "BatchNorm") + + # Concat + in1 = layers.Input(shape=(8,)) + in2 = layers.Input(shape=(8,)) + out = layers.Concatenate()([in1, in2]) + model = models.Model([in1, in2], out) + train_and_save(model, "Concat") + + # Conv2D channels_first + inp = layers.Input(shape=(3, 8, 8)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_first', activation='relu')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_channels_first") + + # Conv2D channels_last + inp = layers.Input(shape=(8, 8, 3)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last', activation='leaky_relu')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_channels_last") + + # Conv2D padding_same + inp = layers.Input(shape=(8, 8, 3)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_padding_same") + + # Conv2D padding_valid + inp = layers.Input(shape=(8, 8, 3)) + out = layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last', activation='elu')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_padding_valid") + + # Dense + inp = layers.Input(shape=(10,)) + out = layers.Dense(5, activation='tanh')(inp) + model = models.Model(inp, out) + train_and_save(model, "Dense") + + # ELU + inp = layers.Input(shape=(10,)) + out = layers.ELU(alpha=0.5)(inp) + model = models.Model(inp, out) + train_and_save(model, "ELU") + + # Flatten + inp = layers.Input(shape=(4, 5)) + out = layers.Flatten()(inp) + model = models.Model(inp, out) + train_and_save(model, "Flatten") + + # GlobalAveragePooling2D channels first + inp = layers.Input(shape=(3, 4, 6)) + out = layers.GlobalAveragePooling2D(data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "GlobalAveragePooling2D_channels_first") + + # GlobalAveragePooling2D channels last + inp = layers.Input(shape=(4, 6, 3)) + out = layers.GlobalAveragePooling2D(data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "GlobalAveragePooling2D_channels_last") + + # LayerNorm + inp = layers.Input(shape=(10, 3, 5)) + out = layers.LayerNormalization(axis=-1)(inp) + model = models.Model(inp, out) + train_and_save(model, "LayerNorm") + + # LeakyReLU + inp = layers.Input(shape=(10,)) + out = layers.LeakyReLU()(inp) + model = models.Model(inp, out) + train_and_save(model, "LeakyReLU") + + # MaxPooling2D channels_first + inp = layers.Input(shape=(3, 8, 8)) + out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "MaxPool2D_channels_first") + + # MaxPooling2D channels_last + inp = layers.Input(shape=(8, 8, 3)) + out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "MaxPool2D_channels_last") + + # Multiply + in1 = layers.Input(shape=(8,)) + in2 = layers.Input(shape=(8,)) + out = layers.Multiply()([in1, in2]) + model = models.Model([in1, in2], out) + train_and_save(model, "Multiply") + + # Permute + inp = layers.Input(shape=(3, 4, 5)) + out = layers.Permute((2, 1, 3))(inp) + model = models.Model(inp, out) + train_and_save(model, "Permute") + + # ReLU + inp = layers.Input(shape=(10,)) + out = layers.ReLU()(inp) + model = models.Model(inp, out) + train_and_save(model, "ReLU") + + # Reshape + inp = layers.Input(shape=(4, 5)) + out = layers.Reshape((2, 10))(inp) + model = models.Model(inp, out) + train_and_save(model, "Reshape") + + # Softmax + inp = layers.Input(shape=(10,)) + out = layers.Softmax()(inp) + model = models.Model(inp, out) + train_and_save(model, "Softmax") + + # Subtract + in1 = layers.Input(shape=(8,)) + in2 = layers.Input(shape=(8,)) + out = layers.Subtract()([in1, in2]) + model = models.Model([in1, in2], out) + train_and_save(model, "Subtract") + + # Layer Combination + + inp = layers.Input(shape=(32, 32, 3)) + x = layers.Conv2D(8, (3,3), padding="same", activation="relu")(inp) + x = layers.MaxPooling2D((2,2))(x) + x = layers.Reshape((16, 16, 8))(x) + x = layers.Permute((3, 1, 2))(x) + x = layers.Flatten()(x) + out = layers.Dense(10, activation="softmax")(x) + model = models.Model(inp, out) + train_and_save(model, "Layer_Combination_1") + + inp = layers.Input(shape=(20,)) + x = layers.Dense(32, activation="tanh")(inp) + x = layers.Dense(16)(x) + x = layers.ELU()(x) + x = layers.LayerNormalization()(x) + out = layers.Dense(5, activation="sigmoid")(x) + model = models.Model(inp, out) + train_and_save(model, "Layer_Combination_2") + + inp1 = layers.Input(shape=(16,)) + inp2 = layers.Input(shape=(16,)) + d1 = layers.Dense(16, activation="relu")(inp1) + d2 = layers.Dense(16, activation="selu")(inp2) + add = layers.Add()([d1, d2]) + sub = layers.Subtract()([d1, d2]) + mul = layers.Multiply()([d1, d2]) + merged = layers.Concatenate()([add, sub, mul]) + merged = layers.LeakyReLU(alpha=0.1)(merged) + out = layers.Dense(4, activation="softmax")(merged) + model = models.Model([inp1, inp2], out) + train_and_save(model, "Layer_Combination_3") diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py new file mode 100644 index 0000000000000..20c03f31c69fc --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py @@ -0,0 +1,205 @@ +def generate_keras_sequential(dst_dir): + + from keras import models, layers + import numpy as np + + # Helper training function + def train_and_save(model, name): + x_train = np.random.rand(32, *model.input_shape[1:]) + y_train = np.random.rand(32, *model.output_shape[1:]) + model.compile(optimizer='adam', loss='mean_squared_error', metrics=['mae']) + model.fit(x_train, y_train, epochs=1, verbose=0) + model.save(f"{dst_dir}/Sequential_{name}_test.h5") + + # Binary Ops: Add, Subtract, Multiply are not typical in Sequential - skipping those + # Concat (not applicable in Sequential without multi-input) + + # Activation Functions + for act in ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']: + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.Activation(act) + ]) + train_and_save(model, f"Activation_layer_{act.capitalize()}") + # Along with this, Keras also allows explicit delcaration of activation layers such as: + # ELU, ReLU, LeakyReLU, Softmax + + # AveragePooling2D channels_first + model = models.Sequential([ + layers.Input(shape=(3, 8, 8)), + layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first') + ]) + train_and_save(model, "AveragePooling2D_channels_first") + + # AveragePooling2D channels_last + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_last') + ]) + train_and_save(model, "AveragePooling2D_channels_last") + + # BatchNorm + model = models.Sequential([ + layers.Input(shape=(10, 3, 5)), + layers.BatchNormalization(axis=2) + ]) + train_and_save(model, "BatchNorm") + + # Conv2D channels_first + model = models.Sequential([ + layers.Input(shape=(3, 8, 8)), + layers.Conv2D(4, (3, 3), data_format='channels_first') + ]) + train_and_save(model, "Conv2D_channels_first") + + # Conv2D channels_last + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.Conv2D(4, (3, 3), data_format='channels_last', activation='tanh') + ]) + train_and_save(model, "Conv2D_channels_last") + + # Conv2D padding_same + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last', activation='selu') + ]) + train_and_save(model, "Conv2D_padding_same") + + # Conv2D padding_valid + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last', activation='swish') + ]) + train_and_save(model, "Conv2D_padding_valid") + + # Dense + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.Dense(5, activation='sigmoid') + ]) + train_and_save(model, "Dense") + + # ELU + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.ELU(alpha=0.5) + ]) + train_and_save(model, "ELU") + + # Flatten + model = models.Sequential([ + layers.Input(shape=(4, 5)), + layers.Flatten() + ]) + train_and_save(model, "Flatten") + + # GlobalAveragePooling2D channels first + model = models.Sequential([ + layers.Input(shape=(3, 4, 6)), + layers.GlobalAveragePooling2D(data_format='channels_first') + ]) + train_and_save(model, "GlobalAveragePooling2D_channels_first") + + # GlobalAveragePooling2D channels last + model = models.Sequential([ + layers.Input(shape=(4, 6, 3)), + layers.GlobalAveragePooling2D(data_format='channels_last') + ]) + train_and_save(model, "GlobalAveragePooling2D_channels_last") + + # LayerNorm + model = models.Sequential([ + layers.Input(shape=(10, 3, 5)), + layers.LayerNormalization(axis=-1) + ]) + train_and_save(model, "LayerNorm") + + # LeakyReLU + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.LeakyReLU() + ]) + train_and_save(model, "LeakyReLU") + + # MaxPooling2D channels_first + model = models.Sequential([ + layers.Input(shape=(3, 8, 8)), + layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_first') + ]) + train_and_save(model, "MaxPool2D_channels_first") + + # MaxPooling2D channels_last + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last') + ]) + train_and_save(model, "MaxPool2D_channels_last") + + # Permute + model = models.Sequential([ + layers.Input(shape=(3, 4, 5)), + layers.Permute((2, 1, 3)) + ]) + train_and_save(model, "Permute") + + # Reshape + model = models.Sequential([ + layers.Input(shape=(4, 5)), + layers.Reshape((2, 10)) + ]) + train_and_save(model, "Reshape") + + # ReLU + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.ReLU() + ]) + train_and_save(model, "ReLU") + + # Softmax + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.Softmax() + ]) + train_and_save(model, "Softmax") + + # Layer Combination + + modelA = models.Sequential([ + layers.Input(shape=(32, 32, 3)), + layers.Conv2D(16, (3,3), padding='same', activation='swish'), + layers.AveragePooling2D((2,2), data_format='channels_last'), + layers.GlobalAveragePooling2D(data_format='channels_last'), + layers.Dense(10, activation='softmax'), + ]) + train_and_save(modelA, "Layer_Combination_1") + + modelB = models.Sequential([ + layers.Input(shape=(3, 32, 32)), + layers.Conv2D(8, (3,3), padding='valid', data_format='channels_first', activation='relu'), + layers.MaxPooling2D((2,2), data_format='channels_first'), + layers.Flatten(), + layers.Dense(128, activation='relu'), + layers.Reshape((16, 8)), + layers.Permute((2, 1)), + layers.Flatten(), + layers.Dense(32), + layers.LeakyReLU(alpha=0.1), + layers.Dense(10, activation='softmax'), + ]) + train_and_save(modelB, "Layer_Combination_2") + + modelC = models.Sequential([ + layers.Input(shape=(4, 8, 2)), + layers.Permute((2, 1, 3)), + layers.Reshape((8, 8, 1)), + layers.Conv2D(4, (3,3), padding='same', activation='relu'), + layers.AveragePooling2D((2,2)), + layers.BatchNormalization(), + layers.Flatten(), + layers.Dense(32, activation='elu'), + layers.Dense(8, activation='swish'), + layers.Dense(3, activation='softmax'), + ]) + train_and_save(modelC, "Layer_Combination_3") \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py new file mode 100644 index 0000000000000..834f9d0698163 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py @@ -0,0 +1,55 @@ +from cppyy import gbl as gbl_namespace +from .. import get_keras_version + +def MakeKerasBatchNorm(layer): + """ + Create a Keras-compatible batch normalization operation using SOFIE framework. + + This function takes a dictionary representing a batch normalization layer and its + attributes and constructs a Keras-compatible batch normalization operation using + the SOFIE framework. Batch normalization is used to normalize the activations of + a neural network, typically applied after the convolutional or dense layers. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + gamma, beta, moving mean, moving variance, epsilon, + momentum, data type (assumed to be float), and other relevant information. + + Returns: + ROperator_BatchNormalization: A SOFIE framework operator representing the batch normalization operation. + """ + + keras_version = get_keras_version() + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + gamma = attributes["gamma"] + beta = attributes["beta"] + moving_mean = attributes["moving_mean"] + moving_variance = attributes["moving_variance"] + fLayerDType = layer["layerDType"] + fNX = str(finput[0]) + fNY = str(foutput[0]) + + if keras_version < '2.16': + fNScale = gamma.name + fNB = beta.name + fNMean = moving_mean.name + fNVar = moving_variance.name + else: + fNScale = gamma.path + fNB = beta.path + fNMean = moving_mean.path + fNVar = moving_variance.path + + epsilon = attributes["epsilon"] + momentum = attributes["momentum"] + + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BatchNormalization('float')(epsilon, momentum, 0, fNX, fNScale, fNB, fNMean, fNVar, fNY) + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator BatchNormalization does not yet support input type " + fLayerDType + ) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py new file mode 100644 index 0000000000000..ff35fd2032653 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py @@ -0,0 +1,23 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasBinary(layer): + input = layer['layerInput'] + output = layer['layerOutput'] + fLayerType = layer['layerType'] + fLayerDType = layer['layerDType'] + fX1 = input[0] + fX2 = input[1] + fY = output[0] + op = None + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + if fLayerType == "Add": + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BasicBinary(float, gbl_namespace.TMVA.Experimental.SOFIE.EBasicBinaryOperator.Add)(fX1, fX2, fY) + elif fLayerType == "Subtract": + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BasicBinary(float, gbl_namespace.TMVA.Experimental.SOFIE.EBasicBinaryOperator.Sub)(fX1, fX2, fY) + else: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BasicBinary(float, gbl_namespace.TMVA.Experimental.SOFIE.EBasicBinaryOperator.Mul)(fX1, fX2, fY) + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator BasicBinary does not yet support input type " + fLayerDType + ) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py new file mode 100644 index 0000000000000..340aa4e9cb452 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py @@ -0,0 +1,17 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasConcat(layer): + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer["layerDType"] + attributes = layer['layerAttributes'] + input = [str(i) for i in finput] + output = str(foutput[0]) + axis = int(attributes["axis"]) + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Concat(input, axis, 0, output) + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Concat does not yet support input type " + fLayerDType + ) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py new file mode 100644 index 0000000000000..98fe21b1cc887 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py @@ -0,0 +1,73 @@ +from cppyy import gbl as gbl_namespace +import math +from .. import get_keras_version + +def MakeKerasConv(layer): + """ + Create a Keras-compatible convolutional layer operation using SOFIE framework. + + This function takes a dictionary representing a convolutional layer and its attributes and + constructs a Keras-compatible convolutional layer operation using the SOFIE framework. + A convolutional layer applies a convolution operation between the input tensor and a set + of learnable filters (kernels). + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + data type (must be float), weight and bias name, kernel size, dilations, padding and strides. + When padding is same (keep in the same dimensions), the padding shape is calculated. + + Returns: + ROperator_Conv: A SOFIE framework operator representing the convolutional layer operation. + """ + + keras_version = get_keras_version() + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + attributes = layer['layerAttributes'] + fWeightNames = layer["layerWeight"] + fKernelName = fWeightNames[0] + fBiasName = fWeightNames[1] + fAttrDilations = attributes["dilation_rate"] + fAttrGroup = int(attributes["groups"]) + fAttrKernelShape = attributes["kernel_size"] + fKerasPadding = str(attributes["padding"]) + fAttrStrides = attributes["strides"] + fAttrPads = [] + + if fKerasPadding == 'valid': + fAttrAutopad = 'VALID' + elif fKerasPadding == 'same': + fAttrAutopad = 'NOTSET' + if keras_version < '2.16': + fInputShape = attributes['_build_input_shape'] + else: + fInputShape = attributes['_build_shapes_dict']['input_shape'] + inputHeight = fInputShape[1] + inputWidth = fInputShape[2] + outputHeight = math.ceil(float(inputHeight) / float(fAttrStrides[0])) + outputWidth = math.ceil(float(inputWidth) / float(fAttrStrides[1])) + padding_height = max((outputHeight - 1) * fAttrStrides[0] + fAttrKernelShape[0] - inputHeight, 0) + padding_width = max((outputWidth - 1) * fAttrStrides[1] + fAttrKernelShape[1] - inputWidth, 0) + padding_top = math.floor(padding_height / 2) + padding_bottom = padding_height - padding_top + padding_left = math.floor(padding_width / 2) + padding_right = padding_width - padding_left + fAttrPads = [padding_top, padding_bottom, padding_left, padding_right] + else: + raise RuntimeError( + "TMVA::SOFIE - RModel Keras Parser doesn't yet supports Convolution layer with padding " + fKerasPadding + ) + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Conv['float'](fAttrAutopad, fAttrDilations, fAttrGroup, + fAttrKernelShape, fAttrPads, fAttrStrides, + fLayerInputName, fKernelName, fBiasName, + fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Conv does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py new file mode 100644 index 0000000000000..7e6e787a97095 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py @@ -0,0 +1,37 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasDense(layer): + """ + Create a Keras-compatible dense (fully connected) layer operation using SOFIE framework. + + This function takes a dictionary representing a dense layer and its attributes and + constructs a Keras-compatible dense (fully connected) layer operation using the SOFIE framework. + A dense layer applies a matrix multiplication between the input tensor and weight matrix, + and adds a bias term. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + layer weight names, and data type - must be float. + + Returns: + ROperator_Gemm: A SOFIE framework operator representing the dense layer operation. + """ + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + fWeightNames = layer["layerWeight"] + fKernelName = fWeightNames[0] + fBiasName = fWeightNames[1] + attr_alpha = 1.0 + attr_beta = 1.0 + attr_transA = 0 + attr_transB = 0 + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Gemm['float'](attr_alpha, attr_beta, attr_transA, attr_transB, fLayerInputName, fKernelName, fBiasName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Gemm does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py new file mode 100644 index 0000000000000..7a291117e837e --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py @@ -0,0 +1,35 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasELU(layer): + """ + Create a Keras-compatible exponential linear Unit (ELU) activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible ELU activation operation using the SOFIE framework. + ELU is an activation function that modifies only the negative part of ReLU by + applying an exponential curve. It allows small negative values instead of zeros. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type, which must be float. + + Returns: + ROperator_Elu: A SOFIE framework operator representing the ELU activation operation. + """ + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + attributes = layer['layerAttributes'] + if 'alpha' in attributes.keys(): + fAlpha = attributes['alpha'] + else: + fAlpha = 1.0 + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Elu('float')(fAlpha, fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Relu does not yet support input type " + fLayerDType + ) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py new file mode 100644 index 0000000000000..8b28382ebc4a0 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py @@ -0,0 +1,36 @@ +from cppyy import gbl as gbl_namespace +from .. import get_keras_version + +def MakeKerasFlatten(layer): + """ + Create a Keras-compatible flattening operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible flattening operation using the SOFIE framework. + Flattening is the process of converting a multi-dimensional tensor into a + one-dimensional tensor. Assumes layerDtype is float. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + name, data type, and other relevant information. + + Returns: + ROperator_Reshape: A SOFIE framework operator representing the flattening operation. + """ + + keras_version = get_keras_version() + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + if keras_version < '2.16': + flayername = attributes['_name'] + else: + flayername = attributes['name'] + fOpMode = gbl_namespace.TMVA.Experimental.SOFIE.ReshapeOpMode.Flatten + fLayerDType = layer['layerDType'] + fNameData = finput[0] + fNameOutput = foutput[0] + fNameShape = flayername + "ReshapeAxes" + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape(fOpMode, 0, fNameData, fNameShape, fNameOutput) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py new file mode 100644 index 0000000000000..4921a268e6a5d --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py @@ -0,0 +1,15 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasIdentity(layer): + input = layer['layerInput'] + output = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = input[0] + fLayerOutputName = output[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Identity('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Identity does not yet support input type " + fLayerDType + ) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py new file mode 100644 index 0000000000000..b10ce58d239a9 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py @@ -0,0 +1,62 @@ +from cppyy import gbl as gbl_namespace +from .. import get_keras_version + +def MakeKerasLayerNorm(layer): + """ + Create a Keras-compatible layer normalization operation using SOFIE framework. + + This function takes a dictionary representing a layer normalization layer and its + attributes and constructs a Keras-compatible layer normalization operation using + the SOFIE framework. Unlike Batch normalization, Layer normalization used to normalize + the activations of a layer across the entire layer, independently for each sample in + the batch. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + gamma, beta, epsilon, data type (assumed to be float), and other + relevant information. + + Returns: + ROperator_BatchNormalization: A SOFIE framework operator representing the layer normalization operation. + """ + + keras_version = get_keras_version() + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + gamma = attributes["gamma"] + beta = attributes["beta"] + axes = attributes['axis'] + if '_build_input_shape' in attributes.keys(): + num_input_shapes = len(attributes['_build_input_shape']) + elif '_build_shapes_dict' in attributes.keys(): + num_input_shapes = len(list(attributes['_build_shapes_dict']['input_shape'])) + if len(axes) == 1: + axis = axes[0] + if axis < 0: + axis += num_input_shapes + else: + raise Exception("TMVA.SOFIE - LayerNormalization layer - parsing different axes at once is not supported") + fLayerDType = layer["layerDType"] + fNX = str(finput[0]) + fNY = str(foutput[0]) + + if keras_version < '2.16': + fNScale = gamma.name + fNB = beta.name + else: + fNScale = gamma.path + fNB = beta.path + + epsilon = attributes["epsilon"] + fNInvStdDev = [] + + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_LayerNormalization('float')(axis, epsilon, 1, fNX, fNScale, fNB, fNY, "", fNInvStdDev) + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator BatchNormalization does not yet support input type " + fLayerDType + ) + + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py new file mode 100644 index 0000000000000..c0b95b04b27eb --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py @@ -0,0 +1,44 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasLeakyRelu(layer): + """ + Create a Keras-compatible Leaky ReLU activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible Leaky ReLU activation operation using the SOFIE framework. + Leaky ReLU is a variation of the ReLU activation function that allows small negative + values to pass through, introducing non-linearity while preventing "dying" neurons. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + attributes, and data type - must be float. + + Returns: + ROperator_LeakyRelu: A SOFIE framework operator representing the Leaky ReLU activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + attributes = layer['layerAttributes'] + + if 'alpha' in attributes.keys(): + fAlpha = float(attributes["alpha"]) + elif 'negative_slope' in attributes.keys(): + fAlpha = float(attributes['negative_slope']) + elif 'activation' in attributes.keys(): + fAlpha = 0.2 + else: + raise RuntimeError ( + "Failed to extract alpha value from LeakyReLU" + ) + + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_LeakyRelu('float')(fAlpha, fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator LeakyRelu does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py new file mode 100644 index 0000000000000..f43fc09ee0afe --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py @@ -0,0 +1,36 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasPermute(layer): + """ + Create a Keras-compatible permutation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible permutation operation using the SOFIE framework. + Permutation is an operation that rearranges the dimensions of a tensor based on + specified dimensions. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + attributes, and data type - must be float. + + Returns: + ROperator_Transpose: A SOFIE framework operator representing the permutation operation. + """ + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + attributes = layer['layerAttributes'] + fAttributePermute = list(attributes["dims"]) + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + if len(fAttributePermute) > 0: + fAttributePermute = [0] + fAttributePermute # for the batch dimension from the input + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttributePermute, fLayerInputName, fLayerOutputName) #gbl_namespace.TMVA.Experimental.SOFIE.fPermuteDims + else: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Transpose does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py new file mode 100644 index 0000000000000..364d2be8da147 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py @@ -0,0 +1,78 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasPooling(layer): + """ + Create a Keras-compatible pooling layer operation using SOFIE framework. + + This function takes a dictionary representing a pooling layer and its attributes and + constructs a Keras-compatible pooling layer operation using the SOFIE framework. + Pooling layers downsample the input tensor by selecting a representative value from + a group of neighboring values, either by taking the maximum or the average. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + layer type (the selection rule), the pool size, padding, strides, and data type. + + Returns: + ROperator_Pool: A SOFIE framework operator representing the pooling layer operation. + """ + + # Extract attributes from layer data + fLayerDType = layer['layerDType'] + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerType = layer['layerType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + pool_atrr = gbl_namespace.TMVA.Experimental.SOFIE.RAttributes_Pool() + attributes = layer['layerAttributes'] + # Set default values for GlobalAveragePooling2D + fAttrKernelShape = [] + fKerasPadding = 'valid' + fAttrStrides = [] + if fLayerType != 'GlobalAveragePooling2D': + fAttrKernelShape = attributes["pool_size"] + fKerasPadding = str(attributes["padding"]) + fAttrStrides = attributes["strides"] + + # Set default values + fAttrDilations = (1,1) + fpads = [0,0,0,0,0,0] + pool_atrr.ceil_mode = 0 + pool_atrr.count_include_pad = 0 + pool_atrr.storage_order = 0 + + if fKerasPadding == 'valid': + fAttrAutopad = 'VALID' + elif fKerasPadding == 'same': + fAttrAutopad = 'NOTSET' + else: + raise RuntimeError( + "TMVA::SOFIE - RModel Keras Parser doesn't yet support Pooling layer with padding " + fKerasPadding + ) + pool_atrr.dilations = list(fAttrDilations) + pool_atrr.strides = list(fAttrStrides) + pool_atrr.pads = fpads + pool_atrr.kernel_shape = list(fAttrKernelShape) + pool_atrr.auto_pad = fAttrAutopad + + # Choose pooling type + if 'Max' in fLayerType: + PoolMode = gbl_namespace.TMVA.Experimental.SOFIE.PoolOpMode.MaxPool + elif 'AveragePool' in fLayerType: + PoolMode = gbl_namespace.TMVA.Experimental.SOFIE.PoolOpMode.AveragePool + elif 'GlobalAverage' in fLayerType: + PoolMode = gbl_namespace.TMVA.Experimental.SOFIE.PoolOpMode.GloabalAveragePool + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator poolong does not yet support pooling type " + fLayerType + ) + + # Create operator + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Pool['float'](PoolMode, pool_atrr, fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Pooling does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py new file mode 100644 index 0000000000000..9da1407a8911d --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py @@ -0,0 +1,30 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasReLU(layer): + """ + Create a Keras-compatible rectified linear unit (ReLU) activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible ReLU activation operation using the SOFIE framework. + ReLU is a popular activation function that replaces all negative values in a tensor + with zero, while leaving positive values unchanged. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type, which must be float. + + Returns: + ROperator_Relu: A SOFIE framework operator representing the ReLU activation operation. + """ + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Relu('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Relu does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py new file mode 100644 index 0000000000000..c83822f43e080 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py @@ -0,0 +1,34 @@ +from cppyy import gbl as gbl_namespace +from .. import get_keras_version + +def MakeKerasReshape(layer): + """ + Create a Keras-compatible reshaping operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible reshaping operation using the SOFIE framework. Assumes layerDtype is float. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + name, data type, and other relevant information. + + Returns: + ROperator_Reshape: A SOFIE framework operator representing the reshaping operation. + """ + + keras_version = get_keras_version() + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + if keras_version < '2.16': + flayername = attributes['_name'] + else: + flayername = attributes['name'] + fOpMode = gbl_namespace.TMVA.Experimental.SOFIE.ReshapeOpMode.Reshape + fLayerDType = layer['layerDType'] + fNameData = finput[0] + fNameOutput = foutput[0] + fNameShape = flayername + "ReshapeAxes" + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape(fOpMode, 0, fNameData, fNameShape, fNameOutput) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py new file mode 100644 index 0000000000000..f2f3d628e0aed --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py @@ -0,0 +1,92 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasRNN(layer): + """ + Create a Keras-compatible RNN (Recurrent Neural Network) layer operation using SOFIE framework. + + This function takes a dictionary representing an RNN layer and its attributes and + constructs a Keras-compatible RNN layer operation using the SOFIE framework. + RNN layers are used to model sequences, and they maintain internal states that are + updated through recurrent connections. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + layer type, attributes, weights, and data type - must be float. + + Returns: + ROperator_RNN: A SOFIE framework operator representing the RNN layer operation. + """ + + # Extract required information from the layer dictionary + fLayerDType = layer['layerDType'] + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + direction = attributes['direction'] + hidden_size = attributes["hidden_size"] + layout = int(attributes["layout"]) + nameX = finput[0] + nameY = foutput[0] + nameW = layer["layerWeight"][0] + nameR = layer["layerWeight"][1] + if len(layer["layerWeight"]) > 2: + nameB = layer["layerWeight"][2] + else: + nameB = "" + + # Check if the provided activation function is supported + fPActivation = attributes['activation'] + if not fPActivation.__name__ in ['relu', 'sigmoid', 'tanh', 'softsign', 'softplus']: #avoiding functions with parameters + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator RNN does not yet support activation function " + fPActivation.__name__ + ) + + activations = [fPActivation.__name__[0].upper()+fPActivation.__name__[1:]] + + #set default values + activation_alpha = [] + activation_beta = [] + clip = 0.0 + nameY_h = "" + nameInitial_h = "" + name_seq_len = "" + + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + if layer['layerType'] == "SimpleRNN": + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_RNN['float'](activation_alpha, activation_beta, activations, clip, direction, hidden_size, layout, nameX, nameW, nameR, nameB, name_seq_len, nameInitial_h, nameY, nameY_h) + + elif layer['layerType'] == "GRU": + #an additional activation function is required, given by the user + activations.insert(0, attributes['recurrent_activation'].__name__[0].upper() + attributes['recurrent_activation'].__name__[1:]) + + #new variable needed: + linear_before_reset = attributes['linear_before_reset'] + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_GRU['float'](activation_alpha, activation_beta, activations, clip, direction, hidden_size, layout, linear_before_reset, nameX, nameW, nameR, nameB, name_seq_len, nameInitial_h, nameY, nameY_h) + + elif layer['layerType'] == "LSTM": + #an additional activation function is required, the first given by the user, the second set to tanh as default + fPRecurrentActivation = attributes['recurrent_activation'] + if not fPActivation.__name__ in ['relu', 'sigmoid', 'tanh', 'softsign', 'softplus']: #avoiding functions with parameters + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator RNN does not yet support recurrent activation function " + fPActivation.__name__ + ) + fPRecurrentActivationName = fPRecurrentActivation.__name__[0].upper()+fPRecurrentActivation.__name__[1:] + activations.insert(0,fPRecurrentActivationName) + activations.insert(2,'Tanh') + + #new variables needed: + input_forget = 0 + nameInitial_c = "" + nameP = "" #No peephole connections in keras LSTM model + nameY_c = "" + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_LSTM['float'](activation_alpha, activation_beta, activations, clip, direction, hidden_size, input_forget, layout, nameX, nameW, nameR, nameB, name_seq_len, nameInitial_h, nameInitial_c, nameP, nameY, nameY_h, nameY_c) + + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator RNN does not yet support operator type " + layer['layerType'] + ) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator RNN does not yet support input type " + fLayerDType + ) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py new file mode 100644 index 0000000000000..53349086440ec --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py @@ -0,0 +1,31 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasSeLU(layer): + """ + Create a Keras-compatible scaled exponential linear unit (SeLU) activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible SeLU activation operation using the SOFIE framework. + SeLU is a type of activation function that introduces self-normalizing properties + to the neural network. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type - must be float32. + + Returns: + ROperator_Selu: A SOFIE framework operator representing the SeLU activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Selu('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Selu does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py new file mode 100644 index 0000000000000..8d50032c53fdb --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py @@ -0,0 +1,31 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasSigmoid(layer): + """ + Create a Keras-compatible sigmoid activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible sigmoid activation operation using the SOFIE framework. + Sigmoid is a commonly used activation function that maps input values to the range + between 0 and 1, providing a way to introduce non-linearity in neural networks. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type - must be float. + + Returns: + ROperator_Sigmoid: A SOFIE framework operator representing the sigmoid activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Sigmoid('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py new file mode 100644 index 0000000000000..f00efc136b486 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py @@ -0,0 +1,32 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasSoftmax(layer): + """ + Create a Keras-compatible softmax activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible softmax activation operation using the SOFIE framework. + Softmax is an activation function that converts input values into a probability + distribution, often used in the output layer of a neural network for multi-class + classification tasks. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type - must be float. + + Returns: + ROperator_Softmax: A SOFIE framework operator representing the softmax activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Softmax('float')(-1, fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Softmax does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py new file mode 100644 index 0000000000000..43ae130d91c0f --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py @@ -0,0 +1,31 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasSwish(layer): + """ + Create a Keras-compatible swish activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible swish activation operation using the SOFIE framework. + Swish is an activation function that aims to combine the benefits of ReLU and sigmoid, + allowing some non-linearity while still keeping positive values unbounded. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type. + + Returns: + ROperator_Swish: A SOFIE framework operator representing the swish activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Swish('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Swish does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py new file mode 100644 index 0000000000000..4d9e62cd5da1d --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py @@ -0,0 +1,31 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasTanh(layer): + """ + Create a Keras-compatible hyperbolic tangent (tanh) activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible tanh activation operation using the SOFIE framework. + Tanh is an activation function that squashes input values to the range between -1 and 1, + introducing non-linearity in neural networks. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type - must be float. + + Returns: + ROperator_Tanh: A SOFIE framework operator representing the tanh activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Tanh('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Tanh does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py new file mode 100644 index 0000000000000..f916c9fd2a3b3 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py @@ -0,0 +1,542 @@ +from ......_pythonization import pythonization +from cppyy import gbl as gbl_namespace +import os +import time + +from .layers.permute import MakeKerasPermute +from .layers.batchnorm import MakeKerasBatchNorm +from .layers.layernorm import MakeKerasLayerNorm +from .layers.reshape import MakeKerasReshape +from .layers.flatten import MakeKerasFlatten +from .layers.concat import MakeKerasConcat +from .layers.swish import MakeKerasSwish +from .layers.binary import MakeKerasBinary +from .layers.softmax import MakeKerasSoftmax +from .layers.tanh import MakeKerasTanh +from .layers.identity import MakeKerasIdentity +from .layers.relu import MakeKerasReLU +from .layers.elu import MakeKerasELU +from .layers.selu import MakeKerasSeLU +from .layers.sigmoid import MakeKerasSigmoid +from .layers.leaky_relu import MakeKerasLeakyRelu +from .layers.pooling import MakeKerasPooling +from .layers.rnn import MakeKerasRNN +from .layers.dense import MakeKerasDense +from .layers.conv import MakeKerasConv + +from . import get_keras_version + +def MakeKerasActivation(layer): + attributes = layer['layerAttributes'] + activation = attributes['activation'] + fLayerActivation = str(activation.__name__) + + if fLayerActivation in mapKerasLayer.keys(): + return mapKerasLayer[fLayerActivation](layer) + else: + raise Exception("TMVA.SOFIE - parsing keras activation layer " + fLayerActivation + " is not yet supported") + +# Set global dictionaries, mapping layers to corresponding functions that create their ROperator instances +mapKerasLayer = {"Activation": MakeKerasActivation, + "Permute": MakeKerasPermute, + "BatchNormalization": MakeKerasBatchNorm, + "LayerNormalization": MakeKerasLayerNorm, + "Reshape": MakeKerasReshape, + "Flatten": MakeKerasFlatten, + "Concatenate": MakeKerasConcat, + "swish": MakeKerasSwish, + "silu": MakeKerasSwish, + "Add": MakeKerasBinary, + "Subtract": MakeKerasBinary, + "Multiply": MakeKerasBinary, + "Softmax": MakeKerasSoftmax, + "tanh": MakeKerasTanh, + # "Identity": MakeKerasIdentity, + # "Dropout": MakeKerasIdentity, + "ReLU": MakeKerasReLU, + "relu": MakeKerasReLU, + "ELU": MakeKerasELU, + "elu": MakeKerasELU, + "selu": MakeKerasSeLU, + "sigmoid": MakeKerasSigmoid, + "LeakyReLU": MakeKerasLeakyRelu, + "leaky_relu": MakeKerasLeakyRelu, + "softmax": MakeKerasSoftmax, + "MaxPooling2D": MakeKerasPooling, + "AveragePooling2D": MakeKerasPooling, + "GlobalAveragePooling2D": MakeKerasPooling, + # "SimpleRNN": MakeKerasRNN, + # "GRU": MakeKerasRNN, + # "LSTM": MakeKerasRNN, + } + +mapKerasLayerWithActivation = {"Dense": MakeKerasDense,"Conv2D": MakeKerasConv} + +def add_layer_into_RModel(rmodel, layer_data): + """ + Add a Keras layer operation to an existing RModel using the SOFIE framework. + + This function takes an existing RModel and a dictionary representing a Keras layer + and its attributes, and adds the corresponding layer operation to the RModel using + the SOFIE framework. The function supports various types of Keras layers, including + those with or without activation functions. + + Parameters: + rmodel (RModel): An existing RModel to which the layer operation will be added. + layer_data (dict): A dictionary containing layer information including type, + attributes, input, output, and layer data type. + + Returns: + RModel: The updated RModel after adding the layer operation. + + Raises exception: If the provided layer type or activation function is not supported. + """ + + import numpy as np + + keras_version = get_keras_version() + + fLayerType = layer_data['layerType'] + + # reshape and flatten layers don't have weights, but they are needed inside the list of initialized + # tensor list in the Rmodel + if fLayerType == "Reshape" or fLayerType == "Flatten": + Attributes = layer_data['layerAttributes'] + if keras_version < '2.16': + LayerName = Attributes['_name'] + else: + LayerName = Attributes['name'] + + if fLayerType == "Reshape": + TargetShape = np.asarray(Attributes['target_shape']).astype("int") + TargetShape = np.insert(TargetShape,0,0) + else: + if '_build_input_shape' in Attributes.keys(): + input_shape = Attributes['_build_input_shape'] + elif '_build_shapes_dict' in Attributes.keys(): + input_shape = list(Attributes['_build_shapes_dict']['input_shape']) + else: + raise RuntimeError ( + "Failed to extract build input shape from " + fLayerType + " layer" + ) + TargetShape = [ gbl_namespace.TMVA.Experimental.SOFIE.ConvertShapeToLength(input_shape[1:])] + TargetShape = np.asarray(TargetShape) + + # since the AddInitializedTensor method in RModel requires unique pointer, we call a helper function + # in c++ that does the conversion from a regular pointer to unique one in c++ + rmodel.AddInitializedTensor['long'](LayerName+"ReshapeAxes", [len(TargetShape)], TargetShape) + + # These layers only have one operator - excluding the recurrent layers, in which the activation function(s) + # are included in the recurrent operator + if fLayerType in mapKerasLayer.keys(): + Attributes = layer_data['layerAttributes'] + inputs = layer_data['layerInput'] + outputs = layer_data['layerOutput'] + if keras_version < '2.16': + LayerName = Attributes['_name'] + else: + LayerName = Attributes['name'] + + # Convoltion/Pooling layers in keras by default assume the channels dimension is the + # last one, while in onnx (and the SOFIE's RModel) it is the first one (other than batch + # size), so a transpose is needed before and after the pooling, if the data format is + # channels last (can be set to channels first by the user). In case of MaxPool2D and + # Conv2D (with linear activation) channels last, the transpose layers are added as: + + # input output + # transpose layer input_layer_name layer_name + PreTrans + # actual layer layer_name + PreTrans layer_name + PostTrans + # transpose layer layer_name + PostTrans output_layer_name + + fLayerOutput = outputs[0] + if fLayerType == 'GlobalAveragePooling2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0, 3, 1, 2], inputs[0], LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName+"PreTrans" + outputs[0] = LayerName+"Squeeze" + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape( + gbl_namespace.TMVA.Experimental.SOFIE.ReshapeOpMode.Squeeze, + [2, 3], + LayerName + "Squeeze", + fLayerOutput + ) + rmodel.AddOperatorReference(op) + + # Similar case is with Batchnorm, ONNX assumes that the 'axis' is always 1, but Keras + # gives the user the choice of specifying it. So, we have to transpose the input layer + # as 'axis' as the first dimension, apply the BatchNormalization operator and then + # again tranpose it to bring back the original dimensions + elif fLayerType == 'BatchNormalization': + if '_build_input_shape' in Attributes.keys(): + num_input_shapes = len(Attributes['_build_input_shape']) + elif '_build_shapes_dict' in Attributes.keys(): + num_input_shapes = len(list(Attributes['_build_shapes_dict']['input_shape'])) + + axis = Attributes['axis'] + axis = axis[0] if isinstance(axis, list) else axis + if axis < 0: + axis += num_input_shapes + fAttrPerm = list(range(0, num_input_shapes)) + fAttrPerm[1] = axis + fAttrPerm[axis] = 1 + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttrPerm, inputs[0], + LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName + "PreTrans" + outputs[0] = LayerName + "PostTrans" + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttrPerm, LayerName+"PostTrans", + fLayerOutput) + rmodel.AddOperatorReference(op) + + elif fLayerType == 'MaxPooling2D' or fLayerType == 'AveragePooling2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,3,1,2], inputs[0], + LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName+"PreTrans" + outputs[0] = LayerName+"PostTrans" + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,2,3,1], + LayerName+"PostTrans", fLayerOutput) + rmodel.AddOperatorReference(op) + + else: + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + + return rmodel + + # These layers require two operators - dense/conv and their activation function + elif fLayerType in mapKerasLayerWithActivation.keys(): + Attributes = layer_data['layerAttributes'] + if keras_version < '2.16': + LayerName = Attributes['_name'] + else: + LayerName = Attributes['name'] + fPActivation = Attributes['activation'] + LayerActivation = fPActivation.__name__ + if LayerActivation in ['selu', 'sigmoid']: + rmodel.AddNeededStdLib("cmath") + + # if there is an activation function after the layer + if LayerActivation != 'linear': + if not LayerActivation in mapKerasLayer.keys(): + raise Exception("TMVA.SOFIE - parsing keras activation function " + LayerActivation + " is not yet supported") + outputs = layer_data['layerOutput'] + inputs = layer_data['layerInput'] + fActivationLayerOutput = outputs[0] + + # like pooling, convolutional layer from keras requires transpose before and after to match + # the onnx format + # if the data format is channels last (can be set to channels first by the user). + if fLayerType == 'Conv2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,3,1,2], inputs[0], LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName+"PreTrans" + layer_data["layerInput"] = inputs + outputs[0] = LayerName+fLayerType + layer_data['layerOutput'] = outputs + op = mapKerasLayerWithActivation[fLayerType](layer_data) + rmodel.AddOperatorReference(op) + Activation_layer_input = LayerName+fLayerType + if fLayerType == 'Conv2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,2,3,1], LayerName+fLayerType, LayerName+"PostTrans") + rmodel.AddOperatorReference(op) + Activation_layer_input = LayerName + "PostTrans" + + # Adding the activation function + inputs[0] = Activation_layer_input + outputs[0] = fActivationLayerOutput + layer_data['layerInput'] = inputs + layer_data['layerOutput'] = outputs + + rmodel.AddOperatorReference(mapKerasLayer[LayerActivation](layer_data)) + + else: # if layer is conv and the activation is linear, we need to add transpose before and after + if fLayerType == 'Conv2D': + inputs = layer_data['layerInput'] + outputs = layer_data['layerOutput'] + fLayerOutput = outputs[0] + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,3,1,2], inputs[0], LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName+"PreTrans" + layer_data['layerInput'] = inputs + outputs[0] = LayerName+"PostTrans" + rmodel.AddOperatorReference(mapKerasLayerWithActivation[fLayerType](layer_data)) + if fLayerType == 'Conv2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,2,3,1], LayerName+"PostTrans", fLayerOutput) + rmodel.AddOperatorReference(op) + return rmodel + else: + raise Exception("TMVA.SOFIE - parsing keras layer " + fLayerType + " is not yet supported") + +class RModelParser_Keras: + + def Parse(filename, batch_size=1): # If a model does not have a defined batch size, then assuming it is 1 + + # TensoFlow/Keras is too fragile to import unconditionally. As its presence might break several ROOT + # usecases and importing keras globally will slow down importing ROOT, which is not desired. For this, + # we import keras within the functions instead of importing it at the start of the file (i.e. globally). + # So, whenever the parser function is called, only then keras will be imported, and not everytime we + # import ROOT. Also, we can import keras in multiple functions as many times as we want since Python + # caches the imported packages. + + import keras + import numpy as np + + keras_version = get_keras_version() + + #Check if file exists + if not os.path.exists(filename): + raise RuntimeError("Model file {} not found!".format(filename)) + + # load model + keras_model = keras.models.load_model(filename) + keras_model.load_weights(filename) + + # create new RModel object + sep = '/' + if os.name == 'nt': + sep = '\\' + + isep = filename.rfind(sep) + filename_nodir = filename + if isep != -1: + filename_nodir = filename[isep+1:] + + ttime = time.time() + gmt_time = time.gmtime(ttime) + parsetime = time.asctime(gmt_time) + + rmodel = gbl_namespace.TMVA.Experimental.SOFIE.RModel.RModel(filename_nodir, parsetime) + + # iterate over the layers and add them to the RModel + # in case of keras 3.x (particularly in sequential models), the layer input and output name conventions are + # different from keras 2.x. In keras 2.x, the layer input name is consistent with previous layer's output + # name. For e.g., if the sequence of layers is dense -> maxpool, the input and output layer names would be: + # layer | name + # input dense | keras_tensor_1 + # output dense | keras_tensor_2 -- + # | |=> layer names match + # input maxpool | keras_tensor_2 -- + # output maxpool | keras_tensor_3 + # + # but in case of keras 3.x, this changes. + # layer | name + # input dense | keras_tensor_1 + # output dense | keras_tensor_2 -- + # | |=> different layer names + # input maxpool | keras_tensor_3 -- + # output maxpool | keras_tensor_4 + # + # hence, we need to add a custom layer iterator, which would replace the suffix of the layer's input + # and output names + layer_iter = 0 + is_functional_model = True if keras_model.__class__.__name__ == 'Functional' else False + + for layer in keras_model.layers: + layer_data={} + layer_data['layerType']=layer.__class__.__name__ + layer_data['layerAttributes']=layer.__dict__ + if keras_version < '2.16' or is_functional_model: + if 'input_layer' in layer.name: + layer_data['layerInput'] = layer.name + else: + layer_data['layerInput']=[x.name for x in layer.input] if isinstance(layer.input,list) else [layer.input.name] + else: + if 'input_layer' in layer.input.name: + layer_data['layerInput'] = [layer.input.name] + else: + input_layer_name = layer.input.name[:13] + str(layer_iter) + layer_data['layerInput'] = [input_layer_name] + if keras_version < '2.16' or is_functional_model: + layer_data['layerOutput']=[x.name for x in layer.output] if isinstance(layer.output,list) else [layer.output.name] + else: + output_layer_name = layer.output.name[:13] + str(layer_iter+1) + layer_data['layerOutput']=[x.name for x in layer.output] if isinstance(layer.output,list) else [output_layer_name] + layer_iter += 1 + + fLayerType = layer_data['layerType'] + layer_data['layerDType'] = layer.dtype + + if len(layer.weights) > 0: + if keras_version < '2.16': + layer_data['layerWeight'] = [x.name for x in layer.weights] + else: + layer_data['layerWeight'] = [x.path for x in layer.weights] + else: + layer_data['layerWeight'] = [] + + # for convolutional and pooling layers we need to know the format of the data + if layer_data['layerType'] in ['Conv2D', 'MaxPooling2D', 'AveragePooling2D', 'GlobalAveragePooling2D']: + layer_data['channels_last'] = True if layer.data_format == 'channels_last' else False + + # for recurrent type layers we need to extract additional unique information + if layer_data['layerType'] in ["SimpleRNN", "LSTM", "GRU"]: + layer_data['layerAttributes']['activation'] = layer.activation + layer_data['layerAttributes']['direction'] = 'backward' if layer.go_backwards else 'forward' + layer_data['layerAttributes']["units"] = layer.units + layer_data['layerAttributes']["layout"] = layer.input.shape[0] is None + layer_data['layerAttributes']["hidden_size"] = layer.output.shape[-1] + + # for GRU and LSTM we need to extract an additional activation function + if layer_data['layerType'] != "SimpleRNN": + layer_data['layerAttributes']['recurrent_activation'] = layer.recurrent_activation + + # for GRU there are two variants of the reset gate location, we need to know which one is it + if layer_data['layerType'] == "GRU": + layer_data['layerAttributes']['linear_before_reset'] = 1 if layer.reset_after and layer.recurrent_activation.__name__ == "sigmoid" else 0 + + # Ignoring the input layer of the model + if(fLayerType == "InputLayer"): + continue; + + # Adding any required routines depending on the Layer types for generating inference code. + if (fLayerType == "Dense"): + rmodel.AddBlasRoutines({"Gemm", "Gemv"}) + elif (fLayerType == "BatchNormalization"): + rmodel.AddBlasRoutines({"Copy", "Axpy"}) + elif (fLayerType == "Conv1D" or fLayerType == "Conv2D" or fLayerType == "Conv3D"): + rmodel.AddBlasRoutines({"Gemm", "Axpy"}) + rmodel = add_layer_into_RModel(rmodel, layer_data) + + # Extracting model's weights + weight = [] + for idx in range(len(keras_model.get_weights())): + weightProp = {} + if keras_version < '2.16': + weightProp['name'] = keras_model.weights[idx].name + else: + weightProp['name'] = keras_model.weights[idx].path + weightProp['dtype'] = keras_model.get_weights()[idx].dtype.name + if 'conv' in weightProp['name'] and keras_model.weights[idx].shape.ndims == 4: + weightProp['value'] = keras_model.get_weights()[idx].transpose((3, 2, 0, 1)).copy() + else: + weightProp['value'] = keras_model.get_weights()[idx] + weight.append(weightProp) + + # Traversing through all the Weight tensors + for weightIter in range(len(weight)): + fWeightTensor = weight[weightIter] + fWeightName = fWeightTensor['name'] + fWeightDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fWeightTensor['dtype']) + fWeightTensorValue = fWeightTensor['value'] + fWeightTensorSize = 1 + fWeightTensorShape = [] + + #IS IT BATCH SIZE? CHECK ONNX + if 'simple_rnn' in fWeightName or 'lstm' in fWeightName or ('gru' in fWeightName and not 'bias' in fWeightName): + fWeightTensorShape.append(1) + + # Building the shape vector and finding the tensor size + for j in range(len(fWeightTensorValue.shape)): + fWeightTensorShape.append(fWeightTensorValue.shape[j]) + fWeightTensorSize *= fWeightTensorValue.shape[j] + + if fWeightDType == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + fWeightArray = fWeightTensorValue + + # weights conversion format between keras and onnx for lstm: the order of the different + # elements (input, output, forget, cell) inside the vector/matrix is different + if 'lstm' in fWeightName: + if 'kernel' in fWeightName: + units = int(fWeightArray.shape[1]/4) + W_i = fWeightArray[:, :units].copy() + W_f = fWeightArray[:, units: units * 2].copy() + W_c = fWeightArray[:, units * 2: units * 3].copy() + W_o = fWeightArray[:, units * 3:].copy() + fWeightArray[:, units: units * 2] = W_o + fWeightArray[:, units * 2: units * 3] = W_f + fWeightArray[:, units * 3:] = W_c + else: #bias + units = int(fWeightArray.shape[0]/4) + W_i = fWeightArray[:units].copy() + W_f = fWeightArray[units: units * 2].copy() + W_c = fWeightArray[units * 2: units * 3].copy() + W_o = fWeightArray[units * 3:].copy() + fWeightArray[units: units * 2] = W_o + fWeightArray[units * 2: units * 3] = W_f + fWeightArray[units * 3:] = W_c + + # need to make specific adjustments for recurrent weights and biases + if ('simple_rnn' in fWeightName or 'lstm' in fWeightName or 'gru' in fWeightName): + # reshaping weight matrices for recurrent layers due to keras-onnx inconsistencies + if 'kernel' in fWeightName: + fWeightArray = np.transpose(fWeightArray) + fWeightTensorShape[1], fWeightTensorShape[2] = fWeightTensorShape[2], fWeightTensorShape[1] + + fData = fWeightArray.flatten() + + # the recurrent bias and the cell bias can be the same, in which case we need to add a + # vector of zeros for the recurrent bias + if 'bias' in fWeightName and len(fData.shape) == 1: + fWeightTensorShape[1] *= 2 + fRbias = fData.copy()*0 + fData = np.concatenate((fData,fRbias)) + + else: + fData = fWeightArray.flatten() + rmodel.AddInitializedTensor['float'](fWeightName, fWeightTensorShape, fData) + else: + raise TypeError("Type error: TMVA SOFIE does not yet support data layer type: " + fWeightDType) + + # Extracting input tensor info + if keras_version < '2.16': + fPInputs = keras_model.input_names + else: + fPInputs = [x.name for x in keras_model.inputs] + + fPInputShape = keras_model.input_shape if isinstance(keras_model.input_shape, list) else [keras_model.input_shape] + fPInputDType = [] + for idx in range(len(keras_model.inputs)): + dtype = keras_model.inputs[idx].dtype.__str__() + if dtype == "float32": + fPInputDType.append(dtype) + else: + fPInputDType.append(dtype[9:-2]) + + if len(fPInputShape) == 1: + fInputName = fPInputs[0] + fInputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fPInputDType[0]) + if fInputDType == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + if fPInputShape[0][0] is None or fPInputShape[0][0] <= 0: + fPInputShape = list(fPInputShape[0]) + fPInputShape[0] = batch_size + rmodel.AddInputTensorInfo(fInputName, gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT, fPInputShape) + rmodel.AddInputTensorName(fInputName) + else: + raise TypeError("Type error: TMVA SOFIE does not yet support data type " + gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType)) + else: + # Iterating through multiple input tensors + for fInputName, fInputDType, fInputShapeTuple in zip(fPInputs, fPInputDType, fPInputShape): + fInputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType) + if fInputDType == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + if fInputShapeTuple[0] is None or fInputShapeTuple[0] <= 0: + fInputShapeTuple = list(fInputShapeTuple) + fInputShapeTuple[0] = batch_size + rmodel.AddInputTensorInfo(fInputName, gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT, fInputShapeTuple) + rmodel.AddInputTensorName(fInputName) + else: + raise TypeError("Type error: TMVA SOFIE does not yet support data type " + gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType)) + + # Adding OutputTensorInfos + outputNames = [] + if keras_version < '2.16' or is_functional_model: + for layerName in keras_model.output_names: + final_layer = keras_model.get_layer(layerName) + output_layer_name = final_layer.output.name + outputNames.append(output_layer_name) + else: + final_layer = keras_model.outputs[-1] + output_layer_name = final_layer.name[:13] + str(layer_iter) + outputNames.append(output_layer_name) + rmodel.AddOutputTensorNameList(outputNames) + return rmodel + \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py new file mode 100644 index 0000000000000..d9d400c95a53c --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py @@ -0,0 +1,96 @@ +import ROOT + +''' +The test file contains two types of functions: + is_accurate: + - This function checks whether the inference results from SOFIE and Keras are accurate within a specified + tolerance. Since the inference result from Keras is not flattened, the function flattens both tensors before + performing the comparison. + + generate_and_test_inference: + - This function accepts the following inputs: + - Model file path: Path to the input model. + - Destination directory for the generated header file: If set to None, the header file will be generated in + the model's directory. + - Batch size. + - After generating the inference code, we instantiate the session for inference. To validate the results from + SOFIE, we compare the outputs from both SOFIE and Keras. + - Load the Keras model. + - Extract the input dimensions of the Keras model to avoid hardcoding. + - For Sequential models or functional models with a single input: + - Extract the model's input specification and create a NumPy array of ones with the same shape as the + model's input specification, replacing None with the batch size. This becomes the input tensor. + - For functional models with multiple inputs: + - Extract the dimensions for each input, set the batch size, create a NumPy array of ones for each input, + and append each tensor to the list of input tensors. + - These input tensors are then fed to both the instantiated session object and the Keras model. + - Verify the output tensor dimensions: + Since SOFIE always flattens the output tensors before returning them, we need to extract the output tensor + shape from the model object. + - Convert the inference results to NumPy arrays: + The SOFIE result is of type vector, and the Keras result is a TensorFlow tensor. Both are converted to + NumPy arrays before being passed to the is_accurate function for comparison. + +''' + +def is_accurate(tensor_a, tensor_b, tolerance=1e-3): + tensor_a = tensor_a.flatten() + tensor_b = tensor_b.flatten() + for i in range(len(tensor_a)): + difference = abs(tensor_a[i] - tensor_b[i]) + if difference > tolerance: + print(tensor_a[i], tensor_b[i]) + return False + return True + +def generate_and_test_inference(model_file_path: str, generated_header_file_dir: str = None, batch_size=1): + + import tensorflow as tf + import keras + import numpy as np + + print("Tensorflow version: ", tf.__version__) + print("Keras version: ", keras.__version__) + print("Numpy version:", np.__version__) + + model_name = model_file_path[model_file_path.rfind('/')+1:].removesuffix(".h5") + rmodel = ROOT.TMVA.Experimental.SOFIE.RModelParser_Keras.Parse(model_file_path, batch_size) + if generated_header_file_dir is None: + last_idx = model_file_path.rfind("/") + if last_idx == -1: + generated_header_file_dir = "./" + else: + generated_header_file_dir = model_file_path[:last_idx] + generated_header_file_path = generated_header_file_dir + "/" + model_name + ".hxx" + print(f"Generating inference code for the Keras model from {model_file_path} in the header {generated_header_file_path}") + rmodel.Generate() + rmodel.OutputGenerated(generated_header_file_path) + print(f"Compiling SOFIE model {model_name}") + compile_status = ROOT.gInterpreter.Declare(f'#include "{generated_header_file_path}"') + if not compile_status: + raise AssertionError(f"Error compiling header file {generated_header_file_path}") + sofie_model_namespace = getattr(ROOT, "TMVA_SOFIE_" + model_name) + inference_session = sofie_model_namespace.Session(generated_header_file_path.removesuffix(".hxx") + ".dat") + keras_model = keras.models.load_model(model_file_path) + keras_model.load_weights(model_file_path) + if len(keras_model.inputs) == 1: + input_shape = list(keras_model.inputs[0].shape) + input_shape[0] = batch_size + input_tensors = np.ones(input_shape, dtype='float32') + else: + input_tensors = [] + for model_input in keras_model.inputs: + input_shape = list(model_input.shape) + input_shape[0] = batch_size + input_tensors.append(np.ones(input_shape, dtype='float32')) + sofie_inference_result = inference_session.infer(*input_tensors) + sofie_output_tensor_shape = list(rmodel.GetTensorShape(rmodel.GetOutputTensorNames()[0])) # get output shape + # from SOFIE + keras_inference_result = keras_model(input_tensors) + if sofie_output_tensor_shape != list(keras_inference_result.shape): + raise AssertionError("Output tensor dimensions from SOFIE and Keras do not match") + sofie_inference_result = np.asarray(sofie_inference_result) + keras_inference_result = np.asarray(keras_inference_result) + is_inference_accurate = is_accurate(sofie_inference_result, keras_inference_result) + if not is_inference_accurate: + raise AssertionError("Inference results from SOFIE and Keras do not match") \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/test/CMakeLists.txt b/bindings/pyroot/pythonizations/test/CMakeLists.txt index 539316700e149..3b8271cda1378 100644 --- a/bindings/pyroot/pythonizations/test/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/test/CMakeLists.txt @@ -136,6 +136,13 @@ if (tmva) endif() endif() +# SOFIE Keras Parser +if (tmva) + if(NOT MSVC OR CMAKE_SIZEOF_VOID_P EQUAL 4 OR win_broken_tests) + ROOT_ADD_PYUNITTEST(pyroot_pyz_sofie_keras_parser sofie_keras_parser.py) + endif() +endif() + # RTensor pythonizations if (tmva AND dataframe) ROOT_ADD_PYUNITTEST(pyroot_pyz_rtensor rtensor.py PYTHON_DEPS numpy) diff --git a/bindings/pyroot/pythonizations/test/sofie_keras_parser.py b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py new file mode 100644 index 0000000000000..f94697761d44b --- /dev/null +++ b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py @@ -0,0 +1,74 @@ +import unittest +import os +import shutil + +from ROOT._pythonization._tmva._sofie._parser._keras.parser_test_function import generate_and_test_inference +from ROOT._pythonization._tmva._sofie._parser._keras.generate_keras_functional import generate_keras_functional +from ROOT._pythonization._tmva._sofie._parser._keras.generate_keras_sequential import generate_keras_sequential + + +def make_testname(test_case: str): + test_case_name = test_case.replace("_", " ").removesuffix(".h5") + return test_case_name + +models = [ + "AveragePooling2D_channels_first", + "AveragePooling2D_channels_last", + "BatchNorm", + "Conv2D_channels_first", + "Conv2D_channels_last", + "Conv2D_padding_same", + "Conv2D_padding_valid", + "Dense", + "ELU", + "Flatten", + "GlobalAveragePooling2D_channels_first", + "GlobalAveragePooling2D_channels_last", + # "GRU", + "LayerNorm", + "LeakyReLU", + # "LSTM", + "MaxPool2D_channels_first", + "MaxPool2D_channels_last", + "Permute", + "ReLU", + "Reshape", + # "SimpleRNN", + "Softmax", +] + ([f"Activation_layer_{activation_function.capitalize()}" for activation_function in + ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']] + + + [f"Layer_Combination_{i}" for i in range(1, 4)]) + +class SOFIE_Keras_Parser(unittest.TestCase): + + def setUp(self): + base_dir = self._testMethodName[5:] + if os.path.isdir(base_dir): + shutil.rmtree(base_dir) + os.makedirs(base_dir + "/input_models") + os.makedirs(base_dir + "/generated_header_files_dir") + + def run_model_tests(self, model_type: str, generate_function, model_list): + generate_function(f"{model_type}/input_models") + for keras_model in model_list: + keras_model_name = f"{model_type.capitalize()}_{keras_model}_test.h5" + keras_model_path = f"{model_type}/input_models/" + keras_model_name + with self.subTest(msg=make_testname(keras_model_name)): + generate_and_test_inference(keras_model_path, f"{model_type}/generated_header_files_dir") + + def test_sequential(self): + sequential_models = models + self.run_model_tests("sequential", generate_keras_sequential, sequential_models) + + def test_functional(self): + functional_models = models + ["Add", "Concat", "Multiply", "Subtract"] + self.run_model_tests("functional", generate_keras_functional, functional_models) + + @classmethod + def tearDownClass(self): + shutil.rmtree("sequential") + shutil.rmtree("functional") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tmva/pymva/inc/TMVA/MethodPyKeras.h b/tmva/pymva/inc/TMVA/MethodPyKeras.h index 3318539d9d91c..8695cf45d2585 100644 --- a/tmva/pymva/inc/TMVA/MethodPyKeras.h +++ b/tmva/pymva/inc/TMVA/MethodPyKeras.h @@ -5,7 +5,7 @@ * Project: TMVA - a Root-integrated toolkit for multivariate data analysis * * Package: TMVA * * Class : MethodPyKeras * - * * + * * * * * Description: * * Interface for Keras python package which is a wrapper for the Theano and * @@ -20,7 +20,7 @@ * * * Redistribution and use in source and binary forms, with or without * * modification, are permitted according to the terms listed in LICENSE * - * (see tmva/doc/LICENSE) * + * (see tmva/doc/LICENSE) * **********************************************************************************/ #ifndef ROOT_TMVA_MethodPyKeras diff --git a/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx b/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx index f98ce201d400d..e8b5305872972 100644 --- a/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx @@ -331,6 +331,7 @@ public: out << SP << "}\n"; } +<<<<<<< HEAD // if (!fNB.empty()) { // std::string bias = "tensor_" + (fNBroadcastedB.empty() ? fNB : fNBroadcastedB); // out << SP << "// Add the bias to Y\n"; @@ -340,6 +341,82 @@ public: // out << SP << "BLAS::saxpy_(&" << opName << "_n, &" << opName << "_alpha, " << bias << ", &"; // out << opName << "_inc, " << "tensor_" << fNY << ", &" << opName << "_inc);\n"; // } +======= + if (!fNCastedX.empty()) { + out << "// NormalizedX = InvStdDev * (CastedX - Mean)\n"; + for (size_t i = 0; i < fAxis; i++) { + std::string iIdx = "axis_" + std::to_string(i); + out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] + << "; " << iIdx << "++){\n"; + } + for (size_t j = fAxis; j < fSize; j++) { + std::string jIdx = "axis_" + std::to_string(j); + out << SP << SP << "for (size_t " << jIdx << " = 0; " << jIdx << " < " << inputShape[j] + << "; " << jIdx << "++){\n"; + } + out << SP << SP << SP << "tensor_" << fNNormalizedX << "[" << InputIndex << "] = tensor_"; + out << fNInvStdDev << "[" << axesIndex << "] * (tensor_" << fNCastedX << "[" << InputIndex; + out << "] - tensor_" << fNMean << "[" << axesIndex << "])\n"; + for (size_t j = fAxis; j < fSize; j++) { + out << SP << SP << "}\n"; + } + for (size_t i = 0; i < fAxis; i++) { + out << SP << "}\n"; + } + out << "// Y = Scale o NormalizedX"; + for (size_t i = 0; i < fAxis; i++) { + std::string iIdx = "axis_" + std::to_string(i); + out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] + << "; " << iIdx << "++){\n"; + } + for (size_t j = fAxis; j < fSize; j++) { + std::string jIdx = "axis_" + std::to_string(j); + out << SP << SP << "for (size_t " << jIdx << " = 0; " << jIdx << " < " << inputShape[j] + << "; " << jIdx << "++){\n"; + } + out << SP << SP << SP << "tensor_" << fNY << "[" << InputIndex << "] = tensor_" << fNScale; + out << "[" << axesIndex << "] * static_cast<" << fType << ">(tensor_" << fNCastedX << "[" << InputIndex; + out << "]);\n"; + for (size_t j = fAxis; j < fSize; j++) { + out << SP << SP << "}\n"; + } + for (size_t i = 0; i < fAxis; i++) { + out << SP << "}\n"; + } + } else { + out << SP << "// Y = Scale o InvStdDev (X - Mean)\n"; + for (size_t i = 0; i < fAxis; i++) { + std::string iIdx = "axis_" + std::to_string(i); + out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] + << "; " << iIdx << "++){\n"; + } + for (size_t j = fAxis; j < fSize; j++) { + std::string jIdx = "axis_" + std::to_string(j); + out << SP << SP << "for (size_t " << jIdx << " = 0; " << jIdx << " < " << inputShape[j] + << "; " << jIdx << "++){\n"; + } + out << SP << SP << SP << "tensor_" << fNY << "[" << InputIndex << "] = tensor_" << fNScale; + out << "[" << normalizedIndex << "] * tensor_" << fNInvStdDev << "[" << axesIndex; + out << "] * (tensor_" << fNX << "[" << InputIndex << "] - tensor_" << fNMean << "["; + out << axesIndex << "]);\n"; + for (size_t j = fAxis; j < fSize; j++) { + out << SP << SP << "}\n"; + } + for (size_t i = 0; i < fAxis; i++) { + out << SP << "}\n"; + } + } + + if (!fNB.empty()) { + std::string bias = "tensor_" + (fNBroadcastedB.empty() ? fNB : fNBroadcastedB); + out << SP << "// Add the bias to Y\n"; + out << SP << "int " << opName << "_n = " << fLength << ";\n"; + out << SP << "float " << opName << "_alpha = 1.;\n"; + out << SP << "int " << opName << "_inc = 1;\n"; + out << SP << "BLAS::saxpy_(&" << opName << "_n, &" << opName << "_alpha, " << bias << ", &"; + out << opName << "_inc, " << "tensor_" << fNY << ", &" << opName << "_inc);\n"; + } +>>>>>>> dcbb6bff294 (New Keras parser - added support for LayerNorm, BatchNorm ND, ELU layers and added tests for them. Imported Keras within the required functions. Created new CMakeLists.txt file for the keras parser. Made changes in the pythonization CMake file to build the keras parser files) return out.str(); } diff --git a/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx b/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx index a3ed28c4860bc..22ddefbd1919e 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx @@ -70,6 +70,7 @@ public: fAttrAxes(attrAxes) { assert(fOpMode == Squeeze || fOpMode == Unsqueeze); + fOutputTensorNames = { fNOutput }; } // output type is same as input diff --git a/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h b/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h index 7e9618306ba74..2cb09cf3b36da 100644 --- a/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h +++ b/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h @@ -4,7 +4,7 @@ /********************************************************************************** * Project: TMVA - a Root-integrated toolkit for multivariate data analysis * * Package: TMVA * - * * + * * * * * Description: * * Functionality for parsing a saved Keras .H5 model into RModel object * @@ -18,7 +18,7 @@ * * * Redistribution and use in source and binary forms, with or without * * modification, are permitted according to the terms listed in LICENSE * - * (see tmva/doc/LICENSE) * + * (see tmva/doc/LICENSE) * **********************************************************************************/ From 825eb7d12323ceff2dfbedf014d88cb45e7ac992 Mon Sep 17 00:00:00 2001 From: moneta Date: Tue, 13 Jan 2026 15:43:12 +0100 Subject: [PATCH 2/6] [tmva][pymva] Fix a problem with getting tensor input/output names in Keras3 Sequential In Keras3 Sequential output of a layer can have a different name than input of the next layer. Since in sequnrial model each layer has a single input/output use as output names the layer name (which is unique) and set as input name for the next layer [tmva][sofie] Adapt SOFIE tutorial for new Keras parser and remove old C++ parser - use new python keras parser for parsing a model into SOFIE. Since new parser is only Python base, move some tutorials from C++ to Python Remove also tutorial dependency on TMVA_Higgs_Classification by creating and training a model in tutorial TMVA_SOFIE_Keras_HiggsModel.py Adapt also RSofieReader for using new Python Keras parser [tmva][sofie] Disable Conv2D tests of keras parser and add keras dependency to test Fix also an issue in a SOFIE tutorial [tmva][sofie] Fix issue in Keras parser in Redhape operator With this fix now the Keras parser can parse the convolutional model generated by TMVA_CNN_Classification [tmva][sofie] Fix test Keras parser and a bug in Squeeze Fix bug in Squeeze operator when the axes to squeeze are provided. In that case a wrong usage of vector.erase was done. Fix the Keras parser tests when convolutions with channel_first are not supported (e.g. on CPU impelmentations of tensorflows) [tmva][sofie] Fix some remining conflicts left from master rebasing --- .../pythonizations/python/ROOT/_facade.py | 2 +- .../ROOT/_pythonization/_tmva/__init__.py | 2 +- .../_keras/generate_keras_functional.py | 111 +- .../_keras/generate_keras_sequential.py | 136 ++- .../_sofie/_parser/_keras/layers/flatten.py | 6 +- .../_sofie/_parser/_keras/layers/reshape.py | 6 +- .../_tmva/_sofie/_parser/_keras/parser.py | 186 +-- .../_parser/_keras/parser_test_function.py | 30 +- .../pyroot/pythonizations/test/CMakeLists.txt | 4 +- .../pythonizations/test/sofie_keras_parser.py | 54 +- tmva/sofie/inc/TMVA/RModel.hxx | 2 +- .../inc/TMVA/ROperator_LayerNormalization.hxx | 77 -- tmva/sofie/inc/TMVA/ROperator_Reshape.hxx | 14 +- tmva/sofie/src/RModel.cxx | 15 + .../inc/TMVA/RModelParser_Keras.h | 2 +- tmva/sofie_parsers/src/RModelParser_Keras.cxx | 1070 +---------------- tmva/tmva/inc/TMVA/RSofieReader.hxx | 104 +- tutorials/CMakeLists.txt | 17 +- .../machine_learning/TMVA_SOFIE_Inference.py | 9 +- tutorials/machine_learning/TMVA_SOFIE_Keras.C | 78 -- .../machine_learning/TMVA_SOFIE_Keras.py | 86 ++ .../TMVA_SOFIE_Keras_HiggsModel.C | 32 - .../TMVA_SOFIE_Keras_HiggsModel.py | 127 ++ .../machine_learning/TMVA_SOFIE_Models.py | 9 +- .../machine_learning/TMVA_SOFIE_RDataFrame.C | 4 +- .../machine_learning/TMVA_SOFIE_RDataFrame.py | 4 +- .../TMVA_SOFIE_RDataFrame_JIT.C | 16 +- .../TMVA_SOFIE_RSofieReader.C | 2 +- 28 files changed, 643 insertions(+), 1562 deletions(-) delete mode 100644 tutorials/machine_learning/TMVA_SOFIE_Keras.C create mode 100644 tutorials/machine_learning/TMVA_SOFIE_Keras.py delete mode 100644 tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.C create mode 100644 tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index 310e6df3ad1f3..5e38ab280f898 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -412,7 +412,7 @@ def TMVA(self): from ._pythonization import _tmva # noqa: F401 ns = self._fallback_getattr("TMVA") - setattr(ns.Experimental.SOFIE, "RModelParser_Keras", _tmva.RModelParser_Keras) + setattr(ns.Experimental.SOFIE, "PyKeras", _tmva.PyKeras) hasRDF = "dataframe" in self.gROOT.GetConfigFeatures() if hasRDF: try: diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py index c130f55003bc7..ba48bf5ea6d01 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py @@ -38,7 +38,7 @@ def inject_rbatchgenerator(ns): from ._gnn import RModel_GNN, RModel_GraphIndependent -from ._sofie._parser._keras.parser import RModelParser_Keras +from ._sofie._parser._keras.parser import PyKeras hasRDF = "dataframe" in cppyy.gbl.ROOT.GetROOT().GetConfigFeatures() if hasRDF: diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py index 8a433e751c6bc..eb7443f5b6985 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py @@ -1,8 +1,17 @@ def generate_keras_functional(dst_dir): - - from keras import models, layers + + from keras import models, layers, backend import numpy as np - + + def is_channels_first_supported() : + #channel first is not supported on tensorflow CPU versions + from keras import backend + if backend.backend() == "tensorflow" : + import tensorflow as tf + if len(tf.config.list_physical_devices("GPU")) == 0: + return False + return True + # Helper training function def train_and_save(model, name): # Handle multiple inputs dynamically @@ -11,40 +20,43 @@ def train_and_save(model, name): else: x_train = np.random.rand(32, *model.input_shape[1:]) y_train = np.random.rand(32, *model.output_shape[1:]) - + model.compile(optimizer='adam', loss='mean_squared_error', metrics=['mae']) + model.summary() model.fit(x_train, y_train, epochs=1, verbose=0) - model.save(f"{dst_dir}/Functional_{name}_test.h5") + model.save(f"{dst_dir}/Functional_{name}_test.keras") + print("generated and saved functional model",name) + # Activation Functions for act in ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']: inp = layers.Input(shape=(10,)) out = layers.Activation(act)(inp) model = models.Model(inp, out) - train_and_save(model, f"Activation_layer_{act.capitalize()}") + train_and_save(model, f"Activation_layer_{act.capitalize()}") # Along with these, Keras allows explicit delcaration of activation layers such as: # [ELU, ReLU, LeakyReLU, Softmax] - + # Add in1 = layers.Input(shape=(8,)) in2 = layers.Input(shape=(8,)) out = layers.Add()([in1, in2]) model = models.Model([in1, in2], out) train_and_save(model, "Add") - + # AveragePooling2D channels_first inp = layers.Input(shape=(3, 8, 8)) out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first')(inp) model = models.Model(inp, out) train_and_save(model, "AveragePooling2D_channels_first") - + # AveragePooling2D channels_last inp = layers.Input(shape=(8, 8, 3)) out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_last')(inp) model = models.Model(inp, out) train_and_save(model, "AveragePooling2D_channels_last") - # BatchNorm + # BatchNorm inp = layers.Input(shape=(10, 3, 5)) out = layers.BatchNormalization(axis=2)(inp) model = models.Model(inp, out) @@ -56,98 +68,101 @@ def train_and_save(model, name): out = layers.Concatenate()([in1, in2]) model = models.Model([in1, in2], out) train_and_save(model, "Concat") - + # Conv2D channels_first - inp = layers.Input(shape=(3, 8, 8)) - out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_first', activation='relu')(inp) - model = models.Model(inp, out) - train_and_save(model, "Conv2D_channels_first") - + if (is_channels_first_supported()): + inp = layers.Input(shape=(3, 8, 8)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_first', activation='relu')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_channels_first") + # Conv2D channels_last inp = layers.Input(shape=(8, 8, 3)) out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last', activation='leaky_relu')(inp) model = models.Model(inp, out) train_and_save(model, "Conv2D_channels_last") - + # Conv2D padding_same inp = layers.Input(shape=(8, 8, 3)) out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last')(inp) model = models.Model(inp, out) train_and_save(model, "Conv2D_padding_same") - + # Conv2D padding_valid inp = layers.Input(shape=(8, 8, 3)) out = layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last', activation='elu')(inp) model = models.Model(inp, out) train_and_save(model, "Conv2D_padding_valid") - + # Dense inp = layers.Input(shape=(10,)) out = layers.Dense(5, activation='tanh')(inp) model = models.Model(inp, out) train_and_save(model, "Dense") - + # ELU inp = layers.Input(shape=(10,)) out = layers.ELU(alpha=0.5)(inp) model = models.Model(inp, out) train_and_save(model, "ELU") - + # Flatten inp = layers.Input(shape=(4, 5)) out = layers.Flatten()(inp) model = models.Model(inp, out) train_and_save(model, "Flatten") - + # GlobalAveragePooling2D channels first - inp = layers.Input(shape=(3, 4, 6)) - out = layers.GlobalAveragePooling2D(data_format='channels_first')(inp) - model = models.Model(inp, out) - train_and_save(model, "GlobalAveragePooling2D_channels_first") - + if (is_channels_first_supported): + inp = layers.Input(shape=(3, 4, 6)) + out = layers.GlobalAveragePooling2D(data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "GlobalAveragePooling2D_channels_first") + # GlobalAveragePooling2D channels last inp = layers.Input(shape=(4, 6, 3)) out = layers.GlobalAveragePooling2D(data_format='channels_last')(inp) model = models.Model(inp, out) train_and_save(model, "GlobalAveragePooling2D_channels_last") - - # LayerNorm + + # LayerNorm inp = layers.Input(shape=(10, 3, 5)) out = layers.LayerNormalization(axis=-1)(inp) model = models.Model(inp, out) train_and_save(model, "LayerNorm") - + # LeakyReLU inp = layers.Input(shape=(10,)) out = layers.LeakyReLU()(inp) model = models.Model(inp, out) train_and_save(model, "LeakyReLU") - + # MaxPooling2D channels_first - inp = layers.Input(shape=(3, 8, 8)) - out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_first')(inp) - model = models.Model(inp, out) - train_and_save(model, "MaxPool2D_channels_first") - + if (is_channels_first_supported): + inp = layers.Input(shape=(3, 8, 8)) + out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "MaxPool2D_channels_first") + # MaxPooling2D channels_last inp = layers.Input(shape=(8, 8, 3)) out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last')(inp) model = models.Model(inp, out) train_and_save(model, "MaxPool2D_channels_last") - + # Multiply in1 = layers.Input(shape=(8,)) in2 = layers.Input(shape=(8,)) out = layers.Multiply()([in1, in2]) model = models.Model([in1, in2], out) train_and_save(model, "Multiply") - + # Permute inp = layers.Input(shape=(3, 4, 5)) out = layers.Permute((2, 1, 3))(inp) model = models.Model(inp, out) train_and_save(model, "Permute") - + # ReLU inp = layers.Input(shape=(10,)) out = layers.ReLU()(inp) @@ -159,32 +174,32 @@ def train_and_save(model, name): out = layers.Reshape((2, 10))(inp) model = models.Model(inp, out) train_and_save(model, "Reshape") - + # Softmax inp = layers.Input(shape=(10,)) out = layers.Softmax()(inp) model = models.Model(inp, out) train_and_save(model, "Softmax") - + # Subtract in1 = layers.Input(shape=(8,)) in2 = layers.Input(shape=(8,)) out = layers.Subtract()([in1, in2]) model = models.Model([in1, in2], out) train_and_save(model, "Subtract") - + # Layer Combination - - inp = layers.Input(shape=(32, 32, 3)) + + inp = layers.Input(shape=(32, 32, 3)) x = layers.Conv2D(8, (3,3), padding="same", activation="relu")(inp) - x = layers.MaxPooling2D((2,2))(x) - x = layers.Reshape((16, 16, 8))(x) - x = layers.Permute((3, 1, 2))(x) + x = layers.MaxPooling2D((2,2))(x) + x = layers.Reshape((16, 16, 8))(x) + x = layers.Permute((3, 1, 2))(x) x = layers.Flatten()(x) out = layers.Dense(10, activation="softmax")(x) model = models.Model(inp, out) train_and_save(model, "Layer_Combination_1") - + inp = layers.Input(shape=(20,)) x = layers.Dense(32, activation="tanh")(inp) x = layers.Dense(16)(x) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py index 20c03f31c69fc..a519615c13a88 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py @@ -1,19 +1,39 @@ def generate_keras_sequential(dst_dir): - - from keras import models, layers + + from keras import models, layers, backend import numpy as np - + + def is_channels_first_supported() : + #channel first is not supported on tensorflow CPU versions + from keras import backend + if backend.backend() == "tensorflow" : + import tensorflow as tf + if len(tf.config.list_physical_devices("GPU")) == 0: + return False + return True + # Helper training function def train_and_save(model, name): x_train = np.random.rand(32, *model.input_shape[1:]) y_train = np.random.rand(32, *model.output_shape[1:]) model.compile(optimizer='adam', loss='mean_squared_error', metrics=['mae']) model.fit(x_train, y_train, epochs=1, verbose=0) - model.save(f"{dst_dir}/Sequential_{name}_test.h5") + model.summary() + print("fitting sequential model",name) + model.save(f"{dst_dir}/Sequential_{name}_test.keras") + + def is_channels_first_supported() : + #channel first is not supported on tensorflow CPU versions + if backend.backend() == "tensorflow" : + import tensorflow as tf + if len(tf.config.list_physical_devices("GPU")) == 0: + return False + + return True # Binary Ops: Add, Subtract, Multiply are not typical in Sequential - skipping those # Concat (not applicable in Sequential without multi-input) - + # Activation Functions for act in ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']: model = models.Sequential([ @@ -21,16 +41,17 @@ def train_and_save(model, name): layers.Activation(act) ]) train_and_save(model, f"Activation_layer_{act.capitalize()}") - # Along with this, Keras also allows explicit delcaration of activation layers such as: + # Along with this, Keras also allows explicit declaration of activation layers such as: # ELU, ReLU, LeakyReLU, Softmax - + # AveragePooling2D channels_first - model = models.Sequential([ - layers.Input(shape=(3, 8, 8)), - layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first') - ]) - train_and_save(model, "AveragePooling2D_channels_first") - + if (is_channels_first_supported()): + model = models.Sequential([ + layers.Input(shape=(3, 8, 8)), + layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first') + ]) + train_and_save(model, "AveragePooling2D_channels_first") + # AveragePooling2D channels_last model = models.Sequential([ layers.Input(shape=(8, 8, 3)), @@ -38,104 +59,107 @@ def train_and_save(model, name): ]) train_and_save(model, "AveragePooling2D_channels_last") - # BatchNorm + # BatchNorm model = models.Sequential([ layers.Input(shape=(10, 3, 5)), layers.BatchNormalization(axis=2) ]) train_and_save(model, "BatchNorm") - + # Conv2D channels_first - model = models.Sequential([ + if (is_channels_first_supported()): + model = models.Sequential([ layers.Input(shape=(3, 8, 8)), layers.Conv2D(4, (3, 3), data_format='channels_first') - ]) - train_and_save(model, "Conv2D_channels_first") - + ]) + train_and_save(model, "Conv2D_channels_first") + # Conv2D channels_last model = models.Sequential([ layers.Input(shape=(8, 8, 3)), layers.Conv2D(4, (3, 3), data_format='channels_last', activation='tanh') ]) train_and_save(model, "Conv2D_channels_last") - + # Conv2D padding_same model = models.Sequential([ - layers.Input(shape=(8, 8, 3)), + layers.Input(shape=(8, 8, 3)), layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last', activation='selu') ]) train_and_save(model, "Conv2D_padding_same") - + # Conv2D padding_valid model = models.Sequential([ - layers.Input(shape=(8, 8, 3)), + layers.Input(shape=(8, 8, 3)), layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last', activation='swish') ]) train_and_save(model, "Conv2D_padding_valid") - + # Dense model = models.Sequential([ layers.Input(shape=(10,)), layers.Dense(5, activation='sigmoid') ]) train_and_save(model, "Dense") - + # ELU model = models.Sequential([ layers.Input(shape=(10,)), layers.ELU(alpha=0.5) ]) train_and_save(model, "ELU") - + # Flatten model = models.Sequential([ layers.Input(shape=(4, 5)), layers.Flatten() ]) train_and_save(model, "Flatten") - + # GlobalAveragePooling2D channels first - model = models.Sequential([ - layers.Input(shape=(3, 4, 6)), + if (is_channels_first_supported()): + model = models.Sequential([ + layers.Input(shape=(3, 4, 6)), layers.GlobalAveragePooling2D(data_format='channels_first') - ]) - train_and_save(model, "GlobalAveragePooling2D_channels_first") - + ]) + train_and_save(model, "GlobalAveragePooling2D_channels_first") + # GlobalAveragePooling2D channels last model = models.Sequential([ - layers.Input(shape=(4, 6, 3)), + layers.Input(shape=(4, 6, 3)), layers.GlobalAveragePooling2D(data_format='channels_last') ]) train_and_save(model, "GlobalAveragePooling2D_channels_last") - + # LayerNorm model = models.Sequential([ layers.Input(shape=(10, 3, 5)), layers.LayerNormalization(axis=-1) ]) train_and_save(model, "LayerNorm") - + # LeakyReLU model = models.Sequential([ layers.Input(shape=(10,)), layers.LeakyReLU() ]) train_and_save(model, "LeakyReLU") - + # MaxPooling2D channels_first - model = models.Sequential([ + if (is_channels_first_supported()): + model = models.Sequential([ layers.Input(shape=(3, 8, 8)), layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_first') - ]) - train_and_save(model, "MaxPool2D_channels_first") - + ]) + train_and_save(model, "MaxPool2D_channels_first") + # MaxPooling2D channels_last model = models.Sequential([ layers.Input(shape=(8, 8, 3)), layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last') ]) train_and_save(model, "MaxPool2D_channels_last") - + # Permute model = models.Sequential([ layers.Input(shape=(3, 4, 5)), @@ -149,7 +173,7 @@ def train_and_save(model, name): layers.Reshape((2, 10)) ]) train_and_save(model, "Reshape") - + # ReLU model = models.Sequential([ layers.Input(shape=(10,)), @@ -163,9 +187,9 @@ def train_and_save(model, name): layers.Softmax() ]) train_and_save(model, "Softmax") - + # Layer Combination - + modelA = models.Sequential([ layers.Input(shape=(32, 32, 3)), layers.Conv2D(16, (3,3), padding='same', activation='swish'), @@ -176,30 +200,30 @@ def train_and_save(model, name): train_and_save(modelA, "Layer_Combination_1") modelB = models.Sequential([ - layers.Input(shape=(3, 32, 32)), - layers.Conv2D(8, (3,3), padding='valid', data_format='channels_first', activation='relu'), - layers.MaxPooling2D((2,2), data_format='channels_first'), + layers.Input(shape=(32,32,3)), + layers.Conv2D(8, (3,3), padding='valid', data_format='channels_last', activation='relu'), + layers.MaxPooling2D((2,2), data_format='channels_last'), layers.Flatten(), - layers.Dense(128, activation='relu'), - layers.Reshape((16, 8)), - layers.Permute((2, 1)), + layers.Dense(128, activation='relu'), + layers.Reshape((16, 8)), + layers.Permute((2, 1)), layers.Flatten(), layers.Dense(32), - layers.LeakyReLU(alpha=0.1), + layers.LeakyReLU(alpha=0.1), layers.Dense(10, activation='softmax'), ]) train_and_save(modelB, "Layer_Combination_2") modelC = models.Sequential([ layers.Input(shape=(4, 8, 2)), - layers.Permute((2, 1, 3)), - layers.Reshape((8, 8, 1)), + layers.Permute((2, 1, 3)), + layers.Reshape((8, 8, 1)), layers.Conv2D(4, (3,3), padding='same', activation='relu'), - layers.AveragePooling2D((2,2)), + layers.AveragePooling2D((2,2)), layers.BatchNormalization(), layers.Flatten(), - layers.Dense(32, activation='elu'), - layers.Dense(8, activation='swish'), + layers.Dense(32, activation='elu'), + layers.Dense(8, activation='swish'), layers.Dense(3, activation='softmax'), ]) train_and_save(modelC, "Layer_Combination_3") \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py index 8b28382ebc4a0..bf63cbf795872 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py @@ -17,9 +17,9 @@ def MakeKerasFlatten(layer): Returns: ROperator_Reshape: A SOFIE framework operator representing the flattening operation. """ - + keras_version = get_keras_version() - + finput = layer['layerInput'] foutput = layer['layerOutput'] attributes = layer['layerAttributes'] @@ -31,6 +31,6 @@ def MakeKerasFlatten(layer): fLayerDType = layer['layerDType'] fNameData = finput[0] fNameOutput = foutput[0] - fNameShape = flayername + "ReshapeAxes" + fNameShape = flayername + "_shape" op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape(fOpMode, 0, fNameData, fNameShape, fNameOutput) return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py index c83822f43e080..226af35c8a3e2 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py @@ -15,9 +15,9 @@ def MakeKerasReshape(layer): Returns: ROperator_Reshape: A SOFIE framework operator representing the reshaping operation. """ - + keras_version = get_keras_version() - + finput = layer['layerInput'] foutput = layer['layerOutput'] attributes = layer['layerAttributes'] @@ -29,6 +29,6 @@ def MakeKerasReshape(layer): fLayerDType = layer['layerDType'] fNameData = finput[0] fNameOutput = foutput[0] - fNameShape = flayername + "ReshapeAxes" + fNameShape = flayername + "_shape" op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape(fOpMode, 0, fNameData, fNameShape, fNameOutput) return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py index f916c9fd2a3b3..c90f3a4956b99 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py @@ -30,7 +30,7 @@ def MakeKerasActivation(layer): attributes = layer['layerAttributes'] activation = attributes['activation'] fLayerActivation = str(activation.__name__) - + if fLayerActivation in mapKerasLayer.keys(): return mapKerasLayer[fLayerActivation](layer) else: @@ -61,7 +61,7 @@ def MakeKerasActivation(layer): "sigmoid": MakeKerasSigmoid, "LeakyReLU": MakeKerasLeakyRelu, "leaky_relu": MakeKerasLeakyRelu, - "softmax": MakeKerasSoftmax, + "softmax": MakeKerasSoftmax, "MaxPooling2D": MakeKerasPooling, "AveragePooling2D": MakeKerasPooling, "GlobalAveragePooling2D": MakeKerasPooling, @@ -91,25 +91,26 @@ def add_layer_into_RModel(rmodel, layer_data): Raises exception: If the provided layer type or activation function is not supported. """ - + import numpy as np - + keras_version = get_keras_version() - + fLayerType = layer_data['layerType'] - - # reshape and flatten layers don't have weights, but they are needed inside the list of initialized - # tensor list in the Rmodel + + print('Model: parsing layer',fLayerType) + + # reshape and flatten layers don't have weights, but they need constant tensor for the shape if fLayerType == "Reshape" or fLayerType == "Flatten": Attributes = layer_data['layerAttributes'] if keras_version < '2.16': LayerName = Attributes['_name'] else: LayerName = Attributes['name'] - + if fLayerType == "Reshape": - TargetShape = np.asarray(Attributes['target_shape']).astype("int") - TargetShape = np.insert(TargetShape,0,0) + TargetShape = np.asarray(Attributes['target_shape']).astype("int64") + TargetShape = np.insert(TargetShape,0,1) else: if '_build_input_shape' in Attributes.keys(): input_shape = Attributes['_build_input_shape'] @@ -121,12 +122,18 @@ def add_layer_into_RModel(rmodel, layer_data): ) TargetShape = [ gbl_namespace.TMVA.Experimental.SOFIE.ConvertShapeToLength(input_shape[1:])] TargetShape = np.asarray(TargetShape) - - # since the AddInitializedTensor method in RModel requires unique pointer, we call a helper function + + # since the AddInitializedTensor method in RModel requires unique pointer, we call a helper function # in c++ that does the conversion from a regular pointer to unique one in c++ - rmodel.AddInitializedTensor['long'](LayerName+"ReshapeAxes", [len(TargetShape)], TargetShape) - - # These layers only have one operator - excluding the recurrent layers, in which the activation function(s) + #print('adding initialized tensor..',LayerName, TargetShape) + shape_tensor_name = LayerName + "_shape" + shape_data = TargetShape.data + print(TargetShape, shape_data) + print(len(TargetShape)) + rmodel.AddInitializedTensor['int64_t'](shape_tensor_name, [len(TargetShape)], shape_data) + + print('check other layers...') + # These layers only have one operator - excluding the recurrent layers, in which the activation function(s) # are included in the recurrent operator if fLayerType in mapKerasLayer.keys(): Attributes = layer_data['layerAttributes'] @@ -136,18 +143,18 @@ def add_layer_into_RModel(rmodel, layer_data): LayerName = Attributes['_name'] else: LayerName = Attributes['name'] - - # Convoltion/Pooling layers in keras by default assume the channels dimension is the - # last one, while in onnx (and the SOFIE's RModel) it is the first one (other than batch - # size), so a transpose is needed before and after the pooling, if the data format is - # channels last (can be set to channels first by the user). In case of MaxPool2D and + + # Convoltion/Pooling layers in keras by default assume the channels dimension is the + # last one, while in onnx (and the SOFIE's RModel) it is the first one (other than batch + # size), so a transpose is needed before and after the pooling, if the data format is + # channels last (can be set to channels first by the user). In case of MaxPool2D and # Conv2D (with linear activation) channels last, the transpose layers are added as: - + # input output # transpose layer input_layer_name layer_name + PreTrans # actual layer layer_name + PreTrans layer_name + PostTrans # transpose layer layer_name + PostTrans output_layer_name - + fLayerOutput = outputs[0] if fLayerType == 'GlobalAveragePooling2D': if layer_data['channels_last']: @@ -163,7 +170,7 @@ def add_layer_into_RModel(rmodel, layer_data): fLayerOutput ) rmodel.AddOperatorReference(op) - + # Similar case is with Batchnorm, ONNX assumes that the 'axis' is always 1, but Keras # gives the user the choice of specifying it. So, we have to transpose the input layer # as 'axis' as the first dimension, apply the BatchNormalization operator and then @@ -173,7 +180,7 @@ def add_layer_into_RModel(rmodel, layer_data): num_input_shapes = len(Attributes['_build_input_shape']) elif '_build_shapes_dict' in Attributes.keys(): num_input_shapes = len(list(Attributes['_build_shapes_dict']['input_shape'])) - + axis = Attributes['axis'] axis = axis[0] if isinstance(axis, list) else axis if axis < 0: @@ -181,16 +188,16 @@ def add_layer_into_RModel(rmodel, layer_data): fAttrPerm = list(range(0, num_input_shapes)) fAttrPerm[1] = axis fAttrPerm[axis] = 1 - op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttrPerm, inputs[0], + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttrPerm, inputs[0], LayerName+"PreTrans") rmodel.AddOperatorReference(op) inputs[0] = LayerName + "PreTrans" outputs[0] = LayerName + "PostTrans" rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) - op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttrPerm, LayerName+"PostTrans", + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttrPerm, LayerName+"PostTrans", fLayerOutput) rmodel.AddOperatorReference(op) - + elif fLayerType == 'MaxPooling2D' or fLayerType == 'AveragePooling2D': if layer_data['channels_last']: op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,3,1,2], inputs[0], @@ -200,15 +207,15 @@ def add_layer_into_RModel(rmodel, layer_data): outputs[0] = LayerName+"PostTrans" rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) if layer_data['channels_last']: - op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,2,3,1], + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,2,3,1], LayerName+"PostTrans", fLayerOutput) rmodel.AddOperatorReference(op) - + else: rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) - + return rmodel - + # These layers require two operators - dense/conv and their activation function elif fLayerType in mapKerasLayerWithActivation.keys(): Attributes = layer_data['layerAttributes'] @@ -220,7 +227,7 @@ def add_layer_into_RModel(rmodel, layer_data): LayerActivation = fPActivation.__name__ if LayerActivation in ['selu', 'sigmoid']: rmodel.AddNeededStdLib("cmath") - + # if there is an activation function after the layer if LayerActivation != 'linear': if not LayerActivation in mapKerasLayer.keys(): @@ -228,9 +235,9 @@ def add_layer_into_RModel(rmodel, layer_data): outputs = layer_data['layerOutput'] inputs = layer_data['layerInput'] fActivationLayerOutput = outputs[0] - + # like pooling, convolutional layer from keras requires transpose before and after to match - # the onnx format + # the onnx format # if the data format is channels last (can be set to channels first by the user). if fLayerType == 'Conv2D': if layer_data['channels_last']: @@ -248,15 +255,15 @@ def add_layer_into_RModel(rmodel, layer_data): op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,2,3,1], LayerName+fLayerType, LayerName+"PostTrans") rmodel.AddOperatorReference(op) Activation_layer_input = LayerName + "PostTrans" - + # Adding the activation function inputs[0] = Activation_layer_input outputs[0] = fActivationLayerOutput layer_data['layerInput'] = inputs layer_data['layerOutput'] = outputs - + rmodel.AddOperatorReference(mapKerasLayer[LayerActivation](layer_data)) - + else: # if layer is conv and the activation is linear, we need to add transpose before and after if fLayerType == 'Conv2D': inputs = layer_data['layerInput'] @@ -277,49 +284,49 @@ def add_layer_into_RModel(rmodel, layer_data): else: raise Exception("TMVA.SOFIE - parsing keras layer " + fLayerType + " is not yet supported") -class RModelParser_Keras: +class PyKeras: def Parse(filename, batch_size=1): # If a model does not have a defined batch size, then assuming it is 1 - - # TensoFlow/Keras is too fragile to import unconditionally. As its presence might break several ROOT + + # TensoFlow/Keras is too fragile to import unconditionally. As its presence might break several ROOT # usecases and importing keras globally will slow down importing ROOT, which is not desired. For this, - # we import keras within the functions instead of importing it at the start of the file (i.e. globally). - # So, whenever the parser function is called, only then keras will be imported, and not everytime we - # import ROOT. Also, we can import keras in multiple functions as many times as we want since Python + # we import keras within the functions instead of importing it at the start of the file (i.e. globally). + # So, whenever the parser function is called, only then keras will be imported, and not everytime we + # import ROOT. Also, we can import keras in multiple functions as many times as we want since Python # caches the imported packages. - + import keras import numpy as np - + keras_version = get_keras_version() - + #Check if file exists if not os.path.exists(filename): raise RuntimeError("Model file {} not found!".format(filename)) - + # load model keras_model = keras.models.load_model(filename) keras_model.load_weights(filename) - + # create new RModel object sep = '/' if os.name == 'nt': sep = '\\' - + isep = filename.rfind(sep) filename_nodir = filename if isep != -1: filename_nodir = filename[isep+1:] - + ttime = time.time() gmt_time = time.gmtime(ttime) parsetime = time.asctime(gmt_time) - + rmodel = gbl_namespace.TMVA.Experimental.SOFIE.RModel.RModel(filename_nodir, parsetime) - + # iterate over the layers and add them to the RModel - # in case of keras 3.x (particularly in sequential models), the layer input and output name conventions are - # different from keras 2.x. In keras 2.x, the layer input name is consistent with previous layer's output + # in case of keras 3.x (particularly in sequential models), the layer input and output name conventions are + # different from keras 2.x. In keras 2.x, the layer input name is consistent with previous layer's output # name. For e.g., if the sequence of layers is dense -> maxpool, the input and output layer names would be: # layer | name # input dense | keras_tensor_1 @@ -334,38 +341,44 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s # output dense | keras_tensor_2 -- # | |=> different layer names # input maxpool | keras_tensor_3 -- - # output maxpool | keras_tensor_4 + # output maxpool | keras_tensor_4 # # hence, we need to add a custom layer iterator, which would replace the suffix of the layer's input # and output names - layer_iter = 0 + layer_iter = 0 is_functional_model = True if keras_model.__class__.__name__ == 'Functional' else False - for layer in keras_model.layers: layer_data={} layer_data['layerType']=layer.__class__.__name__ layer_data['layerAttributes']=layer.__dict__ + #get input names for layer if keras_version < '2.16' or is_functional_model: if 'input_layer' in layer.name: layer_data['layerInput'] = layer.name else: layer_data['layerInput']=[x.name for x in layer.input] if isinstance(layer.input,list) else [layer.input.name] else: + #case of Keras3 Sequential model : in this case output of layer is input to following one, but names can be different if 'input_layer' in layer.input.name: layer_data['layerInput'] = [layer.input.name] else: - input_layer_name = layer.input.name[:13] + str(layer_iter) + if (layer_iter == 0) : + input_layer_name = "tensor_input_" + layer.name + else : + input_layer_name = "tensor_output_" + keras_model.layers[layer_iter-1].name layer_data['layerInput'] = [input_layer_name] + #get output names of layer if keras_version < '2.16' or is_functional_model: layer_data['layerOutput']=[x.name for x in layer.output] if isinstance(layer.output,list) else [layer.output.name] else: - output_layer_name = layer.output.name[:13] + str(layer_iter+1) + #sequential model in Keras3 + output_layer_name = "tensor_output_" + layer.name layer_data['layerOutput']=[x.name for x in layer.output] if isinstance(layer.output,list) else [output_layer_name] - layer_iter += 1 - + + layer_iter += 1 fLayerType = layer_data['layerType'] layer_data['layerDType'] = layer.dtype - + if len(layer.weights) > 0: if keras_version < '2.16': layer_data['layerWeight'] = [x.name for x in layer.weights] @@ -373,11 +386,11 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s layer_data['layerWeight'] = [x.path for x in layer.weights] else: layer_data['layerWeight'] = [] - + # for convolutional and pooling layers we need to know the format of the data if layer_data['layerType'] in ['Conv2D', 'MaxPooling2D', 'AveragePooling2D', 'GlobalAveragePooling2D']: layer_data['channels_last'] = True if layer.data_format == 'channels_last' else False - + # for recurrent type layers we need to extract additional unique information if layer_data['layerType'] in ["SimpleRNN", "LSTM", "GRU"]: layer_data['layerAttributes']['activation'] = layer.activation @@ -385,15 +398,15 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s layer_data['layerAttributes']["units"] = layer.units layer_data['layerAttributes']["layout"] = layer.input.shape[0] is None layer_data['layerAttributes']["hidden_size"] = layer.output.shape[-1] - + # for GRU and LSTM we need to extract an additional activation function - if layer_data['layerType'] != "SimpleRNN": + if layer_data['layerType'] != "SimpleRNN": layer_data['layerAttributes']['recurrent_activation'] = layer.recurrent_activation - + # for GRU there are two variants of the reset gate location, we need to know which one is it if layer_data['layerType'] == "GRU": layer_data['layerAttributes']['linear_before_reset'] = 1 if layer.reset_after and layer.recurrent_activation.__name__ == "sigmoid" else 0 - + # Ignoring the input layer of the model if(fLayerType == "InputLayer"): continue; @@ -430,20 +443,20 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s fWeightTensorValue = fWeightTensor['value'] fWeightTensorSize = 1 fWeightTensorShape = [] - + #IS IT BATCH SIZE? CHECK ONNX if 'simple_rnn' in fWeightName or 'lstm' in fWeightName or ('gru' in fWeightName and not 'bias' in fWeightName): fWeightTensorShape.append(1) - + # Building the shape vector and finding the tensor size for j in range(len(fWeightTensorValue.shape)): fWeightTensorShape.append(fWeightTensorValue.shape[j]) fWeightTensorSize *= fWeightTensorValue.shape[j] - + if fWeightDType == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: fWeightArray = fWeightTensorValue - - # weights conversion format between keras and onnx for lstm: the order of the different + + # weights conversion format between keras and onnx for lstm: the order of the different # elements (input, output, forget, cell) inside the vector/matrix is different if 'lstm' in fWeightName: if 'kernel' in fWeightName: @@ -464,17 +477,17 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s fWeightArray[units: units * 2] = W_o fWeightArray[units * 2: units * 3] = W_f fWeightArray[units * 3:] = W_c - + # need to make specific adjustments for recurrent weights and biases if ('simple_rnn' in fWeightName or 'lstm' in fWeightName or 'gru' in fWeightName): # reshaping weight matrices for recurrent layers due to keras-onnx inconsistencies if 'kernel' in fWeightName: fWeightArray = np.transpose(fWeightArray) fWeightTensorShape[1], fWeightTensorShape[2] = fWeightTensorShape[2], fWeightTensorShape[1] - + fData = fWeightArray.flatten() - - # the recurrent bias and the cell bias can be the same, in which case we need to add a + + # the recurrent bias and the cell bias can be the same, in which case we need to add a # vector of zeros for the recurrent bias if 'bias' in fWeightName and len(fData.shape) == 1: fWeightTensorShape[1] *= 2 @@ -486,13 +499,13 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s rmodel.AddInitializedTensor['float'](fWeightName, fWeightTensorShape, fData) else: raise TypeError("Type error: TMVA SOFIE does not yet support data layer type: " + fWeightDType) - + # Extracting input tensor info if keras_version < '2.16': fPInputs = keras_model.input_names else: fPInputs = [x.name for x in keras_model.inputs] - + fPInputShape = keras_model.input_shape if isinstance(keras_model.input_shape, list) else [keras_model.input_shape] fPInputDType = [] for idx in range(len(keras_model.inputs)): @@ -501,7 +514,7 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s fPInputDType.append(dtype) else: fPInputDType.append(dtype[9:-2]) - + if len(fPInputShape) == 1: fInputName = fPInputs[0] fInputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fPInputDType[0]) @@ -510,7 +523,7 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s fPInputShape = list(fPInputShape[0]) fPInputShape[0] = batch_size rmodel.AddInputTensorInfo(fInputName, gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT, fPInputShape) - rmodel.AddInputTensorName(fInputName) + rmodel.AddInputTensorName(fInputName) else: raise TypeError("Type error: TMVA SOFIE does not yet support data type " + gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType)) else: @@ -524,8 +537,8 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s rmodel.AddInputTensorInfo(fInputName, gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT, fInputShapeTuple) rmodel.AddInputTensorName(fInputName) else: - raise TypeError("Type error: TMVA SOFIE does not yet support data type " + gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType)) - + raise TypeError("Type error: TMVA SOFIE does not yet support data type " + gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType)) + # Adding OutputTensorInfos outputNames = [] if keras_version < '2.16' or is_functional_model: @@ -534,9 +547,8 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s output_layer_name = final_layer.output.name outputNames.append(output_layer_name) else: - final_layer = keras_model.outputs[-1] - output_layer_name = final_layer.name[:13] + str(layer_iter) + output_layer_name = "tensor_output_" + keras_model.layers[-1].name outputNames.append(output_layer_name) + rmodel.AddOutputTensorNameList(outputNames) return rmodel - \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py index d9d400c95a53c..8b54cef5ebe72 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py @@ -3,32 +3,32 @@ ''' The test file contains two types of functions: is_accurate: - - This function checks whether the inference results from SOFIE and Keras are accurate within a specified - tolerance. Since the inference result from Keras is not flattened, the function flattens both tensors before + - This function checks whether the inference results from SOFIE and Keras are accurate within a specified + tolerance. Since the inference result from Keras is not flattened, the function flattens both tensors before performing the comparison. - + generate_and_test_inference: - This function accepts the following inputs: - Model file path: Path to the input model. - - Destination directory for the generated header file: If set to None, the header file will be generated in + - Destination directory for the generated header file: If set to None, the header file will be generated in the model's directory. - Batch size. - - After generating the inference code, we instantiate the session for inference. To validate the results from + - After generating the inference code, we instantiate the session for inference. To validate the results from SOFIE, we compare the outputs from both SOFIE and Keras. - Load the Keras model. - Extract the input dimensions of the Keras model to avoid hardcoding. - For Sequential models or functional models with a single input: - - Extract the model's input specification and create a NumPy array of ones with the same shape as the + - Extract the model's input specification and create a NumPy array of ones with the same shape as the model's input specification, replacing None with the batch size. This becomes the input tensor. - For functional models with multiple inputs: - - Extract the dimensions for each input, set the batch size, create a NumPy array of ones for each input, + - Extract the dimensions for each input, set the batch size, create a NumPy array of ones for each input, and append each tensor to the list of input tensors. - These input tensors are then fed to both the instantiated session object and the Keras model. - Verify the output tensor dimensions: - Since SOFIE always flattens the output tensors before returning them, we need to extract the output tensor + Since SOFIE always flattens the output tensors before returning them, we need to extract the output tensor shape from the model object. - Convert the inference results to NumPy arrays: - The SOFIE result is of type vector, and the Keras result is a TensorFlow tensor. Both are converted to + The SOFIE result is of type vector, and the Keras result is a TensorFlow tensor. Both are converted to NumPy arrays before being passed to the is_accurate function for comparison. ''' @@ -44,17 +44,17 @@ def is_accurate(tensor_a, tensor_b, tolerance=1e-3): return True def generate_and_test_inference(model_file_path: str, generated_header_file_dir: str = None, batch_size=1): - + import tensorflow as tf import keras import numpy as np - + print("Tensorflow version: ", tf.__version__) print("Keras version: ", keras.__version__) print("Numpy version:", np.__version__) - - model_name = model_file_path[model_file_path.rfind('/')+1:].removesuffix(".h5") - rmodel = ROOT.TMVA.Experimental.SOFIE.RModelParser_Keras.Parse(model_file_path, batch_size) + + model_name = model_file_path[model_file_path.rfind('/')+1:].removesuffix(".keras") + rmodel = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse(model_file_path, batch_size) if generated_header_file_dir is None: last_idx = model_file_path.rfind("/") if last_idx == -1: @@ -93,4 +93,4 @@ def generate_and_test_inference(model_file_path: str, generated_header_file_dir: keras_inference_result = np.asarray(keras_inference_result) is_inference_accurate = is_accurate(sofie_inference_result, keras_inference_result) if not is_inference_accurate: - raise AssertionError("Inference results from SOFIE and Keras do not match") \ No newline at end of file + raise AssertionError("Inference results from SOFIE and Keras do not match") \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/test/CMakeLists.txt b/bindings/pyroot/pythonizations/test/CMakeLists.txt index 3b8271cda1378..bd7852dfd0981 100644 --- a/bindings/pyroot/pythonizations/test/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/test/CMakeLists.txt @@ -136,10 +136,10 @@ if (tmva) endif() endif() -# SOFIE Keras Parser +# SOFIE Keras Parser if (tmva) if(NOT MSVC OR CMAKE_SIZEOF_VOID_P EQUAL 4 OR win_broken_tests) - ROOT_ADD_PYUNITTEST(pyroot_pyz_sofie_keras_parser sofie_keras_parser.py) + ROOT_ADD_PYUNITTEST(pyroot_pyz_sofie_keras_parser sofie_keras_parser.py PYTHON_DEPS keras tensorflow) endif() endif() diff --git a/bindings/pyroot/pythonizations/test/sofie_keras_parser.py b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py index f94697761d44b..9965649019cb8 100644 --- a/bindings/pyroot/pythonizations/test/sofie_keras_parser.py +++ b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py @@ -2,15 +2,27 @@ import os import shutil +def is_channels_first_supported() : + #channel first is not supported on tensorflow CPU versions + from keras import backend + if backend.backend() == "tensorflow" : + import tensorflow as tf + if len(tf.config.list_physical_devices("GPU")) == 0: + return False + + return True + from ROOT._pythonization._tmva._sofie._parser._keras.parser_test_function import generate_and_test_inference from ROOT._pythonization._tmva._sofie._parser._keras.generate_keras_functional import generate_keras_functional from ROOT._pythonization._tmva._sofie._parser._keras.generate_keras_sequential import generate_keras_sequential def make_testname(test_case: str): - test_case_name = test_case.replace("_", " ").removesuffix(".h5") + test_case_name = test_case.replace("_", " ").removesuffix(".keras") return test_case_name + + models = [ "AveragePooling2D_channels_first", "AveragePooling2D_channels_last", @@ -22,53 +34,63 @@ def make_testname(test_case: str): "Dense", "ELU", "Flatten", - "GlobalAveragePooling2D_channels_first", + "GlobalAveragePooling2D_channels_first", #failing "GlobalAveragePooling2D_channels_last", - # "GRU", + #"GRU", "LayerNorm", "LeakyReLU", - # "LSTM", + #"LSTM", "MaxPool2D_channels_first", "MaxPool2D_channels_last", "Permute", "ReLU", "Reshape", - # "SimpleRNN", + #"SimpleRNN", "Softmax", -] + ([f"Activation_layer_{activation_function.capitalize()}" for activation_function in - ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']] + - +] + ([f"Activation_layer_{activation_function.capitalize()}" for activation_function in + ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']] + + [f"Layer_Combination_{i}" for i in range(1, 4)]) +#remove channel first cases if not supported +if (not is_channels_first_supported()): + models = [m for m in models if "channels_first" not in m] + +print(models) + class SOFIE_Keras_Parser(unittest.TestCase): - + def setUp(self): base_dir = self._testMethodName[5:] if os.path.isdir(base_dir): shutil.rmtree(base_dir) os.makedirs(base_dir + "/input_models") - os.makedirs(base_dir + "/generated_header_files_dir") - + os.makedirs(base_dir + "/generated_header_files_dir") + def run_model_tests(self, model_type: str, generate_function, model_list): + print("Generating", model_type," models for testing") generate_function(f"{model_type}/input_models") for keras_model in model_list: - keras_model_name = f"{model_type.capitalize()}_{keras_model}_test.h5" + print("**********************************") + print("Run test for",model_type,"model: ",keras_model) + print("**********************************") + keras_model_name = f"{model_type.capitalize()}_{keras_model}_test.keras" keras_model_path = f"{model_type}/input_models/" + keras_model_name with self.subTest(msg=make_testname(keras_model_name)): generate_and_test_inference(keras_model_path, f"{model_type}/generated_header_files_dir") - + def test_sequential(self): sequential_models = models self.run_model_tests("sequential", generate_keras_sequential, sequential_models) - + def test_functional(self): functional_models = models + ["Add", "Concat", "Multiply", "Subtract"] self.run_model_tests("functional", generate_keras_functional, functional_models) - + @classmethod def tearDownClass(self): shutil.rmtree("sequential") shutil.rmtree("functional") if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tmva/sofie/inc/TMVA/RModel.hxx b/tmva/sofie/inc/TMVA/RModel.hxx index 13d95935d9600..07bbb66df264e 100644 --- a/tmva/sofie/inc/TMVA/RModel.hxx +++ b/tmva/sofie/inc/TMVA/RModel.hxx @@ -210,10 +210,10 @@ public: void ReadInitializedTensorsFromFile(long); long WriteInitializedTensorsToFile(std::string filename = ""); + void PrintSummary() const; void PrintIntermediateTensors() const; void PrintOutputTensors() const; void OutputGenerated(std::string filename = "", bool append = false); - std::vector GetOutputTensorNames() { return fOutputTensorNames; } void SetFilename(std::string filename) { fName = filename; } /* diff --git a/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx b/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx index e8b5305872972..f98ce201d400d 100644 --- a/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx @@ -331,7 +331,6 @@ public: out << SP << "}\n"; } -<<<<<<< HEAD // if (!fNB.empty()) { // std::string bias = "tensor_" + (fNBroadcastedB.empty() ? fNB : fNBroadcastedB); // out << SP << "// Add the bias to Y\n"; @@ -341,82 +340,6 @@ public: // out << SP << "BLAS::saxpy_(&" << opName << "_n, &" << opName << "_alpha, " << bias << ", &"; // out << opName << "_inc, " << "tensor_" << fNY << ", &" << opName << "_inc);\n"; // } -======= - if (!fNCastedX.empty()) { - out << "// NormalizedX = InvStdDev * (CastedX - Mean)\n"; - for (size_t i = 0; i < fAxis; i++) { - std::string iIdx = "axis_" + std::to_string(i); - out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] - << "; " << iIdx << "++){\n"; - } - for (size_t j = fAxis; j < fSize; j++) { - std::string jIdx = "axis_" + std::to_string(j); - out << SP << SP << "for (size_t " << jIdx << " = 0; " << jIdx << " < " << inputShape[j] - << "; " << jIdx << "++){\n"; - } - out << SP << SP << SP << "tensor_" << fNNormalizedX << "[" << InputIndex << "] = tensor_"; - out << fNInvStdDev << "[" << axesIndex << "] * (tensor_" << fNCastedX << "[" << InputIndex; - out << "] - tensor_" << fNMean << "[" << axesIndex << "])\n"; - for (size_t j = fAxis; j < fSize; j++) { - out << SP << SP << "}\n"; - } - for (size_t i = 0; i < fAxis; i++) { - out << SP << "}\n"; - } - out << "// Y = Scale o NormalizedX"; - for (size_t i = 0; i < fAxis; i++) { - std::string iIdx = "axis_" + std::to_string(i); - out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] - << "; " << iIdx << "++){\n"; - } - for (size_t j = fAxis; j < fSize; j++) { - std::string jIdx = "axis_" + std::to_string(j); - out << SP << SP << "for (size_t " << jIdx << " = 0; " << jIdx << " < " << inputShape[j] - << "; " << jIdx << "++){\n"; - } - out << SP << SP << SP << "tensor_" << fNY << "[" << InputIndex << "] = tensor_" << fNScale; - out << "[" << axesIndex << "] * static_cast<" << fType << ">(tensor_" << fNCastedX << "[" << InputIndex; - out << "]);\n"; - for (size_t j = fAxis; j < fSize; j++) { - out << SP << SP << "}\n"; - } - for (size_t i = 0; i < fAxis; i++) { - out << SP << "}\n"; - } - } else { - out << SP << "// Y = Scale o InvStdDev (X - Mean)\n"; - for (size_t i = 0; i < fAxis; i++) { - std::string iIdx = "axis_" + std::to_string(i); - out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] - << "; " << iIdx << "++){\n"; - } - for (size_t j = fAxis; j < fSize; j++) { - std::string jIdx = "axis_" + std::to_string(j); - out << SP << SP << "for (size_t " << jIdx << " = 0; " << jIdx << " < " << inputShape[j] - << "; " << jIdx << "++){\n"; - } - out << SP << SP << SP << "tensor_" << fNY << "[" << InputIndex << "] = tensor_" << fNScale; - out << "[" << normalizedIndex << "] * tensor_" << fNInvStdDev << "[" << axesIndex; - out << "] * (tensor_" << fNX << "[" << InputIndex << "] - tensor_" << fNMean << "["; - out << axesIndex << "]);\n"; - for (size_t j = fAxis; j < fSize; j++) { - out << SP << SP << "}\n"; - } - for (size_t i = 0; i < fAxis; i++) { - out << SP << "}\n"; - } - } - - if (!fNB.empty()) { - std::string bias = "tensor_" + (fNBroadcastedB.empty() ? fNB : fNBroadcastedB); - out << SP << "// Add the bias to Y\n"; - out << SP << "int " << opName << "_n = " << fLength << ";\n"; - out << SP << "float " << opName << "_alpha = 1.;\n"; - out << SP << "int " << opName << "_inc = 1;\n"; - out << SP << "BLAS::saxpy_(&" << opName << "_n, &" << opName << "_alpha, " << bias << ", &"; - out << opName << "_inc, " << "tensor_" << fNY << ", &" << opName << "_inc);\n"; - } ->>>>>>> dcbb6bff294 (New Keras parser - added support for LayerNorm, BatchNorm ND, ELU layers and added tests for them. Imported Keras within the required functions. Created new CMakeLists.txt file for the keras parser. Made changes in the pythonization CMake file to build the keras parser files) return out.str(); } diff --git a/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx b/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx index 22ddefbd1919e..7f241fd04819e 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx @@ -70,6 +70,7 @@ public: fAttrAxes(attrAxes) { assert(fOpMode == Squeeze || fOpMode == Unsqueeze); + fInputTensorNames = { fNData }; fOutputTensorNames = { fNOutput }; } @@ -199,14 +200,23 @@ public: } } } else { - auto &axes = fAttrAxes; + std::cout << "getting shape for Squeeze...from attribute\n"; + auto axes = fAttrAxes; for (size_t i = 0; i < axes.size(); i++) { + std::cout << i << " " << axes[i] << std::endl; if (axes[i] < 0) axes[i] += input_shape.size(); if (!(output_shape[axes[i]] == Dim{1})) throw std::runtime_error("TMVA Squeeze Op : Invalid axis value " + std::to_string(axes[i]) + " for " + ConvertShapeToString(output_shape)); - output_shape.erase(output_shape.begin() + axes[i]); + } + // for calling vector::erase we must sort axes in decreasing order to avoid + std::sort(axes.begin(), axes.end(), std::greater()); + for (auto & axis : axes) { + std::cout << "erase give axis " << axis << " -> "; + for (auto & o : output_shape) std::cout << o << " , "; + std::cout << std::endl; + output_shape.erase(output_shape.begin() + axis); } } ret.push_back(output_shape); diff --git a/tmva/sofie/src/RModel.cxx b/tmva/sofie/src/RModel.cxx index 9caef5ab9da50..80c11a5d8a919 100644 --- a/tmva/sofie/src/RModel.cxx +++ b/tmva/sofie/src/RModel.cxx @@ -1422,6 +1422,21 @@ long RModel::WriteInitializedTensorsToFile(std::string filename) { } } +void RModel::PrintSummary() const { + std::cout << "Summary of model " << GetName() << std::endl; + for(size_t op_idx = 0; op_idx < fOperators.size(); ++op_idx){ + auto& r = *fOperators[op_idx].get(); + std::string raw_name = typeid(r).name(); + // look for ROperator_NAME + std::string name = raw_name.substr(raw_name.find("ROperator_")+10, raw_name.size()); + std::cout << op_idx << " " << name << " : "; + for (auto & t_in : r.GetOpInputTensors()) std::cout << t_in << " "; + std::cout << " ----> "; + for (auto & t_out : r.GetOpOutputTensors()) std::cout << t_out << " "; + std::cout << std::endl; + } +} + void RModel::PrintRequiredInputTensors() const { std::cout << "Model requires following inputs:\n"; for (auto& inputInfo: fInputTensorInfos) { diff --git a/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h b/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h index 2cb09cf3b36da..ed658526065c9 100644 --- a/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h +++ b/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h @@ -36,7 +36,7 @@ namespace TMVA::Experimental::SOFIE::PyKeras { -/// Parser function for translatng Keras .h5 model into a RModel object. +/// Parser function for translating Keras .h5 model into a RModel object. /// Accepts the file location of a Keras model and returns the /// equivalent RModel object. /// One can specify as option a batch size that can be used when the input Keras model diff --git a/tmva/sofie_parsers/src/RModelParser_Keras.cxx b/tmva/sofie_parsers/src/RModelParser_Keras.cxx index 3f923fb4bece8..73a048f1330c7 100644 --- a/tmva/sofie_parsers/src/RModelParser_Keras.cxx +++ b/tmva/sofie_parsers/src/RModelParser_Keras.cxx @@ -1,1079 +1,19 @@ // @(#)root/tmva/pymva $Id$ // Author: Sanjiban Sengupta 2021 -/********************************************************************************** - * Project : TMVA - a Root-integrated toolkit for multivariate data analysis * - * Package : TMVA * - * Function: TMVA::Experimental::SOFIE::PyKeras::Parse * - * * - * Description: * - * Parser function for translating Keras .h5 model to RModel object * - * * - * Example Usage: * - * ~~~ {.cpp} * - * using TMVA::Experimental::SOFIE; * - * RModel model = PyKeras::Parse("trained_model_dense.h5"); * - * ~~~ * - * * - **********************************************************************************/ #include "TMVA/RModelParser_Keras.h" -#include - -#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION -#include - namespace TMVA::Experimental::SOFIE::PyKeras { -namespace { - -// Utility functions (taken from PyMethodBase in PyMVA) - -void PyRunString(TString code, PyObject *globalNS, PyObject *localNS) -{ - PyObject *fPyReturn = PyRun_String(code, Py_single_input, globalNS, localNS); - if (!fPyReturn) { - std::cout << "\nPython error message:\n"; - PyErr_Print(); - throw std::runtime_error("\nFailed to run python code: " + code); - } -} - -const char *PyStringAsString(PyObject *string) -{ - PyObject *encodedString = PyUnicode_AsUTF8String(string); - const char *cstring = PyBytes_AsString(encodedString); - return cstring; -} - -std::vector GetDataFromTuple(PyObject *tupleObject) -{ - std::vector tupleVec; - for (Py_ssize_t tupleIter = 0; tupleIter < PyTuple_Size(tupleObject); ++tupleIter) { - auto itemObj = PyTuple_GetItem(tupleObject, tupleIter); - if (itemObj == Py_None) - tupleVec.push_back(0); // case shape is for example (None,2,3) - else - tupleVec.push_back((size_t)PyLong_AsLong(itemObj)); - } - return tupleVec; -} - -PyObject *GetValueFromDict(PyObject *dict, const char *key) -{ - return PyDict_GetItemWithError(dict, PyUnicode_FromString(key)); -} - -} // namespace - -namespace INTERNAL{ - -// For adding Keras layer into RModel object -void AddKerasLayer(RModel &rmodel, PyObject *fLayer); - -// Declaring Internal Functions for Keras layers which don't have activation as an additional attribute -std::unique_ptr MakeKerasActivation(PyObject *fLayer); // For instantiating ROperator for Keras Activation Layer -std::unique_ptr MakeKerasReLU(PyObject *fLayer); // For instantiating ROperator for Keras ReLU layer -std::unique_ptr MakeKerasSelu(PyObject *fLayer); // For instantiating ROperator for Keras Selu layer -std::unique_ptr MakeKerasSigmoid(PyObject *fLayer); // For instantiating ROperator for Keras Sigmoid layer -std::unique_ptr MakeKerasSwish(PyObject *fLayer); // For instantiating ROperator for Keras Swish layer -std::unique_ptr MakeKerasPermute(PyObject *fLayer); // For instantiating ROperator for Keras Permute Layer -std::unique_ptr MakeKerasBatchNorm(PyObject *fLayer); // For instantiating ROperator for Keras Batch Normalization Layer -std::unique_ptr MakeKerasReshape(PyObject *fLayer); // For instantiating ROperator for Keras Reshape Layer -std::unique_ptr MakeKerasConcat(PyObject *fLayer); // For instantiating ROperator for Keras Concat Layer -std::unique_ptr MakeKerasBinary(PyObject *fLayer); // For instantiating ROperator for Keras binary operations: Add, Subtract & Multiply. -std::unique_ptr MakeKerasSoftmax(PyObject *fLayer); // For instantiating ROperator for Keras Softmax Layer -std::unique_ptr MakeKerasTanh(PyObject *fLayer); // For instantiating ROperator for Keras Tanh Layer -std::unique_ptr MakeKerasLeakyRelu(PyObject *fLayer); // For instantiating ROperator for Keras LeakyRelu Layer -std::unique_ptr MakeKerasIdentity(PyObject *fLayer); // For instantiating ROperator for Keras Identity Layer - - -// Declaring Internal function for Keras layers which have additional activation attribute -std::unique_ptr MakeKerasDense(PyObject *fLayer); // For instantiating ROperator for Keras Dense Layer -std::unique_ptr MakeKerasConv(PyObject *fLayer); // For instantiating ROperator for Keras Conv Layer - -// For mapping Keras layer with the preparatory functions for ROperators -using KerasMethodMap = std::unordered_map (*)(PyObject *fLayer)>; -using KerasMethodMapWithActivation = std::unordered_map (*)(PyObject *fLayer)>; - -const KerasMethodMap mapKerasLayer = { - {"Activation", &MakeKerasActivation}, - {"Permute", &MakeKerasPermute}, - {"BatchNormalization", &MakeKerasBatchNorm}, - {"Reshape", &MakeKerasReshape}, - {"Concatenate", &MakeKerasConcat}, - {"swish", &MakeKerasSwish}, - {"Add", &MakeKerasBinary}, - {"Subtract", &MakeKerasBinary}, - {"Multiply", &MakeKerasBinary}, - {"Softmax", &MakeKerasSoftmax}, - {"tanh", &MakeKerasTanh}, - {"LeakyReLU", &MakeKerasLeakyRelu}, - {"Identity", &MakeKerasIdentity}, - {"Dropout", &MakeKerasIdentity}, - - // For activation layers - {"ReLU", &MakeKerasReLU}, - - // For layers with activation attributes - {"relu", &MakeKerasReLU}, - {"selu", &MakeKerasSelu}, - {"sigmoid", &MakeKerasSigmoid}, - {"softmax", &MakeKerasSoftmax} -}; - -const KerasMethodMapWithActivation mapKerasLayerWithActivation = { - {"Dense", &MakeKerasDense}, - {"Conv2D", &MakeKerasConv}, - }; - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Adds equivalent ROperator with respect to Keras model layer -/// into the referenced RModel object -/// -/// \param[in] rmodel RModel object -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \param[out] RModel object with the added ROperator -/// -/// Function adds equivalent ROperator into the referenced RModel object. -/// Keras models can have layers like Dense and Conv which have activation -/// function as an attribute. Function first searches if layer object is among -/// the ones which don't have activation attribute and then calls the respective -/// preparation function to get the ROperator object, which is then added -/// into the RModel object. If passed layer is among the ones which may have activation -/// attribute, then it checks for the activation attribute, if present then first adds -/// the primary operator into the RModel object, and then adds the operator for the -/// activation function with appropriate changes in the names of input and output -/// tensors for both of them. -/// Example of such layers is the Dense Layer. For a dense layer with input tensor name -/// dense2BiasAdd0 and output tensor name dense3Relu0 with relu as activation attribute -/// will be transformed into a ROperator_Gemm with input tensor name dense2BiasAdd0 -/// & output tensor name dense3Dense (layerName+layerType), and a subsequent -/// ROperator_Relu with input tensor name as dense3Dense and output tensor name -/// as dense3Relu0. -/// -/// For developing new preparatory functions for supporting Keras layers in future, -/// all one needs is to extract the required properties and attributes from the fLayer -/// dictionary which contains all the information about any Keras layer and after -/// any required transformations, these are passed for instantiating the ROperator -/// object. -/// -/// The fLayer dictionary which holds all the information about a Keras layer has -/// following structure:- -/// -/// dict fLayer { 'layerType' : Type of the Keras layer -/// 'layerAttributes' : Attributes of the keras layer as returned by layer.get_config() -/// 'layerInput' : List of names of input tensors -/// 'layerOutput' : List of names of output tensors -/// 'layerDType' : Data-type of the Keras layer -/// 'layerWeight' : List of weight tensor names of Keras layers -/// } -void AddKerasLayer(RModel& rmodel, PyObject* fLayer){ - std::string fLayerType = PyStringAsString(GetValueFromDict(fLayer,"layerType")); - - if(fLayerType == "Reshape"){ - PyObject* fAttributes=GetValueFromDict(fLayer,"layerAttributes"); - std::string fLayerName = PyStringAsString(GetValueFromDict(fAttributes,"_name")); - PyObject* fPTargetShape = GetValueFromDict(fAttributes,"target_shape"); - std::vectorfTargetShape = GetDataFromTuple(fPTargetShape); - std::shared_ptr fData(malloc(fTargetShape.size() * sizeof(int64_t)), free); - std::copy(fTargetShape.begin(),fTargetShape.end(),(int64_t*)fData.get()); - rmodel.AddInitializedTensor(fLayerName+"ReshapeAxes",ETensorType::INT64,{fTargetShape.size()},fData); - } - - //For layers without additional activation attribute - auto findLayer = mapKerasLayer.find(fLayerType); - if(findLayer != mapKerasLayer.end()){ - rmodel.AddOperator((findLayer->second)(fLayer)); - return; - } - - //For layers like Dense & Conv which has additional activation attribute - else if(mapKerasLayerWithActivation.find(fLayerType) != mapKerasLayerWithActivation.end()){ - findLayer = mapKerasLayerWithActivation.find(fLayerType); - PyObject* fAttributes=GetValueFromDict(fLayer,"layerAttributes"); - - std::string fLayerName = PyStringAsString(GetValueFromDict(fAttributes,"_name")); - - PyObject* fPActivation = GetValueFromDict(fAttributes,"activation"); - std::string fLayerActivation = PyStringAsString(PyObject_GetAttrString(fPActivation,"__name__")); - - if(fLayerActivation == "selu" || fLayerActivation == "sigmoid") - rmodel.AddNeededStdLib("cmath"); - - - //Checking if additional attribute exixts - if(fLayerActivation != "linear"){ - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - std::string fActivationLayerOutput = PyStringAsString(PyList_GetItem(fOutputs,0)); - - if(fLayerType == "Conv2D"){ - std::unique_ptr op_pre_transpose; - op_pre_transpose.reset(new ROperator_Transpose({0,3,1,2}, PyStringAsString(PyList_GetItem(fInputs,0)), fLayerName+"PreTrans")); - rmodel.AddOperator(std::move(op_pre_transpose)); - - PyList_SetItem(fInputs,0,PyUnicode_FromString((fLayerName+"PreTrans").c_str())); - PyDict_SetItemString(fLayer,"layerInput",fInputs); - } - - // Making changes in the names of the input and output tensor names - PyList_SetItem(fOutputs,0,PyUnicode_FromString((fLayerName+fLayerType).c_str())); - PyDict_SetItemString(fLayer,"layerOutput",fOutputs); - rmodel.AddOperator((findLayer->second)(fLayer)); - - std::string fActivationLayerInput = fLayerName+fLayerType; - if(fLayerType == "Conv2D"){ - std::unique_ptr op_post_transpose; - op_post_transpose.reset(new ROperator_Transpose({0,2,3,1}, fLayerName+fLayerType, fLayerName+"PostTrans")); - rmodel.AddOperator(std::move(op_post_transpose)); - fActivationLayerInput = fLayerName+"PostTrans"; - } - - PyList_SetItem(fInputs,0,PyUnicode_FromString(fActivationLayerInput.c_str())); - PyList_SetItem(fOutputs,0,PyUnicode_FromString(fActivationLayerOutput.c_str())); - PyDict_SetItemString(fLayer,"layerInput",fInputs); - PyDict_SetItemString(fLayer,"layerOutput",fOutputs); - - auto findActivationLayer = mapKerasLayer.find(fLayerActivation); - if(findActivationLayer == mapKerasLayer.end()){ - throw std::runtime_error("TMVA::SOFIE - Parsing Keras Activation layer " + fLayerActivation + " is not yet supported"); - } - rmodel.AddOperator((findActivationLayer->second)(fLayer)); - - } - else{ - rmodel.AddOperator((findLayer->second)(fLayer)); - } - return; - } - - else{ - throw std::runtime_error("TMVA::SOFIE - Parsing Keras layer " + fLayerType + " is not yet supported"); - } - -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Dense Layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For Keras's Dense layer, the names of the input tensor, output tensor, and -/// weight tensors are extracted, and then are passed to instantiate a -/// ROperator_Gemm object using the required attributes. -std::unique_ptr MakeKerasDense(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - // Extracting names of weight tensors - // The names of Kernel weights and bias weights are found in the list - // of weight tensors from fLayer. - PyObject* fWeightNames = GetValueFromDict(fLayer,"layerWeight"); - std::string fKernelName = PyStringAsString(PyList_GetItem(fWeightNames,0)); - std::string fBiasName = PyStringAsString(PyList_GetItem(fWeightNames,1)); - - std::unique_ptr op; - - float attr_alpha = 1.0; - float attr_beta = 1.0; - int_t attr_transA = 0; - int_t attr_transB = 0; - - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Gemm(attr_alpha, attr_beta, attr_transA, attr_transB, fLayerInputName, fKernelName, fBiasName, fLayerOutputName)); - break; - - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Gemm does not yet support input type " + fLayerDType); - } - return op; -} - - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Conv Layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For Keras's Conv layer, the names of the input tensor, output tensor, and -/// weight tensors are extracted, along with attributes like dilation_rate, -/// groups, kernel size, padding, strides. Padding attribute is then -/// computed for ROperator depending on Keras' attribute parameter. -std::unique_ptr MakeKerasConv(PyObject* fLayer){ - PyObject* fAttributes = GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - // Extracting names of weight tensors - // The names of Kernel weights and bias weights are found in the list - // of weight tensors from fLayer. - PyObject* fWeightNames = GetValueFromDict(fLayer,"layerWeight"); - std::string fKernelName = PyStringAsString(PyList_GetItem(fWeightNames,0)); - std::string fBiasName = PyStringAsString(PyList_GetItem(fWeightNames,1)); - - // Extracting the Conv Node Attributes - PyObject* fDilations = GetValueFromDict(fAttributes,"dilation_rate"); - PyObject* fGroup = GetValueFromDict(fAttributes,"groups"); - PyObject* fKernelShape = GetValueFromDict(fAttributes,"kernel_size"); - PyObject* fPads = GetValueFromDict(fAttributes,"padding"); - PyObject* fStrides = GetValueFromDict(fAttributes,"strides"); - - std::vector fAttrDilations = GetDataFromTuple(fDilations); - - - size_t fAttrGroup = PyLong_AsLong(fGroup); - std::vector fAttrKernelShape = GetDataFromTuple(fKernelShape); - std::vector fAttrStrides = GetDataFromTuple(fStrides); - std::string fAttrAutopad; - std::vectorfAttrPads; - - //Seting the layer padding - std::string fKerasPadding = PyStringAsString(fPads); - if(fKerasPadding == "valid"){ - fAttrAutopad = "VALID"; - } - else if(fKerasPadding == "same"){ - fAttrAutopad="NOTSET"; - PyObject* fInputShape = GetValueFromDict(fAttributes,"_batch_input_shape"); - long inputHeight = PyLong_AsLong(PyTuple_GetItem(fInputShape,1)); - long inputWidth = PyLong_AsLong(PyTuple_GetItem(fInputShape,2)); - - long outputHeight = std::ceil(float(inputHeight) / float(fAttrStrides[0])); - long outputWidth = std::ceil(float(inputWidth) / float(fAttrStrides[1])); - - long padding_height = std::max(long((outputHeight - 1) * fAttrStrides[0] + fAttrKernelShape[0] - inputHeight),0L); - long padding_width = std::max(long((outputWidth - 1) * fAttrStrides[1] + fAttrKernelShape[1] - inputWidth),0L); - - size_t padding_top = std::floor(padding_height/2); - size_t padding_bottom = padding_height - padding_top; - size_t padding_left = std::floor(padding_width/2); - size_t padding_right = padding_width - padding_left; - fAttrPads = {padding_top,padding_bottom,padding_left,padding_right}; - } - else{ - throw std::runtime_error("TMVA::SOFIE - RModel Keras Parser doesn't yet supports Convolution layer with padding " + fKerasPadding); - } - - std::unique_ptr op; - - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Conv(fAttrAutopad, fAttrDilations, fAttrGroup, fAttrKernelShape, fAttrPads, fAttrStrides, fLayerInputName, fKernelName, fBiasName, fLayerOutputName)); - break; - - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Conv does not yet support input type " + fLayerDType); - } - return op; -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras activation layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For Keras's keras.layers.Activation layer, the activation attribute is -/// extracted and appropriate function for adding the function is called. -std::unique_ptr MakeKerasActivation(PyObject* fLayer){ - PyObject* fAttributes=GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fPActivation = GetValueFromDict(fAttributes,"activation"); - std::string fLayerActivation = PyStringAsString(PyObject_GetAttrString(fPActivation,"__name__")); - - auto findLayer = mapKerasLayer.find(fLayerActivation); - if(findLayer == mapKerasLayer.end()){ - throw std::runtime_error("TMVA::SOFIE - Parsing Keras Activation layer " + fLayerActivation + " is not yet supported"); - } - return (findLayer->second)(fLayer); -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras ReLU activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Relu object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasReLU(PyObject* fLayer) -{ - PyObject* fInputs=GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs=GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Relu(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Relu does not yet support input type " + fLayerDType); - } - return op; -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Selu activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Selu object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasSelu(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Selu(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Selu does not yet support input type " + fLayerDType); - } - return op; -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Sigmoid activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Sigmoid object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasSigmoid(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Sigmoid(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType); - } - return op; -} -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Softmax activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Softmax object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasSoftmax(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); +RModel Parse(std::string /*filename*/, int /* batch_size */ ){ - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); + throw std::runtime_error("TMVA::SOFIE C++ Keras parser is deprecated. Use python3 function " + "ROOT.TMVA.Experimental.SOFIE.RModelParser_Keras.Parse('model.keras',batch_size) " ); - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Softmax(/*default axis is -1*/-1,fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType); - } - return op; + return RModel(); } - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Leaky Relu activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_LeakyRelu object, the names of -/// input & output tensors, the data-type and the alpha attribute of the layer -/// are extracted. -std::unique_ptr MakeKerasLeakyRelu(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - PyObject* fAttributes=GetValueFromDict(fLayer,"layerAttributes"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - float fAlpha = (float)PyFloat_AsDouble(GetValueFromDict(fAttributes,"alpha")); - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_LeakyRelu(fAlpha, fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Tanh activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Tanh object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasTanh(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Tanh(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Tanh does not yet support input type " + fLayerDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Swish activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Swish object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasSwish(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Swish(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Swish does not yet support input type " + fLayerDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Permute layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// The Permute layer in Keras has an equivalent Tranpose operator in ONNX. -/// For adding a Transpose operator, the permute dimensions are found, if they -/// exist are passed in instantiating the ROperator, else default values are used. -std::unique_ptr MakeKerasPermute(PyObject* fLayer) -{ - // Extracting required layer information - PyObject* fAttributes=GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fInputs=GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs=GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - // Extracting the permute dimensions present in Attributes of the Keras layer - PyObject* fAttributePermute = GetValueFromDict(fAttributes,"dims"); - std::vectorfPermuteDims; - - // Building vector of permute dimensions from the Tuple object. - for(Py_ssize_t tupleIter=0;tupleIter op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT:{ - - // Adding the permute dimensions if present, else are avoided to use default values. - if (!fPermuteDims.empty()){ - op.reset(new ROperator_Transpose(fPermuteDims, fLayerInputName, fLayerOutputName)); - } - else{ - op.reset(new ROperator_Transpose (fLayerInputName, fLayerOutputName)); - } - break; - } - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Transpose does not yet support input type " + fLayerDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras BatchNorm layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -std::unique_ptr MakeKerasBatchNorm(PyObject* fLayer) -{ - // Extracting required layer information - PyObject* fAttributes = GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - PyObject* fGamma = GetValueFromDict(fAttributes,"gamma"); - PyObject* fBeta = GetValueFromDict(fAttributes,"beta"); - PyObject* fMoving_Mean = GetValueFromDict(fAttributes,"moving_mean"); - PyObject* fMoving_Var = GetValueFromDict(fAttributes,"moving_variance"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fNX = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fNY = PyStringAsString(PyList_GetItem(fOutputs,0)); - std::string fNScale = PyStringAsString(PyObject_GetAttrString(fGamma,"name")); - std::string fNB = PyStringAsString(PyObject_GetAttrString(fBeta,"name")); - std::string fNMean = PyStringAsString(PyObject_GetAttrString(fMoving_Mean,"name")); - std::string fNVar = PyStringAsString(PyObject_GetAttrString(fMoving_Var,"name")); - float fEpsilon = (float)PyFloat_AsDouble(GetValueFromDict(fAttributes,"epsilon")); - float fMomentum = (float)PyFloat_AsDouble(GetValueFromDict(fAttributes,"momentum")); - - std::unique_ptr op; - op.reset(new ROperator_BatchNormalization(fEpsilon, fMomentum, /* training mode */ 0, fNX, fNScale, fNB, fNMean, fNVar, fNY)); - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Reshape layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -std::unique_ptr MakeKerasReshape(PyObject* fLayer) -{ - // Extracting required layer information - PyObject* fAttributes = GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerName = PyStringAsString(GetValueFromDict(fAttributes,"_name")); - - ReshapeOpMode fOpMode = Reshape; - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fNameData = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fNameOutput = PyStringAsString(PyList_GetItem(fOutputs,0)); - std::string fNameShape = fLayerName + "ReshapeAxes"; - std::unique_ptr op; - op.reset(new ROperator_Reshape(fOpMode, /*allow zero*/0, fNameData, fNameShape, fNameOutput)); - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Concat layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -std::unique_ptr MakeKerasConcat(PyObject* fLayer) -{ - PyObject* fAttributes = GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::vector inputs; - for(Py_ssize_t i=0; i op; - op.reset(new ROperator_Concat(inputs, axis, 0, output)); - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras binary operations like Add, -/// subtract, and multiply. -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_BasicBinary object, the names of -/// input & output tensors, the data-type of the layer and the operation type -/// are extracted. -std::unique_ptr MakeKerasBinary(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerType = PyStringAsString(GetValueFromDict(fLayer,"layerType")); - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fX1 = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fX2 = PyStringAsString(PyList_GetItem(fInputs,1)); - std::string fY = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT:{ - if(fLayerType == "Add") - op.reset(new ROperator_BasicBinary (fX1, fX2, fY)); - else if(fLayerType == "Subtract") - op.reset(new ROperator_BasicBinary (fX1, fX2, fY)); - else - op.reset(new ROperator_BasicBinary (fX1, fX2, fY)); - break; - } - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Identity and Dropout Layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// Dropout will have no effect in inference, so instead an Identity operator -/// is added to mimic its presence in the Keras model -std::unique_ptr MakeKerasIdentity(PyObject* fLayer) -{ - PyObject* fInputs=GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs=GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Identity(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Identity does not yet support input type " + fLayerDType); - } - return op; -} - -}//INTERNAL - - -////////////////////////////////////////////////////////////////////////////////// -/// \param[in] filename file location of Keras .h5 -/// \param[in] batch_size if not given, 1 is used if the model does not provide it -/// \return Parsed RModel object -/// -/// The `Parse()` function defined in `TMVA::Experimental::SOFIE::PyKeras` will -/// parse a trained Keras .h5 model into a RModel Object. After loading the model -/// in a Python Session, the included layers are extracted with properties -/// like Layer type, Attributes, Input tensor names, Output tensor names, data-type -/// and names of the weight/initialized tensors. -/// The extracted layers from the model are then passed into `AddKerasLayer()` -/// which prepares the specific ROperator and adds them into the RModel object. -/// The layers are also checked for adding any required routines for executing -/// the generated Inference code. -/// -/// For adding the Initialized tensors into the RModel object, the weights are -/// extracted from the Keras model in the form of NumPy arrays, which are then -/// passed into `AddInitializedTensor()` after appropriate casting. -/// -/// Input tensor infos are required to be added which will contain their names, -/// shapes and data-types. For keras models with single input tensors, the tensor -/// shape is returned as a Tuple object, whereas for multi-input models, -/// the tensor shape is returned as a List of Tuple object containing the shape -/// of the individual input tensors. SOFIE's RModel also requires that the Keras -/// models are initialized with Batch Size. The `GetDataFromTuple()` are called -/// on the Tuple objects, which then returns the shape vector required to call -/// the `AddInputTensorInfo()`. -/// -/// For adding the Output Tensor infos, only the names of the model's output -/// tensors are extracted and are then passed into `AddOutputTensorNameList()`. -/// -/// Provide optionally a batch size that can be used to overwrite the one given by the -/// model. If a batch size is not given 1 is used if the model does not provide a batch size -/// -/// Example Usage: -/// ~~~ {.cpp} -/// using TMVA::Experimental::SOFIE; -/// RModel model = PyKeras::Parse("trained_model_dense.h5"); -/// ~~~ -RModel Parse(std::string filename, int batch_size){ - - char sep = '/'; - #ifdef _WIN32 - sep = '\\'; - #endif - - size_t isep = filename.rfind(sep, filename.length()); - std::string filename_nodir = filename; - if (isep != std::string::npos){ - filename_nodir = (filename.substr(isep+1, filename.length() - isep)); - } - - //Check on whether the Keras .h5 file exists - if(!std::ifstream(filename).good()){ - throw std::runtime_error("Model file "+filename_nodir+" not found!"); - } - - - std::time_t ttime = std::time(0); - std::tm* gmt_time = std::gmtime(&ttime); - std::string parsetime (std::asctime(gmt_time)); - - RModel rmodel(filename_nodir, parsetime); - - //Intializing Python Interpreter and scope dictionaries - Py_Initialize(); - PyObject* main = PyImport_AddModule("__main__"); - PyObject* fGlobalNS = PyModule_GetDict(main); - PyObject* fLocalNS = PyDict_New(); - if (!fGlobalNS) { - throw std::runtime_error("Can't init global namespace for Python"); - } - if (!fLocalNS) { - throw std::runtime_error("Can't init local namespace for Python"); - } - - // Extracting model information - // For each layer: type,name,activation,dtype,input tensor's name, - // output tensor's name, kernel's name, bias's name - // None object is returned for if property doesn't belong to layer - PyRunString("import tensorflow",fGlobalNS,fLocalNS); - PyRunString("import tensorflow.keras as keras",fGlobalNS,fLocalNS); - PyRunString("import tensorflow\n", fGlobalNS, fLocalNS); - PyRunString("if int(keras.__version__.split('.')[0]) >= 3:\n" - " raise RuntimeError(\n" - " 'TMVA SOFIE Keras parser supports Keras 2 only.\\n'\n" - " 'Keras 3 detected. Please export the model to ONNX.\\n'\n" - " )\n", - fGlobalNS, fLocalNS); - PyRunString("from tensorflow.keras.models import load_model",fGlobalNS,fLocalNS); - PyRunString("print('TF/Keras Version: '+ tensorflow.__version__)",fGlobalNS,fLocalNS); - PyRunString(TString::Format("model=load_model('%s')",filename.c_str()),fGlobalNS,fLocalNS); - PyRunString(TString::Format("model.load_weights('%s')",filename.c_str()),fGlobalNS,fLocalNS); - PyRunString("globals().update(locals())",fGlobalNS,fLocalNS); - PyRunString("modelData=[]",fGlobalNS,fLocalNS); - PyRunString("for idx in range(len(model.layers)):\n" - " layer=model.get_layer(index=idx)\n" - " layerData={}\n" - " layerData['layerType']=layer.__class__.__name__\n" - " layerData['layerAttributes']=layer.__dict__\n" - " layerData['layerInput']=[x.name for x in layer.input] if isinstance(layer.input,list) else [layer.input.name]\n" - " layerData['layerOutput']=[x.name for x in layer.output] if isinstance(layer.output,list) else [layer.output.name]\n" - " layerData['layerDType']=layer.dtype\n" - " layerData['layerWeight']=[x.name for x in layer.weights]\n" - " modelData.append(layerData)",fGlobalNS,fLocalNS); - - - PyObject* fPModel = GetValueFromDict(fLocalNS,"modelData"); - PyObject *fLayer; - Py_ssize_t fModelSize = PyList_Size(fPModel); - std::string fLayerType; - - // Traversing through all the layers and passing the Layer object to `AddKerasLayer()` - // for adding the equivalent ROperators into the RModel object. - for(Py_ssize_t fModelIterator=0;fModelIterator fWeightTensorShape; - std::size_t fWeightTensorSize; - - // Traversing through all the Weight tensors - for (Py_ssize_t weightIter = 0; weightIter < PyList_Size(fPWeight); weightIter++){ - fWeightTensor = PyList_GetItem(fPWeight, weightIter); - fWeightName = PyStringAsString(GetValueFromDict(fWeightTensor,"name")); - fWeightDType = ConvertStringToType(PyStringAsString(GetValueFromDict(fWeightTensor,"dtype"))); - - fWeightTensorValue = (PyArrayObject*)GetValueFromDict(fWeightTensor,"value"); - fWeightTensorSize=1; - fWeightTensorShape.clear(); - - // Building the shape vector and finding the tensor size - for(int j=0; j fData(malloc(fWeightTensorSize * sizeof(float)), free); - std::memcpy(fData.get(),fWeightArray, fWeightTensorSize * sizeof(float)); - rmodel.AddInitializedTensor(fWeightName,ETensorType::FLOAT,fWeightTensorShape,fData); - break; - } - default: - throw std::runtime_error("Type error: TMVA SOFIE does not yet weight data layer type"+ConvertTypeToString(fWeightDType)); - } - } - - - // Extracting input tensor info - // For every input tensor inputNames will have their names as string, - // inputShapes will have their shape as Python Tuple, and inputTypes - // will have their dtype as string - PyRunString("inputNames=model.input_names",fGlobalNS,fLocalNS); - PyRunString("inputShapes=model.input_shape if type(model.input_shape)==list else [model.input_shape]",fGlobalNS,fLocalNS); - PyRunString("inputTypes=[]",fGlobalNS,fLocalNS); - PyRunString("for idx in range(len(model.inputs)):\n" - " inputTypes.append(model.inputs[idx].dtype.__str__()[9:-2])",fGlobalNS,fLocalNS); - - PyObject* fPInputs = GetValueFromDict(fLocalNS,"inputNames"); - PyObject* fPInputShapes = GetValueFromDict(fLocalNS,"inputShapes"); - PyObject* fPInputTypes = GetValueFromDict(fLocalNS,"inputTypes"); - - std::string fInputName; - ETensorType fInputDType; - - // For single input models, the model.input_shape will return a tuple - // describing the input tensor shape. For multiple inputs models, - // the model.input_shape will return a list of tuple, each describing - // the input tensor shape. - if(PyTuple_Check(fPInputShapes)){ - fInputName = PyStringAsString(PyList_GetItem(fPInputs,0)); - fInputDType = ConvertStringToType(PyStringAsString(PyList_GetItem(fPInputTypes,0))); - - switch(fInputDType){ - - case ETensorType::FLOAT : { - - // Getting the shape vector from the Tuple object - std::vectorfInputShape = GetDataFromTuple(fPInputShapes); - if (static_cast(fInputShape[0]) <= 0){ - fInputShape[0] = std::max(batch_size,1); - std::cout << "Model has not a defined batch size "; - if (batch_size <=0) std::cout << " assume is 1 "; - else std::cout << " use given value of " << batch_size; - std::cout << " - input shape for tensor " << fInputName << " : " - << TMVA::Experimental::SOFIE::ConvertShapeToString(fInputShape) << std::endl; - } - rmodel.AddInputTensorInfo(fInputName, ETensorType::FLOAT, fInputShape); - rmodel.AddInputTensorName(fInputName); - break; - } - - default: - throw std::runtime_error("Type error: TMVA SOFIE does not yet support data type"+ConvertTypeToString(fInputDType)); - } - - } - - else{ - - // Iterating through multiple input tensors - for(Py_ssize_t inputIter = 0; inputIter < PyList_Size(fPInputs);++inputIter){ - - fInputName = PyStringAsString(PyList_GetItem(fPInputs,inputIter)); - fInputDType = ConvertStringToType(PyStringAsString(PyList_GetItem(fPInputTypes,inputIter))); - - switch(fInputDType){ - case ETensorType::FLOAT : { - PyObject* fInputShapeTuple=PyList_GetItem(fPInputShapes,inputIter); - - std::vectorfInputShape = GetDataFromTuple(fInputShapeTuple); - if (static_cast(fInputShape[0]) <= 0){ - fInputShape[0] = std::max(batch_size,1); - std::cout << "Model has not a defined batch size "; - if (batch_size <=0) std::cout << " assume is 1 "; - else std::cout << " use given value of " << batch_size; - std::cout << " - input shape for tensor " - << fInputName << " : " << TMVA::Experimental::SOFIE::ConvertShapeToString(fInputShape) << std::endl; - } - rmodel.AddInputTensorInfo(fInputName, ETensorType::FLOAT, fInputShape); - rmodel.AddInputTensorName(fInputName); - break; - } - - default: - throw std::runtime_error("Type error: TMVA SOFIE does not yet support data type"+ConvertTypeToString(fInputDType)); - - } - } - } - - - // For adding OutputTensorInfos, the names of the output - // tensors are extracted from the Keras model - PyRunString("outputNames=[]",fGlobalNS,fLocalNS); - PyRunString("for layerName in model.output_names:\n" - " outputNames.append(model.get_layer(layerName).output.name)",fGlobalNS,fLocalNS); - PyObject* fPOutputs = GetValueFromDict(fLocalNS,"outputNames"); - std::vector fOutputNames; - for(Py_ssize_t outputIter = 0; outputIter < PyList_Size(fPOutputs);++outputIter){ - fOutputNames.push_back(PyStringAsString(PyList_GetItem(fPOutputs,outputIter))); - } - rmodel.AddOutputTensorNameList(fOutputNames); - - return rmodel; -} - -} // namespace TMVA::Experimental::SOFIE::PyKeras +} \ No newline at end of file diff --git a/tmva/tmva/inc/TMVA/RSofieReader.hxx b/tmva/tmva/inc/TMVA/RSofieReader.hxx index 5698ff49427ae..dc6f83c4502d6 100644 --- a/tmva/tmva/inc/TMVA/RSofieReader.hxx +++ b/tmva/tmva/inc/TMVA/RSofieReader.hxx @@ -88,9 +88,14 @@ public: std::string fileType = path.substr(pos2+1, path.length()-pos2-1); if (verbose) std::cout << "Parsing SOFIE model " << modelName << " of type " << fileType << std::endl; + // append a suffix to headerfile + std::string modelHeader = modelName + "_fromRSofieR.hxx"; + std::string modelWeights = modelName + "_fromRSofieR.dat"; + // create code for parsing model and generate C++ code for inference // make it in a separate scope to avoid polluting global interpreter space std::string parserCode; + std::string parserPythonCode; // for Python parsers if (type == kONNX) { // check first if we can load the SOFIE parser library if (gSystem->Load("libROOTTMVASofieParser") < 0) { @@ -104,16 +109,15 @@ public: parserCode += "TMVA::Experimental::SOFIE::RModel model = parser.Parse(\"" + path + "\"); \n"; } else if (type == kKeras) { - // use Keras direct parser - if (gSystem->Load("libROOTTMVASofiePyParsers") < 0) { - throw std::runtime_error("RSofieReader: cannot use SOFIE with Keras since libROOTTMVASofiePyParsers is missing"); - } - // assume batch size is first entry in first input ! - std::string batch_size = "-1"; + // use Keras Python parser + parserPythonCode += "\"\"\"\n"; + parserPythonCode += "import ROOT\n"; + + // assume batch size is first entry in first input otherwise set to 1 + std::string batch_size = "1"; // need to fix parser with parm batch sizes if (!inputShapes.empty() && ! inputShapes[0].empty()) batch_size = std::to_string(inputShapes[0][0]); - parserCode += "{\nTMVA::Experimental::SOFIE::RModel model = TMVA::Experimental::SOFIE::PyKeras::Parse(\"" + path + - "\"," + batch_size + "); \n"; + parserPythonCode += "model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse('" + path + "'," + batch_size + ")\n"; } else if (type == kPt) { // use PyTorch direct parser @@ -150,6 +154,8 @@ public: // add custom operators if needed if (fCustomOperators.size() > 0) { + if (!parserPythonCode.empty()) + throw std::runtime_error("Cannot use Custom operator with a Python parser (e.g. from a Keras model)"); for (auto & op : fCustomOperators) { parserCode += "{ auto p = new TMVA::Experimental::SOFIE::ROperator_Custom(\"" @@ -166,58 +172,70 @@ public: } if (verbose) std::cout << "generating the code with batch size = " << batchSize << " ...\n"; - parserCode += "model.Generate(TMVA::Experimental::SOFIE::Options::kDefault," - + ROOT::Math::Util::ToString(batchSize) + ", 0, " + std::to_string(verbose) + "); \n"; - - if (verbose) { - parserCode += "model.PrintRequiredInputTensors();\n"; - parserCode += "model.PrintIntermediateTensors();\n"; - parserCode += "model.PrintOutputTensors();\n"; - } + if (parserPythonCode.empty()) { + parserCode += "model.Generate(TMVA::Experimental::SOFIE::Options::kDefault," + + ROOT::Math::Util::ToString(batchSize) + ", 0, " + std::to_string(verbose) + ");\n"; - // add custom operators if needed -#if 0 - if (fCustomOperators.size() > 0) { + parserCode += "model.OutputGenerated(\"" + modelHeader + "\");\n"; if (verbose) { parserCode += "model.PrintRequiredInputTensors();\n"; parserCode += "model.PrintIntermediateTensors();\n"; parserCode += "model.PrintOutputTensors();\n"; + if (verbose > 1) + parserCode += "model.PrintGenerated(); \n"; } - for (auto & op : fCustomOperators) { - parserCode += "{ auto p = new TMVA::Experimental::SOFIE::ROperator_Custom(\"" - + op.fOpName + "\"," + op.fInputNames + "," + op.fOutputNames + "," + op.fOutputShapes + ",\"" + op.fFileName + "\");\n"; - parserCode += "std::unique_ptr op(p);\n"; - parserCode += "model.AddOperator(std::move(op));\n}\n"; - } - parserCode += "model.Generate(TMVA::Experimental::SOFIE::Options::kDefault," - + ROOT::Math::Util::ToString(batchSize) + "); \n"; - } -#endif - if (verbose > 1) - parserCode += "model.PrintGenerated(); \n"; - parserCode += "model.OutputGenerated();\n"; - parserCode += "int nInputs = model.GetInputTensorNames().size();\n"; + // need information on number of inputs (assume output is 1) + parserCode += "int nInputs = model.GetInputTensorNames().size();\n"; - // need information on number of inputs (assume output is 1) + //end of parsing C++ code + parserCode += "return nInputs;\n}\n"; + } else { + // Python case + parserPythonCode += "model.Generate(ROOT.TMVA.Experimental.SOFIE.Options.kDefault," + + ROOT::Math::Util::ToString(batchSize) + ", 0, " + std::to_string(verbose) + ")\n"; - //end of parsing code, close the scope and return 1 to indicate a success - parserCode += "return nInputs;\n}\n"; - - if (verbose) std::cout << "//ParserCode being executed:\n" << parserCode << std::endl; + parserPythonCode += "model.OutputGenerated('" + modelHeader + "');\n"; + if (verbose) { + parserPythonCode += "model.PrintRequiredInputTensors()\n"; + parserPythonCode += "model.PrintIntermediateTensors()\n"; + parserPythonCode += "model.PrintOutputTensors()\n"; + if (verbose > 1) + parserPythonCode += "model.PrintGenerated()\n"; + } + // end of Python parsing code + parserPythonCode += "\"\"\""; + } + // executing parsing and generating code + int iret = -1; + if (parserPythonCode.empty()) { + if (verbose) { + std::cout << "...ParserCode being executed...:\n"; + std::cout << parserCode << std::endl; + } + iret = gROOT->ProcessLine(parserCode.c_str()); + fNInputs = iret; + } else { + if (verbose) { + std::cout << "executing python3 -c ......" << std::endl; + std::cout << parserPythonCode << std::endl; + } + iret = gSystem->Exec(TString("python3 -c ") + TString(parserPythonCode.c_str())); + fNInputs = 1; + if (!inputShapes.empty()) fNInputs = inputShapes.size(); + } - auto iret = gROOT->ProcessLine(parserCode.c_str()); - if (iret <= 0) { + if (iret < 0) { std::string msg = "RSofieReader: error processing the parser code: \n" + parserCode; throw std::runtime_error(msg); + } else if (verbose) { + std::cout << "Model Header file is generated!" << std::endl; } - fNInputs = iret; if (fNInputs > 3) { throw std::runtime_error("RSofieReader does not yet support model with > 3 inputs"); } // compile now the generated code and create Session class - std::string modelHeader = modelName + ".hxx"; if (verbose) std::cout << "compile generated code from file " <AccessPathName(modelHeader.c_str())) { std::string msg = "RSofieReader: input header file " + modelHeader + " is not existing"; @@ -235,7 +253,7 @@ public: []( char const& c ) -> bool { return !std::isalnum(c); } ), uidName.end()); std::string sessionName = "session_" + uidName; - declCode += sessionClassName + " " + sessionName + ";"; + declCode += sessionClassName + " " + sessionName + "(\"" + modelWeights + "\");"; if (verbose) std::cout << "//global session declaration\n" << declCode << std::endl; diff --git a/tutorials/CMakeLists.txt b/tutorials/CMakeLists.txt index d81933f965f30..5948431174271 100644 --- a/tutorials/CMakeLists.txt +++ b/tutorials/CMakeLists.txt @@ -349,7 +349,7 @@ else() # Check if we support the installed Keras version. Otherwise, veto SOFIE # Keras tutorials. This mirrors the logic in tmva/sofie/test/CMakeLists.txt. # TODO: make sure we also support the newest Keras - set(unsupported_keras_version "3.5.0") + set(unsupported_keras_version "4.0.0") if (ROOT_KERAS_FOUND AND NOT DEFINED ROOT_KERAS_VERSION) message(WARNING "Keras found, but version unknown — cannot verify compatibility.") elseif (ROOT_KERAS_FOUND AND NOT ROOT_KERAS_VERSION VERSION_LESS ${unsupported_keras_version}) @@ -363,10 +363,10 @@ else() list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RSofieReader.C) endif() # These SOFIE tutorials take models trained via PyMVA-PyKeras as input - if (NOT tmva-pymva OR NOT tmva-sofie OR NOT ROOT_KERAS_FOUND OR keras_unsupported) - list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras.C) + if (NOT tmva-sofie OR NOT ROOT_KERAS_FOUND OR keras_unsupported) + list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras.py) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Models.py) - list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras_HiggsModel.C) + list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras_HiggsModel.py) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RDataFrame.C) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RDataFrame_JIT.C) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RSofieReader.C) @@ -642,11 +642,10 @@ set (machine_learning-tmva004_RStandardScaler-depends tutorial-machine_learning- set (machine_learning-pytorch-ApplicationClassificationPyTorch-depends tutorial-machine_learning-pytorch-ClassificationPyTorch-py) set (machine_learning-pytorch-RegressionPyTorch-depends tutorial-machine_learning-pytorch-ApplicationClassificationPyTorch-py) set (machine_learning-pytorch-ApplicationRegressionPyTorch-depends tutorial-machine_learning-pytorch-RegressionPyTorch-py) -set (machine_learning-TMVA_SOFIE_RSofieReader-depends tutorial-machine_learning-TMVA_Higgs_Classification) -set (machine_learning-TMVA_SOFIE_RDataFrame_JIT-depends tutorial-machine_learning-TMVA_SOFIE_RSofieReader) -set (machine_learning-TMVA_SOFIE_Keras_HiggsModel-depends tutorial-machine_learning-TMVA_SOFIE_RDataFrame_JIT) -set (machine_learning-TMVA_SOFIE_RDataFrame-depends tutorial-machine_learning-TMVA_SOFIE_Keras_HiggsModel) -set (machine_learning-TMVA_SOFIE_Inference-depends tutorial-machine_learning-TMVA_SOFIE_RDataFrame) +set (machine_learning-TMVA_SOFIE_RDataFrame-depends tutorial-machine_learning-TMVA_SOFIE_Keras_HiggsModel-py) +set (machine_learning-TMVA_SOFIE_RDataFrame_JIT-depends tutorial-machine_learning-TMVA_SOFIE_RDataFrame) +set (machine_learning-TMVA_SOFIE_RSofieReader-depends tutorial-machine_learning-TMVA_SOFIE_RDataFrame_JIT) +set (machine_learning-TMVA_SOFIE_Inference-depends tutorial-machine_learning-TMVA_SOFIE_RSofieReader) set (machine_learning-keras-RegressionKeras-depends tutorial-machine_learning-pytorch-RegressionPyTorch-py) set (machine_learning-keras-ClassificationKeras-depends tutorial-machine_learning-pytorch-ClassificationPyTorch-py) set (machine_learning-keras-ApplicationRegressionKeras-depends tutorial-machine_learning-keras-RegressionKeras-py) diff --git a/tutorials/machine_learning/TMVA_SOFIE_Inference.py b/tutorials/machine_learning/TMVA_SOFIE_Inference.py index 712420115e7c4..37d013ad5f851 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_Inference.py +++ b/tutorials/machine_learning/TMVA_SOFIE_Inference.py @@ -17,8 +17,10 @@ import numpy as np import ROOT +from os.path import exists + # check if the input file exists -modelFile = "Higgs_trained_model.keras" +modelFile = "HiggsModel.keras" if not exists(modelFile): raise FileNotFoundError("You need to run TMVA_Higgs_Classification.C to generate the Keras trained model") @@ -63,7 +65,10 @@ print("size of data", dataset_size) #instantiate SOFIE session class -session = ROOT.TMVA_SOFIE_Higgs_trained_model.Session() +#session = ROOT.TMVA_SOFIE_HiggsModel.Session() +#get the sofie session namespace +sofie = getattr(ROOT, 'TMVA_SOFIE_' + modelName) +session = sofie.Session() hs = ROOT.TH1D("hs","Signal result",100,0,1) for i in range(0,dataset_size): diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras.C b/tutorials/machine_learning/TMVA_SOFIE_Keras.C deleted file mode 100644 index b000b33e56ce6..0000000000000 --- a/tutorials/machine_learning/TMVA_SOFIE_Keras.C +++ /dev/null @@ -1,78 +0,0 @@ -/// \file -/// \ingroup tutorial_ml -/// \notebook -nodraw -/// This macro provides a simple example for the parsing of Keras .keras file -/// into RModel object and further generating the .hxx header files for inference. -/// -/// \macro_code -/// \macro_output -/// \author Sanjiban Sengupta - -using namespace TMVA::Experimental; - -TString pythonSrc = "\ -import os\n\ -os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'\n\ -\n\ -import numpy as np\n\ -from tensorflow.keras.models import Model\n\ -from tensorflow.keras.layers import Input,Dense,Activation,ReLU\n\ -from tensorflow.keras.optimizers import SGD\n\ -\n\ -input=Input(shape=(64,),batch_size=4)\n\ -x=Dense(32)(input)\n\ -x=Activation('relu')(x)\n\ -x=Dense(16,activation='relu')(x)\n\ -x=Dense(8,activation='relu')(x)\n\ -x=Dense(4)(x)\n\ -output=ReLU()(x)\n\ -model=Model(inputs=input,outputs=output)\n\ -\n\ -randomGenerator=np.random.RandomState(0)\n\ -x_train=randomGenerator.rand(4,64)\n\ -y_train=randomGenerator.rand(4,4)\n\ -\n\ -model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01))\n\ -model.fit(x_train, y_train, epochs=5, batch_size=4)\n\ -model.save('KerasModel.keras')\n"; - - -void TMVA_SOFIE_Keras(const char * modelFile = nullptr, bool printModelInfo = true){ - - // Running the Python script to generate Keras .keras file - - if (modelFile == nullptr) { - TMacro m; - m.AddLine(pythonSrc); - m.SaveSource("make_keras_model.py"); - gSystem->Exec("python3 make_keras_model.py"); - modelFile = "KerasModel.keras"; - } - - //Parsing the saved Keras .keras file into RModel object - SOFIE::RModel model = SOFIE::PyKeras::Parse(modelFile); - - - //Generating inference code - model.Generate(); - // generate output header. By default it will be modelName.hxx - model.OutputGenerated(); - - if (!printModelInfo) return; - - //Printing required input tensors - std::cout<<"\n\n"; - model.PrintRequiredInputTensors(); - - //Printing initialized tensors (weights) - std::cout<<"\n\n"; - model.PrintInitializedTensors(); - - //Printing intermediate tensors - std::cout<<"\n\n"; - model.PrintIntermediateTensors(); - - //Printing generated inference code - std::cout<<"\n\n"; - model.PrintGenerated(); -} diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras.py b/tutorials/machine_learning/TMVA_SOFIE_Keras.py new file mode 100644 index 0000000000000..1473eddf27207 --- /dev/null +++ b/tutorials/machine_learning/TMVA_SOFIE_Keras.py @@ -0,0 +1,86 @@ +### \file +### \ingroup tutorial_ml +### \notebook -nodraw +### This macro provides a simple example for the parsing of Keras .keras file +### into RModel object and further generating the .hxx header files for inference. +### +### \macro_code +### \macro_output +### \author Sanjiban Sengupta and Lorenzo Moneta + + +import ROOT +import os +import sys + +# Enable ROOT in batch mode (same effect as -nodraw) +ROOT.gROOT.SetBatch(True) + +# ----------------------------------------------------------------------------- +# Step 1: Create and train a simple Keras model (via embedded Python) +# ----------------------------------------------------------------------------- + +import tensorflow as tf +from tensorflow.keras.models import Model +from tensorflow.keras.layers import Dense, Input, Activation, Softmax +import numpy as np + +input=Input(shape=(4,),batch_size=2) +x=Dense(32)(input) +x=Activation('relu')(x) +x=Dense(16,activation='relu')(x) +x=Dense(8,activation='relu')(x) +x=Dense(2)(x) +output=Softmax()(x) +model=Model(inputs=input,outputs=output) + +randomGenerator=np.random.RandomState(0) +x_train=randomGenerator.rand(4,4) +y_train=randomGenerator.rand(4,2) + +model.compile(loss='mse', optimizer='adam') +model.fit(x_train, y_train, epochs=3, batch_size=2) +model.save('KerasModel.keras') +model.summary() + +# ----------------------------------------------------------------------------- +# Step 2: Use TMVA::SOFIE to parse the ONNX model +# ----------------------------------------------------------------------------- + +import ROOT + + + +# Parse the ONNX model + +model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse("KerasModel.keras") + +# Generate inference code +model.Generate() +model.OutputGenerated() +#print generated code +print("\n**************************************************") +print(" Generated code") +print("**************************************************\n") +model.PrintGenerated() +print("**************************************************\n\n\n") + +# Compile the generated code +ROOT.gInterpreter.Declare('#include "KerasModel.hxx"') + + +# ----------------------------------------------------------------------------- +# Step 3: Run inference +# ----------------------------------------------------------------------------- + +#instantiate SOFIE session class +session = ROOT.TMVA_SOFIE_KerasModel.Session() + +# Input tensor (same shape as training input) +x = np.array([[0.1, 0.2, 0.3, 0.4],[0.5, 0.6, 0.7, 0.8]], dtype=np.float32) + +# Run inference +y = session.infer(x) + +print("Inference output:", y) + diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.C b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.C deleted file mode 100644 index 876b2c87ff9a3..0000000000000 --- a/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.C +++ /dev/null @@ -1,32 +0,0 @@ -/// \file -/// \ingroup tutorial_ml -/// \notebook -nodraw -/// This macro run the SOFIE parser on the Keras model -/// obtaining running TMVA_Higgs_Classification.C -/// You need to run that macro before this one -/// -/// \author Lorenzo Moneta - -using namespace TMVA::Experimental; - - -void TMVA_SOFIE_Keras_HiggsModel(const char * modelFile = "Higgs_trained_model.keras"){ - - // check if the input file exists - if (gSystem->AccessPathName(modelFile)) { - Error("TMVA_SOFIE_RDataFrame","You need to run TMVA_Higgs_Classification.C to generate the Keras trained model"); - return; - } - - // parse the input Keras model into RModel object - SOFIE::RModel model = SOFIE::PyKeras::Parse(modelFile); - - TString modelHeaderFile = modelFile; - modelHeaderFile.ReplaceAll(".keras",".hxx"); - //Generating inference code - model.Generate(); - model.OutputGenerated(std::string(modelHeaderFile)); - - // copy include in $ROOTSYS/tutorials/ - std::cout << "include is in " << gROOT->GetIncludeDir() << std::endl; -} diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py new file mode 100644 index 0000000000000..7604bb0234c81 --- /dev/null +++ b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py @@ -0,0 +1,127 @@ +### \file +### \ingroup tutorial_ml +### \notebook -nodraw +### This macro run the SOFIE parser on the Keras model +### obtaining running TMVA_Higgs_Classification.C +### You need to run that macro before this one +### +### \author Lorenzo Moneta + + +import ROOT +from os.path import exists +import numpy as np +from keras import models, layers +from sklearn.model_selection import train_test_split + +def CreateModel(nlayers = 4, nunits = 64): + input = layers.Input(shape=(7,)) + x = input + for i in range(1,nlayers) : + y = layers.Dense(nunits, activation='relu')(x) + x = y + + output = layers.Dense(1, activation='sigmoid')(x) + model = models.Model(input, output) + model.compile(loss = 'binary_crossentropy', optimizer = 'adam', weighted_metrics = ['accuracy']) + model.summary() + return model + +def PrepareData() : + #get the input data + inputFile = str(ROOT.gROOT.GetTutorialDir()) + "/machine_learning/data/Higgs_data.root" + + df1 = ROOT.RDataFrame("sig_tree", inputFile) + sigData = df1.AsNumpy(columns=['m_jj', 'm_jjj', 'm_lv', 'm_jlv', 'm_bb', 'm_wbb', 'm_wwbb']) + #print(sigData) + + # stack all the 7 numpy array in a single array (nevents x nvars) + xsig = np.column_stack(list(sigData.values())) + data_sig_size = xsig.shape[0] + print("size of data", data_sig_size) + + # make SOFIE inference on background data + df2 = ROOT.RDataFrame("bkg_tree", inputFile) + bkgData = df2.AsNumpy(columns=['m_jj', 'm_jjj', 'm_lv', 'm_jlv', 'm_bb', 'm_wbb', 'm_wwbb']) + xbkg = np.column_stack(list(bkgData.values())) + data_bkg_size = xbkg.shape[0] + + ysig = np.ones(data_sig_size) + ybkg = np.zeros(data_bkg_size) + inputs_data = np.concatenate((xsig,xbkg),axis=0) + inputs_targets = np.concatenate((ysig,ybkg),axis=0) + + #split data in training and test data + + x_train, x_test, y_train, y_test = train_test_split( + inputs_data, inputs_targets, test_size=0.50, random_state=1234) + + return x_train, y_train, x_test, y_test + +def TrainModel(model, x, y, name) : + model.fit(x,y,epochs=5,batch_size=50) + modelFile = name + '.keras' + model.save(modelFile) + return modelFile + + +def GenerateCode(modelFile = "model.keras") : + + #check if the input file exists + if not exists(modelFile): + raise FileNotFoundError("INput model file not existing. You need to run TMVA_Higgs_Classification.C to generate the Keras trained model") + + + #parse the input Keras model into RModel object (force batch size to be 1) + model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse(modelFile) + + #Generating inference code + model.Generate() + model.OutputGenerated() + + modelName = modelFile.replace(".keras","") + return modelName + +################################################################### +## Step 1 : Create and Train model +################################################################### + +x_train, y_train, x_test, y_test = PrepareData() +#create dense model with 3 layers of 64 units +model = CreateModel(3,64) +modelFile = TrainModel(model,x_train, y_train, 'HiggsModel') + +################################################################### +## Step 2 : Parse model and generate inference code with SOFIE +################################################################### + +modelName = GenerateCode(modelFile) +modelHeaderFile = modelName + ".hxx" + +################################################################### +## Step 3 : Compile the generated C++ model code +################################################################### + +ROOT.gInterpreter.Declare('#include "' + modelHeaderFile + '"') + +################################################################### +## Step 4: Evaluate the model +################################################################### + +#get first the SOFIE session namespace +sofie = getattr(ROOT, 'TMVA_SOFIE_' + modelName) +session = sofie.Session() + +x = np.random.normal(0,1,7).astype(np.float32) +y = session.infer(x) +ykeras = model(x.reshape(1,7)).numpy() + +print("input to model is ",x, "\n\t -> output using SOFIE = ", y[0], " using Keras = ", ykeras[0]) + +if (abs(y[0]-ykeras[0]) > 0.01) : + raiseError('Result is different between SOFIE and Keras') + +print("OK") + + + diff --git a/tutorials/machine_learning/TMVA_SOFIE_Models.py b/tutorials/machine_learning/TMVA_SOFIE_Models.py index 6fa389ada9464..1cef6275dd706 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_Models.py +++ b/tutorials/machine_learning/TMVA_SOFIE_Models.py @@ -68,7 +68,7 @@ def PrepareData() : return x_train, y_train, x_test, y_test def TrainModel(model, x, y, name) : - model.fit(x,y,epochs=10,batch_size=50) + model.fit(x,y,epochs=5,batch_size=50) modelFile = name + '.keras' model.save(modelFile) return modelFile @@ -101,9 +101,12 @@ def GenerateModelCode(modelFile, generatedHeaderFile): generatedHeaderFile = "Higgs_Model.hxx" #need to remove existing header file since we are appending on same one if (os.path.exists(generatedHeaderFile)): - weightFile = "Higgs_Model.root" - print("removing existing files", generatedHeaderFile,weightFile) + print("removing existing file", generatedHeaderFile) os.remove(generatedHeaderFile) + +weightFile = "Higgs_Model.root" +if (os.path.exists(weightFile)): + print("removing existing file", weightFile) os.remove(weightFile) GenerateModelCode(model1, generatedHeaderFile) diff --git a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C index 2292327cf26f5..6569878494b0c 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C +++ b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C @@ -40,13 +40,13 @@ void TMVA_SOFIE_RDataFrame(int nthreads = 2){ ROOT::RDataFrame df1("sig_tree", inputFile); int nslots = df1.GetNSlots(); std::cout << "Running using " << nslots << " threads" << std::endl; - auto h1 = df1.DefineSlot("DNN_Value", SofieFunctor<7, TMVA_SOFIE_Higgs_trained_model::Session>(nslots), + auto h1 = df1.DefineSlot("DNN_Value", SofieFunctor<7, TMVA_SOFIE_HiggsModel::Session>(nslots), {"m_jj", "m_jjj", "m_lv", "m_jlv", "m_bb", "m_wbb", "m_wwbb"}) .Histo1D({"h_sig", "", 100, 0, 1}, "DNN_Value"); ROOT::RDataFrame df2("bkg_tree", inputFile); nslots = df2.GetNSlots(); - auto h2 = df2.DefineSlot("DNN_Value", SofieFunctor<7, TMVA_SOFIE_Higgs_trained_model::Session>(nslots), + auto h2 = df2.DefineSlot("DNN_Value", SofieFunctor<7, TMVA_SOFIE_HiggsModel::Session>(nslots), {"m_jj", "m_jjj", "m_lv", "m_jlv", "m_bb", "m_wbb", "m_wwbb"}) .Histo1D({"h_bkg", "", 100, 0, 1}, "DNN_Value"); diff --git a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.py b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.py index e4e037aef6863..af1c059fb544c 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.py +++ b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.py @@ -15,8 +15,8 @@ import ROOT # check if the input file exists -modelFile = "Higgs_trained_model.keras" -modelName = "Higgs_trained_model" +modelFile = "HiggsModel.keras" +modelName = "HiggsModel" if not exists(modelFile): raise FileNotFoundError("You need to run TMVA_Higgs_Classification.C to generate the Keras trained model") diff --git a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame_JIT.C b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame_JIT.C index 95dd4b1316278..072384019ea51 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame_JIT.C +++ b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame_JIT.C @@ -38,23 +38,15 @@ void CompileModelForRDF(const std::string & headerModelFile, unsigned int ninput return; } -void TMVA_SOFIE_RDataFrame_JIT(std::string modelFile = "Higgs_trained_model.keras"){ +void TMVA_SOFIE_RDataFrame_JIT(std::string modelName = "HiggsModel"){ // check if the input file exists - if (gSystem->AccessPathName(modelFile.c_str())) { - Info("TMVA_SOFIE_RDataFrame","You need to run TMVA_Higgs_Classification.C to generate the Keras trained model"); + std::string modelHeaderFile = modelName + ".hxx"; + if (gSystem->AccessPathName(modelHeaderFile.c_str())) { + Info("TMVA_SOFIE_RDataFrame","You need to run TMVA_SOFIE_Keras_Higgs_Model.py to generate the SOFIE header for the Keras trained model"); return; } - // parse the input Keras model into RModel object - SOFIE::RModel model = SOFIE::PyKeras::Parse(modelFile); - - std::string modelName = modelFile.substr(0,modelFile.find(".keras")); - std::string modelHeaderFile = modelName + std::string(".hxx"); - //Generating inference code - model.Generate(); - model.OutputGenerated(modelHeaderFile); - model.PrintGenerated(); // check that also weigh file exists std::string modelWeightFile = modelName + std::string(".dat"); if (gSystem->AccessPathName(modelWeightFile.c_str())) { diff --git a/tutorials/machine_learning/TMVA_SOFIE_RSofieReader.C b/tutorials/machine_learning/TMVA_SOFIE_RSofieReader.C index f6d4ae1e42316..4da25f8afaea5 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_RSofieReader.C +++ b/tutorials/machine_learning/TMVA_SOFIE_RSofieReader.C @@ -22,7 +22,7 @@ using namespace TMVA::Experimental; void TMVA_SOFIE_RSofieReader(){ - RSofieReader model("Higgs_trained_model.keras"); + RSofieReader model("HiggsModel.keras", {}, true ); // for debugging //RSofieReader model("Higgs_trained_model.keras", {}, true); From cd1c75696da3a83f6ba8ac1314ae73e4375b013e Mon Sep 17 00:00:00 2001 From: moneta Date: Mon, 2 Feb 2026 19:31:00 +0100 Subject: [PATCH 3/6] [tmva][pymva] Fix a bug in generating code for LayerNormalization The bug fixes the closing of the loops on the outer axis. The tests worked because the number of outer axes and normalization axes was the same in the test scripts. --- .../inc/TMVA/ROperator_LayerNormalization.hxx | 35 +++---------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx b/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx index f98ce201d400d..1d2abaa85ef71 100644 --- a/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx @@ -243,7 +243,8 @@ public: // compute mean and std-dev. Save in tensors if requested out << SP << "// Compute the mean\n"; - // Loop over all the dims in [0, fAxis) + + // Loop over all the outer dims in [0, fAxis) for (size_t i = 0; i < fAxis; i++) { std::string iIdx = "axis_" + std::to_string(i); out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] @@ -262,18 +263,8 @@ public: } out << SP << SP << "mean /= " << fType << "(" << fNormalizedLength << ");\n"; - // for (size_t i = fAxis; i < fSize; i++) { - // out << SP << "}\n"; - // } - // tensor_" << fNMean << "[" << axesIndex << "] out << SP << "// Compute the inverse Standard Deviation\n"; - // Loop over the normalized dimensions - // for (size_t i = 0; i < fAxis; i++) { - // std::string iIdx = "axis_" + std::to_string(i); - // out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] - // << "; " << iIdx << "++){\n"; - // } // Set sum = 0 out << SP << SP << fType << " sum = 0.;\n"; @@ -291,9 +282,6 @@ public: out << SP << SP << fType << " invStdDev = 1 / std::sqrt("; out << "sum / " << fType << "(" << fNormalizedLength << ") + " << fAttrEpsilon << ");\n"; - // for (size_t i = 0; i < fAxis; i++) { - // out << SP << "}\n"; - // } // set output mean and invStdDev if requested if (!fNMean.empty()) @@ -304,11 +292,6 @@ public: // scale and add bias out << SP << "// Y = Scale o InvStdDev (X - Mean)\n"; - // for (size_t i = 0; i < fAxis; i++) { - // std::string iIdx = "axis_" + std::to_string(i); - // out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] - // << "; " << iIdx << "++){\n"; - // } for (size_t j = fAxis; j < fSize; j++) { std::string jIdx = "axis_" + std::to_string(j); @@ -324,23 +307,15 @@ public: out << " + tensor_" << fNB << "[" << biasIndex << "]"; out << ";\n"; + // close loops on normalizing dim [..,fAxis,...fSize-1] for (size_t j = fAxis; j < fSize; j++) { out << SP << SP << "}\n"; } - for (size_t i = fAxis; i < fSize; i++) { + // close loops on the other dimensions [0,...,fAxis] + for (size_t i = 0; i < fAxis; i++) { out << SP << "}\n"; } - // if (!fNB.empty()) { - // std::string bias = "tensor_" + (fNBroadcastedB.empty() ? fNB : fNBroadcastedB); - // out << SP << "// Add the bias to Y\n"; - // out << SP << "int " << opName << "_n = " << fLength << ";\n"; - // out << SP << "float " << opName << "_alpha = 1.;\n"; - // out << SP << "int " << opName << "_inc = 1;\n"; - // out << SP << "BLAS::saxpy_(&" << opName << "_n, &" << opName << "_alpha, " << bias << ", &"; - // out << opName << "_inc, " << "tensor_" << fNY << ", &" << opName << "_inc);\n"; - // } - return out.str(); } From 436c2ad75bbd893be6e5bf27751a6ac55b336ae5 Mon Sep 17 00:00:00 2001 From: moneta Date: Tue, 3 Feb 2026 18:07:50 +0100 Subject: [PATCH 4/6] [tmva][sofie] Use wrapper function for Gemm call and remove call to saxpy Avoid using saxpy and use directly the wrapper function in Sofie_common to call blas and automatically also include the bias addition fix doing bias for Conv operator, beta must be equal to 1 now we don't use saxpy --- tmva/sofie/inc/TMVA/ROperator_Conv.hxx | 71 ++++++++++++++++++-------- tmva/sofie/inc/TMVA/ROperator_Gemm.hxx | 1 + 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/tmva/sofie/inc/TMVA/ROperator_Conv.hxx b/tmva/sofie/inc/TMVA/ROperator_Conv.hxx index 87d1ad0a0bf67..2d5cca8c6b17f 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Conv.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Conv.hxx @@ -436,7 +436,10 @@ public: out << SP << "int " << OpName << "_n = " << fShapeW[0] << ";\n"; // output channels out << SP << "int " << OpName << "_k = " << fShapeW[1] * fAttrKernelShape[0] * fAttrKernelShape[1] * fAttrKernelShape[2] << ";\n"; out << SP << "float " << OpName << "_alpha = 1.0;\n"; - out << SP << "float " << OpName << "_beta = 0.0;\n"; + if (fNB != "") + out << SP << "float " << OpName << "_beta = 1.0;\n"; + else // when bias is not present beta needs to be equal to zero to avoid re-using previous results in output tensor + out << SP << "float " << OpName << "_beta = 0.0;\n"; // Loop on batch size @@ -509,11 +512,23 @@ public: << "tensor_" << fNX << "_xcol);\n\n "; } // BLAS - out << SP << SP << "BLAS::sgemm_(&" << OpName << "_transA, &" << OpName << "_transB, &" << OpName << "_m, &" - << OpName << "_n, &" << OpName << "_k, &" << OpName << "_alpha, " << "tensor_" << fNX << "_xcol, &" << OpName - << "_m,\n"; // use m if op_xcol is not transpose , otherwise k - out << SP << SP << SP << "tensor_" << fNX << "_f, &" << OpName << "_k, &" << OpName << "_beta, tensor_" << fNY - << " + out_offset, &" << OpName << "_m);\n"; + out << SP << "TMVA::Experimental::SOFIE::Gemm_Call(" + << "tensor_" << fNY << " + out_offset, false, false, " + << OpName << "_m, " << OpName << "_n, " << OpName << "_k, " + << OpName << "_alpha, " << "tensor_" << fNX << "_xcol, tensor_" << fNX << "_f, " + << OpName << "_beta, "; + if (fNB != "") + out << "tensor_" << fNB; + else + out << "nullptr"; + out << ");\n"; + + + // out << SP << SP << "BLAS::sgemm_(&" << OpName << "_transA, &" << OpName << "_transB, &" << OpName << "_m, &" + // << OpName << "_n, &" << OpName << "_k, &" << OpName << "_alpha, " << "tensor_" << fNX << "_xcol, &" << OpName + // << "_m,\n"; // use m if op_xcol is not transpose , otherwise k + // out << SP << SP << SP << "tensor_" << fNX << "_f, &" << OpName << "_k, &" << OpName << "_beta, tensor_" << fNY + // << " + out_offset, &" << OpName << "_m);\n"; } else { // case of group convolution // Unroll (IM2COL) the input tensor- make loop on groups and repeat operations (IM2COL + GEMM for each @@ -522,8 +537,8 @@ public: out << SP << SP << "for (size_t g = 0; g < " << fAttrGroup << "; g++) {\n"; out << SP << SP << "size_t x_offset = n * " << inputBatchStride << " + g * " << fShapeW[1] << " * " << inputChannelStride << ";\n "; - out << SP << SP << "size_t out_offset = n * " << outputBatchStride << " + g * " - << fShapeW[0] << " * (" << outputChannelStride << ") / " << fAttrGroup << ";\n "; + out << SP << SP << "size_t g_offset = g * " << fShapeW[0] << " * (" << outputChannelStride << ") / " << fAttrGroup << ";\n "; + out << SP << SP << "size_t out_offset = n * " << outputBatchStride << " + g_offset;\n"; if (fDim < 3) { out << SP << SP << "TMVA::Experimental::SOFIE::UTILITY::Im2col(tensor_" << fNX @@ -561,26 +576,38 @@ public: out << SP << SP << SP << "size_t offset_f = g * " << fShapeW[0] * fShapeW[1] * fAttrKernelShape[0] * fAttrKernelShape[1] * fAttrKernelShape[2] / fAttrGroup << ";\n"; - out << SP << SP << "BLAS::sgemm_(&" << OpName << "_transA, &" << OpName << "_transB, &" << OpName << "_m, &" - << OpName << "_n, &" << OpName << "_k, &" << OpName << "_alpha, tensor_" << fNX << "_xcol, &" << OpName - << "_m,\n"; // use m if op_xcol is not transpose , otherwise k - out << SP << SP << SP << "tensor_" << fNX << "_f + offset_f, &" << OpName << "_k, &" << OpName << "_beta, tensor_" << fNY - << " + out_offset" - << ", &" << OpName << "_m);\n"; + + out << SP << "TMVA::Experimental::SOFIE::Gemm_Call(" + << "tensor_" << fNY << " + out_offset, false, false, " + << OpName << "_m, " << OpName << "_n, " << OpName << "_k, " + << OpName << "_alpha, " << "tensor_" << fNX << "_xcol, tensor_" << fNX << "_f + offset_f, " + << OpName << "_beta, "; + if (fNB != "") + out << "tensor_" << fNB << " + g_offset"; + else + out << "nullptr"; + out << ");\n"; + + // out << SP << SP << "BLAS::sgemm_(&" << OpName << "_transA, &" << OpName << "_transB, &" << OpName << "_m, &" + // << OpName << "_n, &" << OpName << "_k, &" << OpName << "_alpha, tensor_" << fNX << "_xcol, &" << OpName + // << "_m,\n"; // use m if op_xcol is not transpose , otherwise k + // out << SP << SP << SP << "tensor_" << fNX << "_f + offset_f, &" << OpName << "_k, &" << OpName << "_beta, tensor_" << fNY + // << " + out_offset" + // << ", &" << OpName << "_m);\n"; out << SP << SP << "}\n"; // end of group loop } - if (fNB != "") { - out << SP << "int " << OpName << "_size = " << outputBatchStride << ";\n"; - out << SP << "float " << OpName << "_gamma = 1.0;\n"; - out << SP << "int " << OpName << "_incx = 1;\n"; - out << SP << "int " << OpName << "_incy = 1;\n"; + // if (fNB != "") { + // out << SP << "int " << OpName << "_size = " << outputBatchStride << ";\n"; + // out << SP << "float " << OpName << "_gamma = 1.0;\n"; + // out << SP << "int " << OpName << "_incx = 1;\n"; + // out << SP << "int " << OpName << "_incy = 1;\n"; - out << SP << "BLAS::saxpy_(&" << OpName << "_size, &" << OpName << "_gamma, tensor_" << fNB << ", &" - << OpName << "_incx, tensor_" << fNY << " + out_offset, &" << OpName << "_incy);\n"; + // out << SP << "BLAS::saxpy_(&" << OpName << "_size, &" << OpName << "_gamma, tensor_" << fNB << ", &" + // << OpName << "_incx, tensor_" << fNY << " + out_offset, &" << OpName << "_incy);\n"; - } + // } out << SP << "}\n"; // end of batch size loop return out.str(); diff --git a/tmva/sofie/inc/TMVA/ROperator_Gemm.hxx b/tmva/sofie/inc/TMVA/ROperator_Gemm.hxx index 7d1c497bed56e..0b84bcc6eec71 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Gemm.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Gemm.hxx @@ -357,6 +357,7 @@ namespace SOFIE{ } // do the bias broadcasting if (fBroadcastBias) { + fAttrBeta = 1.; out << SP << "for (size_t j = 0; j < " << sY[0] << "; j++) { \n"; out << SP << SP << "size_t y_index = "; if (doStackMul) // add offset in caseof stack multiplications (not sure if bias is present in these cases) From 65c39cd2ff2a12c2c9389800219ea2565189e203 Mon Sep 17 00:00:00 2001 From: moneta Date: Mon, 2 Feb 2026 11:56:02 +0100 Subject: [PATCH 5/6] [tmva][sofie][keras parser] Add missing files in CMakeLists The Python files were moved in https://github.com/root-project/root/commit/3281aa99212b99cc2ed0e74840568a56d5a79939 in a different location and it was not done during the rebase. This is now fixed Fix also an issue in parsing input shape from the Keras model and add support also for the different types of input shapes Increase also test tolerance from 10-3 to 10-2 Add also test RModelKerasParser test Fix an issue with RSofieReader forcing a loading of libROOTTMVASofie when compiling the SOFIE code on the flight with RReader [tmva][sofie] Apply review comments from Jonas Move python files used only for testing from the bindings/python code directory to the binding/python test directory --- .../pythonizations/python/CMakeLists.txt | 25 +++++- .../pythonizations/python/ROOT/_facade.py | 3 +- .../ROOT/_pythonization/_tmva/__init__.py | 18 ---- .../_tmva/_sofie/_parser/_keras/parser.py | 43 ++++------ .../generate_keras_functional.py | 20 ++--- .../generate_keras_sequential.py | 20 +---- .../_keras => test}/parser_test_function.py | 27 +++--- .../pythonizations/test/sofie_keras_parser.py | 18 ++-- tmva/sofie/inc/TMVA/ROperator_BasicBinary.hxx | 6 +- tmva/sofie/inc/TMVA/ROperator_Reshape.hxx | 4 +- tmva/sofie/test/CMakeLists.txt | 19 ++-- tmva/sofie/test/TestRModelParserKeras.C | 86 ++++++++++--------- tmva/sofie/test/generateKerasModels.py | 22 ++--- tmva/sofie_parsers/src/RModelParser_Keras.cxx | 4 +- tmva/tmva/inc/TMVA/RSofieReader.hxx | 6 ++ tutorials/CMakeLists.txt | 12 +-- .../machine_learning/TMVA_SOFIE_Inference.py | 11 ++- .../TMVA_SOFIE_Keras_HiggsModel.py | 6 +- .../machine_learning/TMVA_SOFIE_RDataFrame.C | 10 +-- 19 files changed, 167 insertions(+), 193 deletions(-) rename bindings/pyroot/pythonizations/{python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras => test}/generate_keras_functional.py (93%) rename bindings/pyroot/pythonizations/{python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras => test}/generate_keras_sequential.py (91%) rename bindings/pyroot/pythonizations/{python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras => test}/parser_test_function.py (89%) diff --git a/bindings/pyroot/pythonizations/python/CMakeLists.txt b/bindings/pyroot/pythonizations/python/CMakeLists.txt index 4c96c7556c7cf..e471dbdc62932 100644 --- a/bindings/pyroot/pythonizations/python/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/python/CMakeLists.txt @@ -58,7 +58,30 @@ if(tmva) ROOT/_pythonization/_tmva/_rtensor.py ROOT/_pythonization/_tmva/_tree_inference.py ROOT/_pythonization/_tmva/_utils.py - ROOT/_pythonization/_tmva/_gnn.py) + ROOT/_pythonization/_tmva/_gnn.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/__init__.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py) if(dataframe) list(APPEND PYROOT_EXTRA_PYTHON_SOURCES ROOT/_pythonization/_tmva/_batchgenerator.py) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index 5e38ab280f898..29ae80e18683a 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -410,9 +410,10 @@ def TMVA(self): # This line is needed to import the pythonizations in _tmva directory. # The comment suppresses linter errors about unused imports. from ._pythonization import _tmva # noqa: F401 + from ._pythonization._tmva._sofie._parser._keras.parser import PyKeras ns = self._fallback_getattr("TMVA") - setattr(ns.Experimental.SOFIE, "PyKeras", _tmva.PyKeras) + setattr(ns.Experimental.SOFIE, "PyKeras", PyKeras) hasRDF = "dataframe" in self.gROOT.GetConfigFeatures() if hasRDF: try: diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py index ba48bf5ea6d01..e271f232326e3 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py @@ -37,24 +37,6 @@ def inject_rbatchgenerator(ns): return ns -from ._gnn import RModel_GNN, RModel_GraphIndependent -from ._sofie._parser._keras.parser import PyKeras - -hasRDF = "dataframe" in cppyy.gbl.ROOT.GetROOT().GetConfigFeatures() -if hasRDF: - from ._rtensor import ( - get_array_interface, - add_array_interface_property, - RTensorGetitem, - pythonize_rtensor, - _AsRTensor, - ) - -# this should be available only when xgboost is there ? -# We probably don't need a protection here since the code is run only when there is xgboost -from ._tree_inference import SaveXGBoost - - # list of python classes that are used to pythonize TMVA classes python_classes = [Factory, DataLoader, CrossValidation] diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py index c90f3a4956b99..e52977f82b2cf 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py @@ -98,8 +98,6 @@ def add_layer_into_RModel(rmodel, layer_data): fLayerType = layer_data['layerType'] - print('Model: parsing layer',fLayerType) - # reshape and flatten layers don't have weights, but they need constant tensor for the shape if fLayerType == "Reshape" or fLayerType == "Flatten": Attributes = layer_data['layerAttributes'] @@ -132,7 +130,6 @@ def add_layer_into_RModel(rmodel, layer_data): print(len(TargetShape)) rmodel.AddInitializedTensor['int64_t'](shape_tensor_name, [len(TargetShape)], shape_data) - print('check other layers...') # These layers only have one operator - excluding the recurrent layers, in which the activation function(s) # are included in the recurrent operator if fLayerType in mapKerasLayer.keys(): @@ -288,7 +285,7 @@ class PyKeras: def Parse(filename, batch_size=1): # If a model does not have a defined batch size, then assuming it is 1 - # TensoFlow/Keras is too fragile to import unconditionally. As its presence might break several ROOT + # TensorFlow/Keras is too fragile to import unconditionally. As its presence might break several ROOT # usecases and importing keras globally will slow down importing ROOT, which is not desired. For this, # we import keras within the functions instead of importing it at the start of the file (i.e. globally). # So, whenever the parser function is called, only then keras will be imported, and not everytime we @@ -324,6 +321,8 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s rmodel = gbl_namespace.TMVA.Experimental.SOFIE.RModel.RModel(filename_nodir, parsetime) + print("PyKeras: parsing model ",filename) + # iterate over the layers and add them to the RModel # in case of keras 3.x (particularly in sequential models), the layer input and output name conventions are # different from keras 2.x. In keras 2.x, the layer input name is consistent with previous layer's output @@ -516,28 +515,24 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s fPInputDType.append(dtype[9:-2]) if len(fPInputShape) == 1: - fInputName = fPInputs[0] - fInputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fPInputDType[0]) - if fInputDType == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: - if fPInputShape[0][0] is None or fPInputShape[0][0] <= 0: - fPInputShape = list(fPInputShape[0]) - fPInputShape[0] = batch_size - rmodel.AddInputTensorInfo(fInputName, gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT, fPInputShape) - rmodel.AddInputTensorName(fInputName) - else: - raise TypeError("Type error: TMVA SOFIE does not yet support data type " + gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType)) + inputName = fPInputs[0] + inputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fPInputDType[0]) + # convert ot a list to modify batch size + inputShape = list(fPInputShape[0]) + #set the batch size in case of -1 or None as first value + if inputShape[0] is None or inputShape[0] <= 0: + inputShape[0] = batch_size + rmodel.AddInputTensorInfo(inputName, inputDType, inputShape) + rmodel.AddInputTensorName(inputName) else: # Iterating through multiple input tensors - for fInputName, fInputDType, fInputShapeTuple in zip(fPInputs, fPInputDType, fPInputShape): - fInputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType) - if fInputDType == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: - if fInputShapeTuple[0] is None or fInputShapeTuple[0] <= 0: - fInputShapeTuple = list(fInputShapeTuple) - fInputShapeTuple[0] = batch_size - rmodel.AddInputTensorInfo(fInputName, gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT, fInputShapeTuple) - rmodel.AddInputTensorName(fInputName) - else: - raise TypeError("Type error: TMVA SOFIE does not yet support data type " + gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType)) + for inputName, inputDType, inputShapeTuple in zip(fPInputs, fPInputDType, fPInputShape): + inputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(inputDType) + inputShape = list(inputShapeTuple) + if inputShape[0] is None or inputShape[0] <= 0: + inputShape[0] = batch_size + rmodel.AddInputTensorInfo(inputName, inputDType, inputShape) + rmodel.AddInputTensorName(inputName) # Adding OutputTensorInfos outputNames = [] diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py b/bindings/pyroot/pythonizations/test/generate_keras_functional.py similarity index 93% rename from bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py rename to bindings/pyroot/pythonizations/test/generate_keras_functional.py index eb7443f5b6985..2ab6b2d3b3bd0 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py +++ b/bindings/pyroot/pythonizations/test/generate_keras_functional.py @@ -2,15 +2,8 @@ def generate_keras_functional(dst_dir): from keras import models, layers, backend import numpy as np + from parser_test_function import is_channels_first_supported - def is_channels_first_supported() : - #channel first is not supported on tensorflow CPU versions - from keras import backend - if backend.backend() == "tensorflow" : - import tensorflow as tf - if len(tf.config.list_physical_devices("GPU")) == 0: - return False - return True # Helper training function def train_and_save(model, name): @@ -45,10 +38,11 @@ def train_and_save(model, name): train_and_save(model, "Add") # AveragePooling2D channels_first - inp = layers.Input(shape=(3, 8, 8)) - out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first')(inp) - model = models.Model(inp, out) - train_and_save(model, "AveragePooling2D_channels_first") + if (is_channels_first_supported()): + inp = layers.Input(shape=(3, 8, 8)) + out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "AveragePooling2D_channels_first") # AveragePooling2D channels_last inp = layers.Input(shape=(8, 8, 3)) @@ -191,7 +185,7 @@ def train_and_save(model, name): # Layer Combination inp = layers.Input(shape=(32, 32, 3)) - x = layers.Conv2D(8, (3,3), padding="same", activation="relu")(inp) + x = layers.Conv2D(8, (3,3), padding="same", activation="relu", data_format='channels_last')(inp) x = layers.MaxPooling2D((2,2))(x) x = layers.Reshape((16, 16, 8))(x) x = layers.Permute((3, 1, 2))(x) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py b/bindings/pyroot/pythonizations/test/generate_keras_sequential.py similarity index 91% rename from bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py rename to bindings/pyroot/pythonizations/test/generate_keras_sequential.py index a519615c13a88..8c3b6af8ec656 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py +++ b/bindings/pyroot/pythonizations/test/generate_keras_sequential.py @@ -2,15 +2,7 @@ def generate_keras_sequential(dst_dir): from keras import models, layers, backend import numpy as np - - def is_channels_first_supported() : - #channel first is not supported on tensorflow CPU versions - from keras import backend - if backend.backend() == "tensorflow" : - import tensorflow as tf - if len(tf.config.list_physical_devices("GPU")) == 0: - return False - return True + from parser_test_function import is_channels_first_supported # Helper training function def train_and_save(model, name): @@ -22,14 +14,6 @@ def train_and_save(model, name): print("fitting sequential model",name) model.save(f"{dst_dir}/Sequential_{name}_test.keras") - def is_channels_first_supported() : - #channel first is not supported on tensorflow CPU versions - if backend.backend() == "tensorflow" : - import tensorflow as tf - if len(tf.config.list_physical_devices("GPU")) == 0: - return False - - return True # Binary Ops: Add, Subtract, Multiply are not typical in Sequential - skipping those # Concat (not applicable in Sequential without multi-input) @@ -218,7 +202,7 @@ def is_channels_first_supported() : layers.Input(shape=(4, 8, 2)), layers.Permute((2, 1, 3)), layers.Reshape((8, 8, 1)), - layers.Conv2D(4, (3,3), padding='same', activation='relu'), + layers.Conv2D(4, (3,3), padding='same', activation='relu', data_format='channels_last'), layers.AveragePooling2D((2,2)), layers.BatchNormalization(), layers.Flatten(), diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py b/bindings/pyroot/pythonizations/test/parser_test_function.py similarity index 89% rename from bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py rename to bindings/pyroot/pythonizations/test/parser_test_function.py index 8b54cef5ebe72..9cffea59038e4 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py +++ b/bindings/pyroot/pythonizations/test/parser_test_function.py @@ -32,8 +32,17 @@ NumPy arrays before being passed to the is_accurate function for comparison. ''' +def is_channels_first_supported() : + #channel first is not supported on tensorflow CPU versions + from keras import backend + if backend.backend() == "tensorflow" : + import tensorflow as tf + if len(tf.config.list_physical_devices("GPU")) == 0: + return False + + return True -def is_accurate(tensor_a, tensor_b, tolerance=1e-3): +def is_accurate(tensor_a, tensor_b, tolerance=1e-2): tensor_a = tensor_a.flatten() tensor_b = tensor_b.flatten() for i in range(len(tensor_a)): @@ -73,16 +82,12 @@ def generate_and_test_inference(model_file_path: str, generated_header_file_dir: inference_session = sofie_model_namespace.Session(generated_header_file_path.removesuffix(".hxx") + ".dat") keras_model = keras.models.load_model(model_file_path) keras_model.load_weights(model_file_path) - if len(keras_model.inputs) == 1: - input_shape = list(keras_model.inputs[0].shape) - input_shape[0] = batch_size - input_tensors = np.ones(input_shape, dtype='float32') - else: - input_tensors = [] - for model_input in keras_model.inputs: - input_shape = list(model_input.shape) - input_shape[0] = batch_size - input_tensors.append(np.ones(input_shape, dtype='float32')) + + input_tensors = [] + for model_input in keras_model.inputs: + input_shape = list(model_input.shape) + input_shape[0] = batch_size + input_tensors.append(np.ones(input_shape, dtype='float32')) sofie_inference_result = inference_session.infer(*input_tensors) sofie_output_tensor_shape = list(rmodel.GetTensorShape(rmodel.GetOutputTensorNames()[0])) # get output shape # from SOFIE diff --git a/bindings/pyroot/pythonizations/test/sofie_keras_parser.py b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py index 9965649019cb8..81fd80606ec13 100644 --- a/bindings/pyroot/pythonizations/test/sofie_keras_parser.py +++ b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py @@ -2,19 +2,11 @@ import os import shutil -def is_channels_first_supported() : - #channel first is not supported on tensorflow CPU versions - from keras import backend - if backend.backend() == "tensorflow" : - import tensorflow as tf - if len(tf.config.list_physical_devices("GPU")) == 0: - return False - - return True - -from ROOT._pythonization._tmva._sofie._parser._keras.parser_test_function import generate_and_test_inference -from ROOT._pythonization._tmva._sofie._parser._keras.generate_keras_functional import generate_keras_functional -from ROOT._pythonization._tmva._sofie._parser._keras.generate_keras_sequential import generate_keras_sequential + +from parser_test_function import generate_and_test_inference +from parser_test_function import is_channels_first_supported +from generate_keras_functional import generate_keras_functional +from generate_keras_sequential import generate_keras_sequential def make_testname(test_case: str): diff --git a/tmva/sofie/inc/TMVA/ROperator_BasicBinary.hxx b/tmva/sofie/inc/TMVA/ROperator_BasicBinary.hxx index 491b669554118..62298e655f038 100644 --- a/tmva/sofie/inc/TMVA/ROperator_BasicBinary.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_BasicBinary.hxx @@ -121,11 +121,11 @@ public: } if (dynamicInputs & 1 && model.Verbose()) std::cout << BinaryOperatorTrait::Name() << " : input " << fNA << " is dynamic " - << ConvertShapeToString(fDimShapeA) << " "; + << ConvertShapeToString(fDimShapeA) << std::endl; if (dynamicInputs & 2 && model.Verbose()) std::cout << BinaryOperatorTrait::Name() << " : input " << fNB << " is dynamic " - << ConvertShapeToString(fDimShapeB) << " "; - std::cout << std::endl; + << ConvertShapeToString(fDimShapeB) << std::endl; + // check if need to broadcast at initialization time if shapes are known and different // (we could broadcast the tensor tensor to maximum values of dynamic shapes - to be done) // case of known shapes diff --git a/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx b/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx index 7f241fd04819e..313b41eacacbc 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx @@ -246,8 +246,10 @@ public: void Initialize(RModel& model) override { - std::cout << "initialize reshape op type " << fOpMode << " - " << fNInput2 << " " << fNData << std::endl; fVerbose = model.Verbose(); + if (fVerbose) + std::cout << "initialize reshape op type " << fOpMode << " - " << fNInput2 << " " << fNData << std::endl; + if (model.CheckIfTensorAlreadyExist(fNData) == false) { // input must be a graph input, or already initialized intermediate tensor throw std::runtime_error("TMVA Reshape Op Input Tensor " + fNData + " is not found in model"); diff --git a/tmva/sofie/test/CMakeLists.txt b/tmva/sofie/test/CMakeLists.txt index f666d200545af..c91b70b481e51 100644 --- a/tmva/sofie/test/CMakeLists.txt +++ b/tmva/sofie/test/CMakeLists.txt @@ -166,16 +166,10 @@ endif() # Any features that link against libpython are disabled if built with tpython=OFF if (tpython AND ROOT_KERAS_FOUND AND BLAS_FOUND) - set(unsupported_keras_version "3.5.0") + configure_file(generateKerasModels.py generateKerasModels.py COPYONLY) + configure_file(scale_by_2_op.hxx scale_by_2_op.hxx COPYONLY) - # TODO: make sure we also support the newest Keras - if (NOT DEFINED ROOT_KERAS_VERSION) - message(WARNING "Keras found, but version unknown — cannot verify compatibility.") - elseif (ROOT_KERAS_VERSION VERSION_LESS ${unsupported_keras_version}) - configure_file(generateKerasModels.py generateKerasModels.py COPYONLY) - configure_file(scale_by_2_op.hxx scale_by_2_op.hxx COPYONLY) - - ROOT_ADD_GTEST(TestRModelParserKeras TestRModelParserKeras.C + ROOT_ADD_GTEST(TestRModelParserKeras TestRModelParserKeras.C LIBRARIES ROOTTMVASofie Python3::NumPy @@ -184,10 +178,9 @@ if (tpython AND ROOT_KERAS_FOUND AND BLAS_FOUND) INCLUDE_DIRS SYSTEM ${CMAKE_CURRENT_BINARY_DIR} - ) - else() - message(WARNING "Keras version ${ROOT_KERAS_VERSION} is too new for the SOFIE Keras parser (only supports < ${unsupported_keras_version})") - endif() + ENVIRONMENT PYTHONPATH=${localruntimedir} + ) + endif() diff --git a/tmva/sofie/test/TestRModelParserKeras.C b/tmva/sofie/test/TestRModelParserKeras.C index 3fef4a0bbd9f2..e99ac05106ae5 100644 --- a/tmva/sofie/test/TestRModelParserKeras.C +++ b/tmva/sofie/test/TestRModelParserKeras.C @@ -48,10 +48,10 @@ TEST(RModelParser_Keras, SEQUENTIAL) Py_Initialize(); - if (gSystem->AccessPathName("KerasModelSequential.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelSequential.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelSequential.h5",{{4,8}}); + TMVA::Experimental:: RSofieReader r("KerasModelSequential.keras",{{4,8}}); std::vector outputSequential = r.Compute(inputSequential); @@ -68,7 +68,7 @@ TEST(RModelParser_Keras, SEQUENTIAL) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelSequential.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelSequential.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.array([0.12107884, 0.89718615, 0.89123899, 0.32197549," "0.17891638, 0.83555135, 0.98680066, 0.14496809," "0.07255503, 0.55386989, 0.6628149 , 0.29843291," @@ -101,10 +101,10 @@ TEST(RModelParser_Keras, FUNCTIONAL) Py_Initialize(); - if (gSystem->AccessPathName("KerasModelFunctional.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelFunctional.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelFunctional.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelFunctional.keras"); std::vector outputFunctional = r.Compute(inputFunctional); PyObject* main = PyImport_AddModule("__main__"); @@ -120,7 +120,7 @@ TEST(RModelParser_Keras, FUNCTIONAL) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelFunctional.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelFunctional.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.array([0.60828574, 0.50069386, 0.75186709, 0.14968806, 0.7692464 ,0.77027585, 0.75095316, 0.96651197," "0.38536308, 0.95565917, 0.62796356, 0.13818375, 0.65484891,0.89220363, 0.23879365, 0.00635323]).reshape(2,8)",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); @@ -146,10 +146,10 @@ TEST(RModelParser_Keras, BATCH_NORM) 0.69947928, 0.29743695, 0.81379782, 0.39650574}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelBatchNorm.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelBatchNorm.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelBatchNorm.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelBatchNorm.keras"); std::vector outputBatchNorm = r.Compute(inputBatchNorm); PyObject* main = PyImport_AddModule("__main__"); @@ -165,7 +165,7 @@ TEST(RModelParser_Keras, BATCH_NORM) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelBatchNorm.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelBatchNorm.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.array([0.22308163, 0.95274901, 0.44712538, 0.84640867," "0.69947928, 0.29743695, 0.81379782, 0.39650574]).reshape(2,4)",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); @@ -196,10 +196,10 @@ TEST(DISABLED_RModelParser_Keras, CONV_VALID) 1,1,1,1, 1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelConv2D_Valid.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelConv2D_Valid.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelConv2D_Valid.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelConv2D_Valid.keras"); std::vector outputConv2D_Valid = r.Compute(inputConv2D_Valid); PyObject* main = PyImport_AddModule("__main__"); @@ -215,7 +215,7 @@ TEST(DISABLED_RModelParser_Keras, CONV_VALID) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelConv2D_Valid.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelConv2D_Valid.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.ones((1,4,4,1))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); @@ -246,10 +246,10 @@ TEST(DISABLED_RModelParser_Keras, CONV_SAME) 1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelConv2D_Same.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelConv2D_Same.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelConv2D_Same.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelConv2D_Same.keras"); std::vector outputConv2D_Same = r.Compute(inputConv2D_Same); PyObject* main = PyImport_AddModule("__main__"); @@ -265,7 +265,7 @@ TEST(DISABLED_RModelParser_Keras, CONV_SAME) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelConv2D_Same.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelConv2D_Same.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.ones((1,4,4,1))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); @@ -292,10 +292,10 @@ TEST(RModelParser_Keras, RESHAPE) 1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelReshape.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelReshape.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelReshape.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelReshape.keras"); std::vector outputReshape = r.Compute(inputReshape); PyObject* main = PyImport_AddModule("__main__"); @@ -311,7 +311,7 @@ TEST(RModelParser_Keras, RESHAPE) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelReshape.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelReshape.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.ones((1,4,4,1))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); @@ -336,10 +336,10 @@ TEST(RModelParser_Keras, CONCATENATE) std::vectorinputConcatenate_2 = {1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelConcatenate.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelConcatenate.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelConcatenate.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelConcatenate.keras",{{1,2},{1,2}}); std::vector outputConcatenate = r.Compute(inputConcatenate_1, inputConcatenate_2); PyObject* main = PyImport_AddModule("__main__"); @@ -355,15 +355,15 @@ TEST(RModelParser_Keras, CONCATENATE) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelConcatenate.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelConcatenate.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input_1=numpy.ones((1,2))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input_2=numpy.ones((1,2))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model([input_1,input_2]).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputConcatenateSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); + long pOutputConcatenateSize=PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputConcatenate.size(), pOutputConcatenateSize); + //Testing the actual and expected output tensor sizes (can fail if an error eccoured and returns a -1 from Python) + EXPECT_EQ((long) outputConcatenate.size(), pOutputConcatenateSize); PyArrayObject* pConcatenateValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); float* pOutputConcatenate=(float*)PyArray_DATA(pConcatenateValues); @@ -377,14 +377,15 @@ TEST(RModelParser_Keras, CONCATENATE) TEST(RModelParser_Keras, BINARY_OP) { constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinput_BinaryOp_1 = {1,1}; - std::vectorinput_BinaryOp_2 = {1,1}; + // test with batch size =2 input shapes are {2,2} + std::vectorinput_BinaryOp_1 = {1,2,3,4}; + std::vectorinput_BinaryOp_2 = {5,6,7,8}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelBinaryOp.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelBinaryOp.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelBinaryOp.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelBinaryOp.keras", {{2,2},{2,2}}); std::vector outputBinaryOp = r.Compute(input_BinaryOp_1,input_BinaryOp_2); PyObject* main = PyImport_AddModule("__main__"); @@ -400,15 +401,15 @@ TEST(RModelParser_Keras, BINARY_OP) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelBinaryOp.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input1=numpy.array([1,1])",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input2=numpy.array([1,1])",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelBinaryOp.keras')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("input1=numpy.array([[1,2],[3,4]],dtype='float32')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("input2=numpy.array([[5,6],[7,8]],dtype='float32')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model([input1,input2]).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputBinaryOpSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); + long pOutputBinaryOpSize=PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputBinaryOp.size(), pOutputBinaryOpSize); + EXPECT_EQ((long) outputBinaryOp.size(), pOutputBinaryOpSize); PyArrayObject* pBinaryOpValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); float* pOutputBinaryOp=(float*)PyArray_DATA(pBinaryOpValues); @@ -425,10 +426,10 @@ TEST(RModelParser_Keras, ACTIVATIONS) std::vectorinputActivations = {1,1,1,1,1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelActivations.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelActivations.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelActivations.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelActivations.keras"); std::vector outputActivations = r.Compute(inputActivations); PyObject* main = PyImport_AddModule("__main__"); @@ -444,7 +445,7 @@ TEST(RModelParser_Keras, ACTIVATIONS) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelActivations.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelActivations.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.ones((1,8))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); @@ -468,10 +469,10 @@ TEST(RModelParser_Keras, SWISH) std::vectorinput = {1,1,1,1,1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelSwish.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelSwish.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelSwish.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelSwish.keras"); std::vector output = r.Compute(input); PyObject* main = PyImport_AddModule("__main__"); @@ -487,7 +488,7 @@ TEST(RModelParser_Keras, SWISH) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelSwish.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelSwish.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.ones((1,8))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); @@ -507,11 +508,12 @@ TEST(RModelParser_Keras, SWISH) TEST(RModel, CUSTOM_OP) { + GTEST_SKIP() << "Skipping custop op test since it is not yet supported in new Python Keras parser"; constexpr float TOLERANCE = DEFAULT_TOLERANCE; std::vectorinput_custom ={1,1,1,1,1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelCustomOp.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelCustomOp.keras",kFileExists)) GenerateModels(); @@ -531,7 +533,7 @@ TEST(RModel, CUSTOM_OP) PyRun_String("from tensorflow.keras.layers import Lambda",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelCustomOp.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelCustomOp.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("model.add(Lambda(lambda x: x * 2))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.array([1,1,1,1,1,1,1,1]).reshape(1,8)",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); @@ -549,7 +551,7 @@ TEST(RModel, CUSTOM_OP) /*output shapes*/"{{1,4}}", /*header file name with the compute function*/ "scale_by_2_op.hxx"); // need to load model afterwards - r.Load("KerasModelCustomOp.h5",{}, false); + r.Load("KerasModelCustomOp.keras",{}, false); std::vector outputCustomOp = r.Compute(input_custom); //Testing the actual and expected output tensor sizes diff --git a/tmva/sofie/test/generateKerasModels.py b/tmva/sofie/test/generateKerasModels.py index 7fc05741662aa..af0fe2464ce70 100644 --- a/tmva/sofie/test/generateKerasModels.py +++ b/tmva/sofie/test/generateKerasModels.py @@ -24,7 +24,7 @@ def generateFunctionalModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=2) - model.save('KerasModelFunctional.h5') + model.save('KerasModelFunctional.keras') def generateSequentialModel(): model=Sequential() @@ -39,7 +39,7 @@ def generateSequentialModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=4) - model.save('KerasModelSequential.h5') + model.save('KerasModelSequential.keras') def generateBatchNormModel(): model=Sequential() @@ -53,7 +53,7 @@ def generateBatchNormModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=2) - model.save('KerasModelBatchNorm.h5') + model.save('KerasModelBatchNorm.keras') def generateConv2DModel_ValidPadding(): model=Sequential() @@ -65,7 +65,7 @@ def generateConv2DModel_ValidPadding(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=2) - model.save('KerasModelConv2D_Valid.h5') + model.save('KerasModelConv2D_Valid.keras') def generateConv2DModel_SamePadding(): model=Sequential() @@ -77,7 +77,7 @@ def generateConv2DModel_SamePadding(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=2) - model.save('KerasModelConv2D_Same.h5') + model.save('KerasModelConv2D_Same.keras') def generateReshapeModel(): model = Sequential() @@ -90,7 +90,7 @@ def generateReshapeModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=2) - model.save('KerasModelReshape.h5') + model.save('KerasModelReshape.keras') def generateConcatModel(): input_1 = Input(shape=(2,)) @@ -107,7 +107,7 @@ def generateConcatModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit([x1_train,x2_train], y_train, verbose=0, epochs=10, batch_size=1) - model.save('KerasModelConcatenate.h5') + model.save('KerasModelConcatenate.keras') def generateBinaryOpModel(): input1 = Input(shape=(2, )) @@ -124,7 +124,7 @@ def generateBinaryOpModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit([x1_train,x2_train], y_train, epochs=10, verbose=0, batch_size=2) - model.save('KerasModelBinaryOp.h5') + model.save('KerasModelBinaryOp.keras') def generateActivationModel(): input=Input(shape=(8,)) @@ -140,7 +140,7 @@ def generateActivationModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, epochs=10, verbose=0, batch_size=1) - model.save('KerasModelActivations.h5') + model.save('KerasModelActivations.keras') def generateSwishModel(): # Create the Keras model @@ -157,7 +157,7 @@ def generateSwishModel(): x_train=randomGenerator.rand(1,8) y_train=randomGenerator.rand(1,1) model.fit(x_train, y_train, epochs=10, verbose=0, batch_size=1) - model.save('KerasModelSwish.h5') + model.save('KerasModelSwish.keras') def generateCustomModel(): model = Sequential() @@ -167,7 +167,7 @@ def generateCustomModel(): y_train=randomGenerator.rand(1,4) model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=1) - model.save('KerasModelCustomOp.h5') + model.save('KerasModelCustomOp.keras') print("generating Keras models for testing.....") generateFunctionalModel() diff --git a/tmva/sofie_parsers/src/RModelParser_Keras.cxx b/tmva/sofie_parsers/src/RModelParser_Keras.cxx index 73a048f1330c7..9f4d334b4cd37 100644 --- a/tmva/sofie_parsers/src/RModelParser_Keras.cxx +++ b/tmva/sofie_parsers/src/RModelParser_Keras.cxx @@ -11,8 +11,8 @@ namespace TMVA::Experimental::SOFIE::PyKeras { RModel Parse(std::string /*filename*/, int /* batch_size */ ){ - throw std::runtime_error("TMVA::SOFIE C++ Keras parser is deprecated. Use python3 function " - "ROOT.TMVA.Experimental.SOFIE.RModelParser_Keras.Parse('model.keras',batch_size) " ); + throw std::runtime_error("TMVA::SOFIE C++ Keras parser is deprecated. Use the python3 function " + "model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse('model.keras',batch_size=1) " ); return RModel(); } diff --git a/tmva/tmva/inc/TMVA/RSofieReader.hxx b/tmva/tmva/inc/TMVA/RSofieReader.hxx index dc6f83c4502d6..c5d0c07052dc7 100644 --- a/tmva/tmva/inc/TMVA/RSofieReader.hxx +++ b/tmva/tmva/inc/TMVA/RSofieReader.hxx @@ -222,6 +222,7 @@ public: } iret = gSystem->Exec(TString("python3 -c ") + TString(parserPythonCode.c_str())); fNInputs = 1; + // need number of inputs from input shapes if (!inputShapes.empty()) fNInputs = inputShapes.size(); } @@ -257,6 +258,11 @@ public: if (verbose) std::cout << "//global session declaration\n" << declCode << std::endl; + // need to load the ROOTTMVASOFIE library for some symbols used in generated code + iret = gSystem->Load("libROOTTMVASofie"); + if (iret < 0) + throw std::runtime_error("Error loading libROOTTMVASofie library"); + bool ret = gInterpreter->Declare(declCode.c_str()); if (!ret) { std::string msg = "RSofieReader: error compiling inference code and creating session class\n" + declCode; diff --git a/tutorials/CMakeLists.txt b/tutorials/CMakeLists.txt index 5948431174271..5ac1030f83f3a 100644 --- a/tutorials/CMakeLists.txt +++ b/tutorials/CMakeLists.txt @@ -346,16 +346,6 @@ else() ROOT_FIND_PYTHON_MODULE(graph_nets) ROOT_FIND_PYTHON_MODULE(onnx) - # Check if we support the installed Keras version. Otherwise, veto SOFIE - # Keras tutorials. This mirrors the logic in tmva/sofie/test/CMakeLists.txt. - # TODO: make sure we also support the newest Keras - set(unsupported_keras_version "4.0.0") - if (ROOT_KERAS_FOUND AND NOT DEFINED ROOT_KERAS_VERSION) - message(WARNING "Keras found, but version unknown — cannot verify compatibility.") - elseif (ROOT_KERAS_FOUND AND NOT ROOT_KERAS_VERSION VERSION_LESS ${unsupported_keras_version}) - message(WARNING "Keras version ${ROOT_KERAS_VERSION} is too new for the SOFIE Keras parser (only supports < ${unsupported_keras_version}). Corresponding tutorials will not be tested.") - set(keras_unsupported TRUE) - endif() if (NOT BLAS_FOUND) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_GNN_Application.C) @@ -363,7 +353,7 @@ else() list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RSofieReader.C) endif() # These SOFIE tutorials take models trained via PyMVA-PyKeras as input - if (NOT tmva-sofie OR NOT ROOT_KERAS_FOUND OR keras_unsupported) + if (NOT tmva-sofie OR NOT ROOT_KERAS_FOUND) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras.py) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Models.py) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras_HiggsModel.py) diff --git a/tutorials/machine_learning/TMVA_SOFIE_Inference.py b/tutorials/machine_learning/TMVA_SOFIE_Inference.py index 37d013ad5f851..4669d15d99bd4 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_Inference.py +++ b/tutorials/machine_learning/TMVA_SOFIE_Inference.py @@ -62,7 +62,7 @@ # stack all the 7 numpy array in a single array (nevents x nvars) xsig = np.column_stack(list(sigData.values())) dataset_size = xsig.shape[0] -print("size of data", dataset_size) +print("size of signal data", dataset_size) #instantiate SOFIE session class #session = ROOT.TMVA_SOFIE_HiggsModel.Session() @@ -70,22 +70,29 @@ sofie = getattr(ROOT, 'TMVA_SOFIE_' + modelName) session = sofie.Session() +print("Evaluating SOFIE models on signal data") hs = ROOT.TH1D("hs","Signal result",100,0,1) for i in range(0,dataset_size): result = session.infer(xsig[i,:]) + if (i % dataset_size/10 == 0) : + print("result for signal event ",i,result[0]) hs.Fill(result[0]) - +print("using RDsataFrame to extract input data in a numpy array") # make SOFIE inference on background data df2 = ROOT.RDataFrame("bkg_tree", inputFile) bkgData = df2.AsNumpy(columns=['m_jj', 'm_jjj', 'm_lv', 'm_jlv', 'm_bb', 'm_wbb', 'm_wwbb']) xbkg = np.column_stack(list(bkgData.values())) dataset_size = xbkg.shape[0] +print("size of background data", dataset_size) hb = ROOT.TH1D("hb","Background result",100,0,1) for i in range(0,dataset_size): result = session.infer(xbkg[i,:]) + if (i % dataset_size/10 == 0) : + print("result for background event ",i,result[0]) + hb.Fill(result[0]) diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py index 7604bb0234c81..8639156710344 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py +++ b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py @@ -62,7 +62,7 @@ def TrainModel(model, x, y, name) : model.fit(x,y,epochs=5,batch_size=50) modelFile = name + '.keras' model.save(modelFile) - return modelFile + return model, modelFile def GenerateCode(modelFile = "model.keras") : @@ -89,7 +89,7 @@ def GenerateCode(modelFile = "model.keras") : x_train, y_train, x_test, y_test = PrepareData() #create dense model with 3 layers of 64 units model = CreateModel(3,64) -modelFile = TrainModel(model,x_train, y_train, 'HiggsModel') +model, modelFile = TrainModel(model,x_train, y_train, 'HiggsModel') ################################################################### ## Step 2 : Parse model and generate inference code with SOFIE @@ -119,7 +119,7 @@ def GenerateCode(modelFile = "model.keras") : print("input to model is ",x, "\n\t -> output using SOFIE = ", y[0], " using Keras = ", ykeras[0]) if (abs(y[0]-ykeras[0]) > 0.01) : - raiseError('Result is different between SOFIE and Keras') + raise RuntimeError('ERROR: Result is different between SOFIE and Keras') print("OK") diff --git a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C index 6569878494b0c..4bc663c4019ec 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C +++ b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C @@ -4,15 +4,13 @@ /// This macro provides an example of using a trained model with Keras /// and make inference using SOFIE and RDataFrame /// This macro uses as input a Keras model generated with the -/// TMVA_Higgs_Classification.C tutorial +/// Python tutorial TMVA_SOFIE_Keras_HiggsModel.py /// You need to run that macro before to generate the trained Keras model -/// Then you need to run the macro TMVA_SOFIE_Keras_HiggsModel.C to generate the corresponding -/// header file using SOFIE. +/// and also the corresponding header file with SOFIE which can then be used for inference /// /// Execute in this order: /// ``` -/// root TMVA_Higgs_Classification.C -/// root TMVA_SOFIE_Keras_HiggsModel.C +/// python3 TMVA_SOFIE_Keras_HiggsModel.py /// root TMVA_SOFIE_RDataFrame.C /// ``` /// @@ -25,7 +23,7 @@ using namespace TMVA::Experimental; // need to add the current directory (from where we are running this macro) // to the include path for Cling R__ADD_INCLUDE_PATH($PWD) -#include "Higgs_trained_model.hxx" +#include "HiggsModel.hxx" #include "TMVA/SOFIEHelpers.hxx" using namespace TMVA::Experimental; From cc7d91d1d6ff17866cb69ed7dca7457f7564bbd3 Mon Sep 17 00:00:00 2001 From: moneta Date: Fri, 6 Feb 2026 15:32:49 +0100 Subject: [PATCH 6/6] [tmva] Apply ruff to tmva python files Apply to tutorials and bindings files used by the new keras parser --- .../_sofie/_parser/_keras/layers/batchnorm.py | 2 + .../_sofie/_parser/_keras/layers/binary.py | 1 + .../_sofie/_parser/_keras/layers/concat.py | 1 + .../_sofie/_parser/_keras/layers/conv.py | 5 ++- .../_sofie/_parser/_keras/layers/dense.py | 1 + .../_tmva/_sofie/_parser/_keras/layers/elu.py | 1 + .../_sofie/_parser/_keras/layers/flatten.py | 2 + .../_sofie/_parser/_keras/layers/identity.py | 1 + .../_sofie/_parser/_keras/layers/layernorm.py | 2 + .../_parser/_keras/layers/leaky_relu.py | 1 + .../_sofie/_parser/_keras/layers/permute.py | 1 + .../_sofie/_parser/_keras/layers/pooling.py | 1 + .../_sofie/_parser/_keras/layers/relu.py | 1 + .../_sofie/_parser/_keras/layers/reshape.py | 2 + .../_tmva/_sofie/_parser/_keras/layers/rnn.py | 5 ++- .../_sofie/_parser/_keras/layers/selu.py | 1 + .../_sofie/_parser/_keras/layers/sigmoid.py | 1 + .../_sofie/_parser/_keras/layers/softmax.py | 1 + .../_sofie/_parser/_keras/layers/swish.py | 1 + .../_sofie/_parser/_keras/layers/tanh.py | 1 + .../_tmva/_sofie/_parser/_keras/parser.py | 40 +++++++++---------- .../test/generate_keras_functional.py | 2 +- .../test/generate_keras_sequential.py | 2 +- .../test/parser_test_function.py | 2 +- tmva/sofie/test/Conv1dModelGenerator.py | 3 +- tmva/sofie/test/Conv2dModelGenerator.py | 3 +- tmva/sofie/test/Conv3dModelGenerator.py | 3 +- tmva/sofie/test/ConvTrans2dModelGenerator.py | 3 +- tmva/sofie/test/LinearModelGenerator.py | 3 +- tmva/sofie/test/RecurrentModelGenerator.py | 4 +- tmva/sofie/test/generateKerasModels.py | 20 ++++++++-- .../RBatchGenerator_PyTorch.py | 2 +- .../TMVA_CNN_Classification.py | 16 ++++---- .../TMVA_Higgs_Classification.py | 8 ++-- .../TMVA_RNN_Classification.py | 18 +++++---- tutorials/machine_learning/TMVA_SOFIE_GNN.py | 7 ++-- .../machine_learning/TMVA_SOFIE_GNN_Parser.py | 13 +++--- .../machine_learning/TMVA_SOFIE_Inference.py | 4 +- .../machine_learning/TMVA_SOFIE_Keras.py | 10 ++--- .../TMVA_SOFIE_Keras_HiggsModel.py | 6 ++- tutorials/machine_learning/TMVA_SOFIE_ONNX.py | 12 +++--- .../machine_learning/tmva101_Training.py | 3 +- tutorials/machine_learning/tmva102_Testing.py | 7 +--- 43 files changed, 127 insertions(+), 96 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py index 834f9d0698163..11110851342e3 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py @@ -1,6 +1,8 @@ from cppyy import gbl as gbl_namespace + from .. import get_keras_version + def MakeKerasBatchNorm(layer): """ Create a Keras-compatible batch normalization operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py index ff35fd2032653..9aa6324af27eb 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasBinary(layer): input = layer['layerInput'] output = layer['layerOutput'] diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py index 340aa4e9cb452..013afe831585e 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasConcat(layer): finput = layer['layerInput'] foutput = layer['layerOutput'] diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py index 98fe21b1cc887..047e9d52603e0 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py @@ -1,7 +1,10 @@ -from cppyy import gbl as gbl_namespace import math + +from cppyy import gbl as gbl_namespace + from .. import get_keras_version + def MakeKerasConv(layer): """ Create a Keras-compatible convolutional layer operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py index 7e6e787a97095..cfcd079dc8909 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasDense(layer): """ Create a Keras-compatible dense (fully connected) layer operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py index 7a291117e837e..6d8c1eccbd985 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasELU(layer): """ Create a Keras-compatible exponential linear Unit (ELU) activation operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py index bf63cbf795872..1fd5042b6650a 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py @@ -1,6 +1,8 @@ from cppyy import gbl as gbl_namespace + from .. import get_keras_version + def MakeKerasFlatten(layer): """ Create a Keras-compatible flattening operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py index 4921a268e6a5d..fb3ba6783b8b0 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasIdentity(layer): input = layer['layerInput'] output = layer['layerOutput'] diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py index b10ce58d239a9..55b7039ee0e4c 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py @@ -1,6 +1,8 @@ from cppyy import gbl as gbl_namespace + from .. import get_keras_version + def MakeKerasLayerNorm(layer): """ Create a Keras-compatible layer normalization operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py index c0b95b04b27eb..4eef107d3e5f3 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasLeakyRelu(layer): """ Create a Keras-compatible Leaky ReLU activation operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py index f43fc09ee0afe..04daea02235a3 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasPermute(layer): """ Create a Keras-compatible permutation operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py index 364d2be8da147..8d08104cec743 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasPooling(layer): """ Create a Keras-compatible pooling layer operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py index 9da1407a8911d..24419da59396e 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasReLU(layer): """ Create a Keras-compatible rectified linear unit (ReLU) activation operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py index 226af35c8a3e2..5d77978be54c5 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py @@ -1,6 +1,8 @@ from cppyy import gbl as gbl_namespace + from .. import get_keras_version + def MakeKerasReshape(layer): """ Create a Keras-compatible reshaping operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py index f2f3d628e0aed..3902d501432f0 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasRNN(layer): """ Create a Keras-compatible RNN (Recurrent Neural Network) layer operation using SOFIE framework. @@ -36,7 +37,7 @@ def MakeKerasRNN(layer): # Check if the provided activation function is supported fPActivation = attributes['activation'] - if not fPActivation.__name__ in ['relu', 'sigmoid', 'tanh', 'softsign', 'softplus']: #avoiding functions with parameters + if fPActivation.__name__ not in ['relu', 'sigmoid', 'tanh', 'softsign', 'softplus']: #avoiding functions with parameters raise RuntimeError( "TMVA::SOFIE - Unsupported - Operator RNN does not yet support activation function " + fPActivation.__name__ ) @@ -66,7 +67,7 @@ def MakeKerasRNN(layer): elif layer['layerType'] == "LSTM": #an additional activation function is required, the first given by the user, the second set to tanh as default fPRecurrentActivation = attributes['recurrent_activation'] - if not fPActivation.__name__ in ['relu', 'sigmoid', 'tanh', 'softsign', 'softplus']: #avoiding functions with parameters + if fPActivation.__name__ not in ['relu', 'sigmoid', 'tanh', 'softsign', 'softplus']: #avoiding functions with parameters raise RuntimeError( "TMVA::SOFIE - Unsupported - Operator RNN does not yet support recurrent activation function " + fPActivation.__name__ ) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py index 53349086440ec..62c386b7e6363 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasSeLU(layer): """ Create a Keras-compatible scaled exponential linear unit (SeLU) activation operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py index 8d50032c53fdb..92e3159822393 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasSigmoid(layer): """ Create a Keras-compatible sigmoid activation operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py index f00efc136b486..f23b1f46f6a6d 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasSoftmax(layer): """ Create a Keras-compatible softmax activation operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py index 43ae130d91c0f..db683b9f5f393 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasSwish(layer): """ Create a Keras-compatible swish activation operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py index 4d9e62cd5da1d..35020d6c6da76 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py @@ -1,5 +1,6 @@ from cppyy import gbl as gbl_namespace + def MakeKerasTanh(layer): """ Create a Keras-compatible hyperbolic tangent (tanh) activation operation using SOFIE framework. diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py index e52977f82b2cf..585af3f9da04c 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py @@ -1,30 +1,28 @@ -from ......_pythonization import pythonization -from cppyy import gbl as gbl_namespace import os import time -from .layers.permute import MakeKerasPermute +from cppyy import gbl as gbl_namespace + +from . import get_keras_version from .layers.batchnorm import MakeKerasBatchNorm -from .layers.layernorm import MakeKerasLayerNorm -from .layers.reshape import MakeKerasReshape -from .layers.flatten import MakeKerasFlatten -from .layers.concat import MakeKerasConcat -from .layers.swish import MakeKerasSwish from .layers.binary import MakeKerasBinary -from .layers.softmax import MakeKerasSoftmax -from .layers.tanh import MakeKerasTanh -from .layers.identity import MakeKerasIdentity -from .layers.relu import MakeKerasReLU +from .layers.concat import MakeKerasConcat +from .layers.conv import MakeKerasConv +from .layers.dense import MakeKerasDense from .layers.elu import MakeKerasELU -from .layers.selu import MakeKerasSeLU -from .layers.sigmoid import MakeKerasSigmoid +from .layers.flatten import MakeKerasFlatten +from .layers.layernorm import MakeKerasLayerNorm from .layers.leaky_relu import MakeKerasLeakyRelu +from .layers.permute import MakeKerasPermute from .layers.pooling import MakeKerasPooling -from .layers.rnn import MakeKerasRNN -from .layers.dense import MakeKerasDense -from .layers.conv import MakeKerasConv +from .layers.relu import MakeKerasReLU +from .layers.reshape import MakeKerasReshape +from .layers.selu import MakeKerasSeLU +from .layers.sigmoid import MakeKerasSigmoid +from .layers.softmax import MakeKerasSoftmax +from .layers.swish import MakeKerasSwish +from .layers.tanh import MakeKerasTanh -from . import get_keras_version def MakeKerasActivation(layer): attributes = layer['layerAttributes'] @@ -227,7 +225,7 @@ def add_layer_into_RModel(rmodel, layer_data): # if there is an activation function after the layer if LayerActivation != 'linear': - if not LayerActivation in mapKerasLayer.keys(): + if LayerActivation not in mapKerasLayer.keys(): raise Exception("TMVA.SOFIE - parsing keras activation function " + LayerActivation + " is not yet supported") outputs = layer_data['layerOutput'] inputs = layer_data['layerInput'] @@ -408,7 +406,7 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s # Ignoring the input layer of the model if(fLayerType == "InputLayer"): - continue; + continue # Adding any required routines depending on the Layer types for generating inference code. if (fLayerType == "Dense"): @@ -444,7 +442,7 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s fWeightTensorShape = [] #IS IT BATCH SIZE? CHECK ONNX - if 'simple_rnn' in fWeightName or 'lstm' in fWeightName or ('gru' in fWeightName and not 'bias' in fWeightName): + if 'simple_rnn' in fWeightName or 'lstm' in fWeightName or ('gru' in fWeightName and 'bias' not in fWeightName): fWeightTensorShape.append(1) # Building the shape vector and finding the tensor size diff --git a/bindings/pyroot/pythonizations/test/generate_keras_functional.py b/bindings/pyroot/pythonizations/test/generate_keras_functional.py index 2ab6b2d3b3bd0..11f7bdefda00e 100644 --- a/bindings/pyroot/pythonizations/test/generate_keras_functional.py +++ b/bindings/pyroot/pythonizations/test/generate_keras_functional.py @@ -1,7 +1,7 @@ def generate_keras_functional(dst_dir): - from keras import models, layers, backend import numpy as np + from keras import layers, models from parser_test_function import is_channels_first_supported diff --git a/bindings/pyroot/pythonizations/test/generate_keras_sequential.py b/bindings/pyroot/pythonizations/test/generate_keras_sequential.py index 8c3b6af8ec656..40b6c645b1fd4 100644 --- a/bindings/pyroot/pythonizations/test/generate_keras_sequential.py +++ b/bindings/pyroot/pythonizations/test/generate_keras_sequential.py @@ -1,7 +1,7 @@ def generate_keras_sequential(dst_dir): - from keras import models, layers, backend import numpy as np + from keras import layers, models from parser_test_function import is_channels_first_supported # Helper training function diff --git a/bindings/pyroot/pythonizations/test/parser_test_function.py b/bindings/pyroot/pythonizations/test/parser_test_function.py index 9cffea59038e4..eaa4a0ed5fb2f 100644 --- a/bindings/pyroot/pythonizations/test/parser_test_function.py +++ b/bindings/pyroot/pythonizations/test/parser_test_function.py @@ -54,9 +54,9 @@ def is_accurate(tensor_a, tensor_b, tolerance=1e-2): def generate_and_test_inference(model_file_path: str, generated_header_file_dir: str = None, batch_size=1): - import tensorflow as tf import keras import numpy as np + import tensorflow as tf print("Tensorflow version: ", tf.__version__) print("Keras version: ", keras.__version__) diff --git a/tmva/sofie/test/Conv1dModelGenerator.py b/tmva/sofie/test/Conv1dModelGenerator.py index 12c438094c918..56303d089f77a 100644 --- a/tmva/sofie/test/Conv1dModelGenerator.py +++ b/tmva/sofie/test/Conv1dModelGenerator.py @@ -2,13 +2,12 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn import torch.nn.functional as F - result = [] diff --git a/tmva/sofie/test/Conv2dModelGenerator.py b/tmva/sofie/test/Conv2dModelGenerator.py index 10fccbf8ddc89..04710e5b7a894 100644 --- a/tmva/sofie/test/Conv2dModelGenerator.py +++ b/tmva/sofie/test/Conv2dModelGenerator.py @@ -2,13 +2,12 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn import torch.nn.functional as F - result = [] class Net(nn.Module): diff --git a/tmva/sofie/test/Conv3dModelGenerator.py b/tmva/sofie/test/Conv3dModelGenerator.py index 26a4f855d12cc..8e006b22f4e13 100644 --- a/tmva/sofie/test/Conv3dModelGenerator.py +++ b/tmva/sofie/test/Conv3dModelGenerator.py @@ -2,13 +2,12 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn import torch.nn.functional as F - result = [] class Net(nn.Module): diff --git a/tmva/sofie/test/ConvTrans2dModelGenerator.py b/tmva/sofie/test/ConvTrans2dModelGenerator.py index d39de659e60a7..199995998b3f5 100644 --- a/tmva/sofie/test/ConvTrans2dModelGenerator.py +++ b/tmva/sofie/test/ConvTrans2dModelGenerator.py @@ -2,13 +2,12 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn import torch.nn.functional as F - result = [] class Net(nn.Module): diff --git a/tmva/sofie/test/LinearModelGenerator.py b/tmva/sofie/test/LinearModelGenerator.py index e96cf77501c9f..155ce3043d7ff 100644 --- a/tmva/sofie/test/LinearModelGenerator.py +++ b/tmva/sofie/test/LinearModelGenerator.py @@ -2,13 +2,12 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn import torch.nn.functional as F - result = [] class Net(nn.Module): diff --git a/tmva/sofie/test/RecurrentModelGenerator.py b/tmva/sofie/test/RecurrentModelGenerator.py index ec7c742461d3d..9050e5db97868 100644 --- a/tmva/sofie/test/RecurrentModelGenerator.py +++ b/tmva/sofie/test/RecurrentModelGenerator.py @@ -2,12 +2,10 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn -import torch.nn.functional as F - result = [] verbose=False diff --git a/tmva/sofie/test/generateKerasModels.py b/tmva/sofie/test/generateKerasModels.py index af0fe2464ce70..163e5de5a75e8 100644 --- a/tmva/sofie/test/generateKerasModels.py +++ b/tmva/sofie/test/generateKerasModels.py @@ -1,14 +1,28 @@ import os + os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" os.environ["CUDA_VISIBLE_DEVICES"] = "" import numpy as np -import tensorflow as tf -from tensorflow.keras.models import Model,Sequential -from tensorflow.keras.layers import Input,Dense,Activation,ReLU,LeakyReLU,BatchNormalization,Conv2D,Reshape,Concatenate,Add,Subtract,Multiply +from tensorflow.keras.layers import ( + Activation, + Add, + BatchNormalization, + Concatenate, + Conv2D, + Dense, + Input, + LeakyReLU, + Multiply, + ReLU, + Reshape, + Subtract, +) +from tensorflow.keras.models import Model, Sequential from tensorflow.keras.optimizers import SGD + def generateFunctionalModel(): input=Input(shape=(8,),batch_size=2) x=Dense(16)(input) diff --git a/tutorials/machine_learning/RBatchGenerator_PyTorch.py b/tutorials/machine_learning/RBatchGenerator_PyTorch.py index e85ddd3be90c6..29533f8098cd8 100644 --- a/tutorials/machine_learning/RBatchGenerator_PyTorch.py +++ b/tutorials/machine_learning/RBatchGenerator_PyTorch.py @@ -8,8 +8,8 @@ ### \macro_output ### \author Dante Niewenhuis -import torch import ROOT +import torch tree_name = "sig_tree" file_name = str(ROOT.gROOT.GetTutorialDir()) + "/machine_learning/data/Higgs_data.root" diff --git a/tutorials/machine_learning/TMVA_CNN_Classification.py b/tutorials/machine_learning/TMVA_CNN_Classification.py index a6875abc29586..5525931971183 100644 --- a/tutorials/machine_learning/TMVA_CNN_Classification.py +++ b/tutorials/machine_learning/TMVA_CNN_Classification.py @@ -22,10 +22,10 @@ ## The difference between signal and background is in the gaussian width. ## The width for the background gaussian is slightly larger than the signal width by few % values -import ROOT - -import os import importlib.util +import os + +import ROOT opt = [1, 1, 1, 1, 1] useTMVACNN = opt[0] if len(opt) > 0 else False @@ -117,7 +117,7 @@ def MakeImagesTree(n, nh, nw): useTMVACNN = False useTMVADNN = False -if not "tmva-pymva" in ROOT.gROOT.GetConfigFeatures(): +if "tmva-pymva" not in ROOT.gROOT.GetConfigFeatures(): useKerasCNN = False usePyTorchCNN = False else: @@ -445,13 +445,11 @@ def MakeImagesTree(n, nh, nw): ROOT.Info("TMVA_CNN_Classification", "Building convolutional keras model") # create python script which can be executed # create 2 conv2d layer + maxpool + dense - import tensorflow - from tensorflow.keras.models import Sequential - from tensorflow.keras.optimizers import Adam - # from keras.initializers import TruncatedNormal # from keras import initializations - from tensorflow.keras.layers import Input, Dense, Dropout, Flatten, Conv2D, MaxPooling2D, Reshape + from tensorflow.keras.layers import Conv2D, Dense, Flatten, MaxPooling2D, Reshape + from tensorflow.keras.models import Sequential + from tensorflow.keras.optimizers import Adam # from keras.callbacks import ReduceLROnPlateau model = Sequential() diff --git a/tutorials/machine_learning/TMVA_Higgs_Classification.py b/tutorials/machine_learning/TMVA_Higgs_Classification.py index 1feeeb2996026..7a5f00bdc44b2 100644 --- a/tutorials/machine_learning/TMVA_Higgs_Classification.py +++ b/tutorials/machine_learning/TMVA_Higgs_Classification.py @@ -29,9 +29,10 @@ ## - The third argument is a string option defining some general configuration for the TMVA session. For example all TMVA output can be suppressed by removing the "!" (not) in front of the "Silent" argument in the option string -import ROOT import os +import ROOT + TMVA = ROOT.TMVA TFile = ROOT.TFile @@ -53,7 +54,7 @@ if useKeras: try: - import tensorflow + pass except: ROOT.Warning("TMVA_Higgs_Classification", "Skip using Keras since tensorflow is not available") useKeras = False @@ -315,10 +316,9 @@ if useKeras: ROOT.Info("TMVA_Higgs_Classification", "Building Deep Learning keras model") # create Keras model with 4 layers of 64 units and relu activations - import tensorflow + from tensorflow.keras.layers import Dense from tensorflow.keras.models import Sequential from tensorflow.keras.optimizers import Adam - from tensorflow.keras.layers import Input, Dense model = Sequential() model.add(Dense(64, activation="relu", input_dim=7)) diff --git a/tutorials/machine_learning/TMVA_RNN_Classification.py b/tutorials/machine_learning/TMVA_RNN_Classification.py index f8e5988288bd3..364aa309b6bc6 100644 --- a/tutorials/machine_learning/TMVA_RNN_Classification.py +++ b/tutorials/machine_learning/TMVA_RNN_Classification.py @@ -35,8 +35,6 @@ TFile = ROOT.TFile import os -import importlib - TMVA.Tools.Instance() TMVA.Config.Instance() @@ -163,7 +161,7 @@ def MakeTimeData(n, ntime, ndim): if useKeras: try: - import tensorflow + pass except: ROOT.Warning("TMVA_RNN_Classification", "Skip using Keras since tensorflow cannot be imported") useKeras = False @@ -392,12 +390,18 @@ def MakeTimeData(n, ntime, ndim): print("Building recurrent keras model using a", rnn_types[i], "layer") # create python script which can be executed # create 2 conv2d layer + maxpool + dense - from tensorflow.keras.models import Sequential - from tensorflow.keras.optimizers import Adam - # from keras.initializers import TruncatedNormal # from keras import initializations - from tensorflow.keras.layers import Input, Dense, Dropout, Flatten, SimpleRNN, GRU, LSTM, Reshape, BatchNormalization + from tensorflow.keras.layers import ( + GRU, + LSTM, + Dense, + Flatten, + Reshape, + SimpleRNN, + ) + from tensorflow.keras.models import Sequential + from tensorflow.keras.optimizers import Adam model = Sequential() model.add(Reshape((10, 30), input_shape=(10 * 30,))) diff --git a/tutorials/machine_learning/TMVA_SOFIE_GNN.py b/tutorials/machine_learning/TMVA_SOFIE_GNN.py index fce0345cbe9a1..a11024f6c04ec 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_GNN.py +++ b/tutorials/machine_learning/TMVA_SOFIE_GNN.py @@ -10,11 +10,12 @@ # NumPys builtin openblas later, which will conflict with the system openblas. ROOT.gInterpreter.Load("libopenblaso.so") -import numpy as np +import time + import graph_nets as gn -from graph_nets import utils_tf +import numpy as np import sonnet as snt -import time +from graph_nets import utils_tf # defining graph properties num_nodes = 5 diff --git a/tutorials/machine_learning/TMVA_SOFIE_GNN_Parser.py b/tutorials/machine_learning/TMVA_SOFIE_GNN_Parser.py index 31d1b6eed30d7..50538a453c5e2 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_GNN_Parser.py +++ b/tutorials/machine_learning/TMVA_SOFIE_GNN_Parser.py @@ -7,16 +7,17 @@ ## ## \macro_code -import ROOT +import os -import numpy as np -import graph_nets as gn -from graph_nets import utils_tf -import sonnet as snt #for getting time and memory import time -import os + +import graph_nets as gn +import numpy as np import psutil +import ROOT +import sonnet as snt +from graph_nets import utils_tf # defining graph properties. Number of edges/modes are the maximum num_max_nodes=100 diff --git a/tutorials/machine_learning/TMVA_SOFIE_Inference.py b/tutorials/machine_learning/TMVA_SOFIE_Inference.py index 4669d15d99bd4..78edb73fa6e5e 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_Inference.py +++ b/tutorials/machine_learning/TMVA_SOFIE_Inference.py @@ -14,11 +14,11 @@ ### \macro_output ### \author Lorenzo Moneta +from os.path import exists + import numpy as np import ROOT -from os.path import exists - # check if the input file exists modelFile = "HiggsModel.keras" diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras.py b/tutorials/machine_learning/TMVA_SOFIE_Keras.py index 1473eddf27207..083c768b3bede 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_Keras.py +++ b/tutorials/machine_learning/TMVA_SOFIE_Keras.py @@ -9,9 +9,8 @@ ### \author Sanjiban Sengupta and Lorenzo Moneta + import ROOT -import os -import sys # Enable ROOT in batch mode (same effect as -nodraw) ROOT.gROOT.SetBatch(True) @@ -20,10 +19,9 @@ # Step 1: Create and train a simple Keras model (via embedded Python) # ----------------------------------------------------------------------------- -import tensorflow as tf -from tensorflow.keras.models import Model -from tensorflow.keras.layers import Dense, Input, Activation, Softmax import numpy as np +from tensorflow.keras.layers import Activation, Dense, Input, Softmax +from tensorflow.keras.models import Model input=Input(shape=(4,),batch_size=2) x=Dense(32)(input) @@ -49,8 +47,6 @@ import ROOT - - # Parse the ONNX model model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse("KerasModel.keras") diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py index 8639156710344..10fd3aa2792aa 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py +++ b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py @@ -8,12 +8,14 @@ ### \author Lorenzo Moneta -import ROOT from os.path import exists + import numpy as np -from keras import models, layers +import ROOT +from keras import layers, models from sklearn.model_selection import train_test_split + def CreateModel(nlayers = 4, nunits = 64): input = layers.Input(shape=(7,)) x = input diff --git a/tutorials/machine_learning/TMVA_SOFIE_ONNX.py b/tutorials/machine_learning/TMVA_SOFIE_ONNX.py index 35fd4ca68cef7..c323ccd59d2e1 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_ONNX.py +++ b/tutorials/machine_learning/TMVA_SOFIE_ONNX.py @@ -13,11 +13,13 @@ ## \author Lorenzo Moneta +import inspect + +import numpy as np +import ROOT import torch import torch.nn as nn -import ROOT -import numpy as np -import inspect + def CreateAndTrainModel(modelName): @@ -95,9 +97,9 @@ def ParseModel(modelFile, verbose=False): print("2weight",data) # Generating inference code - model.Generate(); + model.Generate() #generate header file (and .dat file) with modelName+.hxx - model.OutputGenerated(); + model.OutputGenerated() if (verbose) : model.PrintGenerated() diff --git a/tutorials/machine_learning/tmva101_Training.py b/tutorials/machine_learning/tmva101_Training.py index b50742998070f..671cad7836c93 100644 --- a/tutorials/machine_learning/tmva101_Training.py +++ b/tutorials/machine_learning/tmva101_Training.py @@ -12,9 +12,8 @@ ## \date August 2019 ## \author Stefan Wunsch -import ROOT import numpy as np - +import ROOT from tmva100_DataPreparation import variables diff --git a/tutorials/machine_learning/tmva102_Testing.py b/tutorials/machine_learning/tmva102_Testing.py index bcf77bea47f54..7d78e9b6147e5 100644 --- a/tutorials/machine_learning/tmva102_Testing.py +++ b/tutorials/machine_learning/tmva102_Testing.py @@ -10,13 +10,10 @@ ## \date August 2019 ## \author Stefan Wunsch -import ROOT -import pickle -from tmva100_DataPreparation import variables +import ROOT from tmva101_Training import load_data - # Load data x, y_true, w = load_data("test_signal.root", "test_background.root") @@ -29,7 +26,7 @@ y_pred = bdt.Compute(x) # Compute ROC using sklearn -from sklearn.metrics import roc_curve, auc +from sklearn.metrics import auc, roc_curve false_positive_rate, true_positive_rate, _ = roc_curve(y_true, y_pred, sample_weight=w) score = auc(false_positive_rate, true_positive_rate)