From 1f41c23312267847551044005e9657a3705dd304 Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Wed, 20 Aug 2025 13:13:02 +0530 Subject: [PATCH 1/9] [tmva][sofie] Keras Parser --- bindings/pyroot/pythonizations/CMakeLists.txt | 26 +- .../ROOT/_pythonization/_tmva/__init__.py | 1 + .../_tmva/_sofie/_parser/_keras/__init__.py | 3 + .../_keras/generate_keras_functional.py | 202 ++++++++ .../_keras/generate_keras_sequential.py | 187 +++++++ .../_sofie/_parser/_keras/layers/__init__.py | 0 .../_sofie/_parser/_keras/layers/batchnorm.py | 48 ++ .../_sofie/_parser/_keras/layers/binary.py | 23 + .../_sofie/_parser/_keras/layers/concat.py | 11 + .../_sofie/_parser/_keras/layers/conv.py | 70 +++ .../_sofie/_parser/_keras/layers/dense.py | 37 ++ .../_sofie/_parser/_keras/layers/flatten.py | 33 ++ .../_sofie/_parser/_keras/layers/identity.py | 15 + .../_sofie/_parser/_keras/layers/leakyrelu.py | 44 ++ .../_sofie/_parser/_keras/layers/permute.py | 36 ++ .../_sofie/_parser/_keras/layers/pooling.py | 73 +++ .../_sofie/_parser/_keras/layers/relu.py | 30 ++ .../_sofie/_parser/_keras/layers/reshape.py | 31 ++ .../_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 | 479 ++++++++++++++++++ .../_parser/_keras/parser_test_function.py | 89 ++++ .../pyroot/pythonizations/test/CMakeLists.txt | 7 + .../pythonizations/test/sofie_keras_parser.py | 71 +++ 28 files changed, 1763 insertions(+), 1 deletion(-) 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/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/leakyrelu.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/CMakeLists.txt b/bindings/pyroot/pythonizations/CMakeLists.txt index 563f5eb13e0a4..b3a306c4ce731 100644 --- a/bindings/pyroot/pythonizations/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/CMakeLists.txt @@ -58,7 +58,31 @@ 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/generate_keras_functional.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.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/flatten.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leakyrelu.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/_pythonization/_tmva/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py index cc6c614056bb2..b76af2ded8983 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py @@ -44,6 +44,7 @@ def inject_rbatchgenerator(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: 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..0acdb2850aae0 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py @@ -0,0 +1,3 @@ +import keras + +keras_version = 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..7073a67830c63 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py @@ -0,0 +1,202 @@ +# functional_models.py +import numpy as np +from keras import models, layers, activations + +def generate_keras_functional(dst_dir): + # 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) + # print(dst_dir) + model.save(f"{dst_dir}/{name}.h5") + # print(f"Saved {name}.h5") + + # # 1. Dropout (to test SOFIE's Identity operator) + # inp = layers.Input(shape=(10,)) + # out = layers.Dropout(0.5)(inp) + # model = models.Model(inputs=inp, outputs=out) + # train_and_save(model, "Functional_Dropout_test") + + # 2. Binary Operators + # 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, "Functional_Add_test") + + # 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, "Functional_Subtract_test") + + # 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, "Functional_Multiply_test") + + # 3. 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, "Functional_Concat_test") + + # 4. Reshape + inp = layers.Input(shape=(4, 5)) + out = layers.Reshape((2, 10))(inp) + model = models.Model(inp, out) + train_and_save(model, "Functional_Reshape_test") + + # 5. Flatten + inp = layers.Input(shape=(4, 5)) + out = layers.Flatten()(inp) + model = models.Model(inp, out) + train_and_save(model, "Functional_Flatten_test") + + # 6. BatchNorm 1D + inp = layers.Input(shape=(10,)) + out = layers.BatchNormalization()(inp) + model = models.Model(inp, out) + train_and_save(model, "Functional_BatchNorm1D_test") + + # 7. Activation Functions + for act in ['relu', 'selu', 'sigmoid', 'softmax', 'tanh']: + inp = layers.Input(shape=(10,)) + out = layers.Activation(act)(inp) + model = models.Model(inp, out) + train_and_save(model, f"Functional_{act.capitalize()}_test") + + # LeakyReLU + inp = layers.Input(shape=(10,)) + out = layers.LeakyReLU()(inp) + model = models.Model(inp, out) + train_and_save(model, "Functional_LeakyReLU_test") + + # Swish + inp = layers.Input(shape=(10,)) + out = layers.Activation(activations.swish)(inp) + model = models.Model(inp, out) + train_and_save(model, "Functional_Swish_test") + + # 8. Permute + inp = layers.Input(shape=(3, 4, 5)) + out = layers.Permute((2, 1, 3))(inp) + model = models.Model(inp, out) + train_and_save(model, "Functional_Permute_test") + + # 9. Dense + inp = layers.Input(shape=(10,)) + out = layers.Dense(5)(inp) + model = models.Model(inp, out) + train_and_save(model, "Functional_Dense_test") + + # 10. Conv2D channels_last + 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, "Functional_Conv2D_channels_last_test") + + # 10. Conv2D channels_first + inp = layers.Input(shape=(3, 8, 8)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "Functional_Conv2D_channels_first_test") + + # 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, "Functional_Conv2D_padding_same_test") + + # Conv2D padding_valid + inp = layers.Input(shape=(8, 8, 3)) + out = layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "Functional_Conv2D_padding_valid_test") + + # 11. 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, "Functional_MaxPool2D_channels_last_test") + + # 11. 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, "Functional_MaxPool2D_channels_first_test") + + # # 12. RNN - SimpleRNN + # inp = layers.Input(shape=(5, 3)) + # out = layers.SimpleRNN(4, return_sequences=True)(inp) + # model = models.Model(inp, out) + # train_and_save(model, "Functional_SimpleRNN_test") + + # # 12. RNN - LSTM + # inp = layers.Input(shape=(5, 3)) + # out = layers.LSTM(4, return_sequences=True)(inp) + # model = models.Model(inp, out) + # train_and_save(model, "Functional_LSTM_test") + + # # 12. RNN - GRU + # inp = layers.Input(shape=(5, 3)) + # out = layers.GRU(4, return_sequences=True)(inp) + # model = models.Model(inp, out) + # train_and_save(model, "Functional_GRU_test") + + # Layer Combination + + in1 = layers.Input(shape=(16,)) + in2 = layers.Input(shape=(16,)) + x1 = layers.Dense(32, activation="relu")(in1) + x1 = layers.BatchNormalization()(x1) + x2 = layers.Dense(32, activation="sigmoid")(in2) + merged = layers.Concatenate()([x1, x2]) + added = layers.Add()([merged, merged]) + out = layers.Dense(10, activation="softmax")(added) + model1 = models.Model([in1, in2], out) + train_and_save(model1, "Functional_Layer_Combination_1_test") + + + inp1 = layers.Input(shape=(32, 32, 3)) + x1 = layers.Conv2D(8, (3,3), padding="same", data_format="channels_last", activation="relu")(inp1) + x1 = layers.MaxPooling2D((2,2), data_format="channels_last")(x1) + x1 = layers.Flatten()(x1) + inp2 = layers.Input(shape=(3, 32, 32)) + x2 = layers.Conv2D(8, (5,5), padding="valid", data_format="channels_first")(inp2) + x2 = layers.MaxPooling2D((2,2), data_format="channels_first")(x2) + x2 = layers.Flatten()(x2) + merged = layers.Concatenate()([x1, x2]) + out = layers.Dense(20, activation=activations.swish)(merged) + model2 = models.Model([inp1, inp2], out) + train_and_save(model2, "Functional_Layer_Combination_2_test") + + + in1 = layers.Input(shape=(12,)) + in2 = layers.Input(shape=(12,)) + x1 = layers.Dense(24, activation="tanh")(in1) + x1 = layers.Reshape((4, 6))(x1) + x1 = layers.Permute((2,1))(x1) + x2 = layers.Dense(24, activation="relu")(in2) + x2 = layers.Reshape((6, 4))(x2) + mul = layers.Multiply()([x1, x2]) + sub = layers.Subtract()([x1, x2]) + merged = layers.Concatenate()([mul, sub]) + flat = layers.Flatten()(merged) + dense = layers.Dense(16)(flat) + out = layers.LeakyReLU()(dense) + model3 = models.Model([in1, in2], out) + train_and_save(model3, "Functional_Layer_Combination_3_test") + 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..be9caa39c08ba --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py @@ -0,0 +1,187 @@ +# sequential_models.py +import numpy as np +from keras import models, layers, activations + +def generate_keras_sequential(dst_dir): + # 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}/{name}.h5") + # print(f"Saved {name}.h5") + + # 1. Dropout + # model = models.Sequential([ + # layers.Input(shape=(10,)), + # layers.Dropout(0.5) # Dropout + # ]) + # train_and_save(model, "Sequential_Dropout_test") + + # 2. Binary Ops: Add, Subtract, Multiply are not typical in Sequential — skipping here + + # 3. Concat (not applicable in Sequential without multi-input) + + # 4. Reshape + model = models.Sequential([ + layers.Input(shape=(4, 5)), + layers.Reshape((2, 10)) + ]) + train_and_save(model, "Sequential_Reshape_test") + + # 5. Flatten + model = models.Sequential([ + layers.Input(shape=(4, 5)), + layers.Flatten() + ]) + train_and_save(model, "Sequential_Flatten_test") + + # 6. BatchNorm 1D + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.BatchNormalization() + ]) + train_and_save(model, "Sequential_BatchNorm1D_test") + + # # 6. BatchNorm 2D + # model = models.Sequential([ + # layers.Input(shape=(8, 3)), + # layers.BatchNormalization() + # ]) + # train_and_save(model, "Sequential_BatchNorm2D_test") + + # 7. Activation Functions + for act in ['relu', 'selu', 'sigmoid', 'softmax', 'tanh']: + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.Activation(act) + ]) + train_and_save(model, f"Sequential_{act.capitalize()}_test") + + # LeakyReLU + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.LeakyReLU() + ]) + train_and_save(model, "Sequential_LeakyReLU_test") + + # Swish + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.Activation(activations.swish) + ]) + train_and_save(model, "Sequential_Swish_test") + + # 8. Permute + model = models.Sequential([ + layers.Input(shape=(3, 4, 5)), + layers.Permute((2, 1, 3)) + ]) + train_and_save(model, "Sequential_Permute_test") + + # 9. Dense + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.Dense(5) + ]) + train_and_save(model, "Sequential_Dense_test") + + # 10. Conv2D channels_last + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.Conv2D(4, (3, 3), data_format='channels_last') + ]) + train_and_save(model, "Sequential_Conv2D_channels_last_test") + + # 10. 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, "Sequential_Conv2D_channels_first_test") + + # Conv2D padding_same + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last') + ]) + train_and_save(model, "Sequential_Conv2D_padding_same_test") + + # Conv2D padding_valid + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last') + ]) + train_and_save(model, "Sequential_Conv2D_padding_valid_test") + + # 11. 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, "Sequential_MaxPool2D_channels_last_test") + + # 11. 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, "Sequential_MaxPool2D_channels_first_test") + + # # 12. RNN - SimpleRNN + # model = models.Sequential([ + # layers.Input(shape=(5, 3)), + # layers.SimpleRNN(4, return_sequences=True) + # ]) + # train_and_save(model, "Sequential_SimpleRNN_test") + + # # 12. RNN - LSTM + # model = models.Sequential([ + # layers.Input(shape=(5, 3)), + # layers.LSTM(4, return_sequences=True) + # ]) + # train_and_save(model, "Sequential_LSTM_test") + + # # 12. RNN - GRU + # model = models.Sequential([ + # layers.Input(shape=(5, 3)), + # layers.GRU(4, return_sequences=True) + # ]) + # train_and_save(model, "Sequential_GRU_test") + + # Layer combinations + + model = models.Sequential([ + layers.Input(shape=(20,)), + layers.Dense(32, activation="relu"), + layers.BatchNormalization(), + layers.Dense(16, activation="sigmoid"), + layers.Dense(8, activation="softmax"), + ]) + train_and_save(model, "Sequential_Layer_Combination_1_test") + + model2 = models.Sequential([ + layers.Input(shape=(28, 28, 3)), + layers.Conv2D(16, (3,3), padding="same", activation="relu"), + layers.MaxPooling2D((2,2)), + layers.Conv2D(32, (5,5), padding="valid"), + layers.Flatten(), + layers.Dense(32, activation="swish"), + layers.Dense(10, activation="softmax"), + ]) + train_and_save(model2, "Sequential_Layer_Combination_2_test") + + model3 = models.Sequential([ + layers.Input(shape=(3, 32, 32)), + layers.Conv2D(8, (3,3), padding="same", data_format="channels_first"), + layers.MaxPooling2D((2,2), data_format="channels_first"), + layers.Flatten(), + layers.Reshape((64, 32)), + layers.Permute((2,1)), + layers.Flatten(), + layers.Dense(16), + layers.LeakyReLU(), + ]) + + train_and_save(model3, "Sequential_Layer_Combination_3_test") 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..74f4eed4a1849 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py @@ -0,0 +1,48 @@ +from cppyy import gbl as gbl_namespace +from ..._keras import 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. + """ + + 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"] + + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BatchNormalization('float')(epsilon, momentum, 0, fNX, fNScale, fNB, fNMean, fNVar, fNY) + 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..e58d7beb151f9 --- /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,'TMVA::Experimental::SOFIE::EBasicBinaryOperator::Add')(fX1, fX2, fY) + elif fLayerType == "Subtract": + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BasicBinary(float,'TMVA::Experimental::SOFIE::EBasicBinaryOperator::Sub')(fX1, fX2, fY) + else: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BasicBinary(float,'TMVA::Experimental::SOFIE::EBasicBinaryOperator::Mul')(fX1, fX2, fY) + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Identity 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..2d23a47219dfd --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py @@ -0,0 +1,11 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasConcat(layer): + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + input = [str(i) for i in finput] + output = str(foutput[0]) + axis = int(attributes["axis"]) + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Concat(input, axis, 0, output) + 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..adcef679a5626 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py @@ -0,0 +1,70 @@ +from cppyy import gbl as gbl_namespace +import math +from ..._keras import 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. + """ + 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 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/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/flatten.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py new file mode 100644 index 0000000000000..647bd215c1b29 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py @@ -0,0 +1,33 @@ +from cppyy import gbl as gbl_namespace +from ..._keras import 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. + """ + 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/leakyrelu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leakyrelu.py new file mode 100644 index 0000000000000..fedab5d9d8c41 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leakyrelu.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 'activation' in attributes.keys(): + fAlpha = float(attributes['activation'].alpha) + elif 'negative_slope' in attributes.keys(): + fAlpha = float(attributes['negative_slope']) + 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..a4db35e884b11 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py @@ -0,0 +1,73 @@ +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'] + 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 supports Convolution 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..f0f42b49fe2c8 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py @@ -0,0 +1,31 @@ +from cppyy import gbl as gbl_namespace +from ..._keras import 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. + """ + 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..113cfa1b1ab6a --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py @@ -0,0 +1,479 @@ +from ......_pythonization import pythonization +from cppyy import gbl as gbl_namespace +import keras +import numpy as np +import os +import time + +from .layers.permute import MakeKerasPermute +from .layers.batchnorm import MakeKerasBatchNorm +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.selu import MakeKerasSeLU +from .layers.sigmoid import MakeKerasSigmoid +from .layers.leakyrelu import MakeKerasLeakyRelu +from .layers.pooling import MakeKerasPooling +from .layers.rnn import MakeKerasRNN +from .layers.dense import MakeKerasDense +from .layers.conv import MakeKerasConv + +from . import 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, + "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, + "selu": MakeKerasSeLU, + "sigmoid": MakeKerasSigmoid, + "LeakyReLU": MakeKerasLeakyRelu, + "softmax": MakeKerasSoftmax, + "MaxPooling2D": 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. + """ + + 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'] + + # 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 == 'MaxPooling2D': + 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(mapKerasLayer[fLayerType](layer_data)) + if fLayerType == 'MaxPooling2D': + 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 + + # 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 + #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 name matches + # 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 name + # 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 + + 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']: + 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 + + fLayerType = layer_data['layerType'] + # 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: + output_layer= keras_model.get_layer(layerName) + output_layer_name = output_layer.output.name + outputNames.append(output_layer_name) + else: + output_layer = keras_model.layers[-1] + output_layer.name = output_layer.name[:13] + str(layer_iter) + outputNames.append(output_layer_name) + rmodel.AddOutputTensorNameList(outputNames) + return rmodel + +@pythonization("RModelParser_Keras", ns="TMVA::Experimental::SOFIE") +def pythonize_rmodelparser_keras(klass): + # Parameters: + # klass: class to be pythonized + setattr(klass, "Parse", RModelParser_Keras.Parse) \ 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..2935b25a5f73b --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py @@ -0,0 +1,89 @@ +import ROOT +import numpy as np +import keras + +''' +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): + 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[:-4] + ".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 6fbd6448c881e..c8a9f4e9eb44e 100644 --- a/bindings/pyroot/pythonizations/test/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/test/CMakeLists.txt @@ -135,6 +135,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..183aa4566382c --- /dev/null +++ b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py @@ -0,0 +1,71 @@ +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 = [ + "BatchNorm1D", + "Conv2D_channels_first", + "Conv2D_channels_last", + "Conv2D_padding_same", + "Conv2D_padding_valid", + "Dense", + "Flatten", + # "GRU", + "LeakyReLU", + # "LSTM", + "MaxPool2D_channels_first", + "MaxPool2D_channels_last", + "Permute", + "Relu", + "Reshape", + "Selu", + "Sigmoid", + # "SimpleRNN", + "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:] + 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) + + # def tearDown(self): + # base_dir = self._testMethodName[5:] + # shutil.rmtree(base_dir) + + @classmethod + def tearDownClass(self): + shutil.rmtree("sequential") + shutil.rmtree("functional") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From dcbb6bff294d6ad4261fe8413e7983be1047014a Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Sun, 7 Sep 2025 02:46:22 +0530 Subject: [PATCH 2/9] 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 --- bindings/pyroot/pythonizations/CMakeLists.txt | 30 +- .../_sofie/_parser/_keras/CMakeLists.txt | 30 ++ .../_tmva/_sofie/_parser/_keras/__init__.py | 8 +- .../_keras/generate_keras_functional.py | 312 +++++++++--------- .../_keras/generate_keras_sequential.py | 291 ++++++++-------- .../_sofie/_parser/_keras/layers/batchnorm.py | 9 +- .../_sofie/_parser/_keras/layers/binary.py | 8 +- .../_sofie/_parser/_keras/layers/concat.py | 8 +- .../_sofie/_parser/_keras/layers/conv.py | 4 +- .../_tmva/_sofie/_parser/_keras/layers/elu.py | 35 ++ .../_sofie/_parser/_keras/layers/flatten.py | 2 +- .../_sofie/_parser/_keras/layers/layernorm.py | 60 ++++ .../layers/{leakyrelu.py => leaky_relu.py} | 4 +- .../_sofie/_parser/_keras/layers/pooling.py | 21 +- .../_sofie/_parser/_keras/layers/reshape.py | 2 +- .../_tmva/_sofie/_parser/_keras/parser.py | 117 +++++-- .../_parser/_keras/parser_test_function.py | 6 +- .../pythonizations/test/sofie_keras_parser.py | 25 +- tmva/pymva/inc/TMVA/MethodPyKeras.h | 4 +- .../inc/TMVA/ROperator_LayerNormalization.hxx | 8 +- tmva/sofie/inc/TMVA/ROperator_Reshape.hxx | 1 + .../inc/TMVA/RModelParser_Keras.h | 4 +- 22 files changed, 604 insertions(+), 385 deletions(-) create mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/CMakeLists.txt 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/layernorm.py rename bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/{leakyrelu.py => leaky_relu.py} (97%) diff --git a/bindings/pyroot/pythonizations/CMakeLists.txt b/bindings/pyroot/pythonizations/CMakeLists.txt index b3a306c4ce731..c342b7fc85afb 100644 --- a/bindings/pyroot/pythonizations/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/CMakeLists.txt @@ -8,6 +8,8 @@ # CMakeLists.txt file for building ROOT pythonizations libraries ################################################################ +set(PYROOT_EXTRA_PYTHON_SOURCES) + if(dataframe) list(APPEND PYROOT_EXTRA_PYTHON_SOURCES ROOT/_pythonization/_rdf_utils.py @@ -58,37 +60,15 @@ 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/_sofie/_parser/_keras/__init__.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.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/flatten.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leakyrelu.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) + ROOT/_pythonization/_tmva/_gnn.py) if(dataframe) list(APPEND PYROOT_EXTRA_PYTHON_SOURCES ROOT/_pythonization/_tmva/_batchgenerator.py) endif() endif() +add_subdirectory(python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras) + list(APPEND PYROOT_EXTRA_HEADERS inc/TPyDispatcher.h) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/CMakeLists.txt b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/CMakeLists.txt new file mode 100644 index 0000000000000..22ad7be102f10 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/CMakeLists.txt @@ -0,0 +1,30 @@ +if (tmva) + list(APPEND PYROOT_EXTRA_PYTHON_SOURCES + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.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) + set(PYROOT_EXTRA_PYTHON_SOURCES "${PYROOT_EXTRA_PYTHON_SOURCES}" PARENT_SCOPE) +endif() \ No newline at end of file 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 index 0acdb2850aae0..d13e46f0fa358 100644 --- 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 @@ -1,3 +1,7 @@ -import keras +def get_keras_version() -> str: + + import keras + + return keras.__version__ -keras_version = keras.__version__ \ No newline at end of file +keras_version = get_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 index 7073a67830c63..36b3f44ea40fb 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,9 @@ -# functional_models.py import numpy as np -from keras import models, layers, activations def generate_keras_functional(dst_dir): + + from keras import models, layers + # Helper training function def train_and_save(model, name): # Handle multiple inputs dynamically @@ -14,189 +15,196 @@ def train_and_save(model, name): model.compile(optimizer='adam', loss='mean_squared_error', metrics=['mae']) model.fit(x_train, y_train, epochs=1, verbose=0) - # print(dst_dir) - model.save(f"{dst_dir}/{name}.h5") - # print(f"Saved {name}.h5") + model.save(f"{dst_dir}/Functional_{name}_test.h5") - # # 1. Dropout (to test SOFIE's Identity operator) - # inp = layers.Input(shape=(10,)) - # out = layers.Dropout(0.5)(inp) - # model = models.Model(inputs=inp, outputs=out) - # train_and_save(model, "Functional_Dropout_test") - - # 2. Binary Operators + # 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, "Functional_Add_test") - - # 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, "Functional_Subtract_test") + 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") - # 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, "Functional_Multiply_test") + # BatchNorm + inp = layers.Input(shape=(10, 3, 5)) + out = layers.BatchNormalization(axis=2)(inp) + model = models.Model(inp, out) + train_and_save(model, "BatchNorm") - # 3. Concat + # 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, "Functional_Concat_test") - - # 4. Reshape - inp = layers.Input(shape=(4, 5)) - out = layers.Reshape((2, 10))(inp) + 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, "Functional_Reshape_test") - - # 5. Flatten - inp = layers.Input(shape=(4, 5)) - out = layers.Flatten()(inp) + 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, "Functional_Flatten_test") - - # 6. BatchNorm 1D - inp = layers.Input(shape=(10,)) - out = layers.BatchNormalization()(inp) + 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, "Functional_BatchNorm1D_test") - - # 7. Activation Functions - for act in ['relu', 'selu', 'sigmoid', 'softmax', 'tanh']: - inp = layers.Input(shape=(10,)) - out = layers.Activation(act)(inp) - model = models.Model(inp, out) - train_and_save(model, f"Functional_{act.capitalize()}_test") - - # LeakyReLU + 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.LeakyReLU()(inp) + out = layers.Dense(5, activation='tanh')(inp) model = models.Model(inp, out) - train_and_save(model, "Functional_LeakyReLU_test") - - # Swish + train_and_save(model, "Dense") + + # ELU inp = layers.Input(shape=(10,)) - out = layers.Activation(activations.swish)(inp) + out = layers.ELU(alpha=0.5)(inp) model = models.Model(inp, out) - train_and_save(model, "Functional_Swish_test") - - # 8. Permute - inp = layers.Input(shape=(3, 4, 5)) - out = layers.Permute((2, 1, 3))(inp) + 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, "Functional_Permute_test") - - # 9. Dense - inp = layers.Input(shape=(10,)) - out = layers.Dense(5)(inp) + 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, "Functional_Dense_test") - - # 10. Conv2D channels_last - inp = layers.Input(shape=(8, 8, 3)) - out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last')(inp) + 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, "Functional_Conv2D_channels_last_test") - - # 10. Conv2D channels_first - inp = layers.Input(shape=(3, 8, 8)) - out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_first')(inp) + 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, "Functional_Conv2D_channels_first_test") + train_and_save(model, "LayerNorm") - # Conv2D padding_same - inp = layers.Input(shape=(8, 8, 3)) - out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last')(inp) + # LeakyReLU + inp = layers.Input(shape=(10,)) + out = layers.LeakyReLU()(inp) model = models.Model(inp, out) - train_and_save(model, "Functional_Conv2D_padding_same_test") + train_and_save(model, "LeakyReLU") - # Conv2D padding_valid - inp = layers.Input(shape=(8, 8, 3)) - out = layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last')(inp) + # 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, "Functional_Conv2D_padding_valid_test") - - # 11. MaxPooling2D channels_last + 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, "Functional_MaxPool2D_channels_last_test") - - # 11. MaxPooling2D channels_first - inp = layers.Input(shape=(3, 8, 8)) - out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_first')(inp) + 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, "Functional_MaxPool2D_channels_first_test") - - # # 12. RNN - SimpleRNN - # inp = layers.Input(shape=(5, 3)) - # out = layers.SimpleRNN(4, return_sequences=True)(inp) - # model = models.Model(inp, out) - # train_and_save(model, "Functional_SimpleRNN_test") - - # # 12. RNN - LSTM - # inp = layers.Input(shape=(5, 3)) - # out = layers.LSTM(4, return_sequences=True)(inp) - # model = models.Model(inp, out) - # train_and_save(model, "Functional_LSTM_test") + 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") - # # 12. RNN - GRU - # inp = layers.Input(shape=(5, 3)) - # out = layers.GRU(4, return_sequences=True)(inp) - # model = models.Model(inp, out) - # train_and_save(model, "Functional_GRU_test") + # 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 - in1 = layers.Input(shape=(16,)) - in2 = layers.Input(shape=(16,)) - x1 = layers.Dense(32, activation="relu")(in1) - x1 = layers.BatchNormalization()(x1) - x2 = layers.Dense(32, activation="sigmoid")(in2) - merged = layers.Concatenate()([x1, x2]) - added = layers.Add()([merged, merged]) - out = layers.Dense(10, activation="softmax")(added) - model1 = models.Model([in1, in2], out) - train_and_save(model1, "Functional_Layer_Combination_1_test") - - - inp1 = layers.Input(shape=(32, 32, 3)) - x1 = layers.Conv2D(8, (3,3), padding="same", data_format="channels_last", activation="relu")(inp1) - x1 = layers.MaxPooling2D((2,2), data_format="channels_last")(x1) - x1 = layers.Flatten()(x1) - inp2 = layers.Input(shape=(3, 32, 32)) - x2 = layers.Conv2D(8, (5,5), padding="valid", data_format="channels_first")(inp2) - x2 = layers.MaxPooling2D((2,2), data_format="channels_first")(x2) - x2 = layers.Flatten()(x2) - merged = layers.Concatenate()([x1, x2]) - out = layers.Dense(20, activation=activations.swish)(merged) - model2 = models.Model([inp1, inp2], out) - train_and_save(model2, "Functional_Layer_Combination_2_test") - - - in1 = layers.Input(shape=(12,)) - in2 = layers.Input(shape=(12,)) - x1 = layers.Dense(24, activation="tanh")(in1) - x1 = layers.Reshape((4, 6))(x1) - x1 = layers.Permute((2,1))(x1) - x2 = layers.Dense(24, activation="relu")(in2) - x2 = layers.Reshape((6, 4))(x2) - mul = layers.Multiply()([x1, x2]) - sub = layers.Subtract()([x1, x2]) - merged = layers.Concatenate()([mul, sub]) - flat = layers.Flatten()(merged) - dense = layers.Dense(16)(flat) - out = layers.LeakyReLU()(dense) - model3 = models.Model([in1, in2], out) - train_and_save(model3, "Functional_Layer_Combination_3_test") + 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 index be9caa39c08ba..2d7028f919749 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,187 +1,206 @@ -# sequential_models.py import numpy as np -from keras import models, layers, activations def generate_keras_sequential(dst_dir): + + from keras import models, layers + # 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}/{name}.h5") - # print(f"Saved {name}.h5") - - # 1. Dropout - # model = models.Sequential([ - # layers.Input(shape=(10,)), - # layers.Dropout(0.5) # Dropout - # ]) - # train_and_save(model, "Sequential_Dropout_test") - - # 2. Binary Ops: Add, Subtract, Multiply are not typical in Sequential — skipping here - - # 3. Concat (not applicable in Sequential without multi-input) - - # 4. Reshape - model = models.Sequential([ - layers.Input(shape=(4, 5)), - layers.Reshape((2, 10)) - ]) - train_and_save(model, "Sequential_Reshape_test") - - # 5. Flatten - model = models.Sequential([ - layers.Input(shape=(4, 5)), - layers.Flatten() - ]) - train_and_save(model, "Sequential_Flatten_test") - - # 6. BatchNorm 1D - model = models.Sequential([ - layers.Input(shape=(10,)), - layers.BatchNormalization() - ]) - train_and_save(model, "Sequential_BatchNorm1D_test") - - # # 6. BatchNorm 2D - # model = models.Sequential([ - # layers.Input(shape=(8, 3)), - # layers.BatchNormalization() - # ]) - # train_and_save(model, "Sequential_BatchNorm2D_test") + model.save(f"{dst_dir}/Sequential_{name}_test.h5") - # 7. Activation Functions - for act in ['relu', 'selu', 'sigmoid', 'softmax', 'tanh']: + # 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"Sequential_{act.capitalize()}_test") - - # LeakyReLU + 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=(10,)), - layers.LeakyReLU() + layers.Input(shape=(3, 8, 8)), + layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first') ]) - train_and_save(model, "Sequential_LeakyReLU_test") - - # Swish + train_and_save(model, "AveragePooling2D_channels_first") + + # AveragePooling2D channels_last model = models.Sequential([ - layers.Input(shape=(10,)), - layers.Activation(activations.swish) + layers.Input(shape=(8, 8, 3)), + layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_last') ]) - train_and_save(model, "Sequential_Swish_test") + train_and_save(model, "AveragePooling2D_channels_last") - # 8. Permute + # BatchNorm model = models.Sequential([ - layers.Input(shape=(3, 4, 5)), - layers.Permute((2, 1, 3)) + layers.Input(shape=(10, 3, 5)), + layers.BatchNormalization(axis=2) ]) - train_and_save(model, "Sequential_Permute_test") - - # 9. Dense + train_and_save(model, "BatchNorm") + + # Conv2D channels_first model = models.Sequential([ - layers.Input(shape=(10,)), - layers.Dense(5) + layers.Input(shape=(3, 8, 8)), + layers.Conv2D(4, (3, 3), data_format='channels_first') ]) - train_and_save(model, "Sequential_Dense_test") - - # 10. Conv2D channels_last + 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') + layers.Conv2D(4, (3, 3), data_format='channels_last', activation='tanh') ]) - train_and_save(model, "Sequential_Conv2D_channels_last_test") - - # 10. 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, "Sequential_Conv2D_channels_first_test") + 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') + layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last', activation='selu') ]) - train_and_save(model, "Sequential_Conv2D_padding_same_test") + 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') + layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last', activation='swish') ]) - train_and_save(model, "Sequential_Conv2D_padding_valid_test") - - # 11. MaxPooling2D channels_last + train_and_save(model, "Conv2D_padding_valid") + + # Dense model = models.Sequential([ - layers.Input(shape=(8, 8, 3)), - layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last') + layers.Input(shape=(10,)), + layers.Dense(5, activation='sigmoid') ]) - train_and_save(model, "Sequential_MaxPool2D_channels_last_test") - - # 11. MaxPooling2D channels_first + 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, "Sequential_MaxPool2D_channels_first_test") - - # # 12. RNN - SimpleRNN - # model = models.Sequential([ - # layers.Input(shape=(5, 3)), - # layers.SimpleRNN(4, return_sequences=True) - # ]) - # train_and_save(model, "Sequential_SimpleRNN_test") - - # # 12. RNN - LSTM - # model = models.Sequential([ - # layers.Input(shape=(5, 3)), - # layers.LSTM(4, return_sequences=True) - # ]) - # train_and_save(model, "Sequential_LSTM_test") - - # # 12. RNN - GRU - # model = models.Sequential([ - # layers.Input(shape=(5, 3)), - # layers.GRU(4, return_sequences=True) - # ]) - # train_and_save(model, "Sequential_GRU_test") + train_and_save(model, "MaxPool2D_channels_first") - # Layer combinations + # 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=(20,)), - layers.Dense(32, activation="relu"), - layers.BatchNormalization(), - layers.Dense(16, activation="sigmoid"), - layers.Dense(8, activation="softmax"), + layers.Input(shape=(3, 4, 5)), + layers.Permute((2, 1, 3)) ]) - train_and_save(model, "Sequential_Layer_Combination_1_test") + train_and_save(model, "Permute") - model2 = models.Sequential([ - layers.Input(shape=(28, 28, 3)), - layers.Conv2D(16, (3,3), padding="same", activation="relu"), - layers.MaxPooling2D((2,2)), - layers.Conv2D(32, (5,5), padding="valid"), - layers.Flatten(), - layers.Dense(32, activation="swish"), - layers.Dense(10, activation="softmax"), + # Reshape + model = models.Sequential([ + layers.Input(shape=(4, 5)), + layers.Reshape((2, 10)) ]) - train_and_save(model2, "Sequential_Layer_Combination_2_test") + train_and_save(model, "Reshape") + + # ReLU + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.ReLU() + ]) + train_and_save(model, "ReLU") - model3 = models.Sequential([ - layers.Input(shape=(3, 32, 32)), - layers.Conv2D(8, (3,3), padding="same", data_format="channels_first"), - layers.MaxPooling2D((2,2), data_format="channels_first"), - layers.Flatten(), - layers.Reshape((64, 32)), - layers.Permute((2,1)), + # 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(16), - layers.LeakyReLU(), + 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(model3, "Sequential_Layer_Combination_3_test") + 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/batchnorm.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py index 74f4eed4a1849..f5163dbf00425 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,5 +1,5 @@ from cppyy import gbl as gbl_namespace -from ..._keras import keras_version +from .. import keras_version def MakeKerasBatchNorm(layer): """ @@ -44,5 +44,10 @@ def MakeKerasBatchNorm(layer): epsilon = attributes["epsilon"] momentum = attributes["momentum"] - op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BatchNormalization('float')(epsilon, momentum, 0, fNX, fNScale, fNB, fNMean, fNVar, fNY) + 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 index e58d7beb151f9..ff35fd2032653 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 @@ -11,13 +11,13 @@ def MakeKerasBinary(layer): 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,'TMVA::Experimental::SOFIE::EBasicBinaryOperator::Add')(fX1, fX2, fY) + 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,'TMVA::Experimental::SOFIE::EBasicBinaryOperator::Sub')(fX1, fX2, fY) + 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,'TMVA::Experimental::SOFIE::EBasicBinaryOperator::Mul')(fX1, fX2, fY) + 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 Identity does not yet support input type " + fLayerDType + "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 index 2d23a47219dfd..340aa4e9cb452 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 @@ -3,9 +3,15 @@ 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"]) - op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Concat(input, axis, 0, output) + 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 index adcef679a5626..a7ec114dcf878 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,6 +1,6 @@ from cppyy import gbl as gbl_namespace import math -from ..._keras import keras_version +from .. import keras_version def MakeKerasConv(layer): """ @@ -66,5 +66,5 @@ def MakeKerasConv(layer): return op else: raise RuntimeError( - "TMVA::SOFIE - Unsupported - Operator Gemm does not yet support input type " + fLayerDType + "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/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 index 647bd215c1b29..46fb50314692f 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,5 +1,5 @@ from cppyy import gbl as gbl_namespace -from ..._keras import keras_version +from .. import keras_version def MakeKerasFlatten(layer): """ 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..c1c5c3e1c5178 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py @@ -0,0 +1,60 @@ +from cppyy import gbl as gbl_namespace +from .. import 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. + """ + + 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/leakyrelu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py similarity index 97% rename from bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leakyrelu.py rename to bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py index fedab5d9d8c41..c0b95b04b27eb 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leakyrelu.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py @@ -26,10 +26,10 @@ def MakeKerasLeakyRelu(layer): if 'alpha' in attributes.keys(): fAlpha = float(attributes["alpha"]) - elif 'activation' in attributes.keys(): - fAlpha = float(attributes['activation'].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" 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 a4db35e884b11..364d2be8da147 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 @@ -17,7 +17,7 @@ def MakeKerasPooling(layer): ROperator_Pool: A SOFIE framework operator representing the pooling layer operation. """ - #extract attributes from layer data + # Extract attributes from layer data fLayerDType = layer['layerDType'] finput = layer['layerInput'] foutput = layer['layerOutput'] @@ -26,11 +26,16 @@ def MakeKerasPooling(layer): fLayerOutputName = foutput[0] pool_atrr = gbl_namespace.TMVA.Experimental.SOFIE.RAttributes_Pool() attributes = layer['layerAttributes'] - fAttrKernelShape = attributes["pool_size"] - fKerasPadding = str(attributes["padding"]) - fAttrStrides = attributes["strides"] + # 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 + # Set default values fAttrDilations = (1,1) fpads = [0,0,0,0,0,0] pool_atrr.ceil_mode = 0 @@ -43,7 +48,7 @@ def MakeKerasPooling(layer): fAttrAutopad = 'NOTSET' else: raise RuntimeError( - "TMVA::SOFIE - RModel Keras Parser doesn't yet supports Convolution layer with padding " + fKerasPadding + "TMVA::SOFIE - RModel Keras Parser doesn't yet support Pooling layer with padding " + fKerasPadding ) pool_atrr.dilations = list(fAttrDilations) pool_atrr.strides = list(fAttrStrides) @@ -51,7 +56,7 @@ def MakeKerasPooling(layer): pool_atrr.kernel_shape = list(fAttrKernelShape) pool_atrr.auto_pad = fAttrAutopad - #choose pooling type + # Choose pooling type if 'Max' in fLayerType: PoolMode = gbl_namespace.TMVA.Experimental.SOFIE.PoolOpMode.MaxPool elif 'AveragePool' in fLayerType: @@ -63,7 +68,7 @@ def MakeKerasPooling(layer): "TMVA::SOFIE - Unsupported - Operator poolong does not yet support pooling type " + fLayerType ) - #create operator + # 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 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 f0f42b49fe2c8..8ca762986814c 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,5 +1,5 @@ from cppyy import gbl as gbl_namespace -from ..._keras import keras_version +from .. import keras_version def MakeKerasReshape(layer): """ 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 113cfa1b1ab6a..5f8ee850ece6e 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,12 +1,12 @@ from ......_pythonization import pythonization from cppyy import gbl as gbl_namespace -import keras import numpy as np 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 @@ -16,9 +16,10 @@ 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.leakyrelu import MakeKerasLeakyRelu +from .layers.leaky_relu import MakeKerasLeakyRelu from .layers.pooling import MakeKerasPooling from .layers.rnn import MakeKerasRNN from .layers.dense import MakeKerasDense @@ -40,6 +41,7 @@ def MakeKerasActivation(layer): mapKerasLayer = {"Activation": MakeKerasActivation, "Permute": MakeKerasPermute, "BatchNormalization": MakeKerasBatchNorm, + "LayerNormalization": MakeKerasLayerNorm, "Reshape": MakeKerasReshape, "Flatten": MakeKerasFlatten, "Concatenate": MakeKerasConcat, @@ -50,18 +52,23 @@ def MakeKerasActivation(layer): "Multiply": MakeKerasBinary, "Softmax": MakeKerasSoftmax, "tanh": MakeKerasTanh, - "Identity": MakeKerasIdentity, - "Dropout": MakeKerasIdentity, + # "Identity": MakeKerasIdentity, + # "Dropout": MakeKerasIdentity, "ReLU": MakeKerasReLU, "relu": MakeKerasReLU, + "ELU": MakeKerasELU, + "elu": MakeKerasELU, "selu": MakeKerasSeLU, "sigmoid": MakeKerasSigmoid, - "LeakyReLU": MakeKerasLeakyRelu, + "LeakyReLU": MakeKerasLeakyRelu, + "leaky_relu": MakeKerasLeakyRelu, "softmax": MakeKerasSoftmax, "MaxPooling2D": MakeKerasPooling, - "SimpleRNN": MakeKerasRNN, - "GRU": MakeKerasRNN, - "LSTM": MakeKerasRNN, + "AveragePooling2D": MakeKerasPooling, + "GlobalAveragePooling2D": MakeKerasPooling, + # "SimpleRNN": MakeKerasRNN, + # "GRU": MakeKerasRNN, + # "LSTM": MakeKerasRNN, } mapKerasLayerWithActivation = {"Dense": MakeKerasDense,"Conv2D": MakeKerasConv} @@ -127,31 +134,75 @@ def add_layer_into_RModel(rmodel, layer_data): else: LayerName = Attributes['name'] - # 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: + # 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 == 'MaxPooling2D': + 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'] + 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" - layer_data["layerInput"] = inputs outputs[0] = LayerName+"PostTrans" - rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) - if fLayerType == 'MaxPooling2D': + 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 @@ -225,6 +276,16 @@ def add_layer_into_RModel(rmodel, layer_data): 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 + #Check if file exists if not os.path.exists(filename): raise RuntimeError("Model file {} not found!".format(filename)) @@ -256,7 +317,7 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s # layer | name # input dense | keras_tensor_1 # output dense | keras_tensor_2 -- - # | |=> layer name matches + # | |=> layer names match # input maxpool | keras_tensor_2 -- # output maxpool | keras_tensor_3 # @@ -264,7 +325,7 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s # layer | name # input dense | keras_tensor_1 # output dense | keras_tensor_2 -- - # | |=> different layer name + # | |=> different layer names # input maxpool | keras_tensor_3 -- # output maxpool | keras_tensor_4 # @@ -294,8 +355,9 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s 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 - - layer_data['layerDType']=layer.dtype + + fLayerType = layer_data['layerType'] + layer_data['layerDType'] = layer.dtype if len(layer.weights) > 0: if keras_version < '2.16': @@ -306,7 +368,7 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s layer_data['layerWeight'] = [] # for convolutional and pooling layers we need to know the format of the data - if layer_data['layerType'] in ['Conv2D', 'MaxPooling2D']: + 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 @@ -325,7 +387,6 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s if layer_data['layerType'] == "GRU": layer_data['layerAttributes']['linear_before_reset'] = 1 if layer.reset_after and layer.recurrent_activation.__name__ == "sigmoid" else 0 - fLayerType = layer_data['layerType'] # Ignoring the input layer of the model if(fLayerType == "InputLayer"): continue; @@ -429,7 +490,7 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s fPInputDType = [] for idx in range(len(keras_model.inputs)): dtype = keras_model.inputs[idx].dtype.__str__() - if (dtype == "float32"): + if dtype == "float32": fPInputDType.append(dtype) else: fPInputDType.append(dtype[9:-2]) @@ -462,12 +523,12 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s outputNames = [] if keras_version < '2.16' or is_functional_model: for layerName in keras_model.output_names: - output_layer= keras_model.get_layer(layerName) - output_layer_name = output_layer.output.name + final_layer = keras_model.get_layer(layerName) + output_layer_name = final_layer.output.name outputNames.append(output_layer_name) else: - output_layer = keras_model.layers[-1] - output_layer.name = output_layer.name[:13] + str(layer_iter) + 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 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 2935b25a5f73b..774b21674fa11 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 @@ -1,6 +1,5 @@ import ROOT import numpy as np -import keras ''' The test file contains two types of functions: @@ -46,6 +45,9 @@ 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 keras + 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: @@ -63,7 +65,7 @@ def generate_and_test_inference(model_file_path: str, generated_header_file_dir: 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[:-4] + ".dat") + 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: diff --git a/bindings/pyroot/pythonizations/test/sofie_keras_parser.py b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py index 183aa4566382c..f94697761d44b 100644 --- a/bindings/pyroot/pythonizations/test/sofie_keras_parser.py +++ b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py @@ -12,33 +12,40 @@ def make_testname(test_case: str): return test_case_name models = [ - "BatchNorm1D", + "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", + "ReLU", "Reshape", - "Selu", - "Sigmoid", # "SimpleRNN", "Softmax", - "Swish", - "Tanh", -] + [f"Layer_Combination_{i}" for i in range(1, 4)] +] + ([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") @@ -58,10 +65,6 @@ def test_functional(self): functional_models = models + ["Add", "Concat", "Multiply", "Subtract"] self.run_model_tests("functional", generate_keras_functional, functional_models) - # def tearDown(self): - # base_dir = self._testMethodName[5:] - # shutil.rmtree(base_dir) - @classmethod def tearDownClass(self): shutil.rmtree("sequential") 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 239c5332172b0..033c25b694520 100644 --- a/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx @@ -224,7 +224,7 @@ public: } out << SP << SP << "tensor_" << fNMean << "[" << axesIndex << "] = sum / " << fType << "("; out << fNormalizedLength << ");\n"; - for (size_t i = fAxis; i < fSize; i++) { + for (size_t i = 0; i < fAxis; i++) { out << SP << "}\n"; } @@ -273,7 +273,7 @@ public: for (size_t j = fAxis; j < fSize; j++) { out << SP << SP << "}\n"; } - for (size_t i = fAxis; i < fSize; i++) { + for (size_t i = 0; i < fAxis; i++) { out << SP << "}\n"; } out << "// Y = Scale o NormalizedX"; @@ -293,7 +293,7 @@ public: for (size_t j = fAxis; j < fSize; j++) { out << SP << SP << "}\n"; } - for (size_t i = fAxis; i < fSize; i++) { + for (size_t i = 0; i < fAxis; i++) { out << SP << "}\n"; } } else { @@ -315,7 +315,7 @@ public: for (size_t j = fAxis; j < fSize; j++) { out << SP << SP << "}\n"; } - for (size_t i = fAxis; i < fSize; i++) { + for (size_t i = 0; i < fAxis; i++) { out << SP << "}\n"; } } diff --git a/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx b/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx index 2634b68dbc875..daeab721039a0 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 f94d8433b409bc57a0ea142bed900ed44778b3cb Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Fri, 12 Sep 2025 13:18:30 +0530 Subject: [PATCH 3/9] removed get_keras_version function call from tmva __init__ file. Replaced import keras_version with get_keras_version and called it in necessary files --- .../_pythonization/_tmva/_sofie/_parser/_keras/__init__.py | 4 +--- .../_sofie/_parser/_keras/generate_keras_functional.py | 1 - .../_tmva/_sofie/_parser/_keras/layers/batchnorm.py | 4 +++- .../_tmva/_sofie/_parser/_keras/layers/conv.py | 5 ++++- .../_tmva/_sofie/_parser/_keras/layers/flatten.py | 5 ++++- .../_tmva/_sofie/_parser/_keras/layers/layernorm.py | 4 +++- .../_tmva/_sofie/_parser/_keras/layers/reshape.py | 5 ++++- .../_pythonization/_tmva/_sofie/_parser/_keras/parser.py | 6 +++++- 8 files changed, 24 insertions(+), 10 deletions(-) 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 index d13e46f0fa358..5f48c83e89aa1 100644 --- 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 @@ -2,6 +2,4 @@ def get_keras_version() -> str: import keras - return keras.__version__ - -keras_version = get_keras_version() \ No newline at end of file + 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 index 36b3f44ea40fb..d129d1c42ab65 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 @@ -195,7 +195,6 @@ def train_and_save(model, name): 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) 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 f5163dbf00425..834f9d0698163 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,5 +1,5 @@ from cppyy import gbl as gbl_namespace -from .. import keras_version +from .. import get_keras_version def MakeKerasBatchNorm(layer): """ @@ -18,6 +18,8 @@ def MakeKerasBatchNorm(layer): Returns: ROperator_BatchNormalization: A SOFIE framework operator representing the batch normalization operation. """ + + keras_version = get_keras_version() 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 a7ec114dcf878..98fe21b1cc887 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,6 +1,6 @@ from cppyy import gbl as gbl_namespace import math -from .. import keras_version +from .. import get_keras_version def MakeKerasConv(layer): """ @@ -19,6 +19,9 @@ def MakeKerasConv(layer): 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'] 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 46fb50314692f..8b28382ebc4a0 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,5 +1,5 @@ from cppyy import gbl as gbl_namespace -from .. import keras_version +from .. import get_keras_version def MakeKerasFlatten(layer): """ @@ -17,6 +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'] 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 c1c5c3e1c5178..b10ce58d239a9 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,5 +1,5 @@ from cppyy import gbl as gbl_namespace -from .. import keras_version +from .. import get_keras_version def MakeKerasLayerNorm(layer): """ @@ -20,6 +20,8 @@ def MakeKerasLayerNorm(layer): 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'] 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 8ca762986814c..c83822f43e080 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,5 +1,5 @@ from cppyy import gbl as gbl_namespace -from .. import keras_version +from .. import get_keras_version def MakeKerasReshape(layer): """ @@ -15,6 +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'] 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 5f8ee850ece6e..ee3229cc9d662 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 @@ -25,7 +25,7 @@ from .layers.dense import MakeKerasDense from .layers.conv import MakeKerasConv -from . import keras_version +from . import get_keras_version def MakeKerasActivation(layer): attributes = layer['layerAttributes'] @@ -93,6 +93,8 @@ def add_layer_into_RModel(rmodel, layer_data): Raises exception: If the provided layer type or activation function is not supported. """ + 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 @@ -286,6 +288,8 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s import keras + keras_version = get_keras_version() + #Check if file exists if not os.path.exists(filename): raise RuntimeError("Model file {} not found!".format(filename)) From a19f39848905dd63f9c2b1e42dcae9cea9d74f35 Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Sat, 1 Nov 2025 23:00:46 +0530 Subject: [PATCH 4/9] 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. --- bindings/pyroot/pythonizations/CMakeLists.txt | 28 ++++++++++++++++- .../_sofie/_parser/_keras/CMakeLists.txt | 30 ------------------- .../_keras/generate_keras_functional.py | 3 +- .../_keras/generate_keras_sequential.py | 3 +- .../_tmva/_sofie/_parser/_keras/parser.py | 5 +++- .../_parser/_keras/parser_test_function.py | 2 +- 6 files changed, 34 insertions(+), 37 deletions(-) delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/CMakeLists.txt diff --git a/bindings/pyroot/pythonizations/CMakeLists.txt b/bindings/pyroot/pythonizations/CMakeLists.txt index c342b7fc85afb..2b18745a8e76c 100644 --- a/bindings/pyroot/pythonizations/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/CMakeLists.txt @@ -60,7 +60,33 @@ 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/generate_keras_functional.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.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/_pythonization/_tmva/_sofie/_parser/_keras/CMakeLists.txt b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/CMakeLists.txt deleted file mode 100644 index 22ad7be102f10..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/CMakeLists.txt +++ /dev/null @@ -1,30 +0,0 @@ -if (tmva) - list(APPEND PYROOT_EXTRA_PYTHON_SOURCES - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.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) - set(PYROOT_EXTRA_PYTHON_SOURCES "${PYROOT_EXTRA_PYTHON_SOURCES}" PARENT_SCOPE) -endif() \ 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 index d129d1c42ab65..8a433e751c6bc 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,7 @@ -import numpy as np - def generate_keras_functional(dst_dir): from keras import models, layers + import numpy as np # Helper training function def train_and_save(model, name): 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 2d7028f919749..20c03f31c69fc 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,8 +1,7 @@ -import numpy as np - def generate_keras_sequential(dst_dir): from keras import models, layers + import numpy as np # Helper training function def train_and_save(model, name): 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 ee3229cc9d662..13f2532a9f544 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,6 +1,5 @@ from ......_pythonization import pythonization from cppyy import gbl as gbl_namespace -import numpy as np import os import time @@ -93,6 +92,8 @@ 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'] @@ -174,6 +175,7 @@ def add_layer_into_RModel(rmodel, layer_data): 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)) @@ -287,6 +289,7 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s # caches the imported packages. import keras + import numpy as np keras_version = get_keras_version() 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 774b21674fa11..7fb2f8fefc383 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 @@ -1,5 +1,4 @@ import ROOT -import numpy as np ''' The test file contains two types of functions: @@ -47,6 +46,7 @@ def is_accurate(tensor_a, tensor_b, tolerance=1e-3): def generate_and_test_inference(model_file_path: str, generated_header_file_dir: str = None, batch_size=1): import keras + import numpy as np 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) From fef6468edb7667f87335fb3e3a508f2bb29d71d1 Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Sun, 9 Nov 2025 01:08:36 +0530 Subject: [PATCH 5/9] Reverted the Pythonization CMakeLists file to its previous version --- bindings/pyroot/pythonizations/CMakeLists.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bindings/pyroot/pythonizations/CMakeLists.txt b/bindings/pyroot/pythonizations/CMakeLists.txt index 2b18745a8e76c..7676530379c21 100644 --- a/bindings/pyroot/pythonizations/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/CMakeLists.txt @@ -8,8 +8,6 @@ # CMakeLists.txt file for building ROOT pythonizations libraries ################################################################ -set(PYROOT_EXTRA_PYTHON_SOURCES) - if(dataframe) list(APPEND PYROOT_EXTRA_PYTHON_SOURCES ROOT/_pythonization/_rdf_utils.py @@ -93,8 +91,6 @@ if(tmva) endif() endif() -add_subdirectory(python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras) - list(APPEND PYROOT_EXTRA_HEADERS inc/TPyDispatcher.h) From 51116928dd4d0ee0e2d536842cac7b05a9c97749 Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Sun, 9 Nov 2025 01:10:21 +0530 Subject: [PATCH 6/9] Added print statements to display the TensorFlow Keras version used in CI --- .../_tmva/_sofie/_parser/_keras/parser_test_function.py | 5 +++++ 1 file changed, 5 insertions(+) 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 7fb2f8fefc383..d9d400c95a53c 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 @@ -45,9 +45,14 @@ def is_accurate(tensor_a, tensor_b, tolerance=1e-3): 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: From 321a2fa4ceaa85821ff99086ef254f9312e751ad Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Sun, 9 Nov 2025 16:42:22 +0530 Subject: [PATCH 7/9] Correctly inject RModelParser_Keras class into Python interfaces --- bindings/pyroot/pythonizations/python/ROOT/_facade.py | 1 + .../_pythonization/_tmva/_sofie/_parser/_keras/parser.py | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index 2b91ea4b6de0d..3b28e159f26d1 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -427,6 +427,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/_sofie/_parser/_keras/parser.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py index 13f2532a9f544..f916c9fd2a3b3 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 @@ -539,9 +539,4 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s outputNames.append(output_layer_name) rmodel.AddOutputTensorNameList(outputNames) return rmodel - -@pythonization("RModelParser_Keras", ns="TMVA::Experimental::SOFIE") -def pythonize_rmodelparser_keras(klass): - # Parameters: - # klass: class to be pythonized - setattr(klass, "Parse", RModelParser_Keras.Parse) \ No newline at end of file + \ No newline at end of file From 7d939fef8be0e9e45795fd6b3803d443ea2fdae5 Mon Sep 17 00:00:00 2001 From: moneta Date: Tue, 13 Jan 2026 15:43:12 +0100 Subject: [PATCH 8/9] [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/_parser/_keras/parser.py | 171 +++++++++--------- 1 file changed, 89 insertions(+), 82 deletions(-) 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..caa7d1eb80e67 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,14 +91,14 @@ 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 + + # 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'] @@ -106,7 +106,7 @@ def add_layer_into_RModel(rmodel, layer_data): LayerName = Attributes['_name'] else: LayerName = Attributes['name'] - + if fLayerType == "Reshape": TargetShape = np.asarray(Attributes['target_shape']).astype("int") TargetShape = np.insert(TargetShape,0,0) @@ -121,12 +121,12 @@ 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) + + # 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 +136,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 +163,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 +173,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 +181,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 +200,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 +220,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 +228,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 +248,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'] @@ -280,46 +280,46 @@ def add_layer_into_RModel(rmodel, layer_data): 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 + + # 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 +334,46 @@ 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 - + prev_layer_name = "input" 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) + #sequentiall 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 - + prev_layer_name = layer.name + if len(layer.weights) > 0: if keras_version < '2.16': layer_data['layerWeight'] = [x.name for x in layer.weights] @@ -373,11 +381,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 +393,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 +438,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 +472,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 +494,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 +509,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 +518,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 +532,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 +542,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 From af08225bd1f839cf3d5d62286979b32cf10dba4c Mon Sep 17 00:00:00 2001 From: moneta Date: Wed, 14 Jan 2026 18:42:19 +0100 Subject: [PATCH 9/9] [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 --- .../pythonizations/python/ROOT/_facade.py | 2 +- .../ROOT/_pythonization/_tmva/__init__.py | 2 +- .../_tmva/_sofie/_parser/_keras/parser.py | 2 +- .../_parser/_keras/parser_test_function.py | 28 +- .../inc/TMVA/RModelParser_Keras.h | 2 +- tmva/sofie_parsers/src/RModelParser_Keras.cxx | 1070 +---------------- tutorials/CMakeLists.txt | 8 +- .../machine_learning/TMVA_SOFIE_Inference.py | 2 + 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 | 50 + .../machine_learning/TMVA_SOFIE_Models.py | 9 +- .../TMVA_SOFIE_RDataFrame_JIT.C | 16 +- 14 files changed, 175 insertions(+), 1212 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 3b28e159f26d1..c198046663e24 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -427,7 +427,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 b76af2ded8983..23c199d94fb11 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py @@ -44,7 +44,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/parser.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py index caa7d1eb80e67..447d03bf934bb 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 @@ -277,7 +277,7 @@ 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 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..18ad957a4ca16 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) + 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/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/tutorials/CMakeLists.txt b/tutorials/CMakeLists.txt index 9a318e643bdca..a571570d9c242 100644 --- a/tutorials/CMakeLists.txt +++ b/tutorials/CMakeLists.txt @@ -348,7 +348,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}) @@ -362,10 +362,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) diff --git a/tutorials/machine_learning/TMVA_SOFIE_Inference.py b/tutorials/machine_learning/TMVA_SOFIE_Inference.py index 712420115e7c4..24a80a50cd191 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_Inference.py +++ b/tutorials/machine_learning/TMVA_SOFIE_Inference.py @@ -17,6 +17,8 @@ import numpy as np import ROOT +from os.path import exists + # check if the input file exists modelFile = "Higgs_trained_model.keras" 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..43e1df645fbf5 --- /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",-1) + +# 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..b1f67d1f63bf4 --- /dev/null +++ b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py @@ -0,0 +1,50 @@ +### \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 + + + +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 + model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse(modelFile) + + #Generating inference code + model.Generate() + model.OutputGenerated() + + + +modelFile = "Higgs_trained_model.keras" + +GenerateCode(modelFile) +modelHeaderFile = modelFile.replace(".keras",".hxx") + +#test the generated code +ROOT.gInterpreter.Declare('#include "' + modelHeaderFile + '"') + +session = ROOT.TMVA_SOFIE_Higgs_trained_model.Session() + +x = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7], dtype=np.float32) +y = session.infer(x) + +print(" output for x = ",x, " ---> ",y) + + + 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_JIT.C b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame_JIT.C index 95dd4b1316278..f218337445149 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 = "Higgs_trained_model"){ // 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())) {