diff --git a/bindings/pyroot/pythonizations/CMakeLists.txt b/bindings/pyroot/pythonizations/CMakeLists.txt index 57cb4fa8044b7..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 @@ -65,6 +67,8 @@ if(tmva) 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/_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/__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/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 new file mode 100644 index 0000000000000..5f48c83e89aa1 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py @@ -0,0 +1,5 @@ +def get_keras_version() -> str: + + import keras + + return keras.__version__ \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py new file mode 100644 index 0000000000000..d129d1c42ab65 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_functional.py @@ -0,0 +1,209 @@ +import numpy as np + +def generate_keras_functional(dst_dir): + + from keras import models, layers + + # Helper training function + def train_and_save(model, name): + # Handle multiple inputs dynamically + if isinstance(model.input_shape, list): + x_train = [np.random.rand(32, *shape[1:]) for shape in model.input_shape] + else: + x_train = np.random.rand(32, *model.input_shape[1:]) + y_train = np.random.rand(32, *model.output_shape[1:]) + + model.compile(optimizer='adam', loss='mean_squared_error', metrics=['mae']) + model.fit(x_train, y_train, epochs=1, verbose=0) + model.save(f"{dst_dir}/Functional_{name}_test.h5") + + # Activation Functions + for act in ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']: + inp = layers.Input(shape=(10,)) + out = layers.Activation(act)(inp) + model = models.Model(inp, out) + train_and_save(model, f"Activation_layer_{act.capitalize()}") + # Along with these, Keras allows explicit delcaration of activation layers such as: + # [ELU, ReLU, LeakyReLU, Softmax] + + # Add + in1 = layers.Input(shape=(8,)) + in2 = layers.Input(shape=(8,)) + out = layers.Add()([in1, in2]) + model = models.Model([in1, in2], out) + train_and_save(model, "Add") + + # AveragePooling2D channels_first + inp = layers.Input(shape=(3, 8, 8)) + out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "AveragePooling2D_channels_first") + + # AveragePooling2D channels_last + inp = layers.Input(shape=(8, 8, 3)) + out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "AveragePooling2D_channels_last") + + # BatchNorm + inp = layers.Input(shape=(10, 3, 5)) + out = layers.BatchNormalization(axis=2)(inp) + model = models.Model(inp, out) + train_and_save(model, "BatchNorm") + + # Concat + in1 = layers.Input(shape=(8,)) + in2 = layers.Input(shape=(8,)) + out = layers.Concatenate()([in1, in2]) + model = models.Model([in1, in2], out) + train_and_save(model, "Concat") + + # Conv2D channels_first + inp = layers.Input(shape=(3, 8, 8)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_first', activation='relu')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_channels_first") + + # Conv2D channels_last + inp = layers.Input(shape=(8, 8, 3)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last', activation='leaky_relu')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_channels_last") + + # Conv2D padding_same + inp = layers.Input(shape=(8, 8, 3)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_padding_same") + + # Conv2D padding_valid + inp = layers.Input(shape=(8, 8, 3)) + out = layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last', activation='elu')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_padding_valid") + + # Dense + inp = layers.Input(shape=(10,)) + out = layers.Dense(5, activation='tanh')(inp) + model = models.Model(inp, out) + train_and_save(model, "Dense") + + # ELU + inp = layers.Input(shape=(10,)) + out = layers.ELU(alpha=0.5)(inp) + model = models.Model(inp, out) + train_and_save(model, "ELU") + + # Flatten + inp = layers.Input(shape=(4, 5)) + out = layers.Flatten()(inp) + model = models.Model(inp, out) + train_and_save(model, "Flatten") + + # GlobalAveragePooling2D channels first + inp = layers.Input(shape=(3, 4, 6)) + out = layers.GlobalAveragePooling2D(data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "GlobalAveragePooling2D_channels_first") + + # GlobalAveragePooling2D channels last + inp = layers.Input(shape=(4, 6, 3)) + out = layers.GlobalAveragePooling2D(data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "GlobalAveragePooling2D_channels_last") + + # LayerNorm + inp = layers.Input(shape=(10, 3, 5)) + out = layers.LayerNormalization(axis=-1)(inp) + model = models.Model(inp, out) + train_and_save(model, "LayerNorm") + + # LeakyReLU + inp = layers.Input(shape=(10,)) + out = layers.LeakyReLU()(inp) + model = models.Model(inp, out) + train_and_save(model, "LeakyReLU") + + # MaxPooling2D channels_first + inp = layers.Input(shape=(3, 8, 8)) + out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "MaxPool2D_channels_first") + + # MaxPooling2D channels_last + inp = layers.Input(shape=(8, 8, 3)) + out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "MaxPool2D_channels_last") + + # Multiply + in1 = layers.Input(shape=(8,)) + in2 = layers.Input(shape=(8,)) + out = layers.Multiply()([in1, in2]) + model = models.Model([in1, in2], out) + train_and_save(model, "Multiply") + + # Permute + inp = layers.Input(shape=(3, 4, 5)) + out = layers.Permute((2, 1, 3))(inp) + model = models.Model(inp, out) + train_and_save(model, "Permute") + + # ReLU + inp = layers.Input(shape=(10,)) + out = layers.ReLU()(inp) + model = models.Model(inp, out) + train_and_save(model, "ReLU") + + # Reshape + inp = layers.Input(shape=(4, 5)) + out = layers.Reshape((2, 10))(inp) + model = models.Model(inp, out) + train_and_save(model, "Reshape") + + # Softmax + inp = layers.Input(shape=(10,)) + out = layers.Softmax()(inp) + model = models.Model(inp, out) + train_and_save(model, "Softmax") + + # Subtract + in1 = layers.Input(shape=(8,)) + in2 = layers.Input(shape=(8,)) + out = layers.Subtract()([in1, in2]) + model = models.Model([in1, in2], out) + train_and_save(model, "Subtract") + + # Layer Combination + + inp = layers.Input(shape=(32, 32, 3)) + x = layers.Conv2D(8, (3,3), padding="same", activation="relu")(inp) + x = layers.MaxPooling2D((2,2))(x) + x = layers.Reshape((16, 16, 8))(x) + x = layers.Permute((3, 1, 2))(x) + x = layers.Flatten()(x) + out = layers.Dense(10, activation="softmax")(x) + model = models.Model(inp, out) + train_and_save(model, "Layer_Combination_1") + + inp = layers.Input(shape=(20,)) + x = layers.Dense(32, activation="tanh")(inp) + x = layers.Dense(16)(x) + x = layers.ELU()(x) + x = layers.LayerNormalization()(x) + out = layers.Dense(5, activation="sigmoid")(x) + model = models.Model(inp, out) + train_and_save(model, "Layer_Combination_2") + + inp1 = layers.Input(shape=(16,)) + inp2 = layers.Input(shape=(16,)) + d1 = layers.Dense(16, activation="relu")(inp1) + d2 = layers.Dense(16, activation="selu")(inp2) + add = layers.Add()([d1, d2]) + sub = layers.Subtract()([d1, d2]) + mul = layers.Multiply()([d1, d2]) + merged = layers.Concatenate()([add, sub, mul]) + merged = layers.LeakyReLU(alpha=0.1)(merged) + out = layers.Dense(4, activation="softmax")(merged) + model = models.Model([inp1, inp2], out) + train_and_save(model, "Layer_Combination_3") diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py new file mode 100644 index 0000000000000..2d7028f919749 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/generate_keras_sequential.py @@ -0,0 +1,206 @@ +import numpy as np + +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}/Sequential_{name}_test.h5") + + # Binary Ops: Add, Subtract, Multiply are not typical in Sequential - skipping those + # Concat (not applicable in Sequential without multi-input) + + # Activation Functions + for act in ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']: + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.Activation(act) + ]) + train_and_save(model, f"Activation_layer_{act.capitalize()}") + # Along with this, Keras also allows explicit delcaration of activation layers such as: + # ELU, ReLU, LeakyReLU, Softmax + + # AveragePooling2D channels_first + model = models.Sequential([ + layers.Input(shape=(3, 8, 8)), + layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first') + ]) + train_and_save(model, "AveragePooling2D_channels_first") + + # AveragePooling2D channels_last + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_last') + ]) + train_and_save(model, "AveragePooling2D_channels_last") + + # BatchNorm + model = models.Sequential([ + layers.Input(shape=(10, 3, 5)), + layers.BatchNormalization(axis=2) + ]) + train_and_save(model, "BatchNorm") + + # Conv2D channels_first + model = models.Sequential([ + layers.Input(shape=(3, 8, 8)), + layers.Conv2D(4, (3, 3), data_format='channels_first') + ]) + train_and_save(model, "Conv2D_channels_first") + + # Conv2D channels_last + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.Conv2D(4, (3, 3), data_format='channels_last', activation='tanh') + ]) + train_and_save(model, "Conv2D_channels_last") + + # Conv2D padding_same + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last', activation='selu') + ]) + train_and_save(model, "Conv2D_padding_same") + + # Conv2D padding_valid + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last', activation='swish') + ]) + train_and_save(model, "Conv2D_padding_valid") + + # Dense + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.Dense(5, activation='sigmoid') + ]) + train_and_save(model, "Dense") + + # ELU + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.ELU(alpha=0.5) + ]) + train_and_save(model, "ELU") + + # Flatten + model = models.Sequential([ + layers.Input(shape=(4, 5)), + layers.Flatten() + ]) + train_and_save(model, "Flatten") + + # GlobalAveragePooling2D channels first + model = models.Sequential([ + layers.Input(shape=(3, 4, 6)), + layers.GlobalAveragePooling2D(data_format='channels_first') + ]) + train_and_save(model, "GlobalAveragePooling2D_channels_first") + + # GlobalAveragePooling2D channels last + model = models.Sequential([ + layers.Input(shape=(4, 6, 3)), + layers.GlobalAveragePooling2D(data_format='channels_last') + ]) + train_and_save(model, "GlobalAveragePooling2D_channels_last") + + # LayerNorm + model = models.Sequential([ + layers.Input(shape=(10, 3, 5)), + layers.LayerNormalization(axis=-1) + ]) + train_and_save(model, "LayerNorm") + + # LeakyReLU + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.LeakyReLU() + ]) + train_and_save(model, "LeakyReLU") + + # MaxPooling2D channels_first + model = models.Sequential([ + layers.Input(shape=(3, 8, 8)), + layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_first') + ]) + train_and_save(model, "MaxPool2D_channels_first") + + # MaxPooling2D channels_last + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last') + ]) + train_and_save(model, "MaxPool2D_channels_last") + + # Permute + model = models.Sequential([ + layers.Input(shape=(3, 4, 5)), + layers.Permute((2, 1, 3)) + ]) + train_and_save(model, "Permute") + + # Reshape + model = models.Sequential([ + layers.Input(shape=(4, 5)), + layers.Reshape((2, 10)) + ]) + train_and_save(model, "Reshape") + + # ReLU + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.ReLU() + ]) + train_and_save(model, "ReLU") + + # Softmax + model = models.Sequential([ + layers.Input(shape=(10,)), + layers.Softmax() + ]) + train_and_save(model, "Softmax") + + # Layer Combination + + modelA = models.Sequential([ + layers.Input(shape=(32, 32, 3)), + layers.Conv2D(16, (3,3), padding='same', activation='swish'), + layers.AveragePooling2D((2,2), data_format='channels_last'), + layers.GlobalAveragePooling2D(data_format='channels_last'), + layers.Dense(10, activation='softmax'), + ]) + train_and_save(modelA, "Layer_Combination_1") + + modelB = models.Sequential([ + layers.Input(shape=(3, 32, 32)), + layers.Conv2D(8, (3,3), padding='valid', data_format='channels_first', activation='relu'), + layers.MaxPooling2D((2,2), data_format='channels_first'), + layers.Flatten(), + layers.Dense(128, activation='relu'), + layers.Reshape((16, 8)), + layers.Permute((2, 1)), + layers.Flatten(), + layers.Dense(32), + layers.LeakyReLU(alpha=0.1), + layers.Dense(10, activation='softmax'), + ]) + train_and_save(modelB, "Layer_Combination_2") + + modelC = models.Sequential([ + layers.Input(shape=(4, 8, 2)), + layers.Permute((2, 1, 3)), + layers.Reshape((8, 8, 1)), + layers.Conv2D(4, (3,3), padding='same', activation='relu'), + layers.AveragePooling2D((2,2)), + layers.BatchNormalization(), + layers.Flatten(), + layers.Dense(32, activation='elu'), + layers.Dense(8, activation='swish'), + layers.Dense(3, activation='softmax'), + ]) + train_and_save(modelC, "Layer_Combination_3") \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py new file mode 100644 index 0000000000000..834f9d0698163 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py @@ -0,0 +1,55 @@ +from cppyy import gbl as gbl_namespace +from .. import get_keras_version + +def MakeKerasBatchNorm(layer): + """ + Create a Keras-compatible batch normalization operation using SOFIE framework. + + This function takes a dictionary representing a batch normalization layer and its + attributes and constructs a Keras-compatible batch normalization operation using + the SOFIE framework. Batch normalization is used to normalize the activations of + a neural network, typically applied after the convolutional or dense layers. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + gamma, beta, moving mean, moving variance, epsilon, + momentum, data type (assumed to be float), and other relevant information. + + Returns: + ROperator_BatchNormalization: A SOFIE framework operator representing the batch normalization operation. + """ + + keras_version = get_keras_version() + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + gamma = attributes["gamma"] + beta = attributes["beta"] + moving_mean = attributes["moving_mean"] + moving_variance = attributes["moving_variance"] + fLayerDType = layer["layerDType"] + fNX = str(finput[0]) + fNY = str(foutput[0]) + + if keras_version < '2.16': + fNScale = gamma.name + fNB = beta.name + fNMean = moving_mean.name + fNVar = moving_variance.name + else: + fNScale = gamma.path + fNB = beta.path + fNMean = moving_mean.path + fNVar = moving_variance.path + + epsilon = attributes["epsilon"] + momentum = attributes["momentum"] + + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BatchNormalization('float')(epsilon, momentum, 0, fNX, fNScale, fNB, fNMean, fNVar, fNY) + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator BatchNormalization does not yet support input type " + fLayerDType + ) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py new file mode 100644 index 0000000000000..ff35fd2032653 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py @@ -0,0 +1,23 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasBinary(layer): + input = layer['layerInput'] + output = layer['layerOutput'] + fLayerType = layer['layerType'] + fLayerDType = layer['layerDType'] + fX1 = input[0] + fX2 = input[1] + fY = output[0] + op = None + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + if fLayerType == "Add": + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BasicBinary(float, gbl_namespace.TMVA.Experimental.SOFIE.EBasicBinaryOperator.Add)(fX1, fX2, fY) + elif fLayerType == "Subtract": + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BasicBinary(float, gbl_namespace.TMVA.Experimental.SOFIE.EBasicBinaryOperator.Sub)(fX1, fX2, fY) + else: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_BasicBinary(float, gbl_namespace.TMVA.Experimental.SOFIE.EBasicBinaryOperator.Mul)(fX1, fX2, fY) + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator BasicBinary does not yet support input type " + fLayerDType + ) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py new file mode 100644 index 0000000000000..340aa4e9cb452 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py @@ -0,0 +1,17 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasConcat(layer): + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer["layerDType"] + attributes = layer['layerAttributes'] + input = [str(i) for i in finput] + output = str(foutput[0]) + axis = int(attributes["axis"]) + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Concat(input, axis, 0, output) + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Concat does not yet support input type " + fLayerDType + ) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py new file mode 100644 index 0000000000000..98fe21b1cc887 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py @@ -0,0 +1,73 @@ +from cppyy import gbl as gbl_namespace +import math +from .. import get_keras_version + +def MakeKerasConv(layer): + """ + Create a Keras-compatible convolutional layer operation using SOFIE framework. + + This function takes a dictionary representing a convolutional layer and its attributes and + constructs a Keras-compatible convolutional layer operation using the SOFIE framework. + A convolutional layer applies a convolution operation between the input tensor and a set + of learnable filters (kernels). + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + data type (must be float), weight and bias name, kernel size, dilations, padding and strides. + When padding is same (keep in the same dimensions), the padding shape is calculated. + + Returns: + ROperator_Conv: A SOFIE framework operator representing the convolutional layer operation. + """ + + keras_version = get_keras_version() + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + attributes = layer['layerAttributes'] + fWeightNames = layer["layerWeight"] + fKernelName = fWeightNames[0] + fBiasName = fWeightNames[1] + fAttrDilations = attributes["dilation_rate"] + fAttrGroup = int(attributes["groups"]) + fAttrKernelShape = attributes["kernel_size"] + fKerasPadding = str(attributes["padding"]) + fAttrStrides = attributes["strides"] + fAttrPads = [] + + if fKerasPadding == 'valid': + fAttrAutopad = 'VALID' + elif fKerasPadding == 'same': + fAttrAutopad = 'NOTSET' + if keras_version < '2.16': + fInputShape = attributes['_build_input_shape'] + else: + fInputShape = attributes['_build_shapes_dict']['input_shape'] + inputHeight = fInputShape[1] + inputWidth = fInputShape[2] + outputHeight = math.ceil(float(inputHeight) / float(fAttrStrides[0])) + outputWidth = math.ceil(float(inputWidth) / float(fAttrStrides[1])) + padding_height = max((outputHeight - 1) * fAttrStrides[0] + fAttrKernelShape[0] - inputHeight, 0) + padding_width = max((outputWidth - 1) * fAttrStrides[1] + fAttrKernelShape[1] - inputWidth, 0) + padding_top = math.floor(padding_height / 2) + padding_bottom = padding_height - padding_top + padding_left = math.floor(padding_width / 2) + padding_right = padding_width - padding_left + fAttrPads = [padding_top, padding_bottom, padding_left, padding_right] + else: + raise RuntimeError( + "TMVA::SOFIE - RModel Keras Parser doesn't yet supports Convolution layer with padding " + fKerasPadding + ) + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Conv['float'](fAttrAutopad, fAttrDilations, fAttrGroup, + fAttrKernelShape, fAttrPads, fAttrStrides, + fLayerInputName, fKernelName, fBiasName, + fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Conv does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py new file mode 100644 index 0000000000000..7e6e787a97095 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py @@ -0,0 +1,37 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasDense(layer): + """ + Create a Keras-compatible dense (fully connected) layer operation using SOFIE framework. + + This function takes a dictionary representing a dense layer and its attributes and + constructs a Keras-compatible dense (fully connected) layer operation using the SOFIE framework. + A dense layer applies a matrix multiplication between the input tensor and weight matrix, + and adds a bias term. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + layer weight names, and data type - must be float. + + Returns: + ROperator_Gemm: A SOFIE framework operator representing the dense layer operation. + """ + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + fWeightNames = layer["layerWeight"] + fKernelName = fWeightNames[0] + fBiasName = fWeightNames[1] + attr_alpha = 1.0 + attr_beta = 1.0 + attr_transA = 0 + attr_transB = 0 + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Gemm['float'](attr_alpha, attr_beta, attr_transA, attr_transB, fLayerInputName, fKernelName, fBiasName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Gemm does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py new file mode 100644 index 0000000000000..7a291117e837e --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py @@ -0,0 +1,35 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasELU(layer): + """ + Create a Keras-compatible exponential linear Unit (ELU) activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible ELU activation operation using the SOFIE framework. + ELU is an activation function that modifies only the negative part of ReLU by + applying an exponential curve. It allows small negative values instead of zeros. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type, which must be float. + + Returns: + ROperator_Elu: A SOFIE framework operator representing the ELU activation operation. + """ + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + attributes = layer['layerAttributes'] + if 'alpha' in attributes.keys(): + fAlpha = attributes['alpha'] + else: + fAlpha = 1.0 + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Elu('float')(fAlpha, fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Relu does not yet support input type " + fLayerDType + ) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py new file mode 100644 index 0000000000000..8b28382ebc4a0 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py @@ -0,0 +1,36 @@ +from cppyy import gbl as gbl_namespace +from .. import get_keras_version + +def MakeKerasFlatten(layer): + """ + Create a Keras-compatible flattening operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible flattening operation using the SOFIE framework. + Flattening is the process of converting a multi-dimensional tensor into a + one-dimensional tensor. Assumes layerDtype is float. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + name, data type, and other relevant information. + + Returns: + ROperator_Reshape: A SOFIE framework operator representing the flattening operation. + """ + + keras_version = get_keras_version() + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + if keras_version < '2.16': + flayername = attributes['_name'] + else: + flayername = attributes['name'] + fOpMode = gbl_namespace.TMVA.Experimental.SOFIE.ReshapeOpMode.Flatten + fLayerDType = layer['layerDType'] + fNameData = finput[0] + fNameOutput = foutput[0] + fNameShape = flayername + "ReshapeAxes" + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape(fOpMode, 0, fNameData, fNameShape, fNameOutput) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py new file mode 100644 index 0000000000000..4921a268e6a5d --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py @@ -0,0 +1,15 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasIdentity(layer): + input = layer['layerInput'] + output = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = input[0] + fLayerOutputName = output[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Identity('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Identity does not yet support input type " + fLayerDType + ) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py new file mode 100644 index 0000000000000..b10ce58d239a9 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py @@ -0,0 +1,62 @@ +from cppyy import gbl as gbl_namespace +from .. import get_keras_version + +def MakeKerasLayerNorm(layer): + """ + Create a Keras-compatible layer normalization operation using SOFIE framework. + + This function takes a dictionary representing a layer normalization layer and its + attributes and constructs a Keras-compatible layer normalization operation using + the SOFIE framework. Unlike Batch normalization, Layer normalization used to normalize + the activations of a layer across the entire layer, independently for each sample in + the batch. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + gamma, beta, epsilon, data type (assumed to be float), and other + relevant information. + + Returns: + ROperator_BatchNormalization: A SOFIE framework operator representing the layer normalization operation. + """ + + keras_version = get_keras_version() + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + gamma = attributes["gamma"] + beta = attributes["beta"] + axes = attributes['axis'] + if '_build_input_shape' in attributes.keys(): + num_input_shapes = len(attributes['_build_input_shape']) + elif '_build_shapes_dict' in attributes.keys(): + num_input_shapes = len(list(attributes['_build_shapes_dict']['input_shape'])) + if len(axes) == 1: + axis = axes[0] + if axis < 0: + axis += num_input_shapes + else: + raise Exception("TMVA.SOFIE - LayerNormalization layer - parsing different axes at once is not supported") + fLayerDType = layer["layerDType"] + fNX = str(finput[0]) + fNY = str(foutput[0]) + + if keras_version < '2.16': + fNScale = gamma.name + fNB = beta.name + else: + fNScale = gamma.path + fNB = beta.path + + epsilon = attributes["epsilon"] + fNInvStdDev = [] + + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_LayerNormalization('float')(axis, epsilon, 1, fNX, fNScale, fNB, fNY, "", fNInvStdDev) + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator BatchNormalization does not yet support input type " + fLayerDType + ) + + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py new file mode 100644 index 0000000000000..c0b95b04b27eb --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py @@ -0,0 +1,44 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasLeakyRelu(layer): + """ + Create a Keras-compatible Leaky ReLU activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible Leaky ReLU activation operation using the SOFIE framework. + Leaky ReLU is a variation of the ReLU activation function that allows small negative + values to pass through, introducing non-linearity while preventing "dying" neurons. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + attributes, and data type - must be float. + + Returns: + ROperator_LeakyRelu: A SOFIE framework operator representing the Leaky ReLU activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + attributes = layer['layerAttributes'] + + if 'alpha' in attributes.keys(): + fAlpha = float(attributes["alpha"]) + elif 'negative_slope' in attributes.keys(): + fAlpha = float(attributes['negative_slope']) + elif 'activation' in attributes.keys(): + fAlpha = 0.2 + else: + raise RuntimeError ( + "Failed to extract alpha value from LeakyReLU" + ) + + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_LeakyRelu('float')(fAlpha, fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator LeakyRelu does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py new file mode 100644 index 0000000000000..f43fc09ee0afe --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py @@ -0,0 +1,36 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasPermute(layer): + """ + Create a Keras-compatible permutation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible permutation operation using the SOFIE framework. + Permutation is an operation that rearranges the dimensions of a tensor based on + specified dimensions. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + attributes, and data type - must be float. + + Returns: + ROperator_Transpose: A SOFIE framework operator representing the permutation operation. + """ + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + attributes = layer['layerAttributes'] + fAttributePermute = list(attributes["dims"]) + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + if len(fAttributePermute) > 0: + fAttributePermute = [0] + fAttributePermute # for the batch dimension from the input + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttributePermute, fLayerInputName, fLayerOutputName) #gbl_namespace.TMVA.Experimental.SOFIE.fPermuteDims + else: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Transpose does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py new file mode 100644 index 0000000000000..364d2be8da147 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py @@ -0,0 +1,78 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasPooling(layer): + """ + Create a Keras-compatible pooling layer operation using SOFIE framework. + + This function takes a dictionary representing a pooling layer and its attributes and + constructs a Keras-compatible pooling layer operation using the SOFIE framework. + Pooling layers downsample the input tensor by selecting a representative value from + a group of neighboring values, either by taking the maximum or the average. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + layer type (the selection rule), the pool size, padding, strides, and data type. + + Returns: + ROperator_Pool: A SOFIE framework operator representing the pooling layer operation. + """ + + # Extract attributes from layer data + fLayerDType = layer['layerDType'] + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerType = layer['layerType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + pool_atrr = gbl_namespace.TMVA.Experimental.SOFIE.RAttributes_Pool() + attributes = layer['layerAttributes'] + # Set default values for GlobalAveragePooling2D + fAttrKernelShape = [] + fKerasPadding = 'valid' + fAttrStrides = [] + if fLayerType != 'GlobalAveragePooling2D': + fAttrKernelShape = attributes["pool_size"] + fKerasPadding = str(attributes["padding"]) + fAttrStrides = attributes["strides"] + + # Set default values + fAttrDilations = (1,1) + fpads = [0,0,0,0,0,0] + pool_atrr.ceil_mode = 0 + pool_atrr.count_include_pad = 0 + pool_atrr.storage_order = 0 + + if fKerasPadding == 'valid': + fAttrAutopad = 'VALID' + elif fKerasPadding == 'same': + fAttrAutopad = 'NOTSET' + else: + raise RuntimeError( + "TMVA::SOFIE - RModel Keras Parser doesn't yet support Pooling layer with padding " + fKerasPadding + ) + pool_atrr.dilations = list(fAttrDilations) + pool_atrr.strides = list(fAttrStrides) + pool_atrr.pads = fpads + pool_atrr.kernel_shape = list(fAttrKernelShape) + pool_atrr.auto_pad = fAttrAutopad + + # Choose pooling type + if 'Max' in fLayerType: + PoolMode = gbl_namespace.TMVA.Experimental.SOFIE.PoolOpMode.MaxPool + elif 'AveragePool' in fLayerType: + PoolMode = gbl_namespace.TMVA.Experimental.SOFIE.PoolOpMode.AveragePool + elif 'GlobalAverage' in fLayerType: + PoolMode = gbl_namespace.TMVA.Experimental.SOFIE.PoolOpMode.GloabalAveragePool + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator poolong does not yet support pooling type " + fLayerType + ) + + # Create operator + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Pool['float'](PoolMode, pool_atrr, fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Pooling does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py new file mode 100644 index 0000000000000..9da1407a8911d --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py @@ -0,0 +1,30 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasReLU(layer): + """ + Create a Keras-compatible rectified linear unit (ReLU) activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible ReLU activation operation using the SOFIE framework. + ReLU is a popular activation function that replaces all negative values in a tensor + with zero, while leaving positive values unchanged. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type, which must be float. + + Returns: + ROperator_Relu: A SOFIE framework operator representing the ReLU activation operation. + """ + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Relu('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Relu does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py new file mode 100644 index 0000000000000..c83822f43e080 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py @@ -0,0 +1,34 @@ +from cppyy import gbl as gbl_namespace +from .. import get_keras_version + +def MakeKerasReshape(layer): + """ + Create a Keras-compatible reshaping operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible reshaping operation using the SOFIE framework. Assumes layerDtype is float. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + name, data type, and other relevant information. + + Returns: + ROperator_Reshape: A SOFIE framework operator representing the reshaping operation. + """ + + keras_version = get_keras_version() + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + if keras_version < '2.16': + flayername = attributes['_name'] + else: + flayername = attributes['name'] + fOpMode = gbl_namespace.TMVA.Experimental.SOFIE.ReshapeOpMode.Reshape + fLayerDType = layer['layerDType'] + fNameData = finput[0] + fNameOutput = foutput[0] + fNameShape = flayername + "ReshapeAxes" + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape(fOpMode, 0, fNameData, fNameShape, fNameOutput) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py new file mode 100644 index 0000000000000..f2f3d628e0aed --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py @@ -0,0 +1,92 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasRNN(layer): + """ + Create a Keras-compatible RNN (Recurrent Neural Network) layer operation using SOFIE framework. + + This function takes a dictionary representing an RNN layer and its attributes and + constructs a Keras-compatible RNN layer operation using the SOFIE framework. + RNN layers are used to model sequences, and they maintain internal states that are + updated through recurrent connections. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + layer type, attributes, weights, and data type - must be float. + + Returns: + ROperator_RNN: A SOFIE framework operator representing the RNN layer operation. + """ + + # Extract required information from the layer dictionary + fLayerDType = layer['layerDType'] + finput = layer['layerInput'] + foutput = layer['layerOutput'] + attributes = layer['layerAttributes'] + direction = attributes['direction'] + hidden_size = attributes["hidden_size"] + layout = int(attributes["layout"]) + nameX = finput[0] + nameY = foutput[0] + nameW = layer["layerWeight"][0] + nameR = layer["layerWeight"][1] + if len(layer["layerWeight"]) > 2: + nameB = layer["layerWeight"][2] + else: + nameB = "" + + # Check if the provided activation function is supported + fPActivation = attributes['activation'] + if not fPActivation.__name__ in ['relu', 'sigmoid', 'tanh', 'softsign', 'softplus']: #avoiding functions with parameters + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator RNN does not yet support activation function " + fPActivation.__name__ + ) + + activations = [fPActivation.__name__[0].upper()+fPActivation.__name__[1:]] + + #set default values + activation_alpha = [] + activation_beta = [] + clip = 0.0 + nameY_h = "" + nameInitial_h = "" + name_seq_len = "" + + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + if layer['layerType'] == "SimpleRNN": + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_RNN['float'](activation_alpha, activation_beta, activations, clip, direction, hidden_size, layout, nameX, nameW, nameR, nameB, name_seq_len, nameInitial_h, nameY, nameY_h) + + elif layer['layerType'] == "GRU": + #an additional activation function is required, given by the user + activations.insert(0, attributes['recurrent_activation'].__name__[0].upper() + attributes['recurrent_activation'].__name__[1:]) + + #new variable needed: + linear_before_reset = attributes['linear_before_reset'] + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_GRU['float'](activation_alpha, activation_beta, activations, clip, direction, hidden_size, layout, linear_before_reset, nameX, nameW, nameR, nameB, name_seq_len, nameInitial_h, nameY, nameY_h) + + elif layer['layerType'] == "LSTM": + #an additional activation function is required, the first given by the user, the second set to tanh as default + fPRecurrentActivation = attributes['recurrent_activation'] + if not fPActivation.__name__ in ['relu', 'sigmoid', 'tanh', 'softsign', 'softplus']: #avoiding functions with parameters + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator RNN does not yet support recurrent activation function " + fPActivation.__name__ + ) + fPRecurrentActivationName = fPRecurrentActivation.__name__[0].upper()+fPRecurrentActivation.__name__[1:] + activations.insert(0,fPRecurrentActivationName) + activations.insert(2,'Tanh') + + #new variables needed: + input_forget = 0 + nameInitial_c = "" + nameP = "" #No peephole connections in keras LSTM model + nameY_c = "" + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_LSTM['float'](activation_alpha, activation_beta, activations, clip, direction, hidden_size, input_forget, layout, nameX, nameW, nameR, nameB, name_seq_len, nameInitial_h, nameInitial_c, nameP, nameY, nameY_h, nameY_c) + + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator RNN does not yet support operator type " + layer['layerType'] + ) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator RNN does not yet support input type " + fLayerDType + ) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py new file mode 100644 index 0000000000000..53349086440ec --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py @@ -0,0 +1,31 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasSeLU(layer): + """ + Create a Keras-compatible scaled exponential linear unit (SeLU) activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible SeLU activation operation using the SOFIE framework. + SeLU is a type of activation function that introduces self-normalizing properties + to the neural network. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type - must be float32. + + Returns: + ROperator_Selu: A SOFIE framework operator representing the SeLU activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Selu('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Selu does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py new file mode 100644 index 0000000000000..8d50032c53fdb --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py @@ -0,0 +1,31 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasSigmoid(layer): + """ + Create a Keras-compatible sigmoid activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible sigmoid activation operation using the SOFIE framework. + Sigmoid is a commonly used activation function that maps input values to the range + between 0 and 1, providing a way to introduce non-linearity in neural networks. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type - must be float. + + Returns: + ROperator_Sigmoid: A SOFIE framework operator representing the sigmoid activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Sigmoid('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py new file mode 100644 index 0000000000000..f00efc136b486 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py @@ -0,0 +1,32 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasSoftmax(layer): + """ + Create a Keras-compatible softmax activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible softmax activation operation using the SOFIE framework. + Softmax is an activation function that converts input values into a probability + distribution, often used in the output layer of a neural network for multi-class + classification tasks. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type - must be float. + + Returns: + ROperator_Softmax: A SOFIE framework operator representing the softmax activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Softmax('float')(-1, fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Softmax does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py new file mode 100644 index 0000000000000..43ae130d91c0f --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py @@ -0,0 +1,31 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasSwish(layer): + """ + Create a Keras-compatible swish activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible swish activation operation using the SOFIE framework. + Swish is an activation function that aims to combine the benefits of ReLU and sigmoid, + allowing some non-linearity while still keeping positive values unbounded. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type. + + Returns: + ROperator_Swish: A SOFIE framework operator representing the swish activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Swish('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Swish does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py new file mode 100644 index 0000000000000..4d9e62cd5da1d --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py @@ -0,0 +1,31 @@ +from cppyy import gbl as gbl_namespace + +def MakeKerasTanh(layer): + """ + Create a Keras-compatible hyperbolic tangent (tanh) activation operation using SOFIE framework. + + This function takes a dictionary representing a layer and its attributes and + constructs a Keras-compatible tanh activation operation using the SOFIE framework. + Tanh is an activation function that squashes input values to the range between -1 and 1, + introducing non-linearity in neural networks. + + Parameters: + layer (dict): A dictionary containing layer information including input, output, + and data type - must be float. + + Returns: + ROperator_Tanh: A SOFIE framework operator representing the tanh activation operation. + """ + + finput = layer['layerInput'] + foutput = layer['layerOutput'] + fLayerDType = layer['layerDType'] + fLayerInputName = finput[0] + fLayerOutputName = foutput[0] + if gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fLayerDType) == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Tanh('float')(fLayerInputName, fLayerOutputName) + return op + else: + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator Tanh does not yet support input type " + fLayerDType + ) \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py new file mode 100644 index 0000000000000..badc38f122b78 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py @@ -0,0 +1,539 @@ +from ......_pythonization import pythonization +from cppyy import gbl as gbl_namespace +import os +import time + +from .layers.permute import MakeKerasPermute +from .layers.batchnorm import MakeKerasBatchNorm +from .layers.layernorm import MakeKerasLayerNorm +from .layers.reshape import MakeKerasReshape +from .layers.flatten import MakeKerasFlatten +from .layers.concat import MakeKerasConcat +from .layers.swish import MakeKerasSwish +from .layers.binary import MakeKerasBinary +from .layers.softmax import MakeKerasSoftmax +from .layers.tanh import MakeKerasTanh +from .layers.identity import MakeKerasIdentity +from .layers.relu import MakeKerasReLU +from .layers.elu import MakeKerasELU +from .layers.selu import MakeKerasSeLU +from .layers.sigmoid import MakeKerasSigmoid +from .layers.leaky_relu import MakeKerasLeakyRelu +from .layers.pooling import MakeKerasPooling +from .layers.rnn import MakeKerasRNN +from .layers.dense import MakeKerasDense +from .layers.conv import MakeKerasConv + +from . import get_keras_version + +def MakeKerasActivation(layer): + attributes = layer['layerAttributes'] + activation = attributes['activation'] + fLayerActivation = str(activation.__name__) + + if fLayerActivation in mapKerasLayer.keys(): + return mapKerasLayer[fLayerActivation](layer) + else: + raise Exception("TMVA.SOFIE - parsing keras activation layer " + fLayerActivation + " is not yet supported") + +# Set global dictionaries, mapping layers to corresponding functions that create their ROperator instances +mapKerasLayer = {"Activation": MakeKerasActivation, + "Permute": MakeKerasPermute, + "BatchNormalization": MakeKerasBatchNorm, + "LayerNormalization": MakeKerasLayerNorm, + "Reshape": MakeKerasReshape, + "Flatten": MakeKerasFlatten, + "Concatenate": MakeKerasConcat, + "swish": MakeKerasSwish, + "silu": MakeKerasSwish, + "Add": MakeKerasBinary, + "Subtract": MakeKerasBinary, + "Multiply": MakeKerasBinary, + "Softmax": MakeKerasSoftmax, + "tanh": MakeKerasTanh, + # "Identity": MakeKerasIdentity, + # "Dropout": MakeKerasIdentity, + "ReLU": MakeKerasReLU, + "relu": MakeKerasReLU, + "ELU": MakeKerasELU, + "elu": MakeKerasELU, + "selu": MakeKerasSeLU, + "sigmoid": MakeKerasSigmoid, + "LeakyReLU": MakeKerasLeakyRelu, + "leaky_relu": MakeKerasLeakyRelu, + "softmax": MakeKerasSoftmax, + "MaxPooling2D": MakeKerasPooling, + "AveragePooling2D": MakeKerasPooling, + "GlobalAveragePooling2D": MakeKerasPooling, + # "SimpleRNN": MakeKerasRNN, + # "GRU": MakeKerasRNN, + # "LSTM": MakeKerasRNN, + } + +mapKerasLayerWithActivation = {"Dense": MakeKerasDense,"Conv2D": MakeKerasConv} + +def add_layer_into_RModel(rmodel, layer_data): + """ + Add a Keras layer operation to an existing RModel using the SOFIE framework. + + This function takes an existing RModel and a dictionary representing a Keras layer + and its attributes, and adds the corresponding layer operation to the RModel using + the SOFIE framework. The function supports various types of Keras layers, including + those with or without activation functions. + + Parameters: + rmodel (RModel): An existing RModel to which the layer operation will be added. + layer_data (dict): A dictionary containing layer information including type, + attributes, input, output, and layer data type. + + Returns: + RModel: The updated RModel after adding the layer operation. + + Raises exception: If the provided layer type or activation function is not supported. + """ + import numpy as np + + keras_version = get_keras_version() + + fLayerType = layer_data['layerType'] + + # reshape and flatten layers don't have weights, but they are needed inside the list of initialized + # tensor list in the Rmodel + if fLayerType == "Reshape" or fLayerType == "Flatten": + Attributes = layer_data['layerAttributes'] + if keras_version < '2.16': + LayerName = Attributes['_name'] + else: + LayerName = Attributes['name'] + + if fLayerType == "Reshape": + TargetShape = np.asarray(Attributes['target_shape']).astype("int") + TargetShape = np.insert(TargetShape,0,0) + else: + if '_build_input_shape' in Attributes.keys(): + input_shape = Attributes['_build_input_shape'] + elif '_build_shapes_dict' in Attributes.keys(): + input_shape = list(Attributes['_build_shapes_dict']['input_shape']) + else: + raise RuntimeError ( + "Failed to extract build input shape from " + fLayerType + " layer" + ) + TargetShape = [ gbl_namespace.TMVA.Experimental.SOFIE.ConvertShapeToLength(input_shape[1:])] + TargetShape = np.asarray(TargetShape) + + # since the AddInitializedTensor method in RModel requires unique pointer, we call a helper function + # in c++ that does the conversion from a regular pointer to unique one in c++ + rmodel.AddInitializedTensor['long'](LayerName+"ReshapeAxes", [len(TargetShape)], TargetShape) + + # These layers only have one operator - excluding the recurrent layers, in which the activation function(s) + # are included in the recurrent operator + if fLayerType in mapKerasLayer.keys(): + Attributes = layer_data['layerAttributes'] + inputs = layer_data['layerInput'] + outputs = layer_data['layerOutput'] + if keras_version < '2.16': + LayerName = Attributes['_name'] + else: + LayerName = Attributes['name'] + + # Convoltion/Pooling layers in keras by default assume the channels dimension is the + # last one, while in onnx (and the SOFIE's RModel) it is the first one (other than batch + # size), so a transpose is needed before and after the pooling, if the data format is + # channels last (can be set to channels first by the user). In case of MaxPool2D and + # Conv2D (with linear activation) channels last, the transpose layers are added as: + + # input output + # transpose layer input_layer_name layer_name + PreTrans + # actual layer layer_name + PreTrans layer_name + PostTrans + # transpose layer layer_name + PostTrans output_layer_name + + fLayerOutput = outputs[0] + if fLayerType == 'GlobalAveragePooling2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0, 3, 1, 2], inputs[0], LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName+"PreTrans" + outputs[0] = LayerName+"Squeeze" + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape( + gbl_namespace.TMVA.Experimental.SOFIE.ReshapeOpMode.Squeeze, + [2, 3], + LayerName + "Squeeze", + fLayerOutput + ) + rmodel.AddOperatorReference(op) + + # Similar case is with Batchnorm, ONNX assumes that the 'axis' is always 1, but Keras + # gives the user the choice of specifying it. So, we have to transpose the input layer + # as 'axis' as the first dimension, apply the BatchNormalization operator and then + # again tranpose it to bring back the original dimensions + elif fLayerType == 'BatchNormalization': + if '_build_input_shape' in Attributes.keys(): + num_input_shapes = len(Attributes['_build_input_shape']) + elif '_build_shapes_dict' in Attributes.keys(): + num_input_shapes = len(list(Attributes['_build_shapes_dict']['input_shape'])) + + axis = Attributes['axis'] + if axis < 0: + axis += num_input_shapes + fAttrPerm = list(range(0, num_input_shapes)) + fAttrPerm[1] = axis + fAttrPerm[axis] = 1 + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttrPerm, inputs[0], + LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName + "PreTrans" + outputs[0] = LayerName + "PostTrans" + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttrPerm, LayerName+"PostTrans", + fLayerOutput) + rmodel.AddOperatorReference(op) + + elif fLayerType == 'MaxPooling2D' or fLayerType == 'AveragePooling2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,3,1,2], inputs[0], + LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName+"PreTrans" + outputs[0] = LayerName+"PostTrans" + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,2,3,1], + LayerName+"PostTrans", fLayerOutput) + rmodel.AddOperatorReference(op) + + else: + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + + return rmodel + + # These layers require two operators - dense/conv and their activation function + elif fLayerType in mapKerasLayerWithActivation.keys(): + Attributes = layer_data['layerAttributes'] + if keras_version < '2.16': + LayerName = Attributes['_name'] + else: + LayerName = Attributes['name'] + fPActivation = Attributes['activation'] + LayerActivation = fPActivation.__name__ + if LayerActivation in ['selu', 'sigmoid']: + rmodel.AddNeededStdLib("cmath") + + # if there is an activation function after the layer + if LayerActivation != 'linear': + if not LayerActivation in mapKerasLayer.keys(): + raise Exception("TMVA.SOFIE - parsing keras activation function " + LayerActivation + " is not yet supported") + outputs = layer_data['layerOutput'] + inputs = layer_data['layerInput'] + fActivationLayerOutput = outputs[0] + + # like pooling, convolutional layer from keras requires transpose before and after to match + # the onnx format + # if the data format is channels last (can be set to channels first by the user). + if fLayerType == 'Conv2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,3,1,2], inputs[0], LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName+"PreTrans" + layer_data["layerInput"] = inputs + outputs[0] = LayerName+fLayerType + layer_data['layerOutput'] = outputs + op = mapKerasLayerWithActivation[fLayerType](layer_data) + rmodel.AddOperatorReference(op) + Activation_layer_input = LayerName+fLayerType + if fLayerType == 'Conv2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,2,3,1], LayerName+fLayerType, LayerName+"PostTrans") + rmodel.AddOperatorReference(op) + Activation_layer_input = LayerName + "PostTrans" + + # Adding the activation function + inputs[0] = Activation_layer_input + outputs[0] = fActivationLayerOutput + layer_data['layerInput'] = inputs + layer_data['layerOutput'] = outputs + + rmodel.AddOperatorReference(mapKerasLayer[LayerActivation](layer_data)) + + else: # if layer is conv and the activation is linear, we need to add transpose before and after + if fLayerType == 'Conv2D': + inputs = layer_data['layerInput'] + outputs = layer_data['layerOutput'] + fLayerOutput = outputs[0] + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,3,1,2], inputs[0], LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName+"PreTrans" + layer_data['layerInput'] = inputs + outputs[0] = LayerName+"PostTrans" + rmodel.AddOperatorReference(mapKerasLayerWithActivation[fLayerType](layer_data)) + if fLayerType == 'Conv2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,2,3,1], LayerName+"PostTrans", fLayerOutput) + rmodel.AddOperatorReference(op) + return rmodel + else: + raise Exception("TMVA.SOFIE - parsing keras layer " + fLayerType + " is not yet supported") + +class RModelParser_Keras: + + def Parse(filename, batch_size=1): # If a model does not have a defined batch size, then assuming it is 1 + + # TensoFlow/Keras is too fragile to import unconditionally. As its presence might break several ROOT + # usecases and importing keras globally will slow down importing ROOT, which is not desired. For this, + # we import keras within the functions instead of importing it at the start of the file (i.e. globally). + # So, whenever the parser function is called, only then keras will be imported, and not everytime we + # import ROOT. Also, we can import keras in multiple functions as many times as we want since Python + # caches the imported packages. + + import keras + import numpy as np + + keras_version = get_keras_version() + + #Check if file exists + if not os.path.exists(filename): + raise RuntimeError("Model file {} not found!".format(filename)) + + # load model + keras_model = keras.models.load_model(filename) + keras_model.load_weights(filename) + + # create new RModel object + sep = '/' + if os.name == 'nt': + sep = '\\' + + isep = filename.rfind(sep) + filename_nodir = filename + if isep != -1: + filename_nodir = filename[isep+1:] + + ttime = time.time() + gmt_time = time.gmtime(ttime) + parsetime = time.asctime(gmt_time) + + rmodel = gbl_namespace.TMVA.Experimental.SOFIE.RModel.RModel(filename_nodir, parsetime) + + # iterate over the layers and add them to the RModel + # in case of keras 3.x (particularly in sequential models), the layer input and output name conventions are + # different from keras 2.x. In keras 2.x, the layer input name is consistent with previous layer's output + # name. For e.g., if the sequence of layers is dense -> maxpool, the input and output layer names would be: + # layer | name + # input dense | keras_tensor_1 + # output dense | keras_tensor_2 -- + # | |=> layer names match + # input maxpool | keras_tensor_2 -- + # output maxpool | keras_tensor_3 + # + # but in case of keras 3.x, this changes. + # layer | name + # input dense | keras_tensor_1 + # output dense | keras_tensor_2 -- + # | |=> different layer names + # input maxpool | keras_tensor_3 -- + # output maxpool | keras_tensor_4 + # + # hence, we need to add a custom layer iterator, which would replace the suffix of the layer's input + # and output names + layer_iter = 0 + is_functional_model = True if keras_model.__class__.__name__ == 'Functional' else False + + for layer in keras_model.layers: + layer_data={} + layer_data['layerType']=layer.__class__.__name__ + layer_data['layerAttributes']=layer.__dict__ + if keras_version < '2.16' or is_functional_model: + if 'input_layer' in layer.name: + layer_data['layerInput'] = layer.name + else: + layer_data['layerInput']=[x.name for x in layer.input] if isinstance(layer.input,list) else [layer.input.name] + else: + if 'input_layer' in layer.input.name: + layer_data['layerInput'] = [layer.input.name] + else: + input_layer_name = layer.input.name[:13] + str(layer_iter) + layer_data['layerInput'] = [input_layer_name] + if keras_version < '2.16' or is_functional_model: + layer_data['layerOutput']=[x.name for x in layer.output] if isinstance(layer.output,list) else [layer.output.name] + else: + output_layer_name = layer.output.name[:13] + str(layer_iter+1) + layer_data['layerOutput']=[x.name for x in layer.output] if isinstance(layer.output,list) else [output_layer_name] + layer_iter += 1 + + fLayerType = layer_data['layerType'] + layer_data['layerDType'] = layer.dtype + + if len(layer.weights) > 0: + if keras_version < '2.16': + layer_data['layerWeight'] = [x.name for x in layer.weights] + else: + layer_data['layerWeight'] = [x.path for x in layer.weights] + else: + layer_data['layerWeight'] = [] + + # for convolutional and pooling layers we need to know the format of the data + if layer_data['layerType'] in ['Conv2D', 'MaxPooling2D', 'AveragePooling2D', 'GlobalAveragePooling2D']: + layer_data['channels_last'] = True if layer.data_format == 'channels_last' else False + + # for recurrent type layers we need to extract additional unique information + if layer_data['layerType'] in ["SimpleRNN", "LSTM", "GRU"]: + layer_data['layerAttributes']['activation'] = layer.activation + layer_data['layerAttributes']['direction'] = 'backward' if layer.go_backwards else 'forward' + layer_data['layerAttributes']["units"] = layer.units + layer_data['layerAttributes']["layout"] = layer.input.shape[0] is None + layer_data['layerAttributes']["hidden_size"] = layer.output.shape[-1] + + # for GRU and LSTM we need to extract an additional activation function + if layer_data['layerType'] != "SimpleRNN": + layer_data['layerAttributes']['recurrent_activation'] = layer.recurrent_activation + + # for GRU there are two variants of the reset gate location, we need to know which one is it + if layer_data['layerType'] == "GRU": + layer_data['layerAttributes']['linear_before_reset'] = 1 if layer.reset_after and layer.recurrent_activation.__name__ == "sigmoid" else 0 + + # Ignoring the input layer of the model + if(fLayerType == "InputLayer"): + continue; + + # Adding any required routines depending on the Layer types for generating inference code. + if (fLayerType == "Dense"): + rmodel.AddBlasRoutines({"Gemm", "Gemv"}) + elif (fLayerType == "BatchNormalization"): + rmodel.AddBlasRoutines({"Copy", "Axpy"}) + elif (fLayerType == "Conv1D" or fLayerType == "Conv2D" or fLayerType == "Conv3D"): + rmodel.AddBlasRoutines({"Gemm", "Axpy"}) + rmodel = add_layer_into_RModel(rmodel, layer_data) + + # Extracting model's weights + weight = [] + for idx in range(len(keras_model.get_weights())): + weightProp = {} + if keras_version < '2.16': + weightProp['name'] = keras_model.weights[idx].name + else: + weightProp['name'] = keras_model.weights[idx].path + weightProp['dtype'] = keras_model.get_weights()[idx].dtype.name + if 'conv' in weightProp['name'] and keras_model.weights[idx].shape.ndims == 4: + weightProp['value'] = keras_model.get_weights()[idx].transpose((3, 2, 0, 1)).copy() + else: + weightProp['value'] = keras_model.get_weights()[idx] + weight.append(weightProp) + + # Traversing through all the Weight tensors + for weightIter in range(len(weight)): + fWeightTensor = weight[weightIter] + fWeightName = fWeightTensor['name'] + fWeightDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fWeightTensor['dtype']) + fWeightTensorValue = fWeightTensor['value'] + fWeightTensorSize = 1 + fWeightTensorShape = [] + + #IS IT BATCH SIZE? CHECK ONNX + if 'simple_rnn' in fWeightName or 'lstm' in fWeightName or ('gru' in fWeightName and not 'bias' in fWeightName): + fWeightTensorShape.append(1) + + # Building the shape vector and finding the tensor size + for j in range(len(fWeightTensorValue.shape)): + fWeightTensorShape.append(fWeightTensorValue.shape[j]) + fWeightTensorSize *= fWeightTensorValue.shape[j] + + if fWeightDType == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + fWeightArray = fWeightTensorValue + + # weights conversion format between keras and onnx for lstm: the order of the different + # elements (input, output, forget, cell) inside the vector/matrix is different + if 'lstm' in fWeightName: + if 'kernel' in fWeightName: + units = int(fWeightArray.shape[1]/4) + W_i = fWeightArray[:, :units].copy() + W_f = fWeightArray[:, units: units * 2].copy() + W_c = fWeightArray[:, units * 2: units * 3].copy() + W_o = fWeightArray[:, units * 3:].copy() + fWeightArray[:, units: units * 2] = W_o + fWeightArray[:, units * 2: units * 3] = W_f + fWeightArray[:, units * 3:] = W_c + else: #bias + units = int(fWeightArray.shape[0]/4) + W_i = fWeightArray[:units].copy() + W_f = fWeightArray[units: units * 2].copy() + W_c = fWeightArray[units * 2: units * 3].copy() + W_o = fWeightArray[units * 3:].copy() + fWeightArray[units: units * 2] = W_o + fWeightArray[units * 2: units * 3] = W_f + fWeightArray[units * 3:] = W_c + + # need to make specific adjustments for recurrent weights and biases + if ('simple_rnn' in fWeightName or 'lstm' in fWeightName or 'gru' in fWeightName): + # reshaping weight matrices for recurrent layers due to keras-onnx inconsistencies + if 'kernel' in fWeightName: + fWeightArray = np.transpose(fWeightArray) + fWeightTensorShape[1], fWeightTensorShape[2] = fWeightTensorShape[2], fWeightTensorShape[1] + + fData = fWeightArray.flatten() + + # the recurrent bias and the cell bias can be the same, in which case we need to add a + # vector of zeros for the recurrent bias + if 'bias' in fWeightName and len(fData.shape) == 1: + fWeightTensorShape[1] *= 2 + fRbias = fData.copy()*0 + fData = np.concatenate((fData,fRbias)) + + else: + fData = fWeightArray.flatten() + rmodel.AddInitializedTensor['float'](fWeightName, fWeightTensorShape, fData) + else: + raise TypeError("Type error: TMVA SOFIE does not yet support data layer type: " + fWeightDType) + + # Extracting input tensor info + if keras_version < '2.16': + fPInputs = keras_model.input_names + else: + fPInputs = [x.name for x in keras_model.inputs] + + fPInputShape = keras_model.input_shape if isinstance(keras_model.input_shape, list) else [keras_model.input_shape] + fPInputDType = [] + for idx in range(len(keras_model.inputs)): + dtype = keras_model.inputs[idx].dtype.__str__() + if dtype == "float32": + fPInputDType.append(dtype) + else: + fPInputDType.append(dtype[9:-2]) + + if len(fPInputShape) == 1: + fInputName = fPInputs[0] + fInputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fPInputDType[0]) + if fInputDType == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + if fPInputShape[0][0] is None or fPInputShape[0][0] <= 0: + fPInputShape = list(fPInputShape[0]) + fPInputShape[0] = batch_size + rmodel.AddInputTensorInfo(fInputName, gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT, fPInputShape) + rmodel.AddInputTensorName(fInputName) + else: + raise TypeError("Type error: TMVA SOFIE does not yet support data type " + gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType)) + else: + # Iterating through multiple input tensors + for fInputName, fInputDType, fInputShapeTuple in zip(fPInputs, fPInputDType, fPInputShape): + fInputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType) + if fInputDType == gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT: + if fInputShapeTuple[0] is None or fInputShapeTuple[0] <= 0: + fInputShapeTuple = list(fInputShapeTuple) + fInputShapeTuple[0] = batch_size + rmodel.AddInputTensorInfo(fInputName, gbl_namespace.TMVA.Experimental.SOFIE.ETensorType.FLOAT, fInputShapeTuple) + rmodel.AddInputTensorName(fInputName) + else: + raise TypeError("Type error: TMVA SOFIE does not yet support data type " + gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fInputDType)) + + # Adding OutputTensorInfos + outputNames = [] + if keras_version < '2.16' or is_functional_model: + for layerName in keras_model.output_names: + final_layer = keras_model.get_layer(layerName) + output_layer_name = final_layer.output.name + outputNames.append(output_layer_name) + else: + final_layer = keras_model.outputs[-1] + output_layer_name = final_layer.name[:13] + str(layer_iter) + outputNames.append(output_layer_name) + rmodel.AddOutputTensorNameList(outputNames) + return rmodel 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..774b21674fa11 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser_test_function.py @@ -0,0 +1,91 @@ +import ROOT +import numpy as np + +''' +The test file contains two types of functions: + is_accurate: + - This function checks whether the inference results from SOFIE and Keras are accurate within a specified + tolerance. Since the inference result from Keras is not flattened, the function flattens both tensors before + performing the comparison. + + generate_and_test_inference: + - This function accepts the following inputs: + - Model file path: Path to the input model. + - Destination directory for the generated header file: If set to None, the header file will be generated in + the model's directory. + - Batch size. + - After generating the inference code, we instantiate the session for inference. To validate the results from + SOFIE, we compare the outputs from both SOFIE and Keras. + - Load the Keras model. + - Extract the input dimensions of the Keras model to avoid hardcoding. + - For Sequential models or functional models with a single input: + - Extract the model's input specification and create a NumPy array of ones with the same shape as the + model's input specification, replacing None with the batch size. This becomes the input tensor. + - For functional models with multiple inputs: + - Extract the dimensions for each input, set the batch size, create a NumPy array of ones for each input, + and append each tensor to the list of input tensors. + - These input tensors are then fed to both the instantiated session object and the Keras model. + - Verify the output tensor dimensions: + Since SOFIE always flattens the output tensors before returning them, we need to extract the output tensor + shape from the model object. + - Convert the inference results to NumPy arrays: + The SOFIE result is of type vector, and the Keras result is a TensorFlow tensor. Both are converted to + NumPy arrays before being passed to the is_accurate function for comparison. + +''' + +def is_accurate(tensor_a, tensor_b, tolerance=1e-3): + tensor_a = tensor_a.flatten() + tensor_b = tensor_b.flatten() + for i in range(len(tensor_a)): + difference = abs(tensor_a[i] - tensor_b[i]) + if difference > tolerance: + print(tensor_a[i], tensor_b[i]) + return False + return True + +def generate_and_test_inference(model_file_path: str, generated_header_file_dir: str = None, batch_size=1): + + import 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: + last_idx = model_file_path.rfind("/") + if last_idx == -1: + generated_header_file_dir = "./" + else: + generated_header_file_dir = model_file_path[:last_idx] + generated_header_file_path = generated_header_file_dir + "/" + model_name + ".hxx" + print(f"Generating inference code for the Keras model from {model_file_path} in the header {generated_header_file_path}") + rmodel.Generate() + rmodel.OutputGenerated(generated_header_file_path) + print(f"Compiling SOFIE model {model_name}") + compile_status = ROOT.gInterpreter.Declare(f'#include "{generated_header_file_path}"') + if not compile_status: + raise AssertionError(f"Error compiling header file {generated_header_file_path}") + sofie_model_namespace = getattr(ROOT, "TMVA_SOFIE_" + model_name) + inference_session = sofie_model_namespace.Session(generated_header_file_path.removesuffix(".hxx") + ".dat") + keras_model = keras.models.load_model(model_file_path) + keras_model.load_weights(model_file_path) + if len(keras_model.inputs) == 1: + input_shape = list(keras_model.inputs[0].shape) + input_shape[0] = batch_size + input_tensors = np.ones(input_shape, dtype='float32') + else: + input_tensors = [] + for model_input in keras_model.inputs: + input_shape = list(model_input.shape) + input_shape[0] = batch_size + input_tensors.append(np.ones(input_shape, dtype='float32')) + sofie_inference_result = inference_session.infer(*input_tensors) + sofie_output_tensor_shape = list(rmodel.GetTensorShape(rmodel.GetOutputTensorNames()[0])) # get output shape + # from SOFIE + keras_inference_result = keras_model(input_tensors) + if sofie_output_tensor_shape != list(keras_inference_result.shape): + raise AssertionError("Output tensor dimensions from SOFIE and Keras do not match") + sofie_inference_result = np.asarray(sofie_inference_result) + keras_inference_result = np.asarray(keras_inference_result) + is_inference_accurate = is_accurate(sofie_inference_result, keras_inference_result) + if not is_inference_accurate: + raise AssertionError("Inference results from SOFIE and Keras do not match") \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/test/CMakeLists.txt b/bindings/pyroot/pythonizations/test/CMakeLists.txt index 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..f94697761d44b --- /dev/null +++ b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py @@ -0,0 +1,74 @@ +import unittest +import os +import shutil + +from ROOT._pythonization._tmva._sofie._parser._keras.parser_test_function import generate_and_test_inference +from ROOT._pythonization._tmva._sofie._parser._keras.generate_keras_functional import generate_keras_functional +from ROOT._pythonization._tmva._sofie._parser._keras.generate_keras_sequential import generate_keras_sequential + + +def make_testname(test_case: str): + test_case_name = test_case.replace("_", " ").removesuffix(".h5") + return test_case_name + +models = [ + "AveragePooling2D_channels_first", + "AveragePooling2D_channels_last", + "BatchNorm", + "Conv2D_channels_first", + "Conv2D_channels_last", + "Conv2D_padding_same", + "Conv2D_padding_valid", + "Dense", + "ELU", + "Flatten", + "GlobalAveragePooling2D_channels_first", + "GlobalAveragePooling2D_channels_last", + # "GRU", + "LayerNorm", + "LeakyReLU", + # "LSTM", + "MaxPool2D_channels_first", + "MaxPool2D_channels_last", + "Permute", + "ReLU", + "Reshape", + # "SimpleRNN", + "Softmax", +] + ([f"Activation_layer_{activation_function.capitalize()}" for activation_function in + ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']] + + + [f"Layer_Combination_{i}" for i in range(1, 4)]) + +class SOFIE_Keras_Parser(unittest.TestCase): + + def setUp(self): + base_dir = self._testMethodName[5:] + if os.path.isdir(base_dir): + shutil.rmtree(base_dir) + os.makedirs(base_dir + "/input_models") + os.makedirs(base_dir + "/generated_header_files_dir") + + def run_model_tests(self, model_type: str, generate_function, model_list): + generate_function(f"{model_type}/input_models") + for keras_model in model_list: + keras_model_name = f"{model_type.capitalize()}_{keras_model}_test.h5" + keras_model_path = f"{model_type}/input_models/" + keras_model_name + with self.subTest(msg=make_testname(keras_model_name)): + generate_and_test_inference(keras_model_path, f"{model_type}/generated_header_files_dir") + + def test_sequential(self): + sequential_models = models + self.run_model_tests("sequential", generate_keras_sequential, sequential_models) + + def test_functional(self): + functional_models = models + ["Add", "Concat", "Multiply", "Subtract"] + self.run_model_tests("functional", generate_keras_functional, functional_models) + + @classmethod + def tearDownClass(self): + shutil.rmtree("sequential") + shutil.rmtree("functional") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tmva/pymva/CMakeLists.txt b/tmva/pymva/CMakeLists.txt index e443405be7440..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 @@ -26,7 +25,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 deleted file mode 100644 index d60eb2041e650..0000000000000 --- a/tmva/pymva/inc/TMVA/RModelParser_Keras.h +++ /dev/null @@ -1,56 +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{ -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); - -}//PyKeras -}//SOFIE -}//Experimental -}//TMVA -#endif //TMVA_PYMVA_RMODELPARSER_KERAS 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/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 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