diff --git a/bindings/pyroot/pythonizations/python/CMakeLists.txt b/bindings/pyroot/pythonizations/python/CMakeLists.txt index 4c96c7556c7cf..e471dbdc62932 100644 --- a/bindings/pyroot/pythonizations/python/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/python/CMakeLists.txt @@ -58,7 +58,30 @@ if(tmva) ROOT/_pythonization/_tmva/_rtensor.py ROOT/_pythonization/_tmva/_tree_inference.py ROOT/_pythonization/_tmva/_utils.py - ROOT/_pythonization/_tmva/_gnn.py) + ROOT/_pythonization/_tmva/_gnn.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/__init__.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py) if(dataframe) list(APPEND PYROOT_EXTRA_PYTHON_SOURCES ROOT/_pythonization/_tmva/_batchgenerator.py) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index 4695e2b74a972..29ae80e18683a 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -410,8 +410,10 @@ def TMVA(self): # This line is needed to import the pythonizations in _tmva directory. # The comment suppresses linter errors about unused imports. from ._pythonization import _tmva # noqa: F401 + from ._pythonization._tmva._sofie._parser._keras.parser import PyKeras ns = self._fallback_getattr("TMVA") + setattr(ns.Experimental.SOFIE, "PyKeras", PyKeras) hasRDF = "dataframe" in self.gROOT.GetConfigFeatures() if hasRDF: try: 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/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..11110851342e3 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py @@ -0,0 +1,57 @@ +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..9aa6324af27eb --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py @@ -0,0 +1,24 @@ +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..013afe831585e --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py @@ -0,0 +1,18 @@ +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..047e9d52603e0 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py @@ -0,0 +1,76 @@ +import math + +from cppyy import gbl as gbl_namespace + +from .. import get_keras_version + + +def MakeKerasConv(layer): + """ + Create a Keras-compatible convolutional layer operation using SOFIE framework. + + 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..cfcd079dc8909 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py @@ -0,0 +1,38 @@ +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..6d8c1eccbd985 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py @@ -0,0 +1,36 @@ +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..1fd5042b6650a --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py @@ -0,0 +1,38 @@ +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 + "_shape" + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape(fOpMode, 0, fNameData, fNameShape, fNameOutput) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py new file mode 100644 index 0000000000000..fb3ba6783b8b0 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py @@ -0,0 +1,16 @@ +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..55b7039ee0e4c --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py @@ -0,0 +1,64 @@ +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..4eef107d3e5f3 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py @@ -0,0 +1,45 @@ +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..04daea02235a3 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py @@ -0,0 +1,37 @@ +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..8d08104cec743 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py @@ -0,0 +1,79 @@ +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..24419da59396e --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py @@ -0,0 +1,31 @@ +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..5d77978be54c5 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py @@ -0,0 +1,36 @@ +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 + "_shape" + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape(fOpMode, 0, fNameData, fNameShape, fNameOutput) + return op \ No newline at end of file diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py new file mode 100644 index 0000000000000..3902d501432f0 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py @@ -0,0 +1,93 @@ +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 fPActivation.__name__ not in ['relu', 'sigmoid', 'tanh', 'softsign', 'softplus']: #avoiding functions with parameters + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator RNN does not yet support activation function " + fPActivation.__name__ + ) + + 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 fPActivation.__name__ not in ['relu', 'sigmoid', 'tanh', 'softsign', 'softplus']: #avoiding functions with parameters + raise RuntimeError( + "TMVA::SOFIE - Unsupported - Operator RNN does not yet support recurrent activation function " + fPActivation.__name__ + ) + 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..62c386b7e6363 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py @@ -0,0 +1,32 @@ +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..92e3159822393 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py @@ -0,0 +1,32 @@ +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..f23b1f46f6a6d --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py @@ -0,0 +1,33 @@ +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..db683b9f5f393 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py @@ -0,0 +1,32 @@ +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..35020d6c6da76 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py @@ -0,0 +1,32 @@ +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..585af3f9da04c --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py @@ -0,0 +1,547 @@ +import os +import time + +from cppyy import gbl as gbl_namespace + +from . import get_keras_version +from .layers.batchnorm import MakeKerasBatchNorm +from .layers.binary import MakeKerasBinary +from .layers.concat import MakeKerasConcat +from .layers.conv import MakeKerasConv +from .layers.dense import MakeKerasDense +from .layers.elu import MakeKerasELU +from .layers.flatten import MakeKerasFlatten +from .layers.layernorm import MakeKerasLayerNorm +from .layers.leaky_relu import MakeKerasLeakyRelu +from .layers.permute import MakeKerasPermute +from .layers.pooling import MakeKerasPooling +from .layers.relu import MakeKerasReLU +from .layers.reshape import MakeKerasReshape +from .layers.selu import MakeKerasSeLU +from .layers.sigmoid import MakeKerasSigmoid +from .layers.softmax import MakeKerasSoftmax +from .layers.swish import MakeKerasSwish +from .layers.tanh import MakeKerasTanh + + +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 need constant tensor for the shape + if fLayerType == "Reshape" or fLayerType == "Flatten": + Attributes = layer_data['layerAttributes'] + if keras_version < '2.16': + LayerName = Attributes['_name'] + else: + LayerName = Attributes['name'] + + if fLayerType == "Reshape": + TargetShape = np.asarray(Attributes['target_shape']).astype("int64") + TargetShape = np.insert(TargetShape,0,1) + 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++ + #print('adding initialized tensor..',LayerName, TargetShape) + shape_tensor_name = LayerName + "_shape" + shape_data = TargetShape.data + print(TargetShape, shape_data) + print(len(TargetShape)) + rmodel.AddInitializedTensor['int64_t'](shape_tensor_name, [len(TargetShape)], shape_data) + + # These layers only have one operator - excluding the recurrent layers, in which the activation function(s) + # are included in the recurrent operator + if fLayerType in mapKerasLayer.keys(): + Attributes = layer_data['layerAttributes'] + inputs = layer_data['layerInput'] + outputs = layer_data['layerOutput'] + if keras_version < '2.16': + LayerName = Attributes['_name'] + else: + LayerName = Attributes['name'] + + # Convoltion/Pooling layers in keras by default assume the channels dimension is the + # last one, while in onnx (and the SOFIE's RModel) it is the first one (other than batch + # size), so a transpose is needed before and after the pooling, if the data format is + # channels last (can be set to channels first by the user). In case of MaxPool2D and + # Conv2D (with linear activation) channels last, the transpose layers are added as: + + # input output + # transpose layer input_layer_name layer_name + PreTrans + # actual layer layer_name + PreTrans layer_name + PostTrans + # transpose layer layer_name + PostTrans output_layer_name + + fLayerOutput = outputs[0] + if fLayerType == 'GlobalAveragePooling2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0, 3, 1, 2], inputs[0], LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName+"PreTrans" + outputs[0] = LayerName+"Squeeze" + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Reshape( + gbl_namespace.TMVA.Experimental.SOFIE.ReshapeOpMode.Squeeze, + [2, 3], + LayerName + "Squeeze", + fLayerOutput + ) + rmodel.AddOperatorReference(op) + + # Similar case is with Batchnorm, ONNX assumes that the 'axis' is always 1, but Keras + # gives the user the choice of specifying it. So, we have to transpose the input layer + # as 'axis' as the first dimension, apply the BatchNormalization operator and then + # again tranpose it to bring back the original dimensions + elif fLayerType == 'BatchNormalization': + if '_build_input_shape' in Attributes.keys(): + num_input_shapes = len(Attributes['_build_input_shape']) + elif '_build_shapes_dict' in Attributes.keys(): + num_input_shapes = len(list(Attributes['_build_shapes_dict']['input_shape'])) + + axis = Attributes['axis'] + axis = axis[0] if isinstance(axis, list) else axis + if axis < 0: + axis += num_input_shapes + fAttrPerm = list(range(0, num_input_shapes)) + fAttrPerm[1] = axis + fAttrPerm[axis] = 1 + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttrPerm, inputs[0], + LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName + "PreTrans" + outputs[0] = LayerName + "PostTrans" + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')(fAttrPerm, LayerName+"PostTrans", + fLayerOutput) + rmodel.AddOperatorReference(op) + + elif fLayerType == 'MaxPooling2D' or fLayerType == 'AveragePooling2D': + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,3,1,2], inputs[0], + LayerName+"PreTrans") + rmodel.AddOperatorReference(op) + inputs[0] = LayerName+"PreTrans" + outputs[0] = LayerName+"PostTrans" + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + if layer_data['channels_last']: + op = gbl_namespace.TMVA.Experimental.SOFIE.ROperator_Transpose('float')([0,2,3,1], + LayerName+"PostTrans", fLayerOutput) + rmodel.AddOperatorReference(op) + + else: + rmodel.AddOperatorReference(mapKerasLayer[fLayerType](layer_data)) + + return rmodel + + # These layers require two operators - dense/conv and their activation function + elif fLayerType in mapKerasLayerWithActivation.keys(): + Attributes = layer_data['layerAttributes'] + if keras_version < '2.16': + LayerName = Attributes['_name'] + else: + LayerName = Attributes['name'] + fPActivation = Attributes['activation'] + LayerActivation = fPActivation.__name__ + if LayerActivation in ['selu', 'sigmoid']: + rmodel.AddNeededStdLib("cmath") + + # if there is an activation function after the layer + if LayerActivation != 'linear': + if LayerActivation not in mapKerasLayer.keys(): + raise Exception("TMVA.SOFIE - parsing keras activation function " + LayerActivation + " is not yet supported") + outputs = layer_data['layerOutput'] + inputs = layer_data['layerInput'] + 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 PyKeras: + + def Parse(filename, batch_size=1): # If a model does not have a defined batch size, then assuming it is 1 + + # TensorFlow/Keras is too fragile to import unconditionally. As its presence might break several ROOT + # usecases and importing keras globally will slow down importing ROOT, which is not desired. For this, + # we import keras within the functions instead of importing it at the start of the file (i.e. globally). + # So, whenever the parser function is called, only then keras will be imported, and not everytime we + # 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) + + print("PyKeras: parsing model ",filename) + + # iterate over the layers and add them to the RModel + # in case of keras 3.x (particularly in sequential models), the layer input and output name conventions are + # different from keras 2.x. In keras 2.x, the layer input name is consistent with previous layer's output + # 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__ + #get input names for layer + if keras_version < '2.16' or is_functional_model: + if 'input_layer' in layer.name: + layer_data['layerInput'] = layer.name + else: + layer_data['layerInput']=[x.name for x in layer.input] if isinstance(layer.input,list) else [layer.input.name] + else: + #case of Keras3 Sequential model : in this case output of layer is input to following one, but names can be different + if 'input_layer' in layer.input.name: + layer_data['layerInput'] = [layer.input.name] + else: + if (layer_iter == 0) : + input_layer_name = "tensor_input_" + layer.name + else : + input_layer_name = "tensor_output_" + keras_model.layers[layer_iter-1].name + layer_data['layerInput'] = [input_layer_name] + #get output names of layer + if keras_version < '2.16' or is_functional_model: + layer_data['layerOutput']=[x.name for x in layer.output] if isinstance(layer.output,list) else [layer.output.name] + else: + #sequential model in Keras3 + output_layer_name = "tensor_output_" + layer.name + layer_data['layerOutput']=[x.name for x in layer.output] if isinstance(layer.output,list) else [output_layer_name] + + layer_iter += 1 + 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 'bias' not 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: + inputName = fPInputs[0] + inputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(fPInputDType[0]) + # convert ot a list to modify batch size + inputShape = list(fPInputShape[0]) + #set the batch size in case of -1 or None as first value + if inputShape[0] is None or inputShape[0] <= 0: + inputShape[0] = batch_size + rmodel.AddInputTensorInfo(inputName, inputDType, inputShape) + rmodel.AddInputTensorName(inputName) + else: + # Iterating through multiple input tensors + for inputName, inputDType, inputShapeTuple in zip(fPInputs, fPInputDType, fPInputShape): + inputDType = gbl_namespace.TMVA.Experimental.SOFIE.ConvertStringToType(inputDType) + inputShape = list(inputShapeTuple) + if inputShape[0] is None or inputShape[0] <= 0: + inputShape[0] = batch_size + rmodel.AddInputTensorInfo(inputName, inputDType, inputShape) + rmodel.AddInputTensorName(inputName) + + # Adding OutputTensorInfos + outputNames = [] + 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: + output_layer_name = "tensor_output_" + keras_model.layers[-1].name + outputNames.append(output_layer_name) + + rmodel.AddOutputTensorNameList(outputNames) + return rmodel diff --git a/bindings/pyroot/pythonizations/test/CMakeLists.txt b/bindings/pyroot/pythonizations/test/CMakeLists.txt index 539316700e149..bd7852dfd0981 100644 --- a/bindings/pyroot/pythonizations/test/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/test/CMakeLists.txt @@ -136,6 +136,13 @@ if (tmva) endif() endif() +# SOFIE Keras Parser +if (tmva) + if(NOT MSVC OR CMAKE_SIZEOF_VOID_P EQUAL 4 OR win_broken_tests) + ROOT_ADD_PYUNITTEST(pyroot_pyz_sofie_keras_parser sofie_keras_parser.py PYTHON_DEPS keras tensorflow) + 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/generate_keras_functional.py b/bindings/pyroot/pythonizations/test/generate_keras_functional.py new file mode 100644 index 0000000000000..11f7bdefda00e --- /dev/null +++ b/bindings/pyroot/pythonizations/test/generate_keras_functional.py @@ -0,0 +1,217 @@ +def generate_keras_functional(dst_dir): + + import numpy as np + from keras import layers, models + from parser_test_function import is_channels_first_supported + + + # 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.summary() + model.fit(x_train, y_train, epochs=1, verbose=0) + model.save(f"{dst_dir}/Functional_{name}_test.keras") + print("generated and saved functional model",name) + + + # Activation Functions + for act in ['relu', 'elu', 'leaky_relu', 'selu', 'sigmoid', 'softmax', 'swish', 'tanh']: + inp = layers.Input(shape=(10,)) + out = layers.Activation(act)(inp) + model = models.Model(inp, out) + train_and_save(model, f"Activation_layer_{act.capitalize()}") + # 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 + if (is_channels_first_supported()): + inp = layers.Input(shape=(3, 8, 8)) + out = layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "AveragePooling2D_channels_first") + + # AveragePooling2D channels_last + inp = layers.Input(shape=(8, 8, 3)) + 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 + if (is_channels_first_supported()): + inp = layers.Input(shape=(3, 8, 8)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_first', activation='relu')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_channels_first") + + # Conv2D channels_last + inp = layers.Input(shape=(8, 8, 3)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last', activation='leaky_relu')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_channels_last") + + # Conv2D padding_same + inp = layers.Input(shape=(8, 8, 3)) + out = layers.Conv2D(4, (3, 3), padding='same', data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_padding_same") + + # Conv2D padding_valid + inp = layers.Input(shape=(8, 8, 3)) + out = layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last', activation='elu')(inp) + model = models.Model(inp, out) + train_and_save(model, "Conv2D_padding_valid") + + # Dense + inp = layers.Input(shape=(10,)) + out = layers.Dense(5, activation='tanh')(inp) + model = models.Model(inp, out) + train_and_save(model, "Dense") + + # ELU + inp = layers.Input(shape=(10,)) + out = layers.ELU(alpha=0.5)(inp) + model = models.Model(inp, out) + train_and_save(model, "ELU") + + # Flatten + inp = layers.Input(shape=(4, 5)) + out = layers.Flatten()(inp) + model = models.Model(inp, out) + train_and_save(model, "Flatten") + + # GlobalAveragePooling2D channels first + if (is_channels_first_supported): + inp = layers.Input(shape=(3, 4, 6)) + out = layers.GlobalAveragePooling2D(data_format='channels_first')(inp) + model = models.Model(inp, out) + train_and_save(model, "GlobalAveragePooling2D_channels_first") + + # GlobalAveragePooling2D channels last + inp = layers.Input(shape=(4, 6, 3)) + out = layers.GlobalAveragePooling2D(data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "GlobalAveragePooling2D_channels_last") + + # LayerNorm + 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 + if (is_channels_first_supported): + inp = layers.Input(shape=(3, 8, 8)) + out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "MaxPool2D_channels_first") + + # MaxPooling2D channels_last + inp = layers.Input(shape=(8, 8, 3)) + out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last')(inp) + model = models.Model(inp, out) + train_and_save(model, "MaxPool2D_channels_last") + + # Multiply + in1 = layers.Input(shape=(8,)) + in2 = layers.Input(shape=(8,)) + out = layers.Multiply()([in1, in2]) + model = models.Model([in1, in2], out) + train_and_save(model, "Multiply") + + # Permute + inp = layers.Input(shape=(3, 4, 5)) + out = layers.Permute((2, 1, 3))(inp) + model = models.Model(inp, out) + train_and_save(model, "Permute") + + # ReLU + inp = layers.Input(shape=(10,)) + out = layers.ReLU()(inp) + 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", data_format='channels_last')(inp) + x = layers.MaxPooling2D((2,2))(x) + x = layers.Reshape((16, 16, 8))(x) + x = layers.Permute((3, 1, 2))(x) + 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/test/generate_keras_sequential.py b/bindings/pyroot/pythonizations/test/generate_keras_sequential.py new file mode 100644 index 0000000000000..40b6c645b1fd4 --- /dev/null +++ b/bindings/pyroot/pythonizations/test/generate_keras_sequential.py @@ -0,0 +1,213 @@ +def generate_keras_sequential(dst_dir): + + import numpy as np + from keras import layers, models + from parser_test_function import is_channels_first_supported + + # 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.summary() + print("fitting sequential model",name) + model.save(f"{dst_dir}/Sequential_{name}_test.keras") + + + # 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 declaration of activation layers such as: + # ELU, ReLU, LeakyReLU, Softmax + + # AveragePooling2D channels_first + if (is_channels_first_supported()): + model = models.Sequential([ + layers.Input(shape=(3, 8, 8)), + layers.AveragePooling2D(pool_size=(2, 2), data_format='channels_first') + ]) + train_and_save(model, "AveragePooling2D_channels_first") + + # AveragePooling2D channels_last + model = models.Sequential([ + layers.Input(shape=(8, 8, 3)), + 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 + if (is_channels_first_supported()): + model = models.Sequential([ + layers.Input(shape=(3, 8, 8)), + layers.Conv2D(4, (3, 3), data_format='channels_first') + ]) + train_and_save(model, "Conv2D_channels_first") + + # 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 + if (is_channels_first_supported()): + model = models.Sequential([ + layers.Input(shape=(3, 4, 6)), + layers.GlobalAveragePooling2D(data_format='channels_first') + ]) + train_and_save(model, "GlobalAveragePooling2D_channels_first") + + # 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 + if (is_channels_first_supported()): + model = models.Sequential([ + layers.Input(shape=(3, 8, 8)), + layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_first') + ]) + train_and_save(model, "MaxPool2D_channels_first") + + # 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=(32,32,3)), + layers.Conv2D(8, (3,3), padding='valid', data_format='channels_last', activation='relu'), + layers.MaxPooling2D((2,2), data_format='channels_last'), + layers.Flatten(), + layers.Dense(128, activation='relu'), + layers.Reshape((16, 8)), + layers.Permute((2, 1)), + layers.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', data_format='channels_last'), + 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/test/parser_test_function.py b/bindings/pyroot/pythonizations/test/parser_test_function.py new file mode 100644 index 0000000000000..eaa4a0ed5fb2f --- /dev/null +++ b/bindings/pyroot/pythonizations/test/parser_test_function.py @@ -0,0 +1,101 @@ +import ROOT + +''' +The test file contains two types of functions: + is_accurate: + - This function checks whether the inference results from SOFIE and Keras are accurate within a specified + tolerance. Since the inference result from Keras is not flattened, the function flattens both tensors before + performing the comparison. + + generate_and_test_inference: + - This function accepts the following inputs: + - Model file path: Path to the input model. + - Destination directory for the generated header file: If set to None, the header file will be generated in + the model's directory. + - Batch size. + - After generating the inference code, we instantiate the session for inference. To validate the results from + SOFIE, we compare the outputs from both SOFIE and Keras. + - Load the Keras model. + - Extract the input dimensions of the Keras model to avoid hardcoding. + - For Sequential models or functional models with a single input: + - Extract the model's input specification and create a NumPy array of ones with the same shape as the + model's input specification, replacing None with the batch size. This becomes the input tensor. + - For functional models with multiple inputs: + - Extract the dimensions for each input, set the batch size, create a NumPy array of ones for each input, + and append each tensor to the list of input tensors. + - These input tensors are then fed to both the instantiated session object and the Keras model. + - Verify the output tensor dimensions: + Since SOFIE always flattens the output tensors before returning them, we need to extract the output tensor + shape from the model object. + - Convert the inference results to NumPy arrays: + The SOFIE result is of type vector, and the Keras result is a TensorFlow tensor. Both are converted to + NumPy arrays before being passed to the is_accurate function for comparison. + +''' +def is_channels_first_supported() : + #channel first is not supported on tensorflow CPU versions + from keras import backend + if backend.backend() == "tensorflow" : + import tensorflow as tf + if len(tf.config.list_physical_devices("GPU")) == 0: + return False + + return True + +def is_accurate(tensor_a, tensor_b, tolerance=1e-2): + 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 + import numpy as np + import tensorflow as tf + + print("Tensorflow version: ", tf.__version__) + print("Keras version: ", keras.__version__) + print("Numpy version:", np.__version__) + + model_name = model_file_path[model_file_path.rfind('/')+1:].removesuffix(".keras") + rmodel = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse(model_file_path, batch_size) + if generated_header_file_dir is None: + last_idx = model_file_path.rfind("/") + if last_idx == -1: + 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) + + 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/sofie_keras_parser.py b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py new file mode 100644 index 0000000000000..81fd80606ec13 --- /dev/null +++ b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py @@ -0,0 +1,88 @@ +import unittest +import os +import shutil + + +from parser_test_function import generate_and_test_inference +from parser_test_function import is_channels_first_supported +from generate_keras_functional import generate_keras_functional +from generate_keras_sequential import generate_keras_sequential + + +def make_testname(test_case: str): + test_case_name = test_case.replace("_", " ").removesuffix(".keras") + 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", #failing + "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)]) + +#remove channel first cases if not supported +if (not is_channels_first_supported()): + models = [m for m in models if "channels_first" not in m] + +print(models) + +class SOFIE_Keras_Parser(unittest.TestCase): + + def setUp(self): + base_dir = self._testMethodName[5:] + if os.path.isdir(base_dir): + shutil.rmtree(base_dir) + os.makedirs(base_dir + "/input_models") + os.makedirs(base_dir + "/generated_header_files_dir") + + def run_model_tests(self, model_type: str, generate_function, model_list): + print("Generating", model_type," models for testing") + generate_function(f"{model_type}/input_models") + for keras_model in model_list: + print("**********************************") + print("Run test for",model_type,"model: ",keras_model) + print("**********************************") + keras_model_name = f"{model_type.capitalize()}_{keras_model}_test.keras" + keras_model_path = f"{model_type}/input_models/" + keras_model_name + with self.subTest(msg=make_testname(keras_model_name)): + generate_and_test_inference(keras_model_path, f"{model_type}/generated_header_files_dir") + + def test_sequential(self): + sequential_models = models + self.run_model_tests("sequential", generate_keras_sequential, sequential_models) + + def test_functional(self): + functional_models = models + ["Add", "Concat", "Multiply", "Subtract"] + self.run_model_tests("functional", generate_keras_functional, functional_models) + + @classmethod + def tearDownClass(self): + shutil.rmtree("sequential") + shutil.rmtree("functional") + +if __name__ == "__main__": + unittest.main() diff --git a/tmva/pymva/inc/TMVA/MethodPyKeras.h b/tmva/pymva/inc/TMVA/MethodPyKeras.h index 3318539d9d91c..8695cf45d2585 100644 --- a/tmva/pymva/inc/TMVA/MethodPyKeras.h +++ b/tmva/pymva/inc/TMVA/MethodPyKeras.h @@ -5,7 +5,7 @@ * Project: TMVA - a Root-integrated toolkit for multivariate data analysis * * Package: TMVA * * Class : MethodPyKeras * - * * + * * * * * Description: * * Interface for Keras python package which is a wrapper for the Theano and * @@ -20,7 +20,7 @@ * * * Redistribution and use in source and binary forms, with or without * * modification, are permitted according to the terms listed in LICENSE * - * (see tmva/doc/LICENSE) * + * (see tmva/doc/LICENSE) * **********************************************************************************/ #ifndef ROOT_TMVA_MethodPyKeras diff --git a/tmva/sofie/inc/TMVA/RModel.hxx b/tmva/sofie/inc/TMVA/RModel.hxx index 13d95935d9600..07bbb66df264e 100644 --- a/tmva/sofie/inc/TMVA/RModel.hxx +++ b/tmva/sofie/inc/TMVA/RModel.hxx @@ -210,10 +210,10 @@ public: void ReadInitializedTensorsFromFile(long); long WriteInitializedTensorsToFile(std::string filename = ""); + void PrintSummary() const; void PrintIntermediateTensors() const; void PrintOutputTensors() const; void OutputGenerated(std::string filename = "", bool append = false); - std::vector GetOutputTensorNames() { return fOutputTensorNames; } void SetFilename(std::string filename) { fName = filename; } /* diff --git a/tmva/sofie/inc/TMVA/ROperator_BasicBinary.hxx b/tmva/sofie/inc/TMVA/ROperator_BasicBinary.hxx index 491b669554118..62298e655f038 100644 --- a/tmva/sofie/inc/TMVA/ROperator_BasicBinary.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_BasicBinary.hxx @@ -121,11 +121,11 @@ public: } if (dynamicInputs & 1 && model.Verbose()) std::cout << BinaryOperatorTrait::Name() << " : input " << fNA << " is dynamic " - << ConvertShapeToString(fDimShapeA) << " "; + << ConvertShapeToString(fDimShapeA) << std::endl; if (dynamicInputs & 2 && model.Verbose()) std::cout << BinaryOperatorTrait::Name() << " : input " << fNB << " is dynamic " - << ConvertShapeToString(fDimShapeB) << " "; - std::cout << std::endl; + << ConvertShapeToString(fDimShapeB) << std::endl; + // check if need to broadcast at initialization time if shapes are known and different // (we could broadcast the tensor tensor to maximum values of dynamic shapes - to be done) // case of known shapes diff --git a/tmva/sofie/inc/TMVA/ROperator_Conv.hxx b/tmva/sofie/inc/TMVA/ROperator_Conv.hxx index 87d1ad0a0bf67..2d5cca8c6b17f 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Conv.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Conv.hxx @@ -436,7 +436,10 @@ public: out << SP << "int " << OpName << "_n = " << fShapeW[0] << ";\n"; // output channels out << SP << "int " << OpName << "_k = " << fShapeW[1] * fAttrKernelShape[0] * fAttrKernelShape[1] * fAttrKernelShape[2] << ";\n"; out << SP << "float " << OpName << "_alpha = 1.0;\n"; - out << SP << "float " << OpName << "_beta = 0.0;\n"; + if (fNB != "") + out << SP << "float " << OpName << "_beta = 1.0;\n"; + else // when bias is not present beta needs to be equal to zero to avoid re-using previous results in output tensor + out << SP << "float " << OpName << "_beta = 0.0;\n"; // Loop on batch size @@ -509,11 +512,23 @@ public: << "tensor_" << fNX << "_xcol);\n\n "; } // BLAS - out << SP << SP << "BLAS::sgemm_(&" << OpName << "_transA, &" << OpName << "_transB, &" << OpName << "_m, &" - << OpName << "_n, &" << OpName << "_k, &" << OpName << "_alpha, " << "tensor_" << fNX << "_xcol, &" << OpName - << "_m,\n"; // use m if op_xcol is not transpose , otherwise k - out << SP << SP << SP << "tensor_" << fNX << "_f, &" << OpName << "_k, &" << OpName << "_beta, tensor_" << fNY - << " + out_offset, &" << OpName << "_m);\n"; + out << SP << "TMVA::Experimental::SOFIE::Gemm_Call(" + << "tensor_" << fNY << " + out_offset, false, false, " + << OpName << "_m, " << OpName << "_n, " << OpName << "_k, " + << OpName << "_alpha, " << "tensor_" << fNX << "_xcol, tensor_" << fNX << "_f, " + << OpName << "_beta, "; + if (fNB != "") + out << "tensor_" << fNB; + else + out << "nullptr"; + out << ");\n"; + + + // out << SP << SP << "BLAS::sgemm_(&" << OpName << "_transA, &" << OpName << "_transB, &" << OpName << "_m, &" + // << OpName << "_n, &" << OpName << "_k, &" << OpName << "_alpha, " << "tensor_" << fNX << "_xcol, &" << OpName + // << "_m,\n"; // use m if op_xcol is not transpose , otherwise k + // out << SP << SP << SP << "tensor_" << fNX << "_f, &" << OpName << "_k, &" << OpName << "_beta, tensor_" << fNY + // << " + out_offset, &" << OpName << "_m);\n"; } else { // case of group convolution // Unroll (IM2COL) the input tensor- make loop on groups and repeat operations (IM2COL + GEMM for each @@ -522,8 +537,8 @@ public: out << SP << SP << "for (size_t g = 0; g < " << fAttrGroup << "; g++) {\n"; out << SP << SP << "size_t x_offset = n * " << inputBatchStride << " + g * " << fShapeW[1] << " * " << inputChannelStride << ";\n "; - out << SP << SP << "size_t out_offset = n * " << outputBatchStride << " + g * " - << fShapeW[0] << " * (" << outputChannelStride << ") / " << fAttrGroup << ";\n "; + out << SP << SP << "size_t g_offset = g * " << fShapeW[0] << " * (" << outputChannelStride << ") / " << fAttrGroup << ";\n "; + out << SP << SP << "size_t out_offset = n * " << outputBatchStride << " + g_offset;\n"; if (fDim < 3) { out << SP << SP << "TMVA::Experimental::SOFIE::UTILITY::Im2col(tensor_" << fNX @@ -561,26 +576,38 @@ public: out << SP << SP << SP << "size_t offset_f = g * " << fShapeW[0] * fShapeW[1] * fAttrKernelShape[0] * fAttrKernelShape[1] * fAttrKernelShape[2] / fAttrGroup << ";\n"; - out << SP << SP << "BLAS::sgemm_(&" << OpName << "_transA, &" << OpName << "_transB, &" << OpName << "_m, &" - << OpName << "_n, &" << OpName << "_k, &" << OpName << "_alpha, tensor_" << fNX << "_xcol, &" << OpName - << "_m,\n"; // use m if op_xcol is not transpose , otherwise k - out << SP << SP << SP << "tensor_" << fNX << "_f + offset_f, &" << OpName << "_k, &" << OpName << "_beta, tensor_" << fNY - << " + out_offset" - << ", &" << OpName << "_m);\n"; + + out << SP << "TMVA::Experimental::SOFIE::Gemm_Call(" + << "tensor_" << fNY << " + out_offset, false, false, " + << OpName << "_m, " << OpName << "_n, " << OpName << "_k, " + << OpName << "_alpha, " << "tensor_" << fNX << "_xcol, tensor_" << fNX << "_f + offset_f, " + << OpName << "_beta, "; + if (fNB != "") + out << "tensor_" << fNB << " + g_offset"; + else + out << "nullptr"; + out << ");\n"; + + // out << SP << SP << "BLAS::sgemm_(&" << OpName << "_transA, &" << OpName << "_transB, &" << OpName << "_m, &" + // << OpName << "_n, &" << OpName << "_k, &" << OpName << "_alpha, tensor_" << fNX << "_xcol, &" << OpName + // << "_m,\n"; // use m if op_xcol is not transpose , otherwise k + // out << SP << SP << SP << "tensor_" << fNX << "_f + offset_f, &" << OpName << "_k, &" << OpName << "_beta, tensor_" << fNY + // << " + out_offset" + // << ", &" << OpName << "_m);\n"; out << SP << SP << "}\n"; // end of group loop } - if (fNB != "") { - out << SP << "int " << OpName << "_size = " << outputBatchStride << ";\n"; - out << SP << "float " << OpName << "_gamma = 1.0;\n"; - out << SP << "int " << OpName << "_incx = 1;\n"; - out << SP << "int " << OpName << "_incy = 1;\n"; + // if (fNB != "") { + // out << SP << "int " << OpName << "_size = " << outputBatchStride << ";\n"; + // out << SP << "float " << OpName << "_gamma = 1.0;\n"; + // out << SP << "int " << OpName << "_incx = 1;\n"; + // out << SP << "int " << OpName << "_incy = 1;\n"; - out << SP << "BLAS::saxpy_(&" << OpName << "_size, &" << OpName << "_gamma, tensor_" << fNB << ", &" - << OpName << "_incx, tensor_" << fNY << " + out_offset, &" << OpName << "_incy);\n"; + // out << SP << "BLAS::saxpy_(&" << OpName << "_size, &" << OpName << "_gamma, tensor_" << fNB << ", &" + // << OpName << "_incx, tensor_" << fNY << " + out_offset, &" << OpName << "_incy);\n"; - } + // } out << SP << "}\n"; // end of batch size loop return out.str(); diff --git a/tmva/sofie/inc/TMVA/ROperator_Gemm.hxx b/tmva/sofie/inc/TMVA/ROperator_Gemm.hxx index 7d1c497bed56e..0b84bcc6eec71 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Gemm.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Gemm.hxx @@ -357,6 +357,7 @@ namespace SOFIE{ } // do the bias broadcasting if (fBroadcastBias) { + fAttrBeta = 1.; out << SP << "for (size_t j = 0; j < " << sY[0] << "; j++) { \n"; out << SP << SP << "size_t y_index = "; if (doStackMul) // add offset in caseof stack multiplications (not sure if bias is present in these cases) diff --git a/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx b/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx index f98ce201d400d..1d2abaa85ef71 100644 --- a/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_LayerNormalization.hxx @@ -243,7 +243,8 @@ public: // compute mean and std-dev. Save in tensors if requested out << SP << "// Compute the mean\n"; - // Loop over all the dims in [0, fAxis) + + // Loop over all the outer dims in [0, fAxis) for (size_t i = 0; i < fAxis; i++) { std::string iIdx = "axis_" + std::to_string(i); out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] @@ -262,18 +263,8 @@ public: } out << SP << SP << "mean /= " << fType << "(" << fNormalizedLength << ");\n"; - // for (size_t i = fAxis; i < fSize; i++) { - // out << SP << "}\n"; - // } - // tensor_" << fNMean << "[" << axesIndex << "] out << SP << "// Compute the inverse Standard Deviation\n"; - // Loop over the normalized dimensions - // for (size_t i = 0; i < fAxis; i++) { - // std::string iIdx = "axis_" + std::to_string(i); - // out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] - // << "; " << iIdx << "++){\n"; - // } // Set sum = 0 out << SP << SP << fType << " sum = 0.;\n"; @@ -291,9 +282,6 @@ public: out << SP << SP << fType << " invStdDev = 1 / std::sqrt("; out << "sum / " << fType << "(" << fNormalizedLength << ") + " << fAttrEpsilon << ");\n"; - // for (size_t i = 0; i < fAxis; i++) { - // out << SP << "}\n"; - // } // set output mean and invStdDev if requested if (!fNMean.empty()) @@ -304,11 +292,6 @@ public: // scale and add bias out << SP << "// Y = Scale o InvStdDev (X - Mean)\n"; - // for (size_t i = 0; i < fAxis; i++) { - // std::string iIdx = "axis_" + std::to_string(i); - // out << SP << "for (size_t " << iIdx << " = 0; " << iIdx << " < " << inputShape[i] - // << "; " << iIdx << "++){\n"; - // } for (size_t j = fAxis; j < fSize; j++) { std::string jIdx = "axis_" + std::to_string(j); @@ -324,23 +307,15 @@ public: out << " + tensor_" << fNB << "[" << biasIndex << "]"; out << ";\n"; + // close loops on normalizing dim [..,fAxis,...fSize-1] for (size_t j = fAxis; j < fSize; j++) { out << SP << SP << "}\n"; } - for (size_t i = fAxis; i < fSize; i++) { + // close loops on the other dimensions [0,...,fAxis] + for (size_t i = 0; i < fAxis; i++) { out << SP << "}\n"; } - // if (!fNB.empty()) { - // std::string bias = "tensor_" + (fNBroadcastedB.empty() ? fNB : fNBroadcastedB); - // out << SP << "// Add the bias to Y\n"; - // out << SP << "int " << opName << "_n = " << fLength << ";\n"; - // out << SP << "float " << opName << "_alpha = 1.;\n"; - // out << SP << "int " << opName << "_inc = 1;\n"; - // out << SP << "BLAS::saxpy_(&" << opName << "_n, &" << opName << "_alpha, " << bias << ", &"; - // out << opName << "_inc, " << "tensor_" << fNY << ", &" << opName << "_inc);\n"; - // } - return out.str(); } diff --git a/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx b/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx index a3ed28c4860bc..313b41eacacbc 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Reshape.hxx @@ -70,6 +70,8 @@ public: fAttrAxes(attrAxes) { assert(fOpMode == Squeeze || fOpMode == Unsqueeze); + fInputTensorNames = { fNData }; + fOutputTensorNames = { fNOutput }; } // output type is same as input @@ -198,14 +200,23 @@ public: } } } else { - auto &axes = fAttrAxes; + std::cout << "getting shape for Squeeze...from attribute\n"; + auto axes = fAttrAxes; for (size_t i = 0; i < axes.size(); i++) { + std::cout << i << " " << axes[i] << std::endl; if (axes[i] < 0) axes[i] += input_shape.size(); if (!(output_shape[axes[i]] == Dim{1})) throw std::runtime_error("TMVA Squeeze Op : Invalid axis value " + std::to_string(axes[i]) + " for " + ConvertShapeToString(output_shape)); - output_shape.erase(output_shape.begin() + axes[i]); + } + // for calling vector::erase we must sort axes in decreasing order to avoid + std::sort(axes.begin(), axes.end(), std::greater()); + for (auto & axis : axes) { + std::cout << "erase give axis " << axis << " -> "; + for (auto & o : output_shape) std::cout << o << " , "; + std::cout << std::endl; + output_shape.erase(output_shape.begin() + axis); } } ret.push_back(output_shape); @@ -235,8 +246,10 @@ public: void Initialize(RModel& model) override { - std::cout << "initialize reshape op type " << fOpMode << " - " << fNInput2 << " " << fNData << std::endl; fVerbose = model.Verbose(); + if (fVerbose) + std::cout << "initialize reshape op type " << fOpMode << " - " << fNInput2 << " " << fNData << std::endl; + if (model.CheckIfTensorAlreadyExist(fNData) == false) { // input must be a graph input, or already initialized intermediate tensor throw std::runtime_error("TMVA Reshape Op Input Tensor " + fNData + " is not found in model"); diff --git a/tmva/sofie/src/RModel.cxx b/tmva/sofie/src/RModel.cxx index 9caef5ab9da50..80c11a5d8a919 100644 --- a/tmva/sofie/src/RModel.cxx +++ b/tmva/sofie/src/RModel.cxx @@ -1422,6 +1422,21 @@ long RModel::WriteInitializedTensorsToFile(std::string filename) { } } +void RModel::PrintSummary() const { + std::cout << "Summary of model " << GetName() << std::endl; + for(size_t op_idx = 0; op_idx < fOperators.size(); ++op_idx){ + auto& r = *fOperators[op_idx].get(); + std::string raw_name = typeid(r).name(); + // look for ROperator_NAME + std::string name = raw_name.substr(raw_name.find("ROperator_")+10, raw_name.size()); + std::cout << op_idx << " " << name << " : "; + for (auto & t_in : r.GetOpInputTensors()) std::cout << t_in << " "; + std::cout << " ----> "; + for (auto & t_out : r.GetOpOutputTensors()) std::cout << t_out << " "; + std::cout << std::endl; + } +} + void RModel::PrintRequiredInputTensors() const { std::cout << "Model requires following inputs:\n"; for (auto& inputInfo: fInputTensorInfos) { diff --git a/tmva/sofie/test/CMakeLists.txt b/tmva/sofie/test/CMakeLists.txt index f666d200545af..c91b70b481e51 100644 --- a/tmva/sofie/test/CMakeLists.txt +++ b/tmva/sofie/test/CMakeLists.txt @@ -166,16 +166,10 @@ endif() # Any features that link against libpython are disabled if built with tpython=OFF if (tpython AND ROOT_KERAS_FOUND AND BLAS_FOUND) - set(unsupported_keras_version "3.5.0") + configure_file(generateKerasModels.py generateKerasModels.py COPYONLY) + configure_file(scale_by_2_op.hxx scale_by_2_op.hxx COPYONLY) - # TODO: make sure we also support the newest Keras - if (NOT DEFINED ROOT_KERAS_VERSION) - message(WARNING "Keras found, but version unknown — cannot verify compatibility.") - elseif (ROOT_KERAS_VERSION VERSION_LESS ${unsupported_keras_version}) - configure_file(generateKerasModels.py generateKerasModels.py COPYONLY) - configure_file(scale_by_2_op.hxx scale_by_2_op.hxx COPYONLY) - - ROOT_ADD_GTEST(TestRModelParserKeras TestRModelParserKeras.C + ROOT_ADD_GTEST(TestRModelParserKeras TestRModelParserKeras.C LIBRARIES ROOTTMVASofie Python3::NumPy @@ -184,10 +178,9 @@ if (tpython AND ROOT_KERAS_FOUND AND BLAS_FOUND) INCLUDE_DIRS SYSTEM ${CMAKE_CURRENT_BINARY_DIR} - ) - else() - message(WARNING "Keras version ${ROOT_KERAS_VERSION} is too new for the SOFIE Keras parser (only supports < ${unsupported_keras_version})") - endif() + ENVIRONMENT PYTHONPATH=${localruntimedir} + ) + endif() diff --git a/tmva/sofie/test/Conv1dModelGenerator.py b/tmva/sofie/test/Conv1dModelGenerator.py index 12c438094c918..56303d089f77a 100644 --- a/tmva/sofie/test/Conv1dModelGenerator.py +++ b/tmva/sofie/test/Conv1dModelGenerator.py @@ -2,13 +2,12 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn import torch.nn.functional as F - result = [] diff --git a/tmva/sofie/test/Conv2dModelGenerator.py b/tmva/sofie/test/Conv2dModelGenerator.py index 10fccbf8ddc89..04710e5b7a894 100644 --- a/tmva/sofie/test/Conv2dModelGenerator.py +++ b/tmva/sofie/test/Conv2dModelGenerator.py @@ -2,13 +2,12 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn import torch.nn.functional as F - result = [] class Net(nn.Module): diff --git a/tmva/sofie/test/Conv3dModelGenerator.py b/tmva/sofie/test/Conv3dModelGenerator.py index 26a4f855d12cc..8e006b22f4e13 100644 --- a/tmva/sofie/test/Conv3dModelGenerator.py +++ b/tmva/sofie/test/Conv3dModelGenerator.py @@ -2,13 +2,12 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn import torch.nn.functional as F - result = [] class Net(nn.Module): diff --git a/tmva/sofie/test/ConvTrans2dModelGenerator.py b/tmva/sofie/test/ConvTrans2dModelGenerator.py index d39de659e60a7..199995998b3f5 100644 --- a/tmva/sofie/test/ConvTrans2dModelGenerator.py +++ b/tmva/sofie/test/ConvTrans2dModelGenerator.py @@ -2,13 +2,12 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn import torch.nn.functional as F - result = [] class Net(nn.Module): diff --git a/tmva/sofie/test/LinearModelGenerator.py b/tmva/sofie/test/LinearModelGenerator.py index e96cf77501c9f..155ce3043d7ff 100644 --- a/tmva/sofie/test/LinearModelGenerator.py +++ b/tmva/sofie/test/LinearModelGenerator.py @@ -2,13 +2,12 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn import torch.nn.functional as F - result = [] class Net(nn.Module): diff --git a/tmva/sofie/test/RecurrentModelGenerator.py b/tmva/sofie/test/RecurrentModelGenerator.py index ec7c742461d3d..9050e5db97868 100644 --- a/tmva/sofie/test/RecurrentModelGenerator.py +++ b/tmva/sofie/test/RecurrentModelGenerator.py @@ -2,12 +2,10 @@ ### generate COnv2d model using Pytorch -import numpy as np import argparse + import torch import torch.nn as nn -import torch.nn.functional as F - result = [] verbose=False diff --git a/tmva/sofie/test/TestRModelParserKeras.C b/tmva/sofie/test/TestRModelParserKeras.C index 3fef4a0bbd9f2..e99ac05106ae5 100644 --- a/tmva/sofie/test/TestRModelParserKeras.C +++ b/tmva/sofie/test/TestRModelParserKeras.C @@ -48,10 +48,10 @@ TEST(RModelParser_Keras, SEQUENTIAL) Py_Initialize(); - if (gSystem->AccessPathName("KerasModelSequential.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelSequential.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelSequential.h5",{{4,8}}); + TMVA::Experimental:: RSofieReader r("KerasModelSequential.keras",{{4,8}}); std::vector outputSequential = r.Compute(inputSequential); @@ -68,7 +68,7 @@ TEST(RModelParser_Keras, SEQUENTIAL) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelSequential.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelSequential.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.array([0.12107884, 0.89718615, 0.89123899, 0.32197549," "0.17891638, 0.83555135, 0.98680066, 0.14496809," "0.07255503, 0.55386989, 0.6628149 , 0.29843291," @@ -101,10 +101,10 @@ TEST(RModelParser_Keras, FUNCTIONAL) Py_Initialize(); - if (gSystem->AccessPathName("KerasModelFunctional.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelFunctional.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelFunctional.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelFunctional.keras"); std::vector outputFunctional = r.Compute(inputFunctional); PyObject* main = PyImport_AddModule("__main__"); @@ -120,7 +120,7 @@ TEST(RModelParser_Keras, FUNCTIONAL) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelFunctional.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelFunctional.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.array([0.60828574, 0.50069386, 0.75186709, 0.14968806, 0.7692464 ,0.77027585, 0.75095316, 0.96651197," "0.38536308, 0.95565917, 0.62796356, 0.13818375, 0.65484891,0.89220363, 0.23879365, 0.00635323]).reshape(2,8)",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); @@ -146,10 +146,10 @@ TEST(RModelParser_Keras, BATCH_NORM) 0.69947928, 0.29743695, 0.81379782, 0.39650574}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelBatchNorm.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelBatchNorm.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelBatchNorm.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelBatchNorm.keras"); std::vector outputBatchNorm = r.Compute(inputBatchNorm); PyObject* main = PyImport_AddModule("__main__"); @@ -165,7 +165,7 @@ TEST(RModelParser_Keras, BATCH_NORM) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelBatchNorm.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelBatchNorm.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.array([0.22308163, 0.95274901, 0.44712538, 0.84640867," "0.69947928, 0.29743695, 0.81379782, 0.39650574]).reshape(2,4)",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); @@ -196,10 +196,10 @@ TEST(DISABLED_RModelParser_Keras, CONV_VALID) 1,1,1,1, 1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelConv2D_Valid.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelConv2D_Valid.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelConv2D_Valid.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelConv2D_Valid.keras"); std::vector outputConv2D_Valid = r.Compute(inputConv2D_Valid); PyObject* main = PyImport_AddModule("__main__"); @@ -215,7 +215,7 @@ TEST(DISABLED_RModelParser_Keras, CONV_VALID) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelConv2D_Valid.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelConv2D_Valid.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.ones((1,4,4,1))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); @@ -246,10 +246,10 @@ TEST(DISABLED_RModelParser_Keras, CONV_SAME) 1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelConv2D_Same.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelConv2D_Same.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelConv2D_Same.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelConv2D_Same.keras"); std::vector outputConv2D_Same = r.Compute(inputConv2D_Same); PyObject* main = PyImport_AddModule("__main__"); @@ -265,7 +265,7 @@ TEST(DISABLED_RModelParser_Keras, CONV_SAME) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelConv2D_Same.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelConv2D_Same.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.ones((1,4,4,1))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); @@ -292,10 +292,10 @@ TEST(RModelParser_Keras, RESHAPE) 1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelReshape.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelReshape.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelReshape.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelReshape.keras"); std::vector outputReshape = r.Compute(inputReshape); PyObject* main = PyImport_AddModule("__main__"); @@ -311,7 +311,7 @@ TEST(RModelParser_Keras, RESHAPE) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelReshape.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelReshape.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.ones((1,4,4,1))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); @@ -336,10 +336,10 @@ TEST(RModelParser_Keras, CONCATENATE) std::vectorinputConcatenate_2 = {1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelConcatenate.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelConcatenate.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelConcatenate.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelConcatenate.keras",{{1,2},{1,2}}); std::vector outputConcatenate = r.Compute(inputConcatenate_1, inputConcatenate_2); PyObject* main = PyImport_AddModule("__main__"); @@ -355,15 +355,15 @@ TEST(RModelParser_Keras, CONCATENATE) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelConcatenate.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelConcatenate.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input_1=numpy.ones((1,2))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input_2=numpy.ones((1,2))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model([input_1,input_2]).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputConcatenateSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); + long pOutputConcatenateSize=PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); - //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputConcatenate.size(), pOutputConcatenateSize); + //Testing the actual and expected output tensor sizes (can fail if an error eccoured and returns a -1 from Python) + EXPECT_EQ((long) outputConcatenate.size(), pOutputConcatenateSize); PyArrayObject* pConcatenateValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); float* pOutputConcatenate=(float*)PyArray_DATA(pConcatenateValues); @@ -377,14 +377,15 @@ TEST(RModelParser_Keras, CONCATENATE) TEST(RModelParser_Keras, BINARY_OP) { constexpr float TOLERANCE = DEFAULT_TOLERANCE; - std::vectorinput_BinaryOp_1 = {1,1}; - std::vectorinput_BinaryOp_2 = {1,1}; + // test with batch size =2 input shapes are {2,2} + std::vectorinput_BinaryOp_1 = {1,2,3,4}; + std::vectorinput_BinaryOp_2 = {5,6,7,8}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelBinaryOp.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelBinaryOp.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelBinaryOp.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelBinaryOp.keras", {{2,2},{2,2}}); std::vector outputBinaryOp = r.Compute(input_BinaryOp_1,input_BinaryOp_2); PyObject* main = PyImport_AddModule("__main__"); @@ -400,15 +401,15 @@ TEST(RModelParser_Keras, BINARY_OP) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelBinaryOp.h5')",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input1=numpy.array([1,1])",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("input2=numpy.array([1,1])",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelBinaryOp.keras')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("input1=numpy.array([[1,2],[3,4]],dtype='float32')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("input2=numpy.array([[5,6],[7,8]],dtype='float32')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model([input1,input2]).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); - std::size_t pOutputBinaryOpSize=(std::size_t)PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); + long pOutputBinaryOpSize=PyLong_AsLong(PyDict_GetItemString(fLocalNS,"outputSize")); //Testing the actual and expected output tensor sizes - EXPECT_EQ(outputBinaryOp.size(), pOutputBinaryOpSize); + EXPECT_EQ((long) outputBinaryOp.size(), pOutputBinaryOpSize); PyArrayObject* pBinaryOpValues=(PyArrayObject*)PyDict_GetItemString(fLocalNS,"output"); float* pOutputBinaryOp=(float*)PyArray_DATA(pBinaryOpValues); @@ -425,10 +426,10 @@ TEST(RModelParser_Keras, ACTIVATIONS) std::vectorinputActivations = {1,1,1,1,1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelActivations.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelActivations.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelActivations.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelActivations.keras"); std::vector outputActivations = r.Compute(inputActivations); PyObject* main = PyImport_AddModule("__main__"); @@ -444,7 +445,7 @@ TEST(RModelParser_Keras, ACTIVATIONS) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelActivations.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelActivations.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.ones((1,8))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); @@ -468,10 +469,10 @@ TEST(RModelParser_Keras, SWISH) std::vectorinput = {1,1,1,1,1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelSwish.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelSwish.keras",kFileExists)) GenerateModels(); - TMVA::Experimental:: RSofieReader r("KerasModelSwish.h5"); + TMVA::Experimental:: RSofieReader r("KerasModelSwish.keras"); std::vector output = r.Compute(input); PyObject* main = PyImport_AddModule("__main__"); @@ -487,7 +488,7 @@ TEST(RModelParser_Keras, SWISH) PyRun_String("os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("from tensorflow.keras.models import load_model",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelSwish.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelSwish.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.ones((1,8))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("outputSize=output.size",Py_single_input,fGlobalNS,fLocalNS); @@ -507,11 +508,12 @@ TEST(RModelParser_Keras, SWISH) TEST(RModel, CUSTOM_OP) { + GTEST_SKIP() << "Skipping custop op test since it is not yet supported in new Python Keras parser"; constexpr float TOLERANCE = DEFAULT_TOLERANCE; std::vectorinput_custom ={1,1,1,1,1,1,1,1}; Py_Initialize(); - if (gSystem->AccessPathName("KerasModelCustomOp.h5",kFileExists)) + if (gSystem->AccessPathName("KerasModelCustomOp.keras",kFileExists)) GenerateModels(); @@ -531,7 +533,7 @@ TEST(RModel, CUSTOM_OP) PyRun_String("from tensorflow.keras.layers import Lambda",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("import numpy",Py_single_input,fGlobalNS,fLocalNS); - PyRun_String("model=load_model('KerasModelCustomOp.h5')",Py_single_input,fGlobalNS,fLocalNS); + PyRun_String("model=load_model('KerasModelCustomOp.keras')",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("model.add(Lambda(lambda x: x * 2))",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("input=numpy.array([1,1,1,1,1,1,1,1]).reshape(1,8)",Py_single_input,fGlobalNS,fLocalNS); PyRun_String("output=model(input).numpy()",Py_single_input,fGlobalNS,fLocalNS); @@ -549,7 +551,7 @@ TEST(RModel, CUSTOM_OP) /*output shapes*/"{{1,4}}", /*header file name with the compute function*/ "scale_by_2_op.hxx"); // need to load model afterwards - r.Load("KerasModelCustomOp.h5",{}, false); + r.Load("KerasModelCustomOp.keras",{}, false); std::vector outputCustomOp = r.Compute(input_custom); //Testing the actual and expected output tensor sizes diff --git a/tmva/sofie/test/generateKerasModels.py b/tmva/sofie/test/generateKerasModels.py index 7fc05741662aa..163e5de5a75e8 100644 --- a/tmva/sofie/test/generateKerasModels.py +++ b/tmva/sofie/test/generateKerasModels.py @@ -1,14 +1,28 @@ import os + os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" os.environ["CUDA_VISIBLE_DEVICES"] = "" import numpy as np -import tensorflow as tf -from tensorflow.keras.models import Model,Sequential -from tensorflow.keras.layers import Input,Dense,Activation,ReLU,LeakyReLU,BatchNormalization,Conv2D,Reshape,Concatenate,Add,Subtract,Multiply +from tensorflow.keras.layers import ( + Activation, + Add, + BatchNormalization, + Concatenate, + Conv2D, + Dense, + Input, + LeakyReLU, + Multiply, + ReLU, + Reshape, + Subtract, +) +from tensorflow.keras.models import Model, Sequential from tensorflow.keras.optimizers import SGD + def generateFunctionalModel(): input=Input(shape=(8,),batch_size=2) x=Dense(16)(input) @@ -24,7 +38,7 @@ def generateFunctionalModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=2) - model.save('KerasModelFunctional.h5') + model.save('KerasModelFunctional.keras') def generateSequentialModel(): model=Sequential() @@ -39,7 +53,7 @@ def generateSequentialModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=4) - model.save('KerasModelSequential.h5') + model.save('KerasModelSequential.keras') def generateBatchNormModel(): model=Sequential() @@ -53,7 +67,7 @@ def generateBatchNormModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=2) - model.save('KerasModelBatchNorm.h5') + model.save('KerasModelBatchNorm.keras') def generateConv2DModel_ValidPadding(): model=Sequential() @@ -65,7 +79,7 @@ def generateConv2DModel_ValidPadding(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=2) - model.save('KerasModelConv2D_Valid.h5') + model.save('KerasModelConv2D_Valid.keras') def generateConv2DModel_SamePadding(): model=Sequential() @@ -77,7 +91,7 @@ def generateConv2DModel_SamePadding(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=2) - model.save('KerasModelConv2D_Same.h5') + model.save('KerasModelConv2D_Same.keras') def generateReshapeModel(): model = Sequential() @@ -90,7 +104,7 @@ def generateReshapeModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=2) - model.save('KerasModelReshape.h5') + model.save('KerasModelReshape.keras') def generateConcatModel(): input_1 = Input(shape=(2,)) @@ -107,7 +121,7 @@ def generateConcatModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit([x1_train,x2_train], y_train, verbose=0, epochs=10, batch_size=1) - model.save('KerasModelConcatenate.h5') + model.save('KerasModelConcatenate.keras') def generateBinaryOpModel(): input1 = Input(shape=(2, )) @@ -124,7 +138,7 @@ def generateBinaryOpModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit([x1_train,x2_train], y_train, epochs=10, verbose=0, batch_size=2) - model.save('KerasModelBinaryOp.h5') + model.save('KerasModelBinaryOp.keras') def generateActivationModel(): input=Input(shape=(8,)) @@ -140,7 +154,7 @@ def generateActivationModel(): model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, epochs=10, verbose=0, batch_size=1) - model.save('KerasModelActivations.h5') + model.save('KerasModelActivations.keras') def generateSwishModel(): # Create the Keras model @@ -157,7 +171,7 @@ def generateSwishModel(): x_train=randomGenerator.rand(1,8) y_train=randomGenerator.rand(1,1) model.fit(x_train, y_train, epochs=10, verbose=0, batch_size=1) - model.save('KerasModelSwish.h5') + model.save('KerasModelSwish.keras') def generateCustomModel(): model = Sequential() @@ -167,7 +181,7 @@ def generateCustomModel(): y_train=randomGenerator.rand(1,4) model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01)) model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=1) - model.save('KerasModelCustomOp.h5') + model.save('KerasModelCustomOp.keras') print("generating Keras models for testing.....") generateFunctionalModel() diff --git a/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h b/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h index 7e9618306ba74..ed658526065c9 100644 --- a/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h +++ b/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h @@ -4,7 +4,7 @@ /********************************************************************************** * Project: TMVA - a Root-integrated toolkit for multivariate data analysis * * Package: TMVA * - * * + * * * * * Description: * * Functionality for parsing a saved Keras .H5 model into RModel object * @@ -18,7 +18,7 @@ * * * Redistribution and use in source and binary forms, with or without * * modification, are permitted according to the terms listed in LICENSE * - * (see tmva/doc/LICENSE) * + * (see tmva/doc/LICENSE) * **********************************************************************************/ @@ -36,7 +36,7 @@ namespace TMVA::Experimental::SOFIE::PyKeras { -/// Parser function for translatng Keras .h5 model into a RModel object. +/// Parser function for translating Keras .h5 model into a RModel object. /// Accepts the file location of a Keras model and returns the /// equivalent RModel object. /// One can specify as option a batch size that can be used when the input Keras model diff --git a/tmva/sofie_parsers/src/RModelParser_Keras.cxx b/tmva/sofie_parsers/src/RModelParser_Keras.cxx index 3f923fb4bece8..9f4d334b4cd37 100644 --- a/tmva/sofie_parsers/src/RModelParser_Keras.cxx +++ b/tmva/sofie_parsers/src/RModelParser_Keras.cxx @@ -1,1079 +1,19 @@ // @(#)root/tmva/pymva $Id$ // Author: Sanjiban Sengupta 2021 -/********************************************************************************** - * Project : TMVA - a Root-integrated toolkit for multivariate data analysis * - * Package : TMVA * - * Function: TMVA::Experimental::SOFIE::PyKeras::Parse * - * * - * Description: * - * Parser function for translating Keras .h5 model to RModel object * - * * - * Example Usage: * - * ~~~ {.cpp} * - * using TMVA::Experimental::SOFIE; * - * RModel model = PyKeras::Parse("trained_model_dense.h5"); * - * ~~~ * - * * - **********************************************************************************/ #include "TMVA/RModelParser_Keras.h" -#include - -#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION -#include - namespace TMVA::Experimental::SOFIE::PyKeras { -namespace { - -// Utility functions (taken from PyMethodBase in PyMVA) - -void PyRunString(TString code, PyObject *globalNS, PyObject *localNS) -{ - PyObject *fPyReturn = PyRun_String(code, Py_single_input, globalNS, localNS); - if (!fPyReturn) { - std::cout << "\nPython error message:\n"; - PyErr_Print(); - throw std::runtime_error("\nFailed to run python code: " + code); - } -} - -const char *PyStringAsString(PyObject *string) -{ - PyObject *encodedString = PyUnicode_AsUTF8String(string); - const char *cstring = PyBytes_AsString(encodedString); - return cstring; -} - -std::vector GetDataFromTuple(PyObject *tupleObject) -{ - std::vector tupleVec; - for (Py_ssize_t tupleIter = 0; tupleIter < PyTuple_Size(tupleObject); ++tupleIter) { - auto itemObj = PyTuple_GetItem(tupleObject, tupleIter); - if (itemObj == Py_None) - tupleVec.push_back(0); // case shape is for example (None,2,3) - else - tupleVec.push_back((size_t)PyLong_AsLong(itemObj)); - } - return tupleVec; -} - -PyObject *GetValueFromDict(PyObject *dict, const char *key) -{ - return PyDict_GetItemWithError(dict, PyUnicode_FromString(key)); -} - -} // namespace - -namespace INTERNAL{ - -// For adding Keras layer into RModel object -void AddKerasLayer(RModel &rmodel, PyObject *fLayer); - -// Declaring Internal Functions for Keras layers which don't have activation as an additional attribute -std::unique_ptr MakeKerasActivation(PyObject *fLayer); // For instantiating ROperator for Keras Activation Layer -std::unique_ptr MakeKerasReLU(PyObject *fLayer); // For instantiating ROperator for Keras ReLU layer -std::unique_ptr MakeKerasSelu(PyObject *fLayer); // For instantiating ROperator for Keras Selu layer -std::unique_ptr MakeKerasSigmoid(PyObject *fLayer); // For instantiating ROperator for Keras Sigmoid layer -std::unique_ptr MakeKerasSwish(PyObject *fLayer); // For instantiating ROperator for Keras Swish layer -std::unique_ptr MakeKerasPermute(PyObject *fLayer); // For instantiating ROperator for Keras Permute Layer -std::unique_ptr MakeKerasBatchNorm(PyObject *fLayer); // For instantiating ROperator for Keras Batch Normalization Layer -std::unique_ptr MakeKerasReshape(PyObject *fLayer); // For instantiating ROperator for Keras Reshape Layer -std::unique_ptr MakeKerasConcat(PyObject *fLayer); // For instantiating ROperator for Keras Concat Layer -std::unique_ptr MakeKerasBinary(PyObject *fLayer); // For instantiating ROperator for Keras binary operations: Add, Subtract & Multiply. -std::unique_ptr MakeKerasSoftmax(PyObject *fLayer); // For instantiating ROperator for Keras Softmax Layer -std::unique_ptr MakeKerasTanh(PyObject *fLayer); // For instantiating ROperator for Keras Tanh Layer -std::unique_ptr MakeKerasLeakyRelu(PyObject *fLayer); // For instantiating ROperator for Keras LeakyRelu Layer -std::unique_ptr MakeKerasIdentity(PyObject *fLayer); // For instantiating ROperator for Keras Identity Layer - - -// Declaring Internal function for Keras layers which have additional activation attribute -std::unique_ptr MakeKerasDense(PyObject *fLayer); // For instantiating ROperator for Keras Dense Layer -std::unique_ptr MakeKerasConv(PyObject *fLayer); // For instantiating ROperator for Keras Conv Layer - -// For mapping Keras layer with the preparatory functions for ROperators -using KerasMethodMap = std::unordered_map (*)(PyObject *fLayer)>; -using KerasMethodMapWithActivation = std::unordered_map (*)(PyObject *fLayer)>; - -const KerasMethodMap mapKerasLayer = { - {"Activation", &MakeKerasActivation}, - {"Permute", &MakeKerasPermute}, - {"BatchNormalization", &MakeKerasBatchNorm}, - {"Reshape", &MakeKerasReshape}, - {"Concatenate", &MakeKerasConcat}, - {"swish", &MakeKerasSwish}, - {"Add", &MakeKerasBinary}, - {"Subtract", &MakeKerasBinary}, - {"Multiply", &MakeKerasBinary}, - {"Softmax", &MakeKerasSoftmax}, - {"tanh", &MakeKerasTanh}, - {"LeakyReLU", &MakeKerasLeakyRelu}, - {"Identity", &MakeKerasIdentity}, - {"Dropout", &MakeKerasIdentity}, - - // For activation layers - {"ReLU", &MakeKerasReLU}, - - // For layers with activation attributes - {"relu", &MakeKerasReLU}, - {"selu", &MakeKerasSelu}, - {"sigmoid", &MakeKerasSigmoid}, - {"softmax", &MakeKerasSoftmax} -}; - -const KerasMethodMapWithActivation mapKerasLayerWithActivation = { - {"Dense", &MakeKerasDense}, - {"Conv2D", &MakeKerasConv}, - }; - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Adds equivalent ROperator with respect to Keras model layer -/// into the referenced RModel object -/// -/// \param[in] rmodel RModel object -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \param[out] RModel object with the added ROperator -/// -/// Function adds equivalent ROperator into the referenced RModel object. -/// Keras models can have layers like Dense and Conv which have activation -/// function as an attribute. Function first searches if layer object is among -/// the ones which don't have activation attribute and then calls the respective -/// preparation function to get the ROperator object, which is then added -/// into the RModel object. If passed layer is among the ones which may have activation -/// attribute, then it checks for the activation attribute, if present then first adds -/// the primary operator into the RModel object, and then adds the operator for the -/// activation function with appropriate changes in the names of input and output -/// tensors for both of them. -/// Example of such layers is the Dense Layer. For a dense layer with input tensor name -/// dense2BiasAdd0 and output tensor name dense3Relu0 with relu as activation attribute -/// will be transformed into a ROperator_Gemm with input tensor name dense2BiasAdd0 -/// & output tensor name dense3Dense (layerName+layerType), and a subsequent -/// ROperator_Relu with input tensor name as dense3Dense and output tensor name -/// as dense3Relu0. -/// -/// For developing new preparatory functions for supporting Keras layers in future, -/// all one needs is to extract the required properties and attributes from the fLayer -/// dictionary which contains all the information about any Keras layer and after -/// any required transformations, these are passed for instantiating the ROperator -/// object. -/// -/// The fLayer dictionary which holds all the information about a Keras layer has -/// following structure:- -/// -/// dict fLayer { 'layerType' : Type of the Keras layer -/// 'layerAttributes' : Attributes of the keras layer as returned by layer.get_config() -/// 'layerInput' : List of names of input tensors -/// 'layerOutput' : List of names of output tensors -/// 'layerDType' : Data-type of the Keras layer -/// 'layerWeight' : List of weight tensor names of Keras layers -/// } -void AddKerasLayer(RModel& rmodel, PyObject* fLayer){ - std::string fLayerType = PyStringAsString(GetValueFromDict(fLayer,"layerType")); - - if(fLayerType == "Reshape"){ - PyObject* fAttributes=GetValueFromDict(fLayer,"layerAttributes"); - std::string fLayerName = PyStringAsString(GetValueFromDict(fAttributes,"_name")); - PyObject* fPTargetShape = GetValueFromDict(fAttributes,"target_shape"); - std::vectorfTargetShape = GetDataFromTuple(fPTargetShape); - std::shared_ptr fData(malloc(fTargetShape.size() * sizeof(int64_t)), free); - std::copy(fTargetShape.begin(),fTargetShape.end(),(int64_t*)fData.get()); - rmodel.AddInitializedTensor(fLayerName+"ReshapeAxes",ETensorType::INT64,{fTargetShape.size()},fData); - } - - //For layers without additional activation attribute - auto findLayer = mapKerasLayer.find(fLayerType); - if(findLayer != mapKerasLayer.end()){ - rmodel.AddOperator((findLayer->second)(fLayer)); - return; - } - - //For layers like Dense & Conv which has additional activation attribute - else if(mapKerasLayerWithActivation.find(fLayerType) != mapKerasLayerWithActivation.end()){ - findLayer = mapKerasLayerWithActivation.find(fLayerType); - PyObject* fAttributes=GetValueFromDict(fLayer,"layerAttributes"); - - std::string fLayerName = PyStringAsString(GetValueFromDict(fAttributes,"_name")); - - PyObject* fPActivation = GetValueFromDict(fAttributes,"activation"); - std::string fLayerActivation = PyStringAsString(PyObject_GetAttrString(fPActivation,"__name__")); - - if(fLayerActivation == "selu" || fLayerActivation == "sigmoid") - rmodel.AddNeededStdLib("cmath"); - - - //Checking if additional attribute exixts - if(fLayerActivation != "linear"){ - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - std::string fActivationLayerOutput = PyStringAsString(PyList_GetItem(fOutputs,0)); - - if(fLayerType == "Conv2D"){ - std::unique_ptr op_pre_transpose; - op_pre_transpose.reset(new ROperator_Transpose({0,3,1,2}, PyStringAsString(PyList_GetItem(fInputs,0)), fLayerName+"PreTrans")); - rmodel.AddOperator(std::move(op_pre_transpose)); - - PyList_SetItem(fInputs,0,PyUnicode_FromString((fLayerName+"PreTrans").c_str())); - PyDict_SetItemString(fLayer,"layerInput",fInputs); - } - - // Making changes in the names of the input and output tensor names - PyList_SetItem(fOutputs,0,PyUnicode_FromString((fLayerName+fLayerType).c_str())); - PyDict_SetItemString(fLayer,"layerOutput",fOutputs); - rmodel.AddOperator((findLayer->second)(fLayer)); - - std::string fActivationLayerInput = fLayerName+fLayerType; - if(fLayerType == "Conv2D"){ - std::unique_ptr op_post_transpose; - op_post_transpose.reset(new ROperator_Transpose({0,2,3,1}, fLayerName+fLayerType, fLayerName+"PostTrans")); - rmodel.AddOperator(std::move(op_post_transpose)); - fActivationLayerInput = fLayerName+"PostTrans"; - } - - PyList_SetItem(fInputs,0,PyUnicode_FromString(fActivationLayerInput.c_str())); - PyList_SetItem(fOutputs,0,PyUnicode_FromString(fActivationLayerOutput.c_str())); - PyDict_SetItemString(fLayer,"layerInput",fInputs); - PyDict_SetItemString(fLayer,"layerOutput",fOutputs); - - auto findActivationLayer = mapKerasLayer.find(fLayerActivation); - if(findActivationLayer == mapKerasLayer.end()){ - throw std::runtime_error("TMVA::SOFIE - Parsing Keras Activation layer " + fLayerActivation + " is not yet supported"); - } - rmodel.AddOperator((findActivationLayer->second)(fLayer)); - - } - else{ - rmodel.AddOperator((findLayer->second)(fLayer)); - } - return; - } - - else{ - throw std::runtime_error("TMVA::SOFIE - Parsing Keras layer " + fLayerType + " is not yet supported"); - } - -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Dense Layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For Keras's Dense layer, the names of the input tensor, output tensor, and -/// weight tensors are extracted, and then are passed to instantiate a -/// ROperator_Gemm object using the required attributes. -std::unique_ptr MakeKerasDense(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - // Extracting names of weight tensors - // The names of Kernel weights and bias weights are found in the list - // of weight tensors from fLayer. - PyObject* fWeightNames = GetValueFromDict(fLayer,"layerWeight"); - std::string fKernelName = PyStringAsString(PyList_GetItem(fWeightNames,0)); - std::string fBiasName = PyStringAsString(PyList_GetItem(fWeightNames,1)); - - std::unique_ptr op; - - float attr_alpha = 1.0; - float attr_beta = 1.0; - int_t attr_transA = 0; - int_t attr_transB = 0; - - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Gemm(attr_alpha, attr_beta, attr_transA, attr_transB, fLayerInputName, fKernelName, fBiasName, fLayerOutputName)); - break; - - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Gemm does not yet support input type " + fLayerDType); - } - return op; -} - - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Conv Layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For Keras's Conv layer, the names of the input tensor, output tensor, and -/// weight tensors are extracted, along with attributes like dilation_rate, -/// groups, kernel size, padding, strides. Padding attribute is then -/// computed for ROperator depending on Keras' attribute parameter. -std::unique_ptr MakeKerasConv(PyObject* fLayer){ - PyObject* fAttributes = GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - // Extracting names of weight tensors - // The names of Kernel weights and bias weights are found in the list - // of weight tensors from fLayer. - PyObject* fWeightNames = GetValueFromDict(fLayer,"layerWeight"); - std::string fKernelName = PyStringAsString(PyList_GetItem(fWeightNames,0)); - std::string fBiasName = PyStringAsString(PyList_GetItem(fWeightNames,1)); - - // Extracting the Conv Node Attributes - PyObject* fDilations = GetValueFromDict(fAttributes,"dilation_rate"); - PyObject* fGroup = GetValueFromDict(fAttributes,"groups"); - PyObject* fKernelShape = GetValueFromDict(fAttributes,"kernel_size"); - PyObject* fPads = GetValueFromDict(fAttributes,"padding"); - PyObject* fStrides = GetValueFromDict(fAttributes,"strides"); - - std::vector fAttrDilations = GetDataFromTuple(fDilations); - - - size_t fAttrGroup = PyLong_AsLong(fGroup); - std::vector fAttrKernelShape = GetDataFromTuple(fKernelShape); - std::vector fAttrStrides = GetDataFromTuple(fStrides); - std::string fAttrAutopad; - std::vectorfAttrPads; - - //Seting the layer padding - std::string fKerasPadding = PyStringAsString(fPads); - if(fKerasPadding == "valid"){ - fAttrAutopad = "VALID"; - } - else if(fKerasPadding == "same"){ - fAttrAutopad="NOTSET"; - PyObject* fInputShape = GetValueFromDict(fAttributes,"_batch_input_shape"); - long inputHeight = PyLong_AsLong(PyTuple_GetItem(fInputShape,1)); - long inputWidth = PyLong_AsLong(PyTuple_GetItem(fInputShape,2)); - - long outputHeight = std::ceil(float(inputHeight) / float(fAttrStrides[0])); - long outputWidth = std::ceil(float(inputWidth) / float(fAttrStrides[1])); - - long padding_height = std::max(long((outputHeight - 1) * fAttrStrides[0] + fAttrKernelShape[0] - inputHeight),0L); - long padding_width = std::max(long((outputWidth - 1) * fAttrStrides[1] + fAttrKernelShape[1] - inputWidth),0L); - - size_t padding_top = std::floor(padding_height/2); - size_t padding_bottom = padding_height - padding_top; - size_t padding_left = std::floor(padding_width/2); - size_t padding_right = padding_width - padding_left; - fAttrPads = {padding_top,padding_bottom,padding_left,padding_right}; - } - else{ - throw std::runtime_error("TMVA::SOFIE - RModel Keras Parser doesn't yet supports Convolution layer with padding " + fKerasPadding); - } - - std::unique_ptr op; - - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Conv(fAttrAutopad, fAttrDilations, fAttrGroup, fAttrKernelShape, fAttrPads, fAttrStrides, fLayerInputName, fKernelName, fBiasName, fLayerOutputName)); - break; - - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Conv does not yet support input type " + fLayerDType); - } - return op; -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras activation layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For Keras's keras.layers.Activation layer, the activation attribute is -/// extracted and appropriate function for adding the function is called. -std::unique_ptr MakeKerasActivation(PyObject* fLayer){ - PyObject* fAttributes=GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fPActivation = GetValueFromDict(fAttributes,"activation"); - std::string fLayerActivation = PyStringAsString(PyObject_GetAttrString(fPActivation,"__name__")); - - auto findLayer = mapKerasLayer.find(fLayerActivation); - if(findLayer == mapKerasLayer.end()){ - throw std::runtime_error("TMVA::SOFIE - Parsing Keras Activation layer " + fLayerActivation + " is not yet supported"); - } - return (findLayer->second)(fLayer); -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras ReLU activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Relu object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasReLU(PyObject* fLayer) -{ - PyObject* fInputs=GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs=GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Relu(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Relu does not yet support input type " + fLayerDType); - } - return op; -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Selu activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Selu object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasSelu(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Selu(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Selu does not yet support input type " + fLayerDType); - } - return op; -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Sigmoid activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Sigmoid object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasSigmoid(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Sigmoid(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType); - } - return op; -} -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Softmax activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Softmax object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasSoftmax(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); +RModel Parse(std::string /*filename*/, int /* batch_size */ ){ - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); + throw std::runtime_error("TMVA::SOFIE C++ Keras parser is deprecated. Use the python3 function " + "model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse('model.keras',batch_size=1) " ); - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Softmax(/*default axis is -1*/-1,fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType); - } - return op; + return RModel(); } - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Leaky Relu activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_LeakyRelu object, the names of -/// input & output tensors, the data-type and the alpha attribute of the layer -/// are extracted. -std::unique_ptr MakeKerasLeakyRelu(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - PyObject* fAttributes=GetValueFromDict(fLayer,"layerAttributes"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - float fAlpha = (float)PyFloat_AsDouble(GetValueFromDict(fAttributes,"alpha")); - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_LeakyRelu(fAlpha, fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Tanh activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Tanh object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasTanh(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Tanh(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Tanh does not yet support input type " + fLayerDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Swish activation -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Swish object, the names of -/// input & output tensors and the data-type of the layer are extracted. -std::unique_ptr MakeKerasSwish(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Swish(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Swish does not yet support input type " + fLayerDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Permute layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// The Permute layer in Keras has an equivalent Tranpose operator in ONNX. -/// For adding a Transpose operator, the permute dimensions are found, if they -/// exist are passed in instantiating the ROperator, else default values are used. -std::unique_ptr MakeKerasPermute(PyObject* fLayer) -{ - // Extracting required layer information - PyObject* fAttributes=GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fInputs=GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs=GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - // Extracting the permute dimensions present in Attributes of the Keras layer - PyObject* fAttributePermute = GetValueFromDict(fAttributes,"dims"); - std::vectorfPermuteDims; - - // Building vector of permute dimensions from the Tuple object. - for(Py_ssize_t tupleIter=0;tupleIter op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT:{ - - // Adding the permute dimensions if present, else are avoided to use default values. - if (!fPermuteDims.empty()){ - op.reset(new ROperator_Transpose(fPermuteDims, fLayerInputName, fLayerOutputName)); - } - else{ - op.reset(new ROperator_Transpose (fLayerInputName, fLayerOutputName)); - } - break; - } - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Transpose does not yet support input type " + fLayerDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras BatchNorm layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -std::unique_ptr MakeKerasBatchNorm(PyObject* fLayer) -{ - // Extracting required layer information - PyObject* fAttributes = GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - PyObject* fGamma = GetValueFromDict(fAttributes,"gamma"); - PyObject* fBeta = GetValueFromDict(fAttributes,"beta"); - PyObject* fMoving_Mean = GetValueFromDict(fAttributes,"moving_mean"); - PyObject* fMoving_Var = GetValueFromDict(fAttributes,"moving_variance"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fNX = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fNY = PyStringAsString(PyList_GetItem(fOutputs,0)); - std::string fNScale = PyStringAsString(PyObject_GetAttrString(fGamma,"name")); - std::string fNB = PyStringAsString(PyObject_GetAttrString(fBeta,"name")); - std::string fNMean = PyStringAsString(PyObject_GetAttrString(fMoving_Mean,"name")); - std::string fNVar = PyStringAsString(PyObject_GetAttrString(fMoving_Var,"name")); - float fEpsilon = (float)PyFloat_AsDouble(GetValueFromDict(fAttributes,"epsilon")); - float fMomentum = (float)PyFloat_AsDouble(GetValueFromDict(fAttributes,"momentum")); - - std::unique_ptr op; - op.reset(new ROperator_BatchNormalization(fEpsilon, fMomentum, /* training mode */ 0, fNX, fNScale, fNB, fNMean, fNVar, fNY)); - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Reshape layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -std::unique_ptr MakeKerasReshape(PyObject* fLayer) -{ - // Extracting required layer information - PyObject* fAttributes = GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerName = PyStringAsString(GetValueFromDict(fAttributes,"_name")); - - ReshapeOpMode fOpMode = Reshape; - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fNameData = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fNameOutput = PyStringAsString(PyList_GetItem(fOutputs,0)); - std::string fNameShape = fLayerName + "ReshapeAxes"; - std::unique_ptr op; - op.reset(new ROperator_Reshape(fOpMode, /*allow zero*/0, fNameData, fNameShape, fNameOutput)); - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Concat layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -std::unique_ptr MakeKerasConcat(PyObject* fLayer) -{ - PyObject* fAttributes = GetValueFromDict(fLayer,"layerAttributes"); - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::vector inputs; - for(Py_ssize_t i=0; i op; - op.reset(new ROperator_Concat(inputs, axis, 0, output)); - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras binary operations like Add, -/// subtract, and multiply. -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_BasicBinary object, the names of -/// input & output tensors, the data-type of the layer and the operation type -/// are extracted. -std::unique_ptr MakeKerasBinary(PyObject* fLayer){ - PyObject* fInputs = GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs = GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerType = PyStringAsString(GetValueFromDict(fLayer,"layerType")); - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fX1 = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fX2 = PyStringAsString(PyList_GetItem(fInputs,1)); - std::string fY = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT:{ - if(fLayerType == "Add") - op.reset(new ROperator_BasicBinary (fX1, fX2, fY)); - else if(fLayerType == "Subtract") - op.reset(new ROperator_BasicBinary (fX1, fX2, fY)); - else - op.reset(new ROperator_BasicBinary (fX1, fX2, fY)); - break; - } - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator object for Keras Identity and Dropout Layer -/// -/// \param[in] fLayer Python Keras layer as a Dictionary object -/// \return Unique pointer to ROperator object -/// -/// Dropout will have no effect in inference, so instead an Identity operator -/// is added to mimic its presence in the Keras model -std::unique_ptr MakeKerasIdentity(PyObject* fLayer) -{ - PyObject* fInputs=GetValueFromDict(fLayer,"layerInput"); - PyObject* fOutputs=GetValueFromDict(fLayer,"layerOutput"); - - std::string fLayerDType = PyStringAsString(GetValueFromDict(fLayer,"layerDType")); - std::string fLayerInputName = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fLayerOutputName = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fLayerDType)){ - case ETensorType::FLOAT: - op.reset(new ROperator_Identity(fLayerInputName, fLayerOutputName)); - break; - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Identity does not yet support input type " + fLayerDType); - } - return op; -} - -}//INTERNAL - - -////////////////////////////////////////////////////////////////////////////////// -/// \param[in] filename file location of Keras .h5 -/// \param[in] batch_size if not given, 1 is used if the model does not provide it -/// \return Parsed RModel object -/// -/// The `Parse()` function defined in `TMVA::Experimental::SOFIE::PyKeras` will -/// parse a trained Keras .h5 model into a RModel Object. After loading the model -/// in a Python Session, the included layers are extracted with properties -/// like Layer type, Attributes, Input tensor names, Output tensor names, data-type -/// and names of the weight/initialized tensors. -/// The extracted layers from the model are then passed into `AddKerasLayer()` -/// which prepares the specific ROperator and adds them into the RModel object. -/// The layers are also checked for adding any required routines for executing -/// the generated Inference code. -/// -/// For adding the Initialized tensors into the RModel object, the weights are -/// extracted from the Keras model in the form of NumPy arrays, which are then -/// passed into `AddInitializedTensor()` after appropriate casting. -/// -/// Input tensor infos are required to be added which will contain their names, -/// shapes and data-types. For keras models with single input tensors, the tensor -/// shape is returned as a Tuple object, whereas for multi-input models, -/// the tensor shape is returned as a List of Tuple object containing the shape -/// of the individual input tensors. SOFIE's RModel also requires that the Keras -/// models are initialized with Batch Size. The `GetDataFromTuple()` are called -/// on the Tuple objects, which then returns the shape vector required to call -/// the `AddInputTensorInfo()`. -/// -/// For adding the Output Tensor infos, only the names of the model's output -/// tensors are extracted and are then passed into `AddOutputTensorNameList()`. -/// -/// Provide optionally a batch size that can be used to overwrite the one given by the -/// model. If a batch size is not given 1 is used if the model does not provide a batch size -/// -/// Example Usage: -/// ~~~ {.cpp} -/// using TMVA::Experimental::SOFIE; -/// RModel model = PyKeras::Parse("trained_model_dense.h5"); -/// ~~~ -RModel Parse(std::string filename, int batch_size){ - - char sep = '/'; - #ifdef _WIN32 - sep = '\\'; - #endif - - size_t isep = filename.rfind(sep, filename.length()); - std::string filename_nodir = filename; - if (isep != std::string::npos){ - filename_nodir = (filename.substr(isep+1, filename.length() - isep)); - } - - //Check on whether the Keras .h5 file exists - if(!std::ifstream(filename).good()){ - throw std::runtime_error("Model file "+filename_nodir+" not found!"); - } - - - std::time_t ttime = std::time(0); - std::tm* gmt_time = std::gmtime(&ttime); - std::string parsetime (std::asctime(gmt_time)); - - RModel rmodel(filename_nodir, parsetime); - - //Intializing Python Interpreter and scope dictionaries - Py_Initialize(); - PyObject* main = PyImport_AddModule("__main__"); - PyObject* fGlobalNS = PyModule_GetDict(main); - PyObject* fLocalNS = PyDict_New(); - if (!fGlobalNS) { - throw std::runtime_error("Can't init global namespace for Python"); - } - if (!fLocalNS) { - throw std::runtime_error("Can't init local namespace for Python"); - } - - // Extracting model information - // For each layer: type,name,activation,dtype,input tensor's name, - // output tensor's name, kernel's name, bias's name - // None object is returned for if property doesn't belong to layer - PyRunString("import tensorflow",fGlobalNS,fLocalNS); - PyRunString("import tensorflow.keras as keras",fGlobalNS,fLocalNS); - PyRunString("import tensorflow\n", fGlobalNS, fLocalNS); - PyRunString("if int(keras.__version__.split('.')[0]) >= 3:\n" - " raise RuntimeError(\n" - " 'TMVA SOFIE Keras parser supports Keras 2 only.\\n'\n" - " 'Keras 3 detected. Please export the model to ONNX.\\n'\n" - " )\n", - fGlobalNS, fLocalNS); - PyRunString("from tensorflow.keras.models import load_model",fGlobalNS,fLocalNS); - PyRunString("print('TF/Keras Version: '+ tensorflow.__version__)",fGlobalNS,fLocalNS); - PyRunString(TString::Format("model=load_model('%s')",filename.c_str()),fGlobalNS,fLocalNS); - PyRunString(TString::Format("model.load_weights('%s')",filename.c_str()),fGlobalNS,fLocalNS); - PyRunString("globals().update(locals())",fGlobalNS,fLocalNS); - PyRunString("modelData=[]",fGlobalNS,fLocalNS); - PyRunString("for idx in range(len(model.layers)):\n" - " layer=model.get_layer(index=idx)\n" - " layerData={}\n" - " layerData['layerType']=layer.__class__.__name__\n" - " layerData['layerAttributes']=layer.__dict__\n" - " layerData['layerInput']=[x.name for x in layer.input] if isinstance(layer.input,list) else [layer.input.name]\n" - " layerData['layerOutput']=[x.name for x in layer.output] if isinstance(layer.output,list) else [layer.output.name]\n" - " layerData['layerDType']=layer.dtype\n" - " layerData['layerWeight']=[x.name for x in layer.weights]\n" - " modelData.append(layerData)",fGlobalNS,fLocalNS); - - - PyObject* fPModel = GetValueFromDict(fLocalNS,"modelData"); - PyObject *fLayer; - Py_ssize_t fModelSize = PyList_Size(fPModel); - std::string fLayerType; - - // Traversing through all the layers and passing the Layer object to `AddKerasLayer()` - // for adding the equivalent ROperators into the RModel object. - for(Py_ssize_t fModelIterator=0;fModelIterator fWeightTensorShape; - std::size_t fWeightTensorSize; - - // Traversing through all the Weight tensors - for (Py_ssize_t weightIter = 0; weightIter < PyList_Size(fPWeight); weightIter++){ - fWeightTensor = PyList_GetItem(fPWeight, weightIter); - fWeightName = PyStringAsString(GetValueFromDict(fWeightTensor,"name")); - fWeightDType = ConvertStringToType(PyStringAsString(GetValueFromDict(fWeightTensor,"dtype"))); - - fWeightTensorValue = (PyArrayObject*)GetValueFromDict(fWeightTensor,"value"); - fWeightTensorSize=1; - fWeightTensorShape.clear(); - - // Building the shape vector and finding the tensor size - for(int j=0; j fData(malloc(fWeightTensorSize * sizeof(float)), free); - std::memcpy(fData.get(),fWeightArray, fWeightTensorSize * sizeof(float)); - rmodel.AddInitializedTensor(fWeightName,ETensorType::FLOAT,fWeightTensorShape,fData); - break; - } - default: - throw std::runtime_error("Type error: TMVA SOFIE does not yet weight data layer type"+ConvertTypeToString(fWeightDType)); - } - } - - - // Extracting input tensor info - // For every input tensor inputNames will have their names as string, - // inputShapes will have their shape as Python Tuple, and inputTypes - // will have their dtype as string - PyRunString("inputNames=model.input_names",fGlobalNS,fLocalNS); - PyRunString("inputShapes=model.input_shape if type(model.input_shape)==list else [model.input_shape]",fGlobalNS,fLocalNS); - PyRunString("inputTypes=[]",fGlobalNS,fLocalNS); - PyRunString("for idx in range(len(model.inputs)):\n" - " inputTypes.append(model.inputs[idx].dtype.__str__()[9:-2])",fGlobalNS,fLocalNS); - - PyObject* fPInputs = GetValueFromDict(fLocalNS,"inputNames"); - PyObject* fPInputShapes = GetValueFromDict(fLocalNS,"inputShapes"); - PyObject* fPInputTypes = GetValueFromDict(fLocalNS,"inputTypes"); - - std::string fInputName; - ETensorType fInputDType; - - // For single input models, the model.input_shape will return a tuple - // describing the input tensor shape. For multiple inputs models, - // the model.input_shape will return a list of tuple, each describing - // the input tensor shape. - if(PyTuple_Check(fPInputShapes)){ - fInputName = PyStringAsString(PyList_GetItem(fPInputs,0)); - fInputDType = ConvertStringToType(PyStringAsString(PyList_GetItem(fPInputTypes,0))); - - switch(fInputDType){ - - case ETensorType::FLOAT : { - - // Getting the shape vector from the Tuple object - std::vectorfInputShape = GetDataFromTuple(fPInputShapes); - if (static_cast(fInputShape[0]) <= 0){ - fInputShape[0] = std::max(batch_size,1); - std::cout << "Model has not a defined batch size "; - if (batch_size <=0) std::cout << " assume is 1 "; - else std::cout << " use given value of " << batch_size; - std::cout << " - input shape for tensor " << fInputName << " : " - << TMVA::Experimental::SOFIE::ConvertShapeToString(fInputShape) << std::endl; - } - rmodel.AddInputTensorInfo(fInputName, ETensorType::FLOAT, fInputShape); - rmodel.AddInputTensorName(fInputName); - break; - } - - default: - throw std::runtime_error("Type error: TMVA SOFIE does not yet support data type"+ConvertTypeToString(fInputDType)); - } - - } - - else{ - - // Iterating through multiple input tensors - for(Py_ssize_t inputIter = 0; inputIter < PyList_Size(fPInputs);++inputIter){ - - fInputName = PyStringAsString(PyList_GetItem(fPInputs,inputIter)); - fInputDType = ConvertStringToType(PyStringAsString(PyList_GetItem(fPInputTypes,inputIter))); - - switch(fInputDType){ - case ETensorType::FLOAT : { - PyObject* fInputShapeTuple=PyList_GetItem(fPInputShapes,inputIter); - - std::vectorfInputShape = GetDataFromTuple(fInputShapeTuple); - if (static_cast(fInputShape[0]) <= 0){ - fInputShape[0] = std::max(batch_size,1); - std::cout << "Model has not a defined batch size "; - if (batch_size <=0) std::cout << " assume is 1 "; - else std::cout << " use given value of " << batch_size; - std::cout << " - input shape for tensor " - << fInputName << " : " << TMVA::Experimental::SOFIE::ConvertShapeToString(fInputShape) << std::endl; - } - rmodel.AddInputTensorInfo(fInputName, ETensorType::FLOAT, fInputShape); - rmodel.AddInputTensorName(fInputName); - break; - } - - default: - throw std::runtime_error("Type error: TMVA SOFIE does not yet support data type"+ConvertTypeToString(fInputDType)); - - } - } - } - - - // For adding OutputTensorInfos, the names of the output - // tensors are extracted from the Keras model - PyRunString("outputNames=[]",fGlobalNS,fLocalNS); - PyRunString("for layerName in model.output_names:\n" - " outputNames.append(model.get_layer(layerName).output.name)",fGlobalNS,fLocalNS); - PyObject* fPOutputs = GetValueFromDict(fLocalNS,"outputNames"); - std::vector fOutputNames; - for(Py_ssize_t outputIter = 0; outputIter < PyList_Size(fPOutputs);++outputIter){ - fOutputNames.push_back(PyStringAsString(PyList_GetItem(fPOutputs,outputIter))); - } - rmodel.AddOutputTensorNameList(fOutputNames); - - return rmodel; -} - -} // namespace TMVA::Experimental::SOFIE::PyKeras +} \ No newline at end of file diff --git a/tmva/tmva/inc/TMVA/RSofieReader.hxx b/tmva/tmva/inc/TMVA/RSofieReader.hxx index 5698ff49427ae..c5d0c07052dc7 100644 --- a/tmva/tmva/inc/TMVA/RSofieReader.hxx +++ b/tmva/tmva/inc/TMVA/RSofieReader.hxx @@ -88,9 +88,14 @@ public: std::string fileType = path.substr(pos2+1, path.length()-pos2-1); if (verbose) std::cout << "Parsing SOFIE model " << modelName << " of type " << fileType << std::endl; + // append a suffix to headerfile + std::string modelHeader = modelName + "_fromRSofieR.hxx"; + std::string modelWeights = modelName + "_fromRSofieR.dat"; + // create code for parsing model and generate C++ code for inference // make it in a separate scope to avoid polluting global interpreter space std::string parserCode; + std::string parserPythonCode; // for Python parsers if (type == kONNX) { // check first if we can load the SOFIE parser library if (gSystem->Load("libROOTTMVASofieParser") < 0) { @@ -104,16 +109,15 @@ public: parserCode += "TMVA::Experimental::SOFIE::RModel model = parser.Parse(\"" + path + "\"); \n"; } else if (type == kKeras) { - // use Keras direct parser - if (gSystem->Load("libROOTTMVASofiePyParsers") < 0) { - throw std::runtime_error("RSofieReader: cannot use SOFIE with Keras since libROOTTMVASofiePyParsers is missing"); - } - // assume batch size is first entry in first input ! - std::string batch_size = "-1"; + // use Keras Python parser + parserPythonCode += "\"\"\"\n"; + parserPythonCode += "import ROOT\n"; + + // assume batch size is first entry in first input otherwise set to 1 + std::string batch_size = "1"; // need to fix parser with parm batch sizes if (!inputShapes.empty() && ! inputShapes[0].empty()) batch_size = std::to_string(inputShapes[0][0]); - parserCode += "{\nTMVA::Experimental::SOFIE::RModel model = TMVA::Experimental::SOFIE::PyKeras::Parse(\"" + path + - "\"," + batch_size + "); \n"; + parserPythonCode += "model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse('" + path + "'," + batch_size + ")\n"; } else if (type == kPt) { // use PyTorch direct parser @@ -150,6 +154,8 @@ public: // add custom operators if needed if (fCustomOperators.size() > 0) { + if (!parserPythonCode.empty()) + throw std::runtime_error("Cannot use Custom operator with a Python parser (e.g. from a Keras model)"); for (auto & op : fCustomOperators) { parserCode += "{ auto p = new TMVA::Experimental::SOFIE::ROperator_Custom(\"" @@ -166,58 +172,71 @@ public: } if (verbose) std::cout << "generating the code with batch size = " << batchSize << " ...\n"; - parserCode += "model.Generate(TMVA::Experimental::SOFIE::Options::kDefault," - + ROOT::Math::Util::ToString(batchSize) + ", 0, " + std::to_string(verbose) + "); \n"; - - if (verbose) { - parserCode += "model.PrintRequiredInputTensors();\n"; - parserCode += "model.PrintIntermediateTensors();\n"; - parserCode += "model.PrintOutputTensors();\n"; - } + if (parserPythonCode.empty()) { + parserCode += "model.Generate(TMVA::Experimental::SOFIE::Options::kDefault," + + ROOT::Math::Util::ToString(batchSize) + ", 0, " + std::to_string(verbose) + ");\n"; - // add custom operators if needed -#if 0 - if (fCustomOperators.size() > 0) { + parserCode += "model.OutputGenerated(\"" + modelHeader + "\");\n"; if (verbose) { parserCode += "model.PrintRequiredInputTensors();\n"; parserCode += "model.PrintIntermediateTensors();\n"; parserCode += "model.PrintOutputTensors();\n"; + if (verbose > 1) + parserCode += "model.PrintGenerated(); \n"; } - for (auto & op : fCustomOperators) { - parserCode += "{ auto p = new TMVA::Experimental::SOFIE::ROperator_Custom(\"" - + op.fOpName + "\"," + op.fInputNames + "," + op.fOutputNames + "," + op.fOutputShapes + ",\"" + op.fFileName + "\");\n"; - parserCode += "std::unique_ptr op(p);\n"; - parserCode += "model.AddOperator(std::move(op));\n}\n"; - } - parserCode += "model.Generate(TMVA::Experimental::SOFIE::Options::kDefault," - + ROOT::Math::Util::ToString(batchSize) + "); \n"; - } -#endif - if (verbose > 1) - parserCode += "model.PrintGenerated(); \n"; - parserCode += "model.OutputGenerated();\n"; - - parserCode += "int nInputs = model.GetInputTensorNames().size();\n"; - // need information on number of inputs (assume output is 1) + // need information on number of inputs (assume output is 1) + parserCode += "int nInputs = model.GetInputTensorNames().size();\n"; - //end of parsing code, close the scope and return 1 to indicate a success - parserCode += "return nInputs;\n}\n"; + //end of parsing C++ code + parserCode += "return nInputs;\n}\n"; + } else { + // Python case + parserPythonCode += "model.Generate(ROOT.TMVA.Experimental.SOFIE.Options.kDefault," + + ROOT::Math::Util::ToString(batchSize) + ", 0, " + std::to_string(verbose) + ")\n"; - if (verbose) std::cout << "//ParserCode being executed:\n" << parserCode << std::endl; + parserPythonCode += "model.OutputGenerated('" + modelHeader + "');\n"; + if (verbose) { + parserPythonCode += "model.PrintRequiredInputTensors()\n"; + parserPythonCode += "model.PrintIntermediateTensors()\n"; + parserPythonCode += "model.PrintOutputTensors()\n"; + if (verbose > 1) + parserPythonCode += "model.PrintGenerated()\n"; + } + // end of Python parsing code + parserPythonCode += "\"\"\""; + } + // executing parsing and generating code + int iret = -1; + if (parserPythonCode.empty()) { + if (verbose) { + std::cout << "...ParserCode being executed...:\n"; + std::cout << parserCode << std::endl; + } + iret = gROOT->ProcessLine(parserCode.c_str()); + fNInputs = iret; + } else { + if (verbose) { + std::cout << "executing python3 -c ......" << std::endl; + std::cout << parserPythonCode << std::endl; + } + iret = gSystem->Exec(TString("python3 -c ") + TString(parserPythonCode.c_str())); + fNInputs = 1; + // need number of inputs from input shapes + if (!inputShapes.empty()) fNInputs = inputShapes.size(); + } - auto iret = gROOT->ProcessLine(parserCode.c_str()); - if (iret <= 0) { + if (iret < 0) { std::string msg = "RSofieReader: error processing the parser code: \n" + parserCode; throw std::runtime_error(msg); + } else if (verbose) { + std::cout << "Model Header file is generated!" << std::endl; } - fNInputs = iret; if (fNInputs > 3) { throw std::runtime_error("RSofieReader does not yet support model with > 3 inputs"); } // compile now the generated code and create Session class - std::string modelHeader = modelName + ".hxx"; if (verbose) std::cout << "compile generated code from file " <AccessPathName(modelHeader.c_str())) { std::string msg = "RSofieReader: input header file " + modelHeader + " is not existing"; @@ -235,10 +254,15 @@ public: []( char const& c ) -> bool { return !std::isalnum(c); } ), uidName.end()); std::string sessionName = "session_" + uidName; - declCode += sessionClassName + " " + sessionName + ";"; + declCode += sessionClassName + " " + sessionName + "(\"" + modelWeights + "\");"; if (verbose) std::cout << "//global session declaration\n" << declCode << std::endl; + // need to load the ROOTTMVASOFIE library for some symbols used in generated code + iret = gSystem->Load("libROOTTMVASofie"); + if (iret < 0) + throw std::runtime_error("Error loading libROOTTMVASofie library"); + bool ret = gInterpreter->Declare(declCode.c_str()); if (!ret) { std::string msg = "RSofieReader: error compiling inference code and creating session class\n" + declCode; diff --git a/tutorials/CMakeLists.txt b/tutorials/CMakeLists.txt index d81933f965f30..5ac1030f83f3a 100644 --- a/tutorials/CMakeLists.txt +++ b/tutorials/CMakeLists.txt @@ -346,16 +346,6 @@ else() ROOT_FIND_PYTHON_MODULE(graph_nets) ROOT_FIND_PYTHON_MODULE(onnx) - # Check if we support the installed Keras version. Otherwise, veto SOFIE - # Keras tutorials. This mirrors the logic in tmva/sofie/test/CMakeLists.txt. - # TODO: make sure we also support the newest Keras - set(unsupported_keras_version "3.5.0") - if (ROOT_KERAS_FOUND AND NOT DEFINED ROOT_KERAS_VERSION) - message(WARNING "Keras found, but version unknown — cannot verify compatibility.") - elseif (ROOT_KERAS_FOUND AND NOT ROOT_KERAS_VERSION VERSION_LESS ${unsupported_keras_version}) - message(WARNING "Keras version ${ROOT_KERAS_VERSION} is too new for the SOFIE Keras parser (only supports < ${unsupported_keras_version}). Corresponding tutorials will not be tested.") - set(keras_unsupported TRUE) - endif() if (NOT BLAS_FOUND) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_GNN_Application.C) @@ -363,10 +353,10 @@ else() list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RSofieReader.C) endif() # These SOFIE tutorials take models trained via PyMVA-PyKeras as input - if (NOT tmva-pymva OR NOT tmva-sofie OR NOT ROOT_KERAS_FOUND OR keras_unsupported) - list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras.C) + if (NOT tmva-sofie OR NOT ROOT_KERAS_FOUND) + list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras.py) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Models.py) - list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras_HiggsModel.C) + list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras_HiggsModel.py) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RDataFrame.C) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RDataFrame_JIT.C) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RSofieReader.C) @@ -642,11 +632,10 @@ set (machine_learning-tmva004_RStandardScaler-depends tutorial-machine_learning- set (machine_learning-pytorch-ApplicationClassificationPyTorch-depends tutorial-machine_learning-pytorch-ClassificationPyTorch-py) set (machine_learning-pytorch-RegressionPyTorch-depends tutorial-machine_learning-pytorch-ApplicationClassificationPyTorch-py) set (machine_learning-pytorch-ApplicationRegressionPyTorch-depends tutorial-machine_learning-pytorch-RegressionPyTorch-py) -set (machine_learning-TMVA_SOFIE_RSofieReader-depends tutorial-machine_learning-TMVA_Higgs_Classification) -set (machine_learning-TMVA_SOFIE_RDataFrame_JIT-depends tutorial-machine_learning-TMVA_SOFIE_RSofieReader) -set (machine_learning-TMVA_SOFIE_Keras_HiggsModel-depends tutorial-machine_learning-TMVA_SOFIE_RDataFrame_JIT) -set (machine_learning-TMVA_SOFIE_RDataFrame-depends tutorial-machine_learning-TMVA_SOFIE_Keras_HiggsModel) -set (machine_learning-TMVA_SOFIE_Inference-depends tutorial-machine_learning-TMVA_SOFIE_RDataFrame) +set (machine_learning-TMVA_SOFIE_RDataFrame-depends tutorial-machine_learning-TMVA_SOFIE_Keras_HiggsModel-py) +set (machine_learning-TMVA_SOFIE_RDataFrame_JIT-depends tutorial-machine_learning-TMVA_SOFIE_RDataFrame) +set (machine_learning-TMVA_SOFIE_RSofieReader-depends tutorial-machine_learning-TMVA_SOFIE_RDataFrame_JIT) +set (machine_learning-TMVA_SOFIE_Inference-depends tutorial-machine_learning-TMVA_SOFIE_RSofieReader) set (machine_learning-keras-RegressionKeras-depends tutorial-machine_learning-pytorch-RegressionPyTorch-py) set (machine_learning-keras-ClassificationKeras-depends tutorial-machine_learning-pytorch-ClassificationPyTorch-py) set (machine_learning-keras-ApplicationRegressionKeras-depends tutorial-machine_learning-keras-RegressionKeras-py) diff --git a/tutorials/machine_learning/RBatchGenerator_PyTorch.py b/tutorials/machine_learning/RBatchGenerator_PyTorch.py index e85ddd3be90c6..29533f8098cd8 100644 --- a/tutorials/machine_learning/RBatchGenerator_PyTorch.py +++ b/tutorials/machine_learning/RBatchGenerator_PyTorch.py @@ -8,8 +8,8 @@ ### \macro_output ### \author Dante Niewenhuis -import torch import ROOT +import torch tree_name = "sig_tree" file_name = str(ROOT.gROOT.GetTutorialDir()) + "/machine_learning/data/Higgs_data.root" diff --git a/tutorials/machine_learning/TMVA_CNN_Classification.py b/tutorials/machine_learning/TMVA_CNN_Classification.py index a6875abc29586..5525931971183 100644 --- a/tutorials/machine_learning/TMVA_CNN_Classification.py +++ b/tutorials/machine_learning/TMVA_CNN_Classification.py @@ -22,10 +22,10 @@ ## The difference between signal and background is in the gaussian width. ## The width for the background gaussian is slightly larger than the signal width by few % values -import ROOT - -import os import importlib.util +import os + +import ROOT opt = [1, 1, 1, 1, 1] useTMVACNN = opt[0] if len(opt) > 0 else False @@ -117,7 +117,7 @@ def MakeImagesTree(n, nh, nw): useTMVACNN = False useTMVADNN = False -if not "tmva-pymva" in ROOT.gROOT.GetConfigFeatures(): +if "tmva-pymva" not in ROOT.gROOT.GetConfigFeatures(): useKerasCNN = False usePyTorchCNN = False else: @@ -445,13 +445,11 @@ def MakeImagesTree(n, nh, nw): ROOT.Info("TMVA_CNN_Classification", "Building convolutional keras model") # create python script which can be executed # create 2 conv2d layer + maxpool + dense - import tensorflow - from tensorflow.keras.models import Sequential - from tensorflow.keras.optimizers import Adam - # from keras.initializers import TruncatedNormal # from keras import initializations - from tensorflow.keras.layers import Input, Dense, Dropout, Flatten, Conv2D, MaxPooling2D, Reshape + from tensorflow.keras.layers import Conv2D, Dense, Flatten, MaxPooling2D, Reshape + from tensorflow.keras.models import Sequential + from tensorflow.keras.optimizers import Adam # from keras.callbacks import ReduceLROnPlateau model = Sequential() diff --git a/tutorials/machine_learning/TMVA_Higgs_Classification.py b/tutorials/machine_learning/TMVA_Higgs_Classification.py index 1feeeb2996026..7a5f00bdc44b2 100644 --- a/tutorials/machine_learning/TMVA_Higgs_Classification.py +++ b/tutorials/machine_learning/TMVA_Higgs_Classification.py @@ -29,9 +29,10 @@ ## - The third argument is a string option defining some general configuration for the TMVA session. For example all TMVA output can be suppressed by removing the "!" (not) in front of the "Silent" argument in the option string -import ROOT import os +import ROOT + TMVA = ROOT.TMVA TFile = ROOT.TFile @@ -53,7 +54,7 @@ if useKeras: try: - import tensorflow + pass except: ROOT.Warning("TMVA_Higgs_Classification", "Skip using Keras since tensorflow is not available") useKeras = False @@ -315,10 +316,9 @@ if useKeras: ROOT.Info("TMVA_Higgs_Classification", "Building Deep Learning keras model") # create Keras model with 4 layers of 64 units and relu activations - import tensorflow + from tensorflow.keras.layers import Dense from tensorflow.keras.models import Sequential from tensorflow.keras.optimizers import Adam - from tensorflow.keras.layers import Input, Dense model = Sequential() model.add(Dense(64, activation="relu", input_dim=7)) diff --git a/tutorials/machine_learning/TMVA_RNN_Classification.py b/tutorials/machine_learning/TMVA_RNN_Classification.py index f8e5988288bd3..364aa309b6bc6 100644 --- a/tutorials/machine_learning/TMVA_RNN_Classification.py +++ b/tutorials/machine_learning/TMVA_RNN_Classification.py @@ -35,8 +35,6 @@ TFile = ROOT.TFile import os -import importlib - TMVA.Tools.Instance() TMVA.Config.Instance() @@ -163,7 +161,7 @@ def MakeTimeData(n, ntime, ndim): if useKeras: try: - import tensorflow + pass except: ROOT.Warning("TMVA_RNN_Classification", "Skip using Keras since tensorflow cannot be imported") useKeras = False @@ -392,12 +390,18 @@ def MakeTimeData(n, ntime, ndim): print("Building recurrent keras model using a", rnn_types[i], "layer") # create python script which can be executed # create 2 conv2d layer + maxpool + dense - from tensorflow.keras.models import Sequential - from tensorflow.keras.optimizers import Adam - # from keras.initializers import TruncatedNormal # from keras import initializations - from tensorflow.keras.layers import Input, Dense, Dropout, Flatten, SimpleRNN, GRU, LSTM, Reshape, BatchNormalization + from tensorflow.keras.layers import ( + GRU, + LSTM, + Dense, + Flatten, + Reshape, + SimpleRNN, + ) + from tensorflow.keras.models import Sequential + from tensorflow.keras.optimizers import Adam model = Sequential() model.add(Reshape((10, 30), input_shape=(10 * 30,))) diff --git a/tutorials/machine_learning/TMVA_SOFIE_GNN.py b/tutorials/machine_learning/TMVA_SOFIE_GNN.py index fce0345cbe9a1..a11024f6c04ec 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_GNN.py +++ b/tutorials/machine_learning/TMVA_SOFIE_GNN.py @@ -10,11 +10,12 @@ # NumPys builtin openblas later, which will conflict with the system openblas. ROOT.gInterpreter.Load("libopenblaso.so") -import numpy as np +import time + import graph_nets as gn -from graph_nets import utils_tf +import numpy as np import sonnet as snt -import time +from graph_nets import utils_tf # defining graph properties num_nodes = 5 diff --git a/tutorials/machine_learning/TMVA_SOFIE_GNN_Parser.py b/tutorials/machine_learning/TMVA_SOFIE_GNN_Parser.py index 31d1b6eed30d7..50538a453c5e2 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_GNN_Parser.py +++ b/tutorials/machine_learning/TMVA_SOFIE_GNN_Parser.py @@ -7,16 +7,17 @@ ## ## \macro_code -import ROOT +import os -import numpy as np -import graph_nets as gn -from graph_nets import utils_tf -import sonnet as snt #for getting time and memory import time -import os + +import graph_nets as gn +import numpy as np import psutil +import ROOT +import sonnet as snt +from graph_nets import utils_tf # defining graph properties. Number of edges/modes are the maximum num_max_nodes=100 diff --git a/tutorials/machine_learning/TMVA_SOFIE_Inference.py b/tutorials/machine_learning/TMVA_SOFIE_Inference.py index 712420115e7c4..78edb73fa6e5e 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_Inference.py +++ b/tutorials/machine_learning/TMVA_SOFIE_Inference.py @@ -14,11 +14,13 @@ ### \macro_output ### \author Lorenzo Moneta +from os.path import exists + import numpy as np import ROOT # check if the input file exists -modelFile = "Higgs_trained_model.keras" +modelFile = "HiggsModel.keras" if not exists(modelFile): raise FileNotFoundError("You need to run TMVA_Higgs_Classification.C to generate the Keras trained model") @@ -60,27 +62,37 @@ # stack all the 7 numpy array in a single array (nevents x nvars) xsig = np.column_stack(list(sigData.values())) dataset_size = xsig.shape[0] -print("size of data", dataset_size) +print("size of signal data", dataset_size) #instantiate SOFIE session class -session = ROOT.TMVA_SOFIE_Higgs_trained_model.Session() +#session = ROOT.TMVA_SOFIE_HiggsModel.Session() +#get the sofie session namespace +sofie = getattr(ROOT, 'TMVA_SOFIE_' + modelName) +session = sofie.Session() +print("Evaluating SOFIE models on signal data") hs = ROOT.TH1D("hs","Signal result",100,0,1) for i in range(0,dataset_size): result = session.infer(xsig[i,:]) + if (i % dataset_size/10 == 0) : + print("result for signal event ",i,result[0]) hs.Fill(result[0]) - +print("using RDsataFrame to extract input data in a numpy array") # make SOFIE inference on background data df2 = ROOT.RDataFrame("bkg_tree", inputFile) bkgData = df2.AsNumpy(columns=['m_jj', 'm_jjj', 'm_lv', 'm_jlv', 'm_bb', 'm_wbb', 'm_wwbb']) xbkg = np.column_stack(list(bkgData.values())) dataset_size = xbkg.shape[0] +print("size of background data", dataset_size) hb = ROOT.TH1D("hb","Background result",100,0,1) for i in range(0,dataset_size): result = session.infer(xbkg[i,:]) + if (i % dataset_size/10 == 0) : + print("result for background event ",i,result[0]) + hb.Fill(result[0]) diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras.C b/tutorials/machine_learning/TMVA_SOFIE_Keras.C deleted file mode 100644 index b000b33e56ce6..0000000000000 --- a/tutorials/machine_learning/TMVA_SOFIE_Keras.C +++ /dev/null @@ -1,78 +0,0 @@ -/// \file -/// \ingroup tutorial_ml -/// \notebook -nodraw -/// This macro provides a simple example for the parsing of Keras .keras file -/// into RModel object and further generating the .hxx header files for inference. -/// -/// \macro_code -/// \macro_output -/// \author Sanjiban Sengupta - -using namespace TMVA::Experimental; - -TString pythonSrc = "\ -import os\n\ -os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'\n\ -\n\ -import numpy as np\n\ -from tensorflow.keras.models import Model\n\ -from tensorflow.keras.layers import Input,Dense,Activation,ReLU\n\ -from tensorflow.keras.optimizers import SGD\n\ -\n\ -input=Input(shape=(64,),batch_size=4)\n\ -x=Dense(32)(input)\n\ -x=Activation('relu')(x)\n\ -x=Dense(16,activation='relu')(x)\n\ -x=Dense(8,activation='relu')(x)\n\ -x=Dense(4)(x)\n\ -output=ReLU()(x)\n\ -model=Model(inputs=input,outputs=output)\n\ -\n\ -randomGenerator=np.random.RandomState(0)\n\ -x_train=randomGenerator.rand(4,64)\n\ -y_train=randomGenerator.rand(4,4)\n\ -\n\ -model.compile(loss='mean_squared_error', optimizer=SGD(learning_rate=0.01))\n\ -model.fit(x_train, y_train, epochs=5, batch_size=4)\n\ -model.save('KerasModel.keras')\n"; - - -void TMVA_SOFIE_Keras(const char * modelFile = nullptr, bool printModelInfo = true){ - - // Running the Python script to generate Keras .keras file - - if (modelFile == nullptr) { - TMacro m; - m.AddLine(pythonSrc); - m.SaveSource("make_keras_model.py"); - gSystem->Exec("python3 make_keras_model.py"); - modelFile = "KerasModel.keras"; - } - - //Parsing the saved Keras .keras file into RModel object - SOFIE::RModel model = SOFIE::PyKeras::Parse(modelFile); - - - //Generating inference code - model.Generate(); - // generate output header. By default it will be modelName.hxx - model.OutputGenerated(); - - if (!printModelInfo) return; - - //Printing required input tensors - std::cout<<"\n\n"; - model.PrintRequiredInputTensors(); - - //Printing initialized tensors (weights) - std::cout<<"\n\n"; - model.PrintInitializedTensors(); - - //Printing intermediate tensors - std::cout<<"\n\n"; - model.PrintIntermediateTensors(); - - //Printing generated inference code - std::cout<<"\n\n"; - model.PrintGenerated(); -} diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras.py b/tutorials/machine_learning/TMVA_SOFIE_Keras.py new file mode 100644 index 0000000000000..083c768b3bede --- /dev/null +++ b/tutorials/machine_learning/TMVA_SOFIE_Keras.py @@ -0,0 +1,82 @@ +### \file +### \ingroup tutorial_ml +### \notebook -nodraw +### This macro provides a simple example for the parsing of Keras .keras file +### into RModel object and further generating the .hxx header files for inference. +### +### \macro_code +### \macro_output +### \author Sanjiban Sengupta and Lorenzo Moneta + + + +import ROOT + +# Enable ROOT in batch mode (same effect as -nodraw) +ROOT.gROOT.SetBatch(True) + +# ----------------------------------------------------------------------------- +# Step 1: Create and train a simple Keras model (via embedded Python) +# ----------------------------------------------------------------------------- + +import numpy as np +from tensorflow.keras.layers import Activation, Dense, Input, Softmax +from tensorflow.keras.models import Model + +input=Input(shape=(4,),batch_size=2) +x=Dense(32)(input) +x=Activation('relu')(x) +x=Dense(16,activation='relu')(x) +x=Dense(8,activation='relu')(x) +x=Dense(2)(x) +output=Softmax()(x) +model=Model(inputs=input,outputs=output) + +randomGenerator=np.random.RandomState(0) +x_train=randomGenerator.rand(4,4) +y_train=randomGenerator.rand(4,2) + +model.compile(loss='mse', optimizer='adam') +model.fit(x_train, y_train, epochs=3, batch_size=2) +model.save('KerasModel.keras') +model.summary() + +# ----------------------------------------------------------------------------- +# Step 2: Use TMVA::SOFIE to parse the ONNX model +# ----------------------------------------------------------------------------- + +import ROOT + +# Parse the ONNX model + +model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse("KerasModel.keras") + +# Generate inference code +model.Generate() +model.OutputGenerated() +#print generated code +print("\n**************************************************") +print(" Generated code") +print("**************************************************\n") +model.PrintGenerated() +print("**************************************************\n\n\n") + +# Compile the generated code +ROOT.gInterpreter.Declare('#include "KerasModel.hxx"') + + +# ----------------------------------------------------------------------------- +# Step 3: Run inference +# ----------------------------------------------------------------------------- + +#instantiate SOFIE session class +session = ROOT.TMVA_SOFIE_KerasModel.Session() + +# Input tensor (same shape as training input) +x = np.array([[0.1, 0.2, 0.3, 0.4],[0.5, 0.6, 0.7, 0.8]], dtype=np.float32) + +# Run inference +y = session.infer(x) + +print("Inference output:", y) + diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.C b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.C deleted file mode 100644 index 876b2c87ff9a3..0000000000000 --- a/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.C +++ /dev/null @@ -1,32 +0,0 @@ -/// \file -/// \ingroup tutorial_ml -/// \notebook -nodraw -/// This macro run the SOFIE parser on the Keras model -/// obtaining running TMVA_Higgs_Classification.C -/// You need to run that macro before this one -/// -/// \author Lorenzo Moneta - -using namespace TMVA::Experimental; - - -void TMVA_SOFIE_Keras_HiggsModel(const char * modelFile = "Higgs_trained_model.keras"){ - - // check if the input file exists - if (gSystem->AccessPathName(modelFile)) { - Error("TMVA_SOFIE_RDataFrame","You need to run TMVA_Higgs_Classification.C to generate the Keras trained model"); - return; - } - - // parse the input Keras model into RModel object - SOFIE::RModel model = SOFIE::PyKeras::Parse(modelFile); - - TString modelHeaderFile = modelFile; - modelHeaderFile.ReplaceAll(".keras",".hxx"); - //Generating inference code - model.Generate(); - model.OutputGenerated(std::string(modelHeaderFile)); - - // copy include in $ROOTSYS/tutorials/ - std::cout << "include is in " << gROOT->GetIncludeDir() << std::endl; -} diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py new file mode 100644 index 0000000000000..10fd3aa2792aa --- /dev/null +++ b/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.py @@ -0,0 +1,129 @@ +### \file +### \ingroup tutorial_ml +### \notebook -nodraw +### This macro run the SOFIE parser on the Keras model +### obtaining running TMVA_Higgs_Classification.C +### You need to run that macro before this one +### +### \author Lorenzo Moneta + + +from os.path import exists + +import numpy as np +import ROOT +from keras import layers, models +from sklearn.model_selection import train_test_split + + +def CreateModel(nlayers = 4, nunits = 64): + input = layers.Input(shape=(7,)) + x = input + for i in range(1,nlayers) : + y = layers.Dense(nunits, activation='relu')(x) + x = y + + output = layers.Dense(1, activation='sigmoid')(x) + model = models.Model(input, output) + model.compile(loss = 'binary_crossentropy', optimizer = 'adam', weighted_metrics = ['accuracy']) + model.summary() + return model + +def PrepareData() : + #get the input data + inputFile = str(ROOT.gROOT.GetTutorialDir()) + "/machine_learning/data/Higgs_data.root" + + df1 = ROOT.RDataFrame("sig_tree", inputFile) + sigData = df1.AsNumpy(columns=['m_jj', 'm_jjj', 'm_lv', 'm_jlv', 'm_bb', 'm_wbb', 'm_wwbb']) + #print(sigData) + + # stack all the 7 numpy array in a single array (nevents x nvars) + xsig = np.column_stack(list(sigData.values())) + data_sig_size = xsig.shape[0] + print("size of data", data_sig_size) + + # make SOFIE inference on background data + df2 = ROOT.RDataFrame("bkg_tree", inputFile) + bkgData = df2.AsNumpy(columns=['m_jj', 'm_jjj', 'm_lv', 'm_jlv', 'm_bb', 'm_wbb', 'm_wwbb']) + xbkg = np.column_stack(list(bkgData.values())) + data_bkg_size = xbkg.shape[0] + + ysig = np.ones(data_sig_size) + ybkg = np.zeros(data_bkg_size) + inputs_data = np.concatenate((xsig,xbkg),axis=0) + inputs_targets = np.concatenate((ysig,ybkg),axis=0) + + #split data in training and test data + + x_train, x_test, y_train, y_test = train_test_split( + inputs_data, inputs_targets, test_size=0.50, random_state=1234) + + return x_train, y_train, x_test, y_test + +def TrainModel(model, x, y, name) : + model.fit(x,y,epochs=5,batch_size=50) + modelFile = name + '.keras' + model.save(modelFile) + return model, modelFile + + +def GenerateCode(modelFile = "model.keras") : + + #check if the input file exists + if not exists(modelFile): + raise FileNotFoundError("INput model file not existing. You need to run TMVA_Higgs_Classification.C to generate the Keras trained model") + + + #parse the input Keras model into RModel object (force batch size to be 1) + model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse(modelFile) + + #Generating inference code + model.Generate() + model.OutputGenerated() + + modelName = modelFile.replace(".keras","") + return modelName + +################################################################### +## Step 1 : Create and Train model +################################################################### + +x_train, y_train, x_test, y_test = PrepareData() +#create dense model with 3 layers of 64 units +model = CreateModel(3,64) +model, modelFile = TrainModel(model,x_train, y_train, 'HiggsModel') + +################################################################### +## Step 2 : Parse model and generate inference code with SOFIE +################################################################### + +modelName = GenerateCode(modelFile) +modelHeaderFile = modelName + ".hxx" + +################################################################### +## Step 3 : Compile the generated C++ model code +################################################################### + +ROOT.gInterpreter.Declare('#include "' + modelHeaderFile + '"') + +################################################################### +## Step 4: Evaluate the model +################################################################### + +#get first the SOFIE session namespace +sofie = getattr(ROOT, 'TMVA_SOFIE_' + modelName) +session = sofie.Session() + +x = np.random.normal(0,1,7).astype(np.float32) +y = session.infer(x) +ykeras = model(x.reshape(1,7)).numpy() + +print("input to model is ",x, "\n\t -> output using SOFIE = ", y[0], " using Keras = ", ykeras[0]) + +if (abs(y[0]-ykeras[0]) > 0.01) : + raise RuntimeError('ERROR: Result is different between SOFIE and Keras') + +print("OK") + + + diff --git a/tutorials/machine_learning/TMVA_SOFIE_Models.py b/tutorials/machine_learning/TMVA_SOFIE_Models.py index 6fa389ada9464..1cef6275dd706 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_Models.py +++ b/tutorials/machine_learning/TMVA_SOFIE_Models.py @@ -68,7 +68,7 @@ def PrepareData() : return x_train, y_train, x_test, y_test def TrainModel(model, x, y, name) : - model.fit(x,y,epochs=10,batch_size=50) + model.fit(x,y,epochs=5,batch_size=50) modelFile = name + '.keras' model.save(modelFile) return modelFile @@ -101,9 +101,12 @@ def GenerateModelCode(modelFile, generatedHeaderFile): generatedHeaderFile = "Higgs_Model.hxx" #need to remove existing header file since we are appending on same one if (os.path.exists(generatedHeaderFile)): - weightFile = "Higgs_Model.root" - print("removing existing files", generatedHeaderFile,weightFile) + print("removing existing file", generatedHeaderFile) os.remove(generatedHeaderFile) + +weightFile = "Higgs_Model.root" +if (os.path.exists(weightFile)): + print("removing existing file", weightFile) os.remove(weightFile) GenerateModelCode(model1, generatedHeaderFile) diff --git a/tutorials/machine_learning/TMVA_SOFIE_ONNX.py b/tutorials/machine_learning/TMVA_SOFIE_ONNX.py index 35fd4ca68cef7..c323ccd59d2e1 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_ONNX.py +++ b/tutorials/machine_learning/TMVA_SOFIE_ONNX.py @@ -13,11 +13,13 @@ ## \author Lorenzo Moneta +import inspect + +import numpy as np +import ROOT import torch import torch.nn as nn -import ROOT -import numpy as np -import inspect + def CreateAndTrainModel(modelName): @@ -95,9 +97,9 @@ def ParseModel(modelFile, verbose=False): print("2weight",data) # Generating inference code - model.Generate(); + model.Generate() #generate header file (and .dat file) with modelName+.hxx - model.OutputGenerated(); + model.OutputGenerated() if (verbose) : model.PrintGenerated() diff --git a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C index 2292327cf26f5..4bc663c4019ec 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C +++ b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C @@ -4,15 +4,13 @@ /// This macro provides an example of using a trained model with Keras /// and make inference using SOFIE and RDataFrame /// This macro uses as input a Keras model generated with the -/// TMVA_Higgs_Classification.C tutorial +/// Python tutorial TMVA_SOFIE_Keras_HiggsModel.py /// You need to run that macro before to generate the trained Keras model -/// Then you need to run the macro TMVA_SOFIE_Keras_HiggsModel.C to generate the corresponding -/// header file using SOFIE. +/// and also the corresponding header file with SOFIE which can then be used for inference /// /// Execute in this order: /// ``` -/// root TMVA_Higgs_Classification.C -/// root TMVA_SOFIE_Keras_HiggsModel.C +/// python3 TMVA_SOFIE_Keras_HiggsModel.py /// root TMVA_SOFIE_RDataFrame.C /// ``` /// @@ -25,7 +23,7 @@ using namespace TMVA::Experimental; // need to add the current directory (from where we are running this macro) // to the include path for Cling R__ADD_INCLUDE_PATH($PWD) -#include "Higgs_trained_model.hxx" +#include "HiggsModel.hxx" #include "TMVA/SOFIEHelpers.hxx" using namespace TMVA::Experimental; @@ -40,13 +38,13 @@ void TMVA_SOFIE_RDataFrame(int nthreads = 2){ ROOT::RDataFrame df1("sig_tree", inputFile); int nslots = df1.GetNSlots(); std::cout << "Running using " << nslots << " threads" << std::endl; - auto h1 = df1.DefineSlot("DNN_Value", SofieFunctor<7, TMVA_SOFIE_Higgs_trained_model::Session>(nslots), + auto h1 = df1.DefineSlot("DNN_Value", SofieFunctor<7, TMVA_SOFIE_HiggsModel::Session>(nslots), {"m_jj", "m_jjj", "m_lv", "m_jlv", "m_bb", "m_wbb", "m_wwbb"}) .Histo1D({"h_sig", "", 100, 0, 1}, "DNN_Value"); ROOT::RDataFrame df2("bkg_tree", inputFile); nslots = df2.GetNSlots(); - auto h2 = df2.DefineSlot("DNN_Value", SofieFunctor<7, TMVA_SOFIE_Higgs_trained_model::Session>(nslots), + auto h2 = df2.DefineSlot("DNN_Value", SofieFunctor<7, TMVA_SOFIE_HiggsModel::Session>(nslots), {"m_jj", "m_jjj", "m_lv", "m_jlv", "m_bb", "m_wbb", "m_wwbb"}) .Histo1D({"h_bkg", "", 100, 0, 1}, "DNN_Value"); diff --git a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.py b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.py index e4e037aef6863..af1c059fb544c 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.py +++ b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.py @@ -15,8 +15,8 @@ import ROOT # check if the input file exists -modelFile = "Higgs_trained_model.keras" -modelName = "Higgs_trained_model" +modelFile = "HiggsModel.keras" +modelName = "HiggsModel" if not exists(modelFile): raise FileNotFoundError("You need to run TMVA_Higgs_Classification.C to generate the Keras trained model") diff --git a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame_JIT.C b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame_JIT.C index 95dd4b1316278..072384019ea51 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_RDataFrame_JIT.C +++ b/tutorials/machine_learning/TMVA_SOFIE_RDataFrame_JIT.C @@ -38,23 +38,15 @@ void CompileModelForRDF(const std::string & headerModelFile, unsigned int ninput return; } -void TMVA_SOFIE_RDataFrame_JIT(std::string modelFile = "Higgs_trained_model.keras"){ +void TMVA_SOFIE_RDataFrame_JIT(std::string modelName = "HiggsModel"){ // check if the input file exists - if (gSystem->AccessPathName(modelFile.c_str())) { - Info("TMVA_SOFIE_RDataFrame","You need to run TMVA_Higgs_Classification.C to generate the Keras trained model"); + std::string modelHeaderFile = modelName + ".hxx"; + if (gSystem->AccessPathName(modelHeaderFile.c_str())) { + Info("TMVA_SOFIE_RDataFrame","You need to run TMVA_SOFIE_Keras_Higgs_Model.py to generate the SOFIE header for the Keras trained model"); return; } - // parse the input Keras model into RModel object - SOFIE::RModel model = SOFIE::PyKeras::Parse(modelFile); - - std::string modelName = modelFile.substr(0,modelFile.find(".keras")); - std::string modelHeaderFile = modelName + std::string(".hxx"); - //Generating inference code - model.Generate(); - model.OutputGenerated(modelHeaderFile); - model.PrintGenerated(); // check that also weigh file exists std::string modelWeightFile = modelName + std::string(".dat"); if (gSystem->AccessPathName(modelWeightFile.c_str())) { diff --git a/tutorials/machine_learning/TMVA_SOFIE_RSofieReader.C b/tutorials/machine_learning/TMVA_SOFIE_RSofieReader.C index f6d4ae1e42316..4da25f8afaea5 100644 --- a/tutorials/machine_learning/TMVA_SOFIE_RSofieReader.C +++ b/tutorials/machine_learning/TMVA_SOFIE_RSofieReader.C @@ -22,7 +22,7 @@ using namespace TMVA::Experimental; void TMVA_SOFIE_RSofieReader(){ - RSofieReader model("Higgs_trained_model.keras"); + RSofieReader model("HiggsModel.keras", {}, true ); // for debugging //RSofieReader model("Higgs_trained_model.keras", {}, true); diff --git a/tutorials/machine_learning/tmva101_Training.py b/tutorials/machine_learning/tmva101_Training.py index b50742998070f..671cad7836c93 100644 --- a/tutorials/machine_learning/tmva101_Training.py +++ b/tutorials/machine_learning/tmva101_Training.py @@ -12,9 +12,8 @@ ## \date August 2019 ## \author Stefan Wunsch -import ROOT import numpy as np - +import ROOT from tmva100_DataPreparation import variables diff --git a/tutorials/machine_learning/tmva102_Testing.py b/tutorials/machine_learning/tmva102_Testing.py index bcf77bea47f54..7d78e9b6147e5 100644 --- a/tutorials/machine_learning/tmva102_Testing.py +++ b/tutorials/machine_learning/tmva102_Testing.py @@ -10,13 +10,10 @@ ## \date August 2019 ## \author Stefan Wunsch -import ROOT -import pickle -from tmva100_DataPreparation import variables +import ROOT from tmva101_Training import load_data - # Load data x, y_true, w = load_data("test_signal.root", "test_background.root") @@ -29,7 +26,7 @@ y_pred = bdt.Compute(x) # Compute ROC using sklearn -from sklearn.metrics import roc_curve, auc +from sklearn.metrics import auc, roc_curve false_positive_rate, true_positive_rate, _ = roc_curve(y_true, y_pred, sample_weight=w) score = auc(false_positive_rate, true_positive_rate)