From 8028687cb1f382c48b11fbcb2af36037ff8b8fd6 Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Wed, 20 Aug 2025 13:13:02 +0530 Subject: [PATCH 1/6] [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 +++ tmva/pymva/inc/TMVA/RModelParser_Keras.h | 2 + 29 files changed, 1765 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 57cb4fa8044b7..77f4ed9ef7f06 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 ad4dd92ca3794..d27dee27388da 100644 --- a/bindings/pyroot/pythonizations/test/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/test/CMakeLists.txt @@ -134,6 +134,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 diff --git a/tmva/pymva/inc/TMVA/RModelParser_Keras.h b/tmva/pymva/inc/TMVA/RModelParser_Keras.h index d60eb2041e650..7551b02ed5249 100644 --- a/tmva/pymva/inc/TMVA/RModelParser_Keras.h +++ b/tmva/pymva/inc/TMVA/RModelParser_Keras.h @@ -39,6 +39,8 @@ namespace TMVA{ namespace Experimental{ namespace SOFIE{ + +class RModelParser_Keras{}; namespace PyKeras{ From b6cb407f8763f59812367d22874554afba4478f3 Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Sun, 7 Sep 2025 02:46:22 +0530 Subject: [PATCH 2/6] 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/CMakeLists.txt | 1 - tmva/pymva/inc/TMVA/MethodPyKeras.h | 4 +- tmva/pymva/inc/TMVA/RModelParser_Keras.h | 16 +- tmva/pymva/src/RModelParser_Keras.cxx | 1042 ----------------- .../inc/TMVA/ROperator_LayerNormalization.hxx | 8 +- tmva/sofie/inc/TMVA/ROperator_Reshape.hxx | 1 + 24 files changed, 605 insertions(+), 1439 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%) delete mode 100644 tmva/pymva/src/RModelParser_Keras.cxx diff --git a/bindings/pyroot/pythonizations/CMakeLists.txt b/bindings/pyroot/pythonizations/CMakeLists.txt index 77f4ed9ef7f06..3a28e14a3531f 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/CMakeLists.txt b/tmva/pymva/CMakeLists.txt index e443405be7440..ad17b6f0ed03c 100644 --- a/tmva/pymva/CMakeLists.txt +++ b/tmva/pymva/CMakeLists.txt @@ -26,7 +26,6 @@ ROOT_STANDARD_LIBRARY_PACKAGE(PyMVA src/MethodPyKeras.cxx src/MethodPyRandomForest.cxx src/MethodPyTorch.cxx - src/RModelParser_Keras.cxx src/RModelParser_PyTorch.cxx src/PyMethodBase.cxx LIBRARIES diff --git a/tmva/pymva/inc/TMVA/MethodPyKeras.h b/tmva/pymva/inc/TMVA/MethodPyKeras.h index 2e05089431b02..f41452bf2fff3 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/pymva/inc/TMVA/RModelParser_Keras.h b/tmva/pymva/inc/TMVA/RModelParser_Keras.h index 7551b02ed5249..61b01f22321d5 100644 --- a/tmva/pymva/inc/TMVA/RModelParser_Keras.h +++ b/tmva/pymva/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) * **********************************************************************************/ @@ -40,18 +40,8 @@ namespace TMVA{ namespace Experimental{ namespace SOFIE{ -class RModelParser_Keras{}; -namespace PyKeras{ - - -/// Parser function for translatng 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 -/// has not a defined input batch size : e.g. for input = (input_dim,) -RModel Parse(std::string filename, int batch_size = -1); + class RModelParser_Keras{}; -}//PyKeras }//SOFIE }//Experimental }//TMVA diff --git a/tmva/pymva/src/RModelParser_Keras.cxx b/tmva/pymva/src/RModelParser_Keras.cxx deleted file mode 100644 index 9bf54dfd8372e..0000000000000 --- a/tmva/pymva/src/RModelParser_Keras.cxx +++ /dev/null @@ -1,1042 +0,0 @@ -// @(#)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{ -namespace Experimental{ -namespace SOFIE{ -namespace PyKeras{ - -// Referencing Python utility functions present in PyMethodBase -static void(& PyRunString)(TString, PyObject*, PyObject*) = PyMethodBase::PyRunString; -static const char*(& PyStringAsString)(PyObject*) = PyMethodBase::PyStringAsString; -static std::vector(& GetDataFromTuple)(PyObject*) = PyMethodBase::GetDataFromTuple; -static PyObject*(& GetValueFromDict)(PyObject*, const char*) = PyMethodBase::GetValueFromDict; - -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"); - - 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_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; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \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("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; -} -}//PyKeras -}//SOFIE -}//Experimental -}//TMVA 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 From c7ead3c4c6441a9eec31f540ddc2d6cd17586d36 Mon Sep 17 00:00:00 2001 From: PrasannaKasar Date: Fri, 12 Sep 2025 13:18:30 +0530 Subject: [PATCH 3/6] 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 eabdac08b97fcc751bd92ac7ee0a0fd078d82027 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sat, 8 Nov 2025 13:13:27 +0100 Subject: [PATCH 4/6] Avoid gloablly importing numpy --- .../_pythonization/_tmva/_sofie/_parser/_keras/parser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 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 ee3229cc9d662..8ee2f19cb3b51 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 @@ -92,6 +91,7 @@ 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() @@ -287,6 +287,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() @@ -541,4 +542,4 @@ def Parse(filename, batch_size=1): # If a model does not have a defined batch s def pythonize_rmodelparser_keras(klass): # Parameters: # klass: class to be pythonized - setattr(klass, "Parse", RModelParser_Keras.Parse) \ No newline at end of file + setattr(klass, "Parse", RModelParser_Keras.Parse) From 2dd81832ea5fa4d6df82c5efae71cb0c6b5c6760 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sat, 8 Nov 2025 13:15:30 +0100 Subject: [PATCH 5/6] Remove traces of RModelParser_Keras --- tmva/pymva/CMakeLists.txt | 1 - tmva/pymva/inc/TMVA/RModelParser_Keras.h | 48 -- tmva/pymva/test/CMakeLists.txt | 14 - tmva/pymva/test/TestRModelParserKeras.C | 553 ----------------------- 4 files changed, 616 deletions(-) delete mode 100644 tmva/pymva/inc/TMVA/RModelParser_Keras.h delete mode 100644 tmva/pymva/test/TestRModelParserKeras.C diff --git a/tmva/pymva/CMakeLists.txt b/tmva/pymva/CMakeLists.txt index ad17b6f0ed03c..d107a325e1d00 100644 --- a/tmva/pymva/CMakeLists.txt +++ b/tmva/pymva/CMakeLists.txt @@ -17,7 +17,6 @@ ROOT_STANDARD_LIBRARY_PACKAGE(PyMVA TMVA/MethodPyKeras.h TMVA/MethodPyRandomForest.h TMVA/MethodPyTorch.h - TMVA/RModelParser_Keras.h TMVA/RModelParser_PyTorch.h TMVA/PyMethodBase.h SOURCES diff --git a/tmva/pymva/inc/TMVA/RModelParser_Keras.h b/tmva/pymva/inc/TMVA/RModelParser_Keras.h deleted file mode 100644 index 61b01f22321d5..0000000000000 --- a/tmva/pymva/inc/TMVA/RModelParser_Keras.h +++ /dev/null @@ -1,48 +0,0 @@ -// @(#)root/tmva/pymva $Id$ -// Author: Sanjiban Sengupta, 2021 - -/********************************************************************************** - * Project: TMVA - a Root-integrated toolkit for multivariate data analysis * - * Package: TMVA * - * * - * * - * Description: * - * Functionality for parsing a saved Keras .H5 model into RModel object * - * * - * Authors (alphabetical): * - * Sanjiban Sengupta * - * * - * Copyright (c) 2021: * - * CERN, Switzerland * - * * - * * - * 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) * - **********************************************************************************/ - - -#ifndef TMVA_SOFIE_RMODELPARSER_KERAS -#define TMVA_SOFIE_RMODELPARSER_KERAS - -#include "TMVA/RModel.hxx" -#include "TMVA/SOFIE_common.hxx" -#include "TMVA/Types.h" -#include "TMVA/OperatorList.hxx" - -#include "TMVA/PyMethodBase.h" - -#include "Rtypes.h" -#include "TString.h" - - -namespace TMVA{ -namespace Experimental{ -namespace SOFIE{ - - class RModelParser_Keras{}; - -}//SOFIE -}//Experimental -}//TMVA -#endif //TMVA_PYMVA_RMODELPARSER_KERAS diff --git a/tmva/pymva/test/CMakeLists.txt b/tmva/pymva/test/CMakeLists.txt index 9605c0bb5e7fa..a789a44076880 100644 --- a/tmva/pymva/test/CMakeLists.txt +++ b/tmva/pymva/test/CMakeLists.txt @@ -128,18 +128,4 @@ if((ROOT_KERAS_FOUND AND ROOT_THEANO_FOUND) OR (ROOT_KERAS_FOUND AND ROOT_TENSOR LIBRARIES ${Libraries}) ROOT_ADD_TEST(PyMVA-Keras-Multiclass COMMAND testPyKerasMulticlass DEPENDS ${PyMVA-Keras-Multiclass-depends}) - if(BLAS_FOUND) - ROOT_ADD_GTEST(TestRModelParserKeras TestRModelParserKeras.C - LIBRARIES - ROOTTMVASofie - PyMVA - Python3::NumPy - Python3::Python - BLAS::BLAS - INCLUDE_DIRS - SYSTEM - ${CMAKE_CURRENT_BINARY_DIR} - ) - endif() - endif() diff --git a/tmva/pymva/test/TestRModelParserKeras.C b/tmva/pymva/test/TestRModelParserKeras.C deleted file mode 100644 index 69365fd00fe52..0000000000000 --- a/tmva/pymva/test/TestRModelParserKeras.C +++ /dev/null @@ -1,553 +0,0 @@ -#include -#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION -#include - -#include "gtest/gtest.h" -#include - -#include "TSystem.h" -#include "TMVA/RSofieReader.hxx" -#include "TMVA/PyMethodBase.h" - -constexpr float DEFAULT_TOLERANCE = 1e-6f; - -void GenerateModels() { - - FILE* fKerasModels = fopen("generateKerasModels.py", "r"); - if (!fKerasModels) { - std::string msg = "Can't find Python script to generate models in " + std::string(gSystem->pwd()); - throw std::runtime_error(msg); - } - PyRun_SimpleFile(fKerasModels, "generateKerasModels.py"); -} - -TEST(RModelParser_Keras, SEQUENTIAL) -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - // input is 8 x batch size that is fixed to be 4 - std::vector inputSequential = { 0.12107884, 0.89718615, 0.89123899, 0.32197549, - 0.17891638, 0.83555135, 0.98680066, 0.14496809, - 0.07255503, 0.55386989, 0.6628149 , 0.29843291, - 0.71059786, 0.44043452, 0.13792047, 0.93007397, - 0.16799397, 0.75473803, 0.43203355, 0.68360968, - 0.83879351, 0.0558927 , 0.57500447, 0.49063431, - 0.63637339, 0.94483464, 0.11032887, 0.22424818, - 0.50972592, 0.04671024, 0.39230661, 0.80500943}; - - Py_Initialize(); - - if (gSystem->AccessPathName("KerasModelSequential.h5",kFileExists)) - GenerateModels(); - - TMVA::Experimental:: RSofieReader r("KerasModelSequential.h5",{{4,8}}); - std::vector outputSequential = r.Compute(inputSequential); - - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelSequential.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input=numpy.array([0.12107884, 0.89718615, 0.89123899, 0.32197549," - "0.17891638, 0.83555135, 0.98680066, 0.14496809," - "0.07255503, 0.55386989, 0.6628149 , 0.29843291," - "0.71059786, 0.44043452, 0.13792047, 0.93007397," - "0.16799397, 0.75473803, 0.43203355, 0.68360968," - "0.83879351, 0.0558927 , 0.57500447, 0.49063431," - "0.63637339, 0.94483464, 0.11032887, 0.22424818," - "0.50972592, 0.04671024, 0.39230661, 0.80500943]).reshape(4,8)",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputSequentialSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputSequential.size(), pOutputSequentialSize); - - PyArrayObject* pSequentialValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutputSequential=(float*)PyArray_DATA(pSequentialValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < outputSequential.size(); ++i) { - EXPECT_LE(std::abs(outputSequential[i] - pOutputSequential[i]), TOLERANCE); - } -} - -TEST(RModelParser_Keras, FUNCTIONAL) -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vector inputFunctional ={0.60828574, 0.50069386, 0.75186709, 0.14968806, 0.7692464 ,0.77027585, 0.75095316, 0.96651197, - 0.38536308, 0.95565917, 0.62796356, 0.13818375, 0.65484891,0.89220363, 0.23879365, 0.00635323}; - - - Py_Initialize(); - if (gSystem->AccessPathName("KerasModelFunctional.h5",kFileExists)) - GenerateModels(); - - TMVA::Experimental:: RSofieReader r("KerasModelFunctional.h5"); - std::vector outputFunctional = r.Compute(inputFunctional); - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelFunctional.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input=numpy.array([0.60828574, 0.50069386, 0.75186709, 0.14968806, 0.7692464 ,0.77027585, 0.75095316, 0.96651197," - "0.38536308, 0.95565917, 0.62796356, 0.13818375, 0.65484891,0.89220363, 0.23879365, 0.00635323]).reshape(2,8)",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputFunctionalSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputFunctional.size(), pOutputFunctionalSize); - - PyArrayObject* pFunctionalValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutputFunctional=(float*)PyArray_DATA(pFunctionalValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < outputFunctional.size(); ++i) { - EXPECT_LE(std::abs(outputFunctional[i] - pOutputFunctional[i]), TOLERANCE); - } -} - -TEST(RModelParser_Keras, BATCH_NORM) -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinputBatchNorm = {0.22308163, 0.95274901, 0.44712538, 0.84640867, - 0.69947928, 0.29743695, 0.81379782, 0.39650574}; - - Py_Initialize(); - if (gSystem->AccessPathName("KerasModelBatchNorm.h5",kFileExists)) - GenerateModels(); - - TMVA::Experimental:: RSofieReader r("KerasModelBatchNorm.h5"); - std::vector outputBatchNorm = r.Compute(inputBatchNorm); - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelBatchNorm.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input=numpy.array([0.22308163, 0.95274901, 0.44712538, 0.84640867," - "0.69947928, 0.29743695, 0.81379782, 0.39650574]).reshape(2,4)",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputBatchNormSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputBatchNorm.size(), pOutputBatchNormSize); - - PyArrayObject* pBatchNormValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutputBatchNorm=(float*)PyArray_DATA(pBatchNormValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < outputBatchNorm.size(); ++i) { - EXPECT_LE(std::abs(outputBatchNorm[i] - pOutputBatchNorm[i]), TOLERANCE); - } -} - -#if PY_MAJOR_VERSION >= 3 -TEST(RModelParser_Keras, CONV_VALID) -#else -TEST(DISABLED_RModelParser_Keras, CONV_VALID) -#endif -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinputConv2D_Valid = {1,1,1,1, - 1,1,1,1, - 1,1,1,1, - 1,1,1,1}; - Py_Initialize(); - if (gSystem->AccessPathName("KerasModelConv2D_Valid.h5",kFileExists)) - GenerateModels(); - - TMVA::Experimental:: RSofieReader r("KerasModelConv2D_Valid.h5"); - std::vector outputConv2D_Valid = r.Compute(inputConv2D_Valid); - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelConv2D_Valid.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input=numpy.ones((1,4,4,1))",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputConv2DValidSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputConv2D_Valid.size(), pOutputConv2DValidSize); - - PyArrayObject* pConv2DValidValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutputConv2DValid=(float*)PyArray_DATA(pConv2DValidValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < outputConv2D_Valid.size(); ++i) { - EXPECT_LE(std::abs(outputConv2D_Valid[i] - pOutputConv2DValid[i]), TOLERANCE); - } -} - -#if PY_MAJOR_VERSION >= 3 -TEST(RModelParser_Keras, CONV_SAME) -#else -TEST(DISABLED_RModelParser_Keras, CONV_SAME) -#endif -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinputConv2D_Same = {1,1,1,1, - 1,1,1,1, - 1,1,1,1, - 1,1,1,1}; - - Py_Initialize(); - if (gSystem->AccessPathName("KerasModelConv2D_Same.h5",kFileExists)) - GenerateModels(); - - TMVA::Experimental:: RSofieReader r("KerasModelConv2D_Same.h5"); - std::vector outputConv2D_Same = r.Compute(inputConv2D_Same); - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelConv2D_Same.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input=numpy.ones((1,4,4,1))",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputConv2DSameSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputConv2D_Same.size(), pOutputConv2DSameSize); - - PyArrayObject* pConv2DSameValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutputConv2DSame=(float*)PyArray_DATA(pConv2DSameValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < outputConv2D_Same.size(); ++i) { - EXPECT_LE(std::abs(outputConv2D_Same[i] - pOutputConv2DSame[i]), TOLERANCE); - } -} - -TEST(RModelParser_Keras, RESHAPE) -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinputReshape = {1,1,1,1, - 1,1,1,1, - 1,1,1,1, - 1,1,1,1}; - - Py_Initialize(); - if (gSystem->AccessPathName("KerasModelReshape.h5",kFileExists)) - GenerateModels(); - - TMVA::Experimental:: RSofieReader r("KerasModelReshape.h5"); - std::vector outputReshape = r.Compute(inputReshape); - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelReshape.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input=numpy.ones((1,4,4,1))",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputReshapeSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputReshape.size(), pOutputReshapeSize); - - PyArrayObject* pReshapeValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutputReshape=(float*)PyArray_DATA(pReshapeValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < outputReshape.size(); ++i) { - EXPECT_LE(std::abs(outputReshape[i] - pOutputReshape[i]), TOLERANCE); - } -} - -TEST(RModelParser_Keras, CONCATENATE) -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinputConcatenate_1 = {1,1}; - std::vectorinputConcatenate_2 = {1,1}; - - Py_Initialize(); - if (gSystem->AccessPathName("KerasModelConcatenate.h5",kFileExists)) - GenerateModels(); - - TMVA::Experimental:: RSofieReader r("KerasModelConcatenate.h5"); - std::vector outputConcatenate = r.Compute(inputConcatenate_1, inputConcatenate_2); - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelConcatenate.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input_1=numpy.ones((1,2))",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input_2=numpy.ones((1,2))",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model([input_1,input_2]).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputConcatenateSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputConcatenate.size(), pOutputConcatenateSize); - - PyArrayObject* pConcatenateValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutputConcatenate=(float*)PyArray_DATA(pConcatenateValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < outputConcatenate.size(); ++i) { - EXPECT_LE(std::abs(outputConcatenate[i] - pOutputConcatenate[i]), TOLERANCE); - } -} - -TEST(RModelParser_Keras, BINARY_OP) -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinput_BinaryOp_1 = {1,1}; - std::vectorinput_BinaryOp_2 = {1,1}; - - Py_Initialize(); - if (gSystem->AccessPathName("KerasModelBinaryOp.h5",kFileExists)) - GenerateModels(); - - TMVA::Experimental:: RSofieReader r("KerasModelBinaryOp.h5"); - std::vector outputBinaryOp = r.Compute(input_BinaryOp_1,input_BinaryOp_2); - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelBinaryOp.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input1=numpy.array([1,1])",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input2=numpy.array([1,1])",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model([input1,input2]).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputBinaryOpSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputBinaryOp.size(), pOutputBinaryOpSize); - - PyArrayObject* pBinaryOpValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutputBinaryOp=(float*)PyArray_DATA(pBinaryOpValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < outputBinaryOp.size(); ++i) { - EXPECT_LE(std::abs(outputBinaryOp[i] - pOutputBinaryOp[i]), TOLERANCE); - } -} - -TEST(RModelParser_Keras, ACTIVATIONS) -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinputActivations = {1,1,1,1,1,1,1,1}; - - Py_Initialize(); - if (gSystem->AccessPathName("KerasModelActivations.h5",kFileExists)) - GenerateModels(); - - TMVA::Experimental:: RSofieReader r("KerasModelActivations.h5"); - std::vector outputActivations = r.Compute(inputActivations); - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelActivations.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input=numpy.ones((1,8))",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputActivationsSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputActivations.size(), pOutputActivationsSize); - - PyArrayObject* pActivationsValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutputActivations=(float*)PyArray_DATA(pActivationsValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < outputActivations.size(); ++i) { - EXPECT_LE(std::abs(outputActivations[i] - pOutputActivations[i]), TOLERANCE); - } -} - -TEST(RModelParser_Keras, SWISH) -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinput = {1,1,1,1,1,1,1,1}; - - Py_Initialize(); - if (gSystem->AccessPathName("KerasModelSwish.h5",kFileExists)) - GenerateModels(); - - TMVA::Experimental:: RSofieReader r("KerasModelSwish.h5"); - std::vector output = r.Compute(input); - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelSwish.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input=numpy.ones((1,8))",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(output.size(), pOutputSize); - - PyArrayObject* pValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutput=(float*)PyArray_DATA(pValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - pOutput[i]), TOLERANCE); - } -} - -TEST(RModel, CUSTOM_OP) -{ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinput_custom ={1,1,1,1,1,1,1,1}; - - Py_Initialize(); - if (gSystem->AccessPathName("KerasModelCustomOp.h5",kFileExists)) - GenerateModels(); - - - - 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"); - } - PyRun_String("import os",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); - - PyRun_String("from tensorflow.keras.layers import Lambda",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelCustomOp.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model.add(Lambda(lambda x: x * 2))",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input=numpy.array([1,1,1,1,1,1,1,1]).reshape(1,8)",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputCustomOpSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - - // get input name for custom (it is output of one before last) - PyRun_String("outputName = model.get_layer(index=len(model.layers)-2).output.name",Py_single_input,fGlobalNS,fLocalNS); - PyObject *pOutputName = PyDict_GetItemString(fLocalNS, "outputName"); - std::string outputName = TMVA::PyMethodBase::PyStringAsString(pOutputName); - TMVA::Experimental:: RSofieReader r; - r.AddCustomOperator(/*OpName*/ "Scale_by_2", - /*input tensor names where to insert custom op */std::string("{\"" + outputName + "\"}"), - /*output tensor names*/"{\"Scale2Output\"}", - /*output shapes*/"{{1,4}}", - /*header file name with the compute function*/ "scale_by_2_op.hxx"); - // need to load model afterwards - r.Load("KerasModelCustomOp.h5",{}, false); - std::vector outputCustomOp = r.Compute(input_custom); - - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputCustomOp.size(), pOutputCustomOpSize); - - PyArrayObject* pCustomOpValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); - float* pOutputCustomOp=(float*)PyArray_DATA(pCustomOpValues); - - //Testing the actual and expected output tensor values - for (size_t i = 0; i < outputCustomOp.size(); ++i) { - EXPECT_LE(std::abs(outputCustomOp[i] - pOutputCustomOp[i]), TOLERANCE); - } -} \ No newline at end of file From 99a8742c1c8086c7e068cdd206d3cce214506a4c Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sat, 8 Nov 2025 13:23:37 +0100 Subject: [PATCH 6/6] Correctly inject RModelParser_Keras class into Python interfaces This is not a pythonization of a C++ class, but a dedicated Python class that should be injected in the namespace where it should be. --- bindings/pyroot/pythonizations/python/ROOT/_facade.py | 1 + .../_pythonization/_tmva/_sofie/_parser/_keras/parser.py | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index 4c005693be905..31e1f236eba38 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 8ee2f19cb3b51..badc38f122b78 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 @@ -537,9 +537,3 @@ 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)