From 64a153fc95a8b666fe212c7df47793e1ec85d0b0 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Fri, 24 Jul 2026 13:54:03 +0200 Subject: [PATCH] [tmva][sofie] Remove Keras and PyTorch parses from ROOT repository As announced in the Q2 2026 Report and Planning meeting [1], the SOFIE distribution within ROOT will be slimmed down to keep only the core functionality of enabling ONNX model inference on the CPU with ROOT via ONNX parsing and C++ code emit. That means to keep the ONNX parser, and to remove the Keras and PyTorch parsers. these parsers relied on private implementation details of the Keras or PyTorch libraries, which change faster than appropriate for the stability standards of ROOT. We expect users to choose neural net architectures that are serializable by ONNX for production use. The Keras and PyTorch parsers remain valuable work, visible in ROOTs commit history and ready to be re-distributed by other parties, which would be very easy since this is just Python code. It would be trivial to ship this as a Python package that requires ROOT or another distribution of C++ SOFIE. [1] https://indico.cern.ch/event/1699702/ : slide 19 --- README/ReleaseNotes/v642/index.md | 3 + .../pythonizations/python/CMakeLists.txt | 27 +- .../pythonizations/python/ROOT/_facade.py | 5 - .../_tmva/_sofie/_parser/_keras/__init__.py | 5 - .../_sofie/_parser/_keras/layers/__init__.py | 0 .../_sofie/_parser/_keras/layers/batchnorm.py | 56 -- .../_sofie/_parser/_keras/layers/binary.py | 5 - .../_sofie/_parser/_keras/layers/concat.py | 15 - .../_sofie/_parser/_keras/layers/conv.py | 87 --- .../_sofie/_parser/_keras/layers/dense.py | 37 -- .../_tmva/_sofie/_parser/_keras/layers/elu.py | 33 - .../_sofie/_parser/_keras/layers/flatten.py | 36 -- .../_sofie/_parser/_keras/layers/identity.py | 15 - .../_sofie/_parser/_keras/layers/layernorm.py | 63 -- .../_parser/_keras/layers/leaky_relu.py | 41 -- .../_sofie/_parser/_keras/layers/permute.py | 34 -- .../_sofie/_parser/_keras/layers/pooling.py | 77 --- .../_sofie/_parser/_keras/layers/relu.py | 28 - .../_sofie/_parser/_keras/layers/reshape.py | 34 -- .../_tmva/_sofie/_parser/_keras/layers/rnn.py | 159 ----- .../_sofie/_parser/_keras/layers/selu.py | 28 - .../_sofie/_parser/_keras/layers/sigmoid.py | 30 - .../_sofie/_parser/_keras/layers/softmax.py | 31 - .../_sofie/_parser/_keras/layers/swish.py | 28 - .../_sofie/_parser/_keras/layers/tanh.py | 28 - .../_tmva/_sofie/_parser/_keras/parser.py | 565 ------------------ .../_tmva/_sofie/_parser/_pytorch/__init__.py | 0 .../_tmva/_sofie/_parser/_pytorch/parser.py | 421 ------------- .../pyroot/pythonizations/test/CMakeLists.txt | 23 - .../test/generate_keras_functional.py | 250 -------- .../test/generate_keras_sequential.py | 247 -------- .../test/parser_test_function.py | 98 --- .../pythonizations/test/sofie_keras_parser.py | 90 --- .../test/sofie_keras_parser_models.py | 283 --------- .../test/sofie_pytorch_parser.py | 146 ----- tmva/sofie/README.md | 2 - tmva/tmva/inc/TMVA/RSofieReader.hxx | 164 ++--- tutorials/CMakeLists.txt | 8 - .../machine_learning/TMVA_SOFIE_Keras.py | 114 ---- .../machine_learning/TMVA_SOFIE_PyTorch.py | 92 --- tutorials/machine_learning/index.md | 2 - 41 files changed, 38 insertions(+), 3372 deletions(-) delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/__init__.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/__init__.py delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/parser.py delete mode 100644 bindings/pyroot/pythonizations/test/generate_keras_functional.py delete mode 100644 bindings/pyroot/pythonizations/test/generate_keras_sequential.py delete mode 100644 bindings/pyroot/pythonizations/test/parser_test_function.py delete mode 100644 bindings/pyroot/pythonizations/test/sofie_keras_parser.py delete mode 100644 bindings/pyroot/pythonizations/test/sofie_keras_parser_models.py delete mode 100644 bindings/pyroot/pythonizations/test/sofie_pytorch_parser.py delete mode 100644 tutorials/machine_learning/TMVA_SOFIE_Keras.py delete mode 100644 tutorials/machine_learning/TMVA_SOFIE_PyTorch.py diff --git a/README/ReleaseNotes/v642/index.md b/README/ReleaseNotes/v642/index.md index 2db619e44337a..382f1ec85b6a7 100644 --- a/README/ReleaseNotes/v642/index.md +++ b/README/ReleaseNotes/v642/index.md @@ -54,6 +54,9 @@ The following people have contributed to this new version: * The inclusion by external projects of Makefile templates contained within ROOT is deprecated in 6.42, a warning will be raised if you use them. These files will be removed in ROOT 7. * The conversion from Python set to **RooArgSet** is deprecated and won't work anymore in ROOT 6.44. The problem is that Python sets are unordered while RooArgSets are ordered, and this mismatch can lead to subtle problems later on. Prefer conversion from Python lists or tuples, which are ordered too. * The ROOT IO capability for the `TMVA::Experimental::SOFIE::RModel` has been removed. Users should not be encouraged to serialize models in experimental classes. For the serialization of ONNX models one can already use ONNX directly, and even serialize the ONNX bytes to a ROOT file if required. +* The Keras and PyTorch parsers for SOFIE (`TMVA::Experimental::SOFIE::PyKeras` and `PyTorch`) are now removed, so `RSofieReader` only accepts ONNX files. +These parsers relied on private implementation details of Keras and PyTorch, which change faster than is appropriate for ROOT's stability standards. +Users are encouraged to export their models to ONNX and use the retained ONNX parser instead. * The ROOT IO capability for the `TMVA::Experimental::RBDT` class has been removed, along with the `TMVA.Experimental.SaveXGBoost` Python function. Experimental classes should not be persistified since their on-disk layout is not guaranteed to be stable. An `RBDT` is now built directly from an XGBoost model in its native JSON serialization with the new `TMVA::Experimental::RBDT::LoadXGBoost(jsonPath)`, which works both from C++ and Python. To convert a trained model, save it first with XGBoost's `Booster.save_model("model.json")` and then load it with `LoadXGBoost`. * The **JsMVA** feature for interactive TMVA training in Jupyter notebooks is now removed. It was not functional for years and was therefore already excluded from ROOT 6.38. This also removes the `TMVA::IPythonInteractive` class and the related interactive-training interfaces from the TMVA method and fitter classes, such as `MethodBase::ExitFromTraining()` or `FitterBase::SetIPythonInteractive()`. * The **RooStats::DebuggingSampler** and **RooStats::DebuggingTestStat** classes are removed. They were mock implementations of the `TestStatSampler` and `TestStatistic` interfaces that returned uniform random numbers independent of the data, only meant for debugging the RooStats framework itself during its initial development. diff --git a/bindings/pyroot/pythonizations/python/CMakeLists.txt b/bindings/pyroot/pythonizations/python/CMakeLists.txt index 331a5cca5718f..80ebe17a34c76 100644 --- a/bindings/pyroot/pythonizations/python/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/python/CMakeLists.txt @@ -59,32 +59,7 @@ if(tmva) ROOT/_pythonization/_tmva/_rbdt.py ROOT/_pythonization/_tmva/_rtensor.py ROOT/_pythonization/_tmva/_utils.py - ROOT/_pythonization/_tmva/_gnn.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/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 - ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/__init__.py - ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/parser.py) + ROOT/_pythonization/_tmva/_gnn.py) endif() set(py_sources diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index 7aa486ab5efba..a40080124a697 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -565,11 +565,6 @@ def TMVA(self): # The comment suppresses linter errors about unused imports. from ._pythonization import _tmva # noqa: F401 from ._pythonization._tmva._rtensor import _AsRTensor - from ._pythonization._tmva._sofie._parser._keras.parser import PyKeras - from ._pythonization._tmva._sofie._parser._pytorch.parser import PyTorch - - setattr(ns.Experimental.SOFIE, "PyKeras", PyKeras) - setattr(ns.Experimental.SOFIE, "PyTorch", PyTorch) ns.Experimental.AsRTensor = _AsRTensor except ImportError: 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 deleted file mode 100644 index a8cc4b78313aa..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -def get_keras_version() -> str: - - import keras - - return keras.__version__ 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 deleted file mode 100644 index e69de29bb2d1d..0000000000000 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 deleted file mode 100644 index 9c8063cd23e0a..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/batchnorm.py +++ /dev/null @@ -1,56 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - 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 SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = 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 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 deleted file mode 100644 index 6e451446bce5b..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/binary.py +++ /dev/null @@ -1,5 +0,0 @@ -def MakeKerasBinary(layer): - from ROOT.TMVA.Experimental import SOFIE - - inpt = layer["layerInput"] - return SOFIE.createBasicBinary(layer["layerDType"], layer["layerType"], inpt[0], inpt[1], layer["layerOutput"][0]) 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 deleted file mode 100644 index 2d3fa06d4190a..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/concat.py +++ /dev/null @@ -1,15 +0,0 @@ -def MakeKerasConcat(layer): - from ROOT.TMVA.Experimental import SOFIE - - 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 SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = SOFIE.ROperator_Concat(input, axis, 0, output) - else: - raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Concat does not yet support input type " + fLayerDType) - return op 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 deleted file mode 100644 index 8cec6f7321ac5..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py +++ /dev/null @@ -1,87 +0,0 @@ -import math - -from .. import get_keras_version - - -def MakeKerasConv(layer): - """ - Create a Keras-compatible convolutional layer operation using SOFIE framework. - - This function takes a dictionary representing a convolutional layer and its attributes and - constructs a Keras-compatible convolutional layer operation using the SOFIE framework. - A convolutional layer applies a convolution operation between the input tensor and a set - of learnable filters (kernels). - - Parameters: - layer (dict): A dictionary containing layer information including input, output, - data type (must be float), weight and bias name, kernel size, dilations, padding and strides. - When padding is same (keep in the same dimensions), the padding shape is calculated. - - Returns: - ROperator_Conv: A SOFIE framework operator representing the convolutional layer operation. - """ - from ROOT.TMVA.Experimental import SOFIE - - 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"] - if layer.get("channels_last", True): - inputHeight = fInputShape[1] - inputWidth = fInputShape[2] - else: - inputHeight = fInputShape[2] - inputWidth = fInputShape[3] - outputHeight = math.ceil(float(inputHeight) / float(fAttrStrides[0])) - outputWidth = math.ceil(float(inputWidth) / float(fAttrStrides[1])) - effective_kH = fAttrDilations[0] * (fAttrKernelShape[0] - 1) + 1 - effective_kW = fAttrDilations[1] * (fAttrKernelShape[1] - 1) + 1 - padding_height = max((outputHeight - 1) * fAttrStrides[0] + effective_kH - inputHeight, 0) - padding_width = max((outputWidth - 1) * fAttrStrides[1] + effective_kW - 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_left, padding_bottom, padding_right] - else: - raise RuntimeError( - "TMVA::SOFIE - RModel Keras Parser doesn't yet supports Convolution layer with padding " + fKerasPadding - ) - if SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = 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) 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 deleted file mode 100644 index 33cea69476bfd..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/dense.py +++ /dev/null @@ -1,37 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - 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 SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = 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) 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 deleted file mode 100644 index ca7b0f89a3c35..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/elu.py +++ /dev/null @@ -1,33 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - 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 SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = 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 deleted file mode 100644 index b05b1687123f4..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/flatten.py +++ /dev/null @@ -1,36 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - 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 = SOFIE.ReshapeOpMode.Flatten - fNameData = finput[0] - fNameOutput = foutput[0] - fNameShape = flayername + "_shape" - op = SOFIE.ROperator_Reshape(fOpMode, 0, fNameData, fNameShape, fNameOutput) - return op 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 deleted file mode 100644 index 5ee3fd8d43caf..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/identity.py +++ /dev/null @@ -1,15 +0,0 @@ -def MakeKerasIdentity(layer): - from ROOT.TMVA.Experimental import SOFIE - - input = layer["layerInput"] - output = layer["layerOutput"] - fLayerDType = layer["layerDType"] - fLayerInputName = input[0] - fLayerOutputName = output[0] - if SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = 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 deleted file mode 100644 index b7cd97764b198..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/layernorm.py +++ /dev/null @@ -1,63 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - 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 SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = 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 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 deleted file mode 100644 index 0b56e23ea84a4..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/leaky_relu.py +++ /dev/null @@ -1,41 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - 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 SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = SOFIE.ROperator_LeakyRelu("float")(fAlpha, fLayerInputName, fLayerOutputName) - return op - else: - raise RuntimeError( - "TMVA::SOFIE - Unsupported - Operator LeakyRelu does not yet support input type " + fLayerDType - ) 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 deleted file mode 100644 index 358eb18ecedff..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/permute.py +++ /dev/null @@ -1,34 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - finput = layer["layerInput"] - foutput = layer["layerOutput"] - fLayerDType = layer["layerDType"] - fLayerInputName = finput[0] - fLayerOutputName = foutput[0] - attributes = layer["layerAttributes"] - fAttributePermute = list(attributes["dims"]) - if SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - if len(fAttributePermute) > 0: - fAttributePermute = [0] + fAttributePermute # for the batch dimension from the input - op = SOFIE.ROperator_Transpose(fAttributePermute, fLayerInputName, fLayerOutputName) - return op - else: - raise RuntimeError( - "TMVA::SOFIE - Unsupported - Operator Transpose does not yet support input type " + fLayerDType - ) 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 deleted file mode 100644 index 9c8656846feb0..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/pooling.py +++ /dev/null @@ -1,77 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - # 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 = 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 = SOFIE.PoolOpMode.MaxPool - elif "AveragePool" in fLayerType: - PoolMode = SOFIE.PoolOpMode.AveragePool - elif "GlobalAverage" in fLayerType: - PoolMode = SOFIE.PoolOpMode.GloabalAveragePool - else: - raise RuntimeError( - "TMVA::SOFIE - Unsupported - Operator poolong does not yet support pooling type " + fLayerType - ) - - # Create operator - if SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = 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 - ) 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 deleted file mode 100644 index 454da16a2b766..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/relu.py +++ /dev/null @@ -1,28 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - finput = layer["layerInput"] - foutput = layer["layerOutput"] - fLayerDType = layer["layerDType"] - fLayerInputName = finput[0] - fLayerOutputName = foutput[0] - if SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = SOFIE.ROperator_Relu("float")(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/reshape.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py deleted file mode 100644 index ce6901bc4e047..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/reshape.py +++ /dev/null @@ -1,34 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - 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 = SOFIE.ReshapeOpMode.Reshape - fNameData = finput[0] - fNameOutput = foutput[0] - fNameShape = flayername + "_shape" - op = SOFIE.ROperator_Reshape(fOpMode, 0, fNameData, fNameShape, fNameOutput) - return op 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 deleted file mode 100644 index 8d1326e3e5aaa..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/rnn.py +++ /dev/null @@ -1,159 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - # 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 SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - if layer["layerType"] == "SimpleRNN": - op = 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 = 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 = 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 deleted file mode 100644 index 8beacb928dc24..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/selu.py +++ /dev/null @@ -1,28 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - finput = layer["layerInput"] - foutput = layer["layerOutput"] - fLayerDType = layer["layerDType"] - fLayerInputName = finput[0] - fLayerOutputName = foutput[0] - if SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = SOFIE.ROperator_Selu("float")(fLayerInputName, fLayerOutputName) - return op - else: - raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Selu does not yet support input type " + fLayerDType) 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 deleted file mode 100644 index a77d2ec05cc61..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py +++ /dev/null @@ -1,30 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - finput = layer["layerInput"] - foutput = layer["layerOutput"] - fLayerDType = layer["layerDType"] - fLayerInputName = finput[0] - fLayerOutputName = foutput[0] - if SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = SOFIE.ROperator_Sigmoid("float")(fLayerInputName, fLayerOutputName) - return op - else: - raise RuntimeError( - "TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fLayerDType - ) 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 deleted file mode 100644 index e77fc0bd3fab0..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py +++ /dev/null @@ -1,31 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - finput = layer["layerInput"] - foutput = layer["layerOutput"] - fLayerDType = layer["layerDType"] - fLayerInputName = finput[0] - fLayerOutputName = foutput[0] - if SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = SOFIE.ROperator_Softmax(-1, fLayerInputName, fLayerOutputName, False) - return op - else: - raise RuntimeError( - "TMVA::SOFIE - Unsupported - Operator Softmax does not yet support input type " + fLayerDType - ) 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 deleted file mode 100644 index 1bcbb862b3881..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py +++ /dev/null @@ -1,28 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - finput = layer["layerInput"] - foutput = layer["layerOutput"] - fLayerDType = layer["layerDType"] - fLayerInputName = finput[0] - fLayerOutputName = foutput[0] - if SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = SOFIE.ROperator_Swish(fLayerInputName, fLayerOutputName) - return op - else: - raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Swish does not yet support input type " + fLayerDType) 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 deleted file mode 100644 index 64ca3578a686f..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py +++ /dev/null @@ -1,28 +0,0 @@ -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. - """ - from ROOT.TMVA.Experimental import SOFIE - - finput = layer["layerInput"] - foutput = layer["layerOutput"] - fLayerDType = layer["layerDType"] - fLayerInputName = finput[0] - fLayerOutputName = foutput[0] - if SOFIE.ConvertStringToType(fLayerDType) == SOFIE.ETensorType.FLOAT: - op = SOFIE.ROperator_Tanh("float")(fLayerInputName, fLayerOutputName) - return op - else: - raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Tanh does not yet support input type " + fLayerDType) 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 deleted file mode 100644 index 520711c785ba3..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/parser.py +++ /dev/null @@ -1,565 +0,0 @@ -import os -import time - -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 - from ROOT.TMVA.Experimental import SOFIE - - def move_operator(op): - """ - Wrap an operator into a std::unique_ptr to pass it to RModel::AddOperator(). - """ - import ROOT - - # If the object is already held by a smart pointer, just move it. - smartptr = op.__smartptr__() - if smartptr: - return type(smartptr)(ROOT.std.move(smartptr)) - - ROOT.SetOwnership(op, False) - return ROOT.std.unique_ptr[type(op)](op) - - 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 = [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" - rmodel.AddInitializedTensor(shape_tensor_name, SOFIE.ETensorType.UINT64, [len(TargetShape)], TargetShape) - - # These layers only have one operator - excluding the recurrent layers, in which the activation function(s) - # are included in the recurrent operator - if fLayerType in mapKerasLayer.keys(): - Attributes = layer_data["layerAttributes"] - inputs = layer_data["layerInput"] - outputs = layer_data["layerOutput"] - if keras_version < "2.16": - LayerName = Attributes["_name"] - else: - LayerName = Attributes["name"] - - # Convoltion/Pooling layers in keras by default assume the channels dimension is the - # last one, while in onnx (and the SOFIE's RModel) it is the first one (other than batch - # size), so a transpose is needed before and after the pooling, if the data format is - # channels last (can be set to channels first by the user). In case of MaxPool2D and - # Conv2D (with linear activation) channels last, the transpose layers are added as: - - # input output - # transpose layer input_layer_name layer_name + PreTrans - # actual layer layer_name + PreTrans layer_name + PostTrans - # transpose layer layer_name + PostTrans output_layer_name - - fLayerOutput = outputs[0] - if fLayerType == "GlobalAveragePooling2D": - if layer_data["channels_last"]: - op = SOFIE.ROperator_Transpose([0, 3, 1, 2], inputs[0], LayerName + "PreTrans") - rmodel.AddOperator(move_operator(op)) - inputs[0] = LayerName + "PreTrans" - outputs[0] = LayerName + "Squeeze" - rmodel.AddOperator(move_operator(mapKerasLayer[fLayerType](layer_data))) - op = SOFIE.ROperator_Reshape(SOFIE.ReshapeOpMode.Squeeze, [2, 3], LayerName + "Squeeze", fLayerOutput) - rmodel.AddOperator(move_operator(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 = SOFIE.ROperator_Transpose(fAttrPerm, inputs[0], LayerName + "PreTrans") - rmodel.AddOperator(move_operator(op)) - inputs[0] = LayerName + "PreTrans" - outputs[0] = LayerName + "PostTrans" - rmodel.AddOperator(move_operator(mapKerasLayer[fLayerType](layer_data))) - op = SOFIE.ROperator_Transpose(fAttrPerm, LayerName + "PostTrans", fLayerOutput) - rmodel.AddOperator(move_operator(op)) - - elif fLayerType == "MaxPooling2D" or fLayerType == "AveragePooling2D": - if layer_data["channels_last"]: - op = SOFIE.ROperator_Transpose([0, 3, 1, 2], inputs[0], LayerName + "PreTrans") - rmodel.AddOperator(move_operator(op)) - inputs[0] = LayerName + "PreTrans" - outputs[0] = LayerName + "PostTrans" - rmodel.AddOperator(move_operator(mapKerasLayer[fLayerType](layer_data))) - if layer_data["channels_last"]: - op = SOFIE.ROperator_Transpose([0, 2, 3, 1], LayerName + "PostTrans", fLayerOutput) - rmodel.AddOperator(move_operator(op)) - - else: - rmodel.AddOperator(move_operator(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 = SOFIE.ROperator_Transpose([0, 3, 1, 2], inputs[0], LayerName + "PreTrans") - rmodel.AddOperator(move_operator(op)) - inputs[0] = LayerName + "PreTrans" - layer_data["layerInput"] = inputs - outputs[0] = LayerName + fLayerType - layer_data["layerOutput"] = outputs - op = mapKerasLayerWithActivation[fLayerType](layer_data) - rmodel.AddOperator(move_operator(op)) - Activation_layer_input = LayerName + fLayerType - if fLayerType == "Conv2D": - if layer_data["channels_last"]: - op = SOFIE.ROperator_Transpose( - [0, 2, 3, 1], LayerName + fLayerType, LayerName + "PostTrans" - ) - rmodel.AddOperator(move_operator(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.AddOperator(move_operator(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 = SOFIE.ROperator_Transpose([0, 3, 1, 2], inputs[0], LayerName + "PreTrans") - rmodel.AddOperator(move_operator(op)) - inputs[0] = LayerName + "PreTrans" - layer_data["layerInput"] = inputs - outputs[0] = LayerName + "PostTrans" - rmodel.AddOperator(move_operator(mapKerasLayerWithActivation[fLayerType](layer_data))) - if fLayerType == "Conv2D": - if layer_data["channels_last"]: - op = SOFIE.ROperator_Transpose([0, 2, 3, 1], LayerName + "PostTrans", fLayerOutput) - rmodel.AddOperator(move_operator(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 - from ROOT.TMVA.Experimental import SOFIE - - 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 = SOFIE.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 = 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 == 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_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_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(fWeightName, SOFIE.ETensorType.FLOAT, 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 = 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 = 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/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/parser.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/parser.py deleted file mode 100644 index 7a403a2903886..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/parser.py +++ /dev/null @@ -1,421 +0,0 @@ -import os -import time - - -def move_operator(op): - """ - Wrap an operator into a std::unique_ptr to pass it to RModel::AddOperator(). - """ - import ROOT - - # If the object is already held by a smart pointer, just move it. - smartptr = op.__smartptr__() - if smartptr: - return type(smartptr)(ROOT.std.move(smartptr)) - - ROOT.SetOwnership(op, False) - return ROOT.std.unique_ptr[type(op)](op) - - -def _node_get(node, key): - """ - Get the value of an attribute of a PyTorch ONNX Graph node. - - The helper function is used to avoid dependency on the onnx submodule (for the - subscript operator of torch._C.Node), as done in https://github.com/pytorch/pytorch/pull/82628 - """ - sel = node.kindOf(key) - return getattr(node, sel)(key) - - -def MakePyTorchGemm(node): - """ - Create a SOFIE Gemm operator from a PyTorch ONNX Graph node. - - For PyTorch's Linear layer having Gemm operation in its ONNX graph, - the names of the input tensor, output tensor are extracted, and then - are passed to instantiate a ROperator_Gemm object using the required attributes. - The node inputs are a list of tensor names, which includes the names of the - input tensor and the weight tensors. - - Parameters: - node (dict): A dictionary containing node information including type, attributes, - input & output tensor names and the data type (must be float). - - Returns: - ROperator_Gemm: A SOFIE framework operator representing the Gemm operation. - """ - from ROOT.TMVA.Experimental import SOFIE - - attributes = node["nodeAttributes"] - inputs = node["nodeInputs"] - outputs = node["nodeOutputs"] - node_dtype = node["nodeDType"][0] - - # Extracting the parameters for the Gemm operator - name_a = inputs[0] - name_b = inputs[1] - name_c = inputs[2] - name_y = outputs[0] - attr_alpha = float(attributes["alpha"]) - attr_beta = float(attributes["beta"]) - - if "transB" in attributes: - attr_transB = int(attributes["transB"]) - attr_transA = int(not attr_transB) - else: - attr_transA = int(attributes["transA"]) - attr_transB = int(not attr_transA) - - if SOFIE.ConvertStringToType(node_dtype) == SOFIE.ETensorType.FLOAT: - return SOFIE.ROperator_Gemm["float"]( - attr_alpha, attr_beta, attr_transA, attr_transB, name_a, name_b, name_c, name_y - ) - else: - raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Gemm does not yet support input type " + node_dtype) - - -def MakePyTorchConv(node): - """ - Create a SOFIE Conv operator from a PyTorch ONNX Graph node. - - For the Conv operator of PyTorch's ONNX Graph, attributes like dilations, group, - kernel shape, pads and strides are found, and are passed in instantiating the - ROperator object with autopad default to `NOTSET`. - - Parameters: - node (dict): A dictionary containing node information including type, attributes, - input & output tensor names and the data type (must be float). - - Returns: - ROperator_Conv: A SOFIE framework operator representing the Conv operation. - """ - from ROOT.TMVA.Experimental import SOFIE - - attributes = node["nodeAttributes"] - inputs = node["nodeInputs"] - outputs = node["nodeOutputs"] - node_dtype = node["nodeDType"][0] - - # Extracting the Conv node attributes - attr_autopad = "NOTSET" - attr_dilations = list(attributes["dilations"]) - attr_group = int(attributes["group"]) - attr_kernel_shape = list(attributes["kernel_shape"]) - attr_pads = list(attributes["pads"]) - attr_strides = list(attributes["strides"]) - name_x = inputs[0] - name_w = inputs[1] - name_b = inputs[2] - name_y = outputs[0] - - if SOFIE.ConvertStringToType(node_dtype) == SOFIE.ETensorType.FLOAT: - return SOFIE.ROperator_Conv["float"]( - attr_autopad, - attr_dilations, - attr_group, - attr_kernel_shape, - attr_pads, - attr_strides, - name_x, - name_w, - name_b, - name_y, - ) - else: - raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Conv does not yet support input type " + node_dtype) - - -def MakePyTorchRelu(node): - """ - Create a SOFIE Relu operator from a PyTorch ONNX Graph node. - - For instantiating a ROperator_Relu object, the names of - input & output tensors and the data type of the node are extracted. - - Parameters: - node (dict): A dictionary containing node information including type, attributes, - input & output tensor names and the data type (must be float). - - Returns: - ROperator_Relu: A SOFIE framework operator representing the Relu operation. - """ - from ROOT.TMVA.Experimental import SOFIE - - inputs = node["nodeInputs"] - outputs = node["nodeOutputs"] - node_dtype = node["nodeDType"][0] - - if SOFIE.ConvertStringToType(node_dtype) == SOFIE.ETensorType.FLOAT: - return SOFIE.ROperator_Relu["float"](inputs[0], outputs[0]) - else: - raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Relu does not yet support input type " + node_dtype) - - -def MakePyTorchSelu(node): - """ - Create a SOFIE Selu operator from a PyTorch ONNX Graph node. - - For instantiating a ROperator_Selu object, the names of - input & output tensors and the data type of the node are extracted. - - Parameters: - node (dict): A dictionary containing node information including type, attributes, - input & output tensor names and the data type (must be float). - - Returns: - ROperator_Selu: A SOFIE framework operator representing the Selu operation. - """ - from ROOT.TMVA.Experimental import SOFIE - - inputs = node["nodeInputs"] - outputs = node["nodeOutputs"] - node_dtype = node["nodeDType"][0] - - if SOFIE.ConvertStringToType(node_dtype) == SOFIE.ETensorType.FLOAT: - return SOFIE.ROperator_Selu["float"](inputs[0], outputs[0]) - else: - raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Selu does not yet support input type " + node_dtype) - - -def MakePyTorchSigmoid(node): - """ - Create a SOFIE Sigmoid operator from a PyTorch ONNX Graph node. - - For instantiating a ROperator_Sigmoid object, the names of - input & output tensors and the data type of the node are extracted. - - Parameters: - node (dict): A dictionary containing node information including type, attributes, - input & output tensor names and the data type (must be float). - - Returns: - ROperator_Sigmoid: A SOFIE framework operator representing the Sigmoid operation. - """ - from ROOT.TMVA.Experimental import SOFIE - - inputs = node["nodeInputs"] - outputs = node["nodeOutputs"] - node_dtype = node["nodeDType"][0] - - if SOFIE.ConvertStringToType(node_dtype) == SOFIE.ETensorType.FLOAT: - return SOFIE.ROperator_Sigmoid["float"](inputs[0], outputs[0]) - else: - raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + node_dtype) - - -def MakePyTorchTranspose(node): - """ - Create a SOFIE Transpose operator from a PyTorch ONNX Graph node. - - For the Transpose operator of PyTorch's ONNX Graph, the permute dimensions are - found, and are passed in instantiating the ROperator object. - - Parameters: - node (dict): A dictionary containing node information including type, attributes - and input & output tensor names. - - Returns: - ROperator_Transpose: A SOFIE framework operator representing the Transpose operation. - """ - from ROOT.TMVA.Experimental import SOFIE - - attributes = node["nodeAttributes"] - inputs = node["nodeInputs"] - outputs = node["nodeOutputs"] - - # Extracting the permute dimensions for the transpose - attr_perm = [int(dim) for dim in attributes["perm"]] - - return SOFIE.ROperator_Transpose(attr_perm, inputs[0], outputs[0]) - - -# Set global dictionary, mapping PyTorch ONNX Graph nodes to corresponding functions -# that create their ROperator instances -mapPyTorchNode = { - "onnx::Gemm": MakePyTorchGemm, - "onnx::Conv": MakePyTorchConv, - "onnx::Relu": MakePyTorchRelu, - "onnx::Selu": MakePyTorchSelu, - "onnx::Sigmoid": MakePyTorchSigmoid, - "onnx::Transpose": MakePyTorchTranspose, -} - - -def MakePyTorchNode(node): - """ - Prepare the equivalent ROperator with respect to a PyTorch ONNX Graph node. - - The function searches for the passed PyTorch ONNX Graph node in the map, and calls - the specific preparatory function, subsequently returning the ROperator object. - - For developing new preparatory functions for supporting PyTorch ONNX Graph nodes - in the future, all one needs is to extract the required properties and attributes - from the node dictionary, which contains all the information about any PyTorch ONNX - Graph node, and after any required transformations, these are passed for instantiating - the ROperator object. - - The node dictionary which holds all the information about a PyTorch ONNX Graph's node - has the following structure: - - dict node { 'nodeType' : Type of node (operator) - 'nodeAttributes' : Attributes of the node - 'nodeInputs' : List of names of input tensors - 'nodeOutputs' : List of names of output tensors - 'nodeDType' : Data type of the operator node - } - - Parameters: - node (dict): A dictionary representing a PyTorch ONNX Graph node. - - Returns: - ROperator: A SOFIE framework operator representing the node operation. - """ - node_type = node["nodeType"] - if node_type not in mapPyTorchNode: - raise RuntimeError("TMVA::SOFIE - Parsing PyTorch node " + node_type + " is not yet supported") - return mapPyTorchNode[node_type](node) - - -class PyTorch: - def Parse(filename, input_shapes, input_dtypes=None): - """ - Parse a trained PyTorch .pt model into a RModel object. - - The parser uses internal functions of PyTorch to convert any PyTorch model - into its equivalent ONNX Graph. For this conversion, dummy inputs are built - which are passed through the model and the applied operators are recorded - for populating the ONNX graph. This requires the shapes and data types of - the input tensors, which are used for building the dummy inputs. - After the said conversion, the nodes of the ONNX graph are then traversed to - extract properties like node type, attributes and input & output tensor names, - and the equivalent ROperator instances are added into the RModel object. - - The internal function used to convert the model to a graph object returns a - list which contains a Graph object and a dictionary of weights. This dictionary - is used to add the initialized tensors of the model into the RModel object. - - For adding the input tensor infos, the names of the input tensors are extracted - from the PyTorch ONNX graph object. The vectors of shapes & data types passed - into the Parse() function are used for the shapes and the data types of the - input tensors. - - For the output tensor infos, the names of the output tensors are also extracted - from the Graph object and are then added into the RModel object. - - Parameters: - filename (str): File location of the PyTorch .pt model. - input_shapes: List of input shape lists. - input_dtypes: Optional list of SOFIE.ETensorType for the data types of the - input tensors. Defaults to float for all input tensors. - - Returns: - RModel: The parsed model. - - Example usage: - ~~~ {.py} - import ROOT - model = ROOT.TMVA.Experimental.SOFIE.PyTorch.Parse("trained_model_dense.pt", [[120, 1]]) - ~~~ - """ - - # PyTorch is too fragile to import unconditionally. As its presence might break several ROOT - # usecases and importing torch globally will slow down importing ROOT, which is not desired. - # For this, we import torch 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 torch will be - # imported, and not everytime we import ROOT. Also, we can import torch in multiple functions - # as many times as we want since Python caches the imported packages. - - import torch - from ROOT.TMVA.Experimental import SOFIE - from torch.onnx.utils import _model_to_graph - - # Check if file exists - if not os.path.exists(filename): - raise RuntimeError("Model file {} not found!".format(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 = SOFIE.RModel(filename_nodir, parsetime) - - print("Torch Version: " + torch.__version__) - - # The data types of the input tensors default to float - if input_dtypes is None: - input_dtypes = [SOFIE.ETensorType.FLOAT] * len(input_shapes) - - # Load the model and prepare it for the conversion to an ONNX graph - model = torch.jit.load(filename) - model.cpu() - model.eval() - - # Build dummy inputs for the model from the given input shapes - dummy_inputs = [torch.rand(*[int(dim) for dim in shape]) for shape in input_shapes] - - # Get the ONNX graph from the model using the dummy inputs - graph = _model_to_graph(model, dummy_inputs) - - # Iterate over the nodes of the ONNX graph and add the equivalent operators to the RModel - for node in graph[0].nodes(): - node_data = {} - node_data["nodeType"] = node.kind() - node_data["nodeAttributes"] = {name: _node_get(node, name) for name in node.attributeNames()} - node_data["nodeInputs"] = [x.debugName() for x in node.inputs()] - node_data["nodeOutputs"] = [x.debugName() for x in node.outputs()] - node_data["nodeDType"] = [x.type().scalarType() for x in node.outputs()] - - # Adding required routines depending on the node types for generating inference code - node_type = node_data["nodeType"] - if node_type == "onnx::Gemm": - rmodel.AddBlasRoutines(["Gemm", "Gemv"]) - elif node_type == "onnx::Selu" or node_type == "onnx::Sigmoid": - rmodel.AddNeededStdLib("cmath") - elif node_type == "onnx::Conv": - rmodel.AddBlasRoutines(["Gemm", "Axpy"]) - - rmodel.AddOperator(move_operator(MakePyTorchNode(node_data))) - - # Extract the model weights to add the initialized tensors to the RModel - for weight_name, weight_tensor in graph[1].items(): - # e.g. "torch.FloatTensor" -> "Float" - weight_dtype = SOFIE.ConvertStringToType(weight_tensor.type()[6:-6]) - if weight_dtype == SOFIE.ETensorType.FLOAT: - weight_value = weight_tensor.numpy() - rmodel.AddInitializedTensor( - weight_name, SOFIE.ETensorType.FLOAT, list(weight_value.shape), weight_value.flatten() - ) - else: - raise RuntimeError( - "Type error: TMVA SOFIE does not yet supports weights of data type " - + SOFIE.ConvertTypeToString(weight_dtype) - ) - - # Extract the input tensor infos (the first graph input is the model itself) - input_names = [x.debugName() for x in model.graph.inputs()][1:] - for input_name, input_shape, input_dtype in zip(input_names, input_shapes, input_dtypes): - if input_dtype == SOFIE.ETensorType.FLOAT: - rmodel.AddInputTensorInfo(input_name, SOFIE.ETensorType.FLOAT, [int(dim) for dim in input_shape]) - rmodel.AddInputTensorName(input_name) - else: - raise RuntimeError( - "Type Error: TMVA SOFIE does not yet support the input tensor data type " - + SOFIE.ConvertTypeToString(input_dtype) - ) - - # Extract the output tensor names - output_names = [x.debugName() for x in graph[0].outputs()] - rmodel.AddOutputTensorNameList(output_names) - - return rmodel diff --git a/bindings/pyroot/pythonizations/test/CMakeLists.txt b/bindings/pyroot/pythonizations/test/CMakeLists.txt index a4baa0c51b9c7..f18a2a59577b9 100644 --- a/bindings/pyroot/pythonizations/test/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/test/CMakeLists.txt @@ -114,29 +114,6 @@ 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) - # Tests on full models, translated from the former C++ googletest tmva/sofie/test/TestRModelParserKeras.C - ROOT_ADD_PYUNITTEST(pyroot_pyz_sofie_keras_parser_models sofie_keras_parser_models.py PYTHON_DEPS keras tensorflow) - endif() -endif() - -# SOFIE PyTorch Parser (translated from the former C++ googletest tmva/sofie/test/TestRModelParserPyTorch.C) -if (tmva) - # onnx 1.19.0 has a bug that makes this version unusable: - # https://github.com/onnx/onnx/issues/7249 - # The PyTorch parser imports onnx indirectly via torch.onnx, so the test - # has to be disabled with that onnx version. - ROOT_FIND_PYTHON_MODULE(onnx) - if(NOT (ROOT_ONNX_FOUND AND DEFINED ROOT_ONNX_VERSION AND ROOT_ONNX_VERSION VERSION_EQUAL "1.19.0")) - if(NOT MSVC OR win_broken_tests) - ROOT_ADD_PYUNITTEST(pyroot_pyz_sofie_pytorch_parser sofie_pytorch_parser.py PYTHON_DEPS torch onnx) - endif() - 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 deleted file mode 100644 index 37ca85aba0c0b..0000000000000 --- a/bindings/pyroot/pythonizations/test/generate_keras_functional.py +++ /dev/null @@ -1,250 +0,0 @@ -import warnings - -def generate_keras_functional(dst_dir): - - import numpy as np - import keras - 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() - if len(model.trainable_weights) > 0: - model.fit(x_train, y_train, epochs=1, verbose=0) - - with warnings.catch_warnings(): - # Some object inside TensorFlow/Keras has an outdated __array__ implementation - warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*__array__.*copy keyword.*") - 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_first_padding_same_stride2 - if (is_channels_first_supported()): - inp = layers.Input(shape=(2, 5, 5)) - out = layers.Conv2D(4, (3, 3), padding='same', strides=(2, 2), data_format='channels_first', activation='relu')(inp) - model = models.Model(inp, out) - train_and_save(model, "Conv2D_channels_first_padding_same_stride2") - - # 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_same with dilation_rate=2 - inp = layers.Input(shape=(8, 8, 3)) - out = layers.Conv2D(4, (3, 3), padding='same', dilation_rate=2, data_format='channels_last', activation='relu')(inp) - model = models.Model(inp, out) - train_and_save(model, "Conv2D_padding_same_dilation") - - # Conv2D padding_same with an asymmetric (non-square) kernel, so that the height and width - # padding amounts differ. - inp = layers.Input(shape=(8, 8, 3)) - out = layers.Conv2D(4, (3, 5), padding='same', data_format='channels_last', activation='relu')(inp) - model = models.Model(inp, out) - train_and_save(model, "Conv2D_padding_same_asymmetric_kernel") - - # 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_first')(inp) - model = models.Model(inp, out) - train_and_save(model, "MaxPool2D_channels_first") - - # MaxPooling2D channels_last - inp = layers.Input(shape=(8, 8, 3)) - out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last')(inp) - model = models.Model(inp, out) - train_and_save(model, "MaxPool2D_channels_last") - - # Multiply - in1 = layers.Input(shape=(8,)) - in2 = layers.Input(shape=(8,)) - out = layers.Multiply()([in1, in2]) - model = models.Model([in1, in2], out) - train_and_save(model, "Multiply") - - # Permute - inp = layers.Input(shape=(3, 4, 5)) - out = layers.Permute((2, 1, 3))(inp) - model = models.Model(inp, out) - train_and_save(model, "Permute") - - # ReLU - inp = layers.Input(shape=(10,)) - out = layers.ReLU()(inp) - model = models.Model(inp, out) - train_and_save(model, "ReLU") - - # Reshape - inp = layers.Input(shape=(4, 5)) - out = layers.Reshape((2, 10))(inp) - model = models.Model(inp, out) - train_and_save(model, "Reshape") - - # Softmax - inp = layers.Input(shape=(10,)) - out = layers.Softmax()(inp) - model = models.Model(inp, out) - train_and_save(model, "Softmax") - - # Subtract - in1 = layers.Input(shape=(8,)) - in2 = layers.Input(shape=(8,)) - out = layers.Subtract()([in1, in2]) - model = models.Model([in1, in2], out) - train_and_save(model, "Subtract") - - # Layer Combination - - inp = layers.Input(shape=(32, 32, 3)) - x = layers.Conv2D(8, (3,3), padding="same", activation="relu", 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]) - # `alpha` was renamed to `negative_slope` in Keras 3 - if keras.__version__ >= "3.0": - merged = layers.LeakyReLU(negative_slope=0.1)(merged) - else: - 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 deleted file mode 100644 index da000074d5481..0000000000000 --- a/bindings/pyroot/pythonizations/test/generate_keras_sequential.py +++ /dev/null @@ -1,247 +0,0 @@ -import warnings - -def generate_keras_sequential(dst_dir): - - import keras - 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']) - if len(model.trainable_weights) > 0: - model.fit(x_train, y_train, epochs=1, verbose=0) - model.summary() - print("fitting sequential model",name) - - with warnings.catch_warnings(): - # Some object inside TensorFlow/Keras has an outdated __array__ implementation - warnings.filterwarnings("ignore", category=DeprecationWarning, message=".*__array__.*copy keyword.*") - 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_first_padding_same_stride2 - if (is_channels_first_supported()): - model = models.Sequential([ - layers.Input(shape=(2, 5, 5)), - layers.Conv2D(4, (3, 3), padding='same', strides=(2, 2), data_format='channels_first', activation='relu') - ]) - train_and_save(model, "Conv2D_channels_first_padding_same_stride2") - - # 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_same with dilation_rate=2 - model = models.Sequential([ - layers.Input(shape=(8, 8, 3)), - layers.Conv2D(4, (3, 3), padding='same', dilation_rate=2, data_format='channels_last', activation='relu') - ]) - train_and_save(model, "Conv2D_padding_same_dilation") - - # Conv2D padding_same with an asymmetric (non-square) kernel, so that the height and width - # padding amounts differ. - model = models.Sequential([ - layers.Input(shape=(8, 8, 3)), - layers.Conv2D(4, (3, 5), padding='same', data_format='channels_last', activation='relu') - ]) - train_and_save(model, "Conv2D_padding_same_asymmetric_kernel") - - # 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") - - # `alpha` was renamed to `negative_slope` in Keras 3 - negative_slope_key = "negative_slope" if keras.__version__ >= "3.0" else "alpha" - - 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(**{negative_slope_key : 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") diff --git a/bindings/pyroot/pythonizations/test/parser_test_function.py b/bindings/pyroot/pythonizations/test/parser_test_function.py deleted file mode 100644 index d6aeae1d377e7..0000000000000 --- a/bindings/pyroot/pythonizations/test/parser_test_function.py +++ /dev/null @@ -1,98 +0,0 @@ -import ROOT - -''' -The test file contains two types of functions: - - 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 np.testing.assert_allclose 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 os - if os.environ.get("ROOT_TMVA_SOFIE_KERAS_CPU_CHANNELS_FIRST", "") == "1": - return True - import tensorflow as tf - if len(tf.config.list_physical_devices("GPU")) == 0: - return False - - return True - -def generate_and_test_inference( - model_file_path: str, generated_header_file_dir: str = None, batch_size=1, input_tensors=None, atol=1e-2 -): - - 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) - - if input_tensors is None: - 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 explicitly forbids input tensor lists of size 1 - if len(keras_model.inputs) == 1: - keras_inference_result = keras_model(input_tensors[0]) - else: - 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") - - np.testing.assert_allclose( - np.asarray(sofie_inference_result).flatten(), - np.asarray(keras_inference_result).flatten(), - atol=atol, - rtol=0.0, # explicitly disable relative tolerance (NumPy uses |a - b| <= atol + rtol * |b|) - ) diff --git a/bindings/pyroot/pythonizations/test/sofie_keras_parser.py b/bindings/pyroot/pythonizations/test/sofie_keras_parser.py deleted file mode 100644 index 5196419bc60fb..0000000000000 --- a/bindings/pyroot/pythonizations/test/sofie_keras_parser.py +++ /dev/null @@ -1,90 +0,0 @@ -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_channels_first_padding_same_stride2", - "Conv2D_padding_same", - "Conv2D_padding_same_dilation", - "Conv2D_padding_same_asymmetric_kernel", - "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/bindings/pyroot/pythonizations/test/sofie_keras_parser_models.py b/bindings/pyroot/pythonizations/test/sofie_keras_parser_models.py deleted file mode 100644 index bd6ba0724fad6..0000000000000 --- a/bindings/pyroot/pythonizations/test/sofie_keras_parser_models.py +++ /dev/null @@ -1,283 +0,0 @@ -import os -import shutil -import unittest - -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 -from parser_test_function import generate_and_test_inference - -# Test the SOFIE Keras parser on full multi-layer models (sequential and -# functional), comparing the SOFIE inference results with the outputs of the -# original Keras models. In contrast to sofie_keras_parser.py, which tests the -# individual layers one by one, the models here are trained and use batch -# sizes larger than one, so the batch size stored in the saved model is also -# exercised. -# -# This is the Python translation of the former C++ googletest -# tmva/sofie/test/TestRModelParserKeras.C. - -WORK_DIR = "keras_parser_models" - -TOLERANCE = 1e-6 - - -def generate_keras_models(dst_dir): - 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 - - random_generator = np.random.RandomState(0) - - def train_and_save(model, x_train, y_train, name, batch_size): - model.compile(loss="mean_squared_error", optimizer=SGD(learning_rate=0.01)) - model.fit(x_train, y_train, verbose=0, epochs=10, batch_size=batch_size) - model.save(f"{dst_dir}/{name}.keras") - - # Functional model - input = Input(shape=(8,), batch_size=2) - x = Dense(16)(input) - x = Activation("relu")(x) - x = Dense(32)(x) - x = ReLU()(x) - output = Dense(4, activation="selu")(x) - model = Model(inputs=input, outputs=output) - train_and_save(model, random_generator.rand(2, 8), random_generator.rand(2, 4), "KerasModelFunctional", 2) - - # Sequential model - model = Sequential() - model.add(Dense(8)) - model.add(ReLU()) - model.add(Dense(6)) - model.add(Activation("sigmoid")) - train_and_save(model, random_generator.rand(4, 8), random_generator.rand(4, 6), "KerasModelSequential", 4) - - # Batch normalization model - model = Sequential() - model.add(Dense(4)) - model.add(BatchNormalization()) - model.add(Dense(2)) - train_and_save(model, random_generator.rand(4, 4), random_generator.rand(4, 2), "KerasModelBatchNorm", 2) - - # Conv2D model with valid padding - model = Sequential() - model.add(Conv2D(8, kernel_size=3, activation="relu", input_shape=(4, 4, 1), padding="valid")) - train_and_save( - model, random_generator.rand(1, 4, 4, 1), random_generator.rand(1, 2, 2, 8), "KerasModelConv2D_Valid", 2 - ) - - # Conv2D model with same padding - model = Sequential() - model.add(Conv2D(8, kernel_size=3, activation="relu", input_shape=(4, 4, 1), padding="same")) - train_and_save( - model, random_generator.rand(1, 4, 4, 1), random_generator.rand(1, 4, 4, 8), "KerasModelConv2D_Same", 2 - ) - - # Conv2D model with same padding and dilation - model = Sequential() - model.add(Conv2D(4, kernel_size=3, activation="relu", input_shape=(8, 8, 1), padding="same", dilation_rate=2)) - train_and_save( - model, random_generator.rand(1, 8, 8, 1), random_generator.rand(1, 8, 8, 4), "KerasModelConv2D_SameDilated", 2 - ) - - # Reshape model - model = Sequential() - model.add(Conv2D(8, kernel_size=3, activation="relu", input_shape=(4, 4, 1), padding="same")) - model.add(Reshape((32, 4))) - train_and_save(model, random_generator.rand(1, 4, 4, 1), random_generator.rand(1, 32, 4), "KerasModelReshape", 2) - - # Concatenate model - input_1 = Input(shape=(2,)) - dense_1 = Dense(3)(input_1) - input_2 = Input(shape=(2,)) - dense_2 = Dense(3)(input_2) - concat = Concatenate(axis=1)([dense_1, dense_2]) - model = Model(inputs=[input_1, input_2], outputs=concat) - train_and_save( - model, - [random_generator.rand(1, 2), random_generator.rand(1, 2)], - random_generator.rand(1, 6), - "KerasModelConcatenate", - 1, - ) - - # Binary operators model (Add, Subtract, Multiply) - input_1 = Input(shape=(2,)) - input_2 = Input(shape=(2,)) - add = Add()([input_1, input_2]) - subtract = Subtract()([add, input_1]) - multiply = Multiply()([subtract, input_2]) - model = Model(inputs=[input_1, input_2], outputs=multiply) - train_and_save( - model, - [random_generator.rand(2, 2), random_generator.rand(2, 2)], - random_generator.rand(2, 2), - "KerasModelBinaryOp", - 2, - ) - - # Model with different activations (tanh, LeakyReLU, softmax) - input = Input(shape=(8,)) - x = Dense(16, activation="tanh")(input) - x = Dense(32)(x) - x = LeakyReLU()(x) - output = Dense(4, activation="softmax")(x) - model = Model(inputs=input, outputs=output) - train_and_save(model, random_generator.rand(1, 8), random_generator.rand(1, 4), "KerasModelActivations", 1) - - # Swish activation model - model = Sequential() - model.add(Dense(64, activation="swish", input_shape=(8,))) - model.add(Dense(32, activation="swish")) - model.add(Dense(1, activation="swish")) - train_and_save(model, random_generator.rand(1, 8), random_generator.rand(1, 1), "KerasModelSwish", 1) - - -class SOFIE_Keras_Parser_Models(unittest.TestCase): - @classmethod - def setUpClass(cls): - if os.path.isdir(WORK_DIR): - shutil.rmtree(WORK_DIR) - os.makedirs(WORK_DIR) - print("Generating Keras models for testing") - generate_keras_models(WORK_DIR) - - @classmethod - def tearDownClass(cls): - shutil.rmtree(WORK_DIR) - - def run_model_test(self, model_name, input_tensors, batch_size): - generate_and_test_inference( - f"{WORK_DIR}/{model_name}.keras", - WORK_DIR, - batch_size=batch_size, - input_tensors=input_tensors, - atol=TOLERANCE, - ) - - def test_sequential(self): - # input is 8 x batch size that is fixed to be 4 - input_tensor = np.array( - [ - 0.12107884, - 0.89718615, - 0.89123899, - 0.32197549, - 0.17891638, - 0.83555135, - 0.98680066, - 0.14496809, - 0.07255503, - 0.55386989, - 0.6628149, - 0.29843291, - 0.71059786, - 0.44043452, - 0.13792047, - 0.93007397, - 0.16799397, - 0.75473803, - 0.43203355, - 0.68360968, - 0.83879351, - 0.0558927, - 0.57500447, - 0.49063431, - 0.63637339, - 0.94483464, - 0.11032887, - 0.22424818, - 0.50972592, - 0.04671024, - 0.39230661, - 0.80500943, - ], - dtype=np.float32, - ).reshape(4, 8) - self.run_model_test("KerasModelSequential", [input_tensor], batch_size=4) - - def test_functional(self): - input_tensor = np.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, - ], - dtype=np.float32, - ).reshape(2, 8) - self.run_model_test("KerasModelFunctional", [input_tensor], batch_size=2) - - def test_batch_norm(self): - input_tensor = np.array( - [0.22308163, 0.95274901, 0.44712538, 0.84640867, 0.69947928, 0.29743695, 0.81379782, 0.39650574], - dtype=np.float32, - ).reshape(2, 4) - self.run_model_test("KerasModelBatchNorm", [input_tensor], batch_size=2) - - def test_conv2d_valid_padding(self): - input_tensor = np.ones((1, 4, 4, 1), dtype=np.float32) - self.run_model_test("KerasModelConv2D_Valid", [input_tensor], batch_size=1) - - def test_conv2d_same_padding(self): - input_tensor = np.ones((1, 4, 4, 1), dtype=np.float32) - self.run_model_test("KerasModelConv2D_Same", [input_tensor], batch_size=1) - - def test_conv2d_same_padding_dilated(self): - input_tensor = np.ones((1, 8, 8, 1), dtype=np.float32) - self.run_model_test("KerasModelConv2D_SameDilated", [input_tensor], batch_size=1) - - def test_reshape(self): - input_tensor = np.ones((1, 4, 4, 1), dtype=np.float32) - self.run_model_test("KerasModelReshape", [input_tensor], batch_size=1) - - def test_concatenate(self): - input_tensors = [np.ones((1, 2), dtype=np.float32), np.ones((1, 2), dtype=np.float32)] - self.run_model_test("KerasModelConcatenate", input_tensors, batch_size=1) - - def test_binary_op(self): - # test with batch size = 2, input shapes are (2,2) - input_tensors = [ - np.array([[1, 2], [3, 4]], dtype=np.float32), - np.array([[5, 6], [7, 8]], dtype=np.float32), - ] - self.run_model_test("KerasModelBinaryOp", input_tensors, batch_size=2) - - def test_activations(self): - input_tensor = np.ones((1, 8), dtype=np.float32) - self.run_model_test("KerasModelActivations", [input_tensor], batch_size=1) - - def test_swish(self): - input_tensor = np.ones((1, 8), dtype=np.float32) - self.run_model_test("KerasModelSwish", [input_tensor], batch_size=1) - - -if __name__ == "__main__": - unittest.main() diff --git a/bindings/pyroot/pythonizations/test/sofie_pytorch_parser.py b/bindings/pyroot/pythonizations/test/sofie_pytorch_parser.py deleted file mode 100644 index f2cc258d1ba30..0000000000000 --- a/bindings/pyroot/pythonizations/test/sofie_pytorch_parser.py +++ /dev/null @@ -1,146 +0,0 @@ -import os -import shutil -import unittest - -import numpy as np -import ROOT - -# Test the SOFIE PyTorch parser (TMVA::Experimental::SOFIE::PyTorch::Parse) by -# parsing TorchScript models, generating and compiling the inference code and -# comparing the SOFIE results with the outputs of the original PyTorch models. -# -# This is the Python translation of the former C++ googletest -# tmva/sofie/test/TestRModelParserPyTorch.C. - -WORK_DIR = "pytorch_parser_models" - - -def generate_pytorch_models(dst_dir): - import torch - import torch.nn as nn - - def train_and_save(model, x, y, name, n_iterations): - criterion = nn.MSELoss() - optimizer = torch.optim.SGD(model.parameters(), lr=0.01) - - for _ in range(n_iterations): - y_pred = model(x) - loss = criterion(y_pred, y) - optimizer.zero_grad() - loss.backward() - optimizer.step() - - model.eval() - m = torch.jit.script(model) - torch.jit.save(m, f"{dst_dir}/{name}.pt") - - # Sequential model - model = nn.Sequential( - nn.Linear(4, 8), - nn.ReLU(), - nn.Linear(8, 6), - nn.SELU(), - ) - train_and_save(model, torch.randn(2, 4), torch.randn(2, 6), "PyTorchModelSequential", 2000) - - # Module model - class Model(nn.Module): - def __init__(self): - super(Model, self).__init__() - self.fc1 = nn.Linear(6, 36) - self.fc2 = nn.Linear(36, 12) - self.relu = nn.ReLU() - self.sigmoid = nn.Sigmoid() - - def forward(self, x): - x = self.fc1(x) - x = self.relu(x) - x = self.fc2(x) - x = self.sigmoid(x) - x = torch.transpose(x, 1, 0) - return x - - train_and_save(Model(), torch.randn(2, 6), torch.randn(12, 2), "PyTorchModelModule", 2000) - - # Convolution model - model = nn.Sequential( - nn.Conv2d(6, 5, 3, stride=2), - nn.ReLU(), - ) - train_and_save(model, torch.randn(5, 6, 5, 5), torch.randn(5, 5, 2, 2), "PyTorchModelConvolution", 100) - - -class SOFIE_PyTorch_Parser(unittest.TestCase): - @classmethod - def setUpClass(cls): - if os.path.isdir(WORK_DIR): - shutil.rmtree(WORK_DIR) - os.makedirs(WORK_DIR) - print("Generating PyTorch models for testing") - generate_pytorch_models(WORK_DIR) - - @classmethod - def tearDownClass(cls): - shutil.rmtree(WORK_DIR) - - def generate_and_test_inference(self, model_name, input_tensor, atol): - import torch - - model_path = f"{WORK_DIR}/{model_name}.pt" - - # Parse the TorchScript model. The PyTorch parser needs the input - # tensor shapes, which also fix the batch size (the first dimension). - input_shapes = ROOT.std.vector["std::vector"]() - input_shapes.push_back(input_tensor.shape) - rmodel = ROOT.TMVA.Experimental.SOFIE.PyTorch.Parse(model_path, input_shapes) - - # Generate and compile the SOFIE inference code - batch_size = input_tensor.shape[0] - rmodel.Generate(ROOT.TMVA.Experimental.SOFIE.Options.kDefault, batch_size) - header_path = f"{WORK_DIR}/{model_name}.hxx" - rmodel.OutputGenerated(header_path) - print(f"Compiling SOFIE model {model_name}") - if not ROOT.gInterpreter.Declare(f'#include "{header_path}"'): - raise AssertionError(f"Error compiling header file {header_path}") - sofie_model_namespace = getattr(ROOT, "TMVA_SOFIE_" + model_name) - inference_session = sofie_model_namespace.Session(header_path.removesuffix(".hxx") + ".dat") - - sofie_inference_result = np.asarray(inference_session.infer(input_tensor)).flatten() - - # Evaluate the original model with PyTorch on the same input - torch_model = torch.jit.load(model_path) - torch_model.eval() - torch_inference_result = torch_model(torch.from_numpy(input_tensor)).detach().numpy() - - # Compare the output tensor sizes and values - self.assertEqual(len(sofie_inference_result), torch_inference_result.size) - np.testing.assert_allclose( - sofie_inference_result, - torch_inference_result.flatten(), - atol=atol, - rtol=0.0, # explicitly disable relative tolerance (NumPy uses |a - b| <= atol + rtol * |b|) - ) - - def test_sequential_model(self): - input_tensor = np.array( - [[-1.6207, 0.6133, 0.5058, -1.2560], [-0.7750, -1.6701, 0.8171, -0.2858]], dtype=np.float32 - ) - self.generate_and_test_inference("PyTorchModelSequential", input_tensor, atol=1e-6) - - def test_module_model(self): - input_tensor = np.array( - [ - [0.5516, 0.3585, -0.4854, -1.3884, 0.8057, -0.9449], - [0.5626, -0.6466, -1.8818, 0.4736, 1.1102, 1.8694], - ], - dtype=np.float32, - ) - self.generate_and_test_inference("PyTorchModelModule", input_tensor, atol=1e-6) - - def test_convolution_model(self): - input_tensor = np.arange(1, 751, dtype=np.float32).reshape(5, 6, 5, 5) - self.generate_and_test_inference("PyTorchModelConvolution", input_tensor, atol=1e-3) - - -if __name__ == "__main__": - unittest.main() diff --git a/tmva/sofie/README.md b/tmva/sofie/README.md index a9b0bfed90528..3312f46ee0330 100644 --- a/tmva/sofie/README.md +++ b/tmva/sofie/README.md @@ -175,9 +175,7 @@ parser.CheckModel("example_model.ONNX"); - **Tutorials** - [TMVA_SOFIE_Inference](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_Inference.py) - - [TMVA_SOFIE_Keras](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_Keras.C) - [TMVA_SOFIE_ONNX](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_ONNX.C) - - [TMVA_SOFIE_PyTorch](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_PyTorch.py) - [TMVA_SOFIE_PyTorch_HiggsModel](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_PyTorch_HiggsModel.py) - [TMVA_SOFIE_RDataFrame](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C) - [TMVA_SOFIE_RDataFrame](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.py) diff --git a/tmva/tmva/inc/TMVA/RSofieReader.hxx b/tmva/tmva/inc/TMVA/RSofieReader.hxx index 41a9c7c1749ff..dab65db3efb6c 100644 --- a/tmva/tmva/inc/TMVA/RSofieReader.hxx +++ b/tmva/tmva/inc/TMVA/RSofieReader.hxx @@ -37,10 +37,7 @@ namespace Experimental { /// TMVA::RSofieReader class for reading external Machine Learning models -/// in ONNX files, Keras .h5 or .keras files or PyTorch .pt files -/// and performing the inference using SOFIE -/// It is reccomended to use ONNX if possible since there is a larger support for -/// model operators. +/// in ONNX files and performing the inference using SOFIE. class RSofieReader { @@ -58,26 +55,9 @@ public: void Load(const std::string &path, std::vector> inputShapes = {}, int verbose = 0) { - enum EModelType {kONNX, kKeras, kPt, kROOT, kNotDef}; // type of model - EModelType type = kNotDef; - - size_t pos2 = std::string::npos; - if ( (pos2 = path.find(".onnx")) != std::string::npos) { - if (verbose) std::cout << "input model type is ONNX" << std::endl; - type = kONNX; - } else if ( (pos2 = path.find(".h5")) != std::string::npos || (pos2 = path.find(".keras")) != std::string::npos) { - if (verbose) std::cout << "input model type is Keras" << std::endl; - type = kKeras; - } else if ( (pos2 = path.find(".pt")) != std::string::npos) { - if (verbose) std::cout << "input model type is PyTorch" << std::endl; - type = kPt; - } else if ( (pos2 = path.find(".root")) != std::string::npos) { - if (verbose) std::cout << "input model type is ROOT" << std::endl; - type = kROOT; - } - - if (type == kNotDef) { - throw std::runtime_error("Input file is not an ONNX or Keras or PyTorch file"); + auto pos2 = path.find(".onnx"); + if (pos2 == std::string::npos) { + throw std::runtime_error("Input file is not an ONNX file"); } auto pos1 = path.rfind("/"); if (pos1 == std::string::npos) @@ -95,68 +75,20 @@ public: // 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) { - throw std::runtime_error("RSofieReader: cannot use SOFIE with ONNX since libROOTTMVASofieParser is missing"); - } - gInterpreter->Declare("#include \"TMVA/RModelParser_ONNX.hxx\""); - parserCode += "{\nTMVA::Experimental::SOFIE::RModelParser_ONNX parser ; \n"; - if (verbose == 2) - parserCode += "TMVA::Experimental::SOFIE::RModel model = parser.Parse(\"" + path + "\",true); \n"; - else - parserCode += "TMVA::Experimental::SOFIE::RModel model = parser.Parse(\"" + path + "\"); \n"; - } - else if (type == kKeras) { - // 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]); - parserPythonCode += "model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse('" + path + "'," + batch_size + ")\n"; - } - else if (type == kPt) { - // use PyTorch Python parser - if (inputShapes.size() == 0) { - throw std::runtime_error("RSofieReader: cannot use SOFIE with PyTorch since the input tensor shape is missing and is needed by the PyTorch parser"); - } - std::string inputShapesStr = "["; - for (unsigned int i = 0; i < inputShapes.size(); i++) { - inputShapesStr += "["; - for (unsigned int j = 0; j < inputShapes[i].size(); j++) { - inputShapesStr += ROOT::Math::Util::ToString(inputShapes[i][j]); - if (j < inputShapes[i].size()-1) inputShapesStr += ", "; - } - inputShapesStr += "]"; - if (i < inputShapes.size()-1) inputShapesStr += ", "; - } - inputShapesStr += "]"; - parserPythonCode += "\"\"\"\n"; - parserPythonCode += "import ROOT\n"; - parserPythonCode += - "model = ROOT.TMVA.Experimental.SOFIE.PyTorch.Parse('" + path + "', " + inputShapesStr + ")\n"; - } - else if (type == kROOT) { - // use parser from ROOT - parserCode += "{\nauto fileRead = TFile::Open(\"" + path + "\",\"READ\");\n"; - parserCode += "TMVA::Experimental::SOFIE::RModel * modelPtr;\n"; - parserCode += "auto keyList = fileRead->GetListOfKeys(); TString name;\n"; - parserCode += "for (const auto&& k : *keyList) { \n"; - parserCode += " TString cname = ((TKey*)k)->GetClassName(); if (cname==\"TMVA::Experimental::SOFIE::RModel\") name = k->GetName(); }\n"; - parserCode += "fileRead->GetObject(name,modelPtr); fileRead->Close(); delete fileRead;\n"; - parserCode += "TMVA::Experimental::SOFIE::RModel & model = *modelPtr;\n"; + + // check first if we can load the SOFIE parser library + if (gSystem->Load("libROOTTMVASofieParser") < 0) { + throw std::runtime_error("RSofieReader: cannot use SOFIE with ONNX since libROOTTMVASofieParser is missing"); } + gInterpreter->Declare("#include \"TMVA/RModelParser_ONNX.hxx\""); + parserCode += "{\nTMVA::Experimental::SOFIE::RModelParser_ONNX parser ; \n"; + if (verbose == 2) + parserCode += "TMVA::Experimental::SOFIE::RModel model = parser.Parse(\"" + path + "\",true); \n"; + else + parserCode += "TMVA::Experimental::SOFIE::RModel model = parser.Parse(\"" + path + "\"); \n"; // 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 or PyTorch model)"); - for (auto & op : fCustomOperators) { parserCode += "{ auto p = new TMVA::Experimental::SOFIE::ROperator_Custom(\"" + op.fOpName + "\"," + op.fInputNames + "," + op.fOutputNames + "," + op.fOutputShapes + ",\"" + op.fFileName + "\");\n"; @@ -172,59 +104,31 @@ public: } if (verbose) std::cout << "generating the code with batch size = " << batchSize << " ...\n"; - if (parserPythonCode.empty()) { - parserCode += "model.Generate(TMVA::Experimental::SOFIE::Options::kDefault," - + ROOT::Math::Util::ToString(batchSize) + ", 0, " + std::to_string(verbose) + ");\n"; - - 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"; - } + parserCode += "model.Generate(TMVA::Experimental::SOFIE::Options::kDefault," + + ROOT::Math::Util::ToString(batchSize) + ", 0, " + std::to_string(verbose) + ");\n"; - // need information on number of inputs (assume output is 1) - parserCode += "int nInputs = model.GetInputTensorNames().size();\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"; - - 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 += "\"\"\""; + 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"; } + + // need information on number of inputs (assume output is 1) + parserCode += "int nInputs = model.GetInputTensorNames().size();\n"; + + //end of parsing C++ code + parserCode += "return nInputs;\n}\n"; // 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(); + if (verbose) { + std::cout << "...ParserCode being executed...:\n"; + std::cout << parserCode << std::endl; } + iret = gROOT->ProcessLine(parserCode.c_str()); + fNInputs = iret; if (iret < 0) { std::string msg = "RSofieReader: error processing the parser code: \n" + parserCode; diff --git a/tutorials/CMakeLists.txt b/tutorials/CMakeLists.txt index dec59768f85c0..2090af7cc1068 100644 --- a/tutorials/CMakeLists.txt +++ b/tutorials/CMakeLists.txt @@ -367,13 +367,6 @@ else() list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RDataFrame.C) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_RSofieReader.C) endif() - # These SOFIE tutorials take models trained via PyMVA-PyKeras as input - if (NOT ROOT_KERAS_FOUND) - list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Keras.py) - endif() - if (NOT ROOT_TORCH_FOUND) - list(APPEND tmva_veto machine_learning/TMVA_SOFIE_PyTorch.py) - endif() # These SOFIE tutorials take the ONNX HiggsModel trained with PyTorch as input if (NOT ROOT_TORCH_FOUND OR NOT ROOT_ONNX_FOUND) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_ONNX.py) @@ -921,7 +914,6 @@ if(pyroot) machine_learning/ml_dataloader_resampling.py machine_learning/ml_dataloader_Higgs_Classification.py machine_learning/TMVA_SOFIE_Models.py - machine_learning/TMVA_SOFIE_PyTorch.py machine_learning/TMVA_SOFIE_PyTorch_HiggsModel.py ) file(GLOB requires_xgboost RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} diff --git a/tutorials/machine_learning/TMVA_SOFIE_Keras.py b/tutorials/machine_learning/TMVA_SOFIE_Keras.py deleted file mode 100644 index a5af486430849..0000000000000 --- a/tutorials/machine_learning/TMVA_SOFIE_Keras.py +++ /dev/null @@ -1,114 +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 and Lorenzo Moneta - - -import contextlib -import warnings - -import numpy as np -import ROOT -from tensorflow.keras.layers import Activation, Dense, Input, Softmax -from tensorflow.keras.models import Model - -# Enable ROOT in batch mode (same effect as -nodraw) -ROOT.gROOT.SetBatch(True) - - -@contextlib.contextmanager -def expect_warning(category, message): - """Silence a known third-party warning and raise if it stops firing. - - Notifies us to drop the workaround once the upstream library is fixed. - """ - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - yield - seen = False - for w in caught: - if issubclass(w.category, category) and message in str(w.message): - seen = True - else: - warnings.warn_explicit(w.message, w.category, w.filename, w.lineno) - if not seen: - raise RuntimeError( - f"Expected {category.__name__} containing {message!r} was not " - "emitted. This tutorial's workaround can probably be removed." - ) - - -# ----------------------------------------------------------------------------- -# Step 1: Create and train a simple Keras model (via embedded Python) -# ----------------------------------------------------------------------------- - -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) - -# Keras' internal ``np.array(x)`` (TensorFlow backend) does not yet implement -# the NumPy 2.0 ``__array__(copy=...)`` signature, so saving the model emits a -# DeprecationWarning that we cannot fix from user code. -if tuple(int(p) for p in np.__version__.split(".")[:2]) >= (2, 0): - ctx = expect_warning(DeprecationWarning, "__array__ implementation doesn't accept a copy keyword") -else: - ctx = contextlib.nullcontext() - -with ctx: - model.save("KerasModel.keras") - -model.summary() - -# ----------------------------------------------------------------------------- -# Step 2: Use TMVA::SOFIE to parse the ONNX model -# ----------------------------------------------------------------------------- - -# 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_PyTorch.py b/tutorials/machine_learning/TMVA_SOFIE_PyTorch.py deleted file mode 100644 index 48a8ddfa66ffb..0000000000000 --- a/tutorials/machine_learning/TMVA_SOFIE_PyTorch.py +++ /dev/null @@ -1,92 +0,0 @@ -### \file -### \ingroup tutorial_ml -### \notebook -nodraw -### This macro provides a simple example for the parsing of PyTorch .pt file -### into RModel object and further generating the .hxx header files for inference. -### -### \macro_code -### \macro_output -### \author Sanjiban Sengupta - -import sys - -import ROOT -import torch -import torch.nn as nn - -SOFIE = ROOT.TMVA.Experimental.SOFIE - -# Python and C++ write to separate stdout buffers; flush both on every line so -# that the Python prints and the RModel printouts appear in order -sys.stdout.reconfigure(line_buffering=True) -ROOT.gInterpreter.ProcessLine("std::cout << std::unitbuf;") - -# ------------------------------------------------------------------------------ -# Step 1: Create, train and save a simple PyTorch model -# ------------------------------------------------------------------------------ - -model = nn.Sequential( - nn.Linear(32, 16), - nn.ReLU(), - nn.Linear(16, 8), - nn.ReLU(), -) - -criterion = nn.MSELoss() -optimizer = torch.optim.SGD(model.parameters(), lr=0.01) - -x = torch.randn(2, 32) -y = torch.randn(2, 8) - -for i in range(500): - y_pred = model(x) - loss = criterion(y_pred, y) - optimizer.zero_grad() - loss.backward() - optimizer.step() - -model.eval() -m = torch.jit.script(model) -torch.jit.save(m, "PyTorchModel.pt") - -# ------------------------------------------------------------------------------ -# Step 2: Parse the saved PyTorch .pt file with TMVA::SOFIE -# ------------------------------------------------------------------------------ - -# Parsing a PyTorch model requires the shape and data-type of input tensor -# Data-type of input tensor defaults to Float if not specified -input_shapes = ROOT.std.vector["std::vector"]() -input_shapes.push_back([2, 32]) - -# Parsing the saved PyTorch .pt file into RModel object -model = SOFIE.PyTorch.Parse("PyTorchModel.pt", input_shapes) - -# Generating inference code -model.Generate() -model.OutputGenerated("PyTorchModel.hxx") - -# Printing required input tensors -print("\n") -model.PrintRequiredInputTensors() - -# Printing initialized tensors (weights) -print("\n") -model.PrintInitializedTensors() - -# Printing intermediate tensors -print("\n") -model.PrintIntermediateTensors() - -# Checking if tensor already exist in model -tensor_exists = bool(model.CheckIfTensorAlreadyExist("0weight")) -print(f'\n\nTensor "0weight" already exist: {str(tensor_exists).lower()}\n') - -tensor_shape = model.GetTensorShape("0weight") -print('Shape of tensor "0weight": ' + ",".join(str(dim) for dim in tensor_shape) + ",") - -tensor_type = model.GetTensorType("0weight") -print(f'\nData type of tensor "0weight": {SOFIE.ConvertTypeToString(tensor_type)}') - -# Printing generated inference code -print() -model.PrintGenerated() diff --git a/tutorials/machine_learning/index.md b/tutorials/machine_learning/index.md index 55044f94d1a8f..9c0bc9d14d68d 100644 --- a/tutorials/machine_learning/index.md +++ b/tutorials/machine_learning/index.md @@ -119,10 +119,8 @@ | **Tutorial** || **Description** | |---------------|-----------------|-----------------| | | TMVA_SOFIE_Inference.py | Using a trained model with PyTorch and make inference using SOFIE directly from Numpy. | -| TMVA_SOFIE_Keras.C | | Parsing of Keras .h5 file into RModel object and further generating the .hxx header files for inference. | | | TMVA_SOFIE_Models.py | Inference with SOFIE using a set of models trained with PyTorch. | | TMVA_SOFIE_ONNX.C | | Parsing of ONNX files into RModel object and further generating the .hxx header files for inference. | -| | TMVA_SOFIE_PyTorch.py | Parsing of PyTorch .pt file into RModel object and further generating the .hxx header files for inference. | | | TMVA_SOFIE_PyTorch_HiggsModel.py | Train a model on the Higgs dataset with PyTorch, export it to ONNX and run the SOFIE parser on it to generate the C++ inference code used by the other Higgs SOFIE tutorials. | | TMVA_SOFIE_RDataFrame.C | TMVA_SOFIE_RDataFrame.py | Inference with SOFIE and RDataFrame, of a model trained with PyTorch. | | TMVA_SOFIE_RDataFrame_JIT.C | | Using a trained model with PyTorch and make inference using SOFIE and RDataFrame. |