From aff6bf762ce76c769632933157b674e9108b665e Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Thu, 9 Jul 2026 19:20:38 +0200 Subject: [PATCH] [tmva][sofie] Remove Keras and PyTorch parses from ROOT repository This is a draft commit to exercise how ROOT without the Keras and PyTorch parsers would do in the CI. --- .../pythonizations/python/CMakeLists.txt | 28 +- .../pythonizations/python/ROOT/_facade.py | 5 - .../ROOT/_pythonization/_tmva/__init__.py | 2 +- .../python/ROOT/_pythonization/_tmva/_gnn.py | 351 ----------- .../_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 | 85 --- .../_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 | 32 - .../test/generate_keras_functional.py | 237 -------- .../test/generate_keras_sequential.py | 232 ------- .../pyroot/pythonizations/test/sofie_gnn.py | 355 ----------- .../pythonizations/test/sofie_keras_parser.py | 88 --- .../test/sofie_keras_parser_models.py | 272 --------- .../test/sofie_pytorch_parser.py | 146 ----- 36 files changed, 2 insertions(+), 3600 deletions(-) delete mode 100644 bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_gnn.py 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/sofie_gnn.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 diff --git a/bindings/pyroot/pythonizations/python/CMakeLists.txt b/bindings/pyroot/pythonizations/python/CMakeLists.txt index c3c3819843b8a..91925eaefe3cf 100644 --- a/bindings/pyroot/pythonizations/python/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/python/CMakeLists.txt @@ -59,33 +59,7 @@ if(tmva) ROOT/_pythonization/_tmva/_rbdt.py ROOT/_pythonization/_tmva/_rtensor.py ROOT/_pythonization/_tmva/_tree_inference.py - ROOT/_pythonization/_tmva/_utils.py - ROOT/_pythonization/_tmva/_gnn.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/__init__.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/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/_utils.py) endif() set(py_sources diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index b845b77dc852f..77ae14be4052d 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -565,13 +565,8 @@ 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 from ._pythonization._tmva._tree_inference import SaveXGBoost - setattr(ns.Experimental.SOFIE, "PyKeras", PyKeras) - setattr(ns.Experimental.SOFIE, "PyTorch", PyTorch) - ns.Experimental.AsRTensor = _AsRTensor ns.Experimental.SaveXGBoost = SaveXGBoost except ImportError: diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py index acb4c6264f278..2866982a2bdb2 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/__init__.py @@ -11,7 +11,7 @@ ################################################################################ from .. import pythonization -from . import _gnn, _rbdt # noqa: F401 # imported so @pythonization functions are found recursively +from . import _rbdt # noqa: F401 # imported so @pythonization functions are found recursively from ._crossvalidation import CrossValidation from ._dataloader import DataLoader from ._factory import Factory diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_gnn.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_gnn.py deleted file mode 100644 index 437682a05b53d..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_gnn.py +++ /dev/null @@ -1,351 +0,0 @@ -# Authors: -# * Sanjiban Sengupta 01/2023 -# * Lorenzo Moneta 01/2023 - -################################################################################ -# Copyright (C) 1995-2020, Rene Brun and Fons Rademakers. # -# All rights reserved. # -# # -# For the licensing terms see $ROOTSYS/LICENSE. # -# For the list of contributors see $ROOTSYS/README/CREDITS. # -################################################################################ - - -from .. import pythonization - - -def getActivationFunction(model): - """ - Get the activation function for the model. - - Parameters: - model: The graph_nets' component model to extract the activation function from. - The component model can be either of the update functions for - nodes, edges or globals. - - Returns: - The activation function enum value. - """ - import ROOT - - function = model._activation.__name__ - if function == 'relu': - return ROOT.TMVA.Experimental.SOFIE.Activation.RELU - else: - return ROOT.TMVA.Experimental.SOFIE.Activation.Invalid - -def make_mlp_model(gin, model, function_target, type): - """ - Create an MLP model and add it to the GNN Initializer. - - Parameters: - gin: The GNN Initializer to which the model will be added. - model: The model extracted from graph_nets's GNN component - function_target: Target for the function to update either of nodes, edges or globals - graph_type: The type of the graph, i.e. GNN or GraphIndependent - - """ - import ROOT - - num_layers = len(model._layers) - activation = getActivationFunction(model) - upd = ROOT.TMVA.Experimental.SOFIE.RFunction_MLP(function_target, num_layers, activation, model._activate_final, type) - kernel_tensor_names = ROOT.std.vector['std::string']() - bias_tensor_names = ROOT.std.vector['std::string']() - - for i in range(0, 2*num_layers, 2): - bias_tensor_names.push_back(model.variables[i].name) - kernel_tensor_names.push_back(model.variables[i+1].name) - val = ROOT.std.vector['std::vector']() - val.push_back(kernel_tensor_names) - val.push_back(bias_tensor_names) - upd.AddInitializedTensors(val) - gin.createUpdateFunction(upd) - -def make_linear_model(gin, model, function_target, type): - """ - Create an Linear model and add it to the GNN Initializer. - - Parameters: - gin: The GNN Initializer to which the model will be added. - model: The model extracted from graph_nets's GNN component - function_target: Target for the function to update either of nodes, edges or globals - graph_type: The type of the graph, i.e. GNN or GraphIndependent - - """ - import ROOT - - activation = ROOT.TMVA.Experimental.SOFIE.Activation.Invalid - upd = ROOT.TMVA.Experimental.SOFIE.RFunction_MLP(function_target, 1, activation, False, type) - kernel_tensor_names = ROOT.std.vector['std::string'](1) - bias_tensor_names = ROOT.std.vector['std::string'](1) - - if (len(model.variables) == 1) : - kernel_tensor_names[0] = model.variables[0].name - else : - bias_tensor_names[0] = model.variables[0].name - kernel_tensor_names[0] = model.variables[1].name - - val = ROOT.std.vector['std::vector']() - val.push_back(kernel_tensor_names) - val.push_back(bias_tensor_names) - upd.AddInitializedTensors(val) - gin.createUpdateFunction(upd) - -def add_layer_norm(gin, module_layer, function_target): - """ - Add a LayerNormalization operator to the particular function target - in the Graph Initializer - - Parameters: - gin: The GNN Initializer to which the LayerNorm operator will be added - module_layer: Extracted LayerNorm from graph_nets' model - function_target: Target for the function to update either of nodes, edges or globals - - """ - import ROOT - - if function_target == ROOT.TMVA.Experimental.SOFIE.FunctionTarget.NODES: - model_block = gin.nodes_update_block - elif function_target == ROOT.TMVA.Experimental.SOFIE.FunctionTarget.EDGES: - model_block = gin.edges_update_block - else: - model_block = gin.globals_update_block - axis = module_layer._axis - eps = module_layer._eps - stash_type = 1 - name_x = model_block.GetFunctionBlock().GetOutputTensorNames()[0] - name_bias = module_layer.offset.name - name_scale = module_layer.scale.name - name_Y = name_x+"output" - model_block.AddLayerNormalization(axis[0], eps, stash_type, name_x, name_scale, name_bias, name_Y) - new_output_tensors = ROOT.std.vector['std::string']() - new_output_tensors.push_back(name_Y) - model_block.GetFunctionBlock().AddOutputTensorNameList(new_output_tensors) - -def add_weights(gin, weights, function_target): - """ - Add weights to respective function targets, either of nodes, edges or globals - - Parameters: - gin: The GNN Initializer to which the weights will be added - weights: Weight information, containing the names, shapes and values of initialized tensors - function_target: Target for the function to update either of nodes, edges or globals - - """ - from ROOT.TMVA.Experimental import SOFIE - - if function_target == SOFIE.FunctionTarget.NODES: - model_block = gin.nodes_update_block - elif function_target == SOFIE.FunctionTarget.EDGES: - model_block = gin.edges_update_block - else: - model_block = gin.globals_update_block - - for i in weights: - model_block.GetFunctionBlock().AddInitializedTensor( - i.name, SOFIE.ETensorType.FLOAT, i.shape.as_list(), i.numpy() - ) - - -def add_aggregate_function(gin, reducer, relation): - """ - Add aggregate function to the Graph Initializer - - Parameters: - gin: The GNN Initializer to which the Aggregate function will be added - reducer: Specifies the means of aggregate, i.e. sum or mean of supplied values - relation: Specifies the relation of aggregate, i.e. Node-Edge, Global-Edge or Global-Node - - """ - import ROOT - - if(reducer == "unsorted_segment_sum"): - agg = ROOT.TMVA.Experimental.SOFIE.RFunction_Sum() - gin.createAggregateFunction[ROOT.TMVA.Experimental.SOFIE.RFunction_Sum](agg, relation) - elif(reducer == "unsorted_segment_mean"): - agg = ROOT.TMVA.Experimental.SOFIE.RFunction_Mean() - gin.createAggregateFunction[ROOT.TMVA.Experimental.SOFIE.RFunction_Mean](agg, relation) - else: - raise RuntimeError("Invalid aggregate function for reduction") - - -def add_update_function(gin, component_model, graph_type, function_target): - """ - Add update function for respective function target, either of nodes, edges or globals - based on the supplied component_model - - Parameters: - gin: The GNN Initializer to which the update function will be added - component_model: The update function to add, either of MLP or Sequential - graph_type: The type of the graph, i.e. GNN or GraphIndependent - function_target: Target for the function to update either of nodes, edges or globals - - """ - if (type(component_model).__name__ == 'MLP'): - make_mlp_model(gin, component_model, function_target, graph_type) - elif (type(component_model).__name__ == 'Sequential'): - for i in component_model._layers: - if(type(i).__name__ == 'MLP'): - make_mlp_model(gin, i, function_target, graph_type) - elif(type(i).__name__ == 'LayerNorm'): - add_layer_norm(gin, i, function_target) - else: - raise RuntimeError("Invalid Model " + type(i).__name__ + " for layer update") - elif (type(component_model).__name__ == 'Linear'): - make_linear_model(gin, component_model, function_target, graph_type) - else: - raise RuntimeError("Invalid Model " + type(component_model).__name__ + " for update function") - add_weights(gin, component_model.variables, function_target) - - - -class RModel_GNN: - """ - Wrapper class for graph_nets' GNN model;s parsing and inference generation - - graph_nets' GNN model comprises of three components, the nodes, edges and globals. - The entire model and its inference is based on the respective update functions, - and aggregate function with other components. - """ - - def ParseFromMemory(graph_module, graph_data, filename = "gnn_network"): - """ - Parse graph_nets' GraphNetwork model and create RModel_GNN. - - Parameters: - graph_module: The graph module built from graph_nets - graph_data: Sample graph input data required for parsing of graph_nets' model - containing dict with keys: {"globals", "nodes", "edges", "senders", "receivers"} - filename: The filename to be used for output of inference code. - - Returns: - An instance of RModel_GNN. - """ - import ROOT - - gin = ROOT.TMVA.Experimental.SOFIE.GNN_Init() - gin.num_nodes = len(graph_data['nodes']) - - # extracting the edges - for sender, receiver in zip(graph_data['senders'], graph_data['receivers']): - gin.edges.push_back(ROOT.std.make_pair['int,int'](int(receiver), int(sender))) - - gin.num_node_features = len(graph_data['nodes'][0]) - gin.num_edge_features = len(graph_data['edges'][0]) - gin.num_global_features = len(graph_data['globals']) - - gin.filename = filename - - # adding the node update function - node_model = graph_module._node_block._node_model - add_update_function(gin, node_model, ROOT.TMVA.Experimental.SOFIE.GraphType.GNN, - ROOT.TMVA.Experimental.SOFIE.FunctionTarget.NODES) - - # adding the edge update function - edge_model = graph_module._edge_block._edge_model - add_update_function(gin, edge_model, ROOT.TMVA.Experimental.SOFIE.GraphType.GNN, - ROOT.TMVA.Experimental.SOFIE.FunctionTarget.EDGES) - - # adding the global update function - global_model = graph_module._global_block._global_model - add_update_function(gin, global_model, ROOT.TMVA.Experimental.SOFIE.GraphType.GNN, - ROOT.TMVA.Experimental.SOFIE.FunctionTarget.GLOBALS) - - # adding edge-node aggregate function - add_aggregate_function(gin, graph_module._node_block._received_edges_aggregator._reducer.__qualname__, ROOT.TMVA.Experimental.SOFIE.FunctionRelation.NODES_EDGES) - - # adding node-global aggregate function - add_aggregate_function(gin, graph_module._global_block._nodes_aggregator._reducer.__qualname__, ROOT.TMVA.Experimental.SOFIE.FunctionRelation.NODES_GLOBALS) - - # adding edge-global aggregate function - add_aggregate_function(gin, graph_module._global_block._edges_aggregator._reducer.__qualname__, ROOT.TMVA.Experimental.SOFIE.FunctionRelation.EDGES_GLOBALS) - - gnn_model = ROOT.TMVA.Experimental.SOFIE.RModel_GNN(gin) - blas_routines = ROOT.std.vector['std::string']() - blas_routines.push_back("Gemm") - blas_routines.push_back("Axpy") - blas_routines.push_back("Gemv") - gnn_model.AddBlasRoutines(blas_routines) - gnn_model.AddNeededStdLib("algorithm") - gnn_model.AddNeededStdLib("cmath") - return gnn_model - - - -class RModel_GraphIndependent: - """ - Wrapper class for graph_nets' GraphIndependent model's parsing and inference generation - - graph_nets' GraphIndependent model is similar to the GNN implementation, with the - difference being that it has no aggregate function. GraphIndependent is useful - for independent transformation on the graph data. - - """ - - def ParseFromMemory(graph_module, graph_data, filename = "graph_independent_network"): - """ - Parse graph_nets' GraphIndependent model and create RModel_GraphIndependent. - - Parameters: - graph_module: The graph module built from graph_nets - graph_data: Sample graph input data required for parsing of graph_nets' model - containing dict with keys: {"globals", "nodes", "edges", "senders", "receivers"} - filename: The filename to be used for output of inference code. - - Returns: - An instance of RModel_GraphIndependent. - """ - import ROOT - - gin = ROOT.TMVA.Experimental.SOFIE.GraphIndependent_Init() - gin.num_nodes = len(graph_data['nodes']) - - # extracting the edges - for sender, receiver in zip(graph_data['senders'], graph_data['receivers']): - gin.edges.push_back(ROOT.std.make_pair['int,int'](int(receiver), int(sender))) - - gin.num_node_features = len(graph_data['nodes'][0]) - gin.num_edge_features = len(graph_data['edges'][0]) - gin.num_global_features = len(graph_data['globals']) - - gin.filename = filename - - - # adding the node update function - # when an update is present in graph_nets, the update function has the _model attribute - # otherwise it is just a simple function defining output = input. - node_model = graph_module._node_model - if (hasattr(node_model,"_model")) : - add_update_function(gin, node_model._model, ROOT.TMVA.Experimental.SOFIE.GraphType.GraphIndependent, - ROOT.TMVA.Experimental.SOFIE.FunctionTarget.NODES) - - # adding the edge update function - edge_model = graph_module._edge_model - if (hasattr(edge_model,"_model")) : - add_update_function(gin, edge_model._model, ROOT.TMVA.Experimental.SOFIE.GraphType.GraphIndependent, - ROOT.TMVA.Experimental.SOFIE.FunctionTarget.EDGES) - - # adding the global update function - global_model = graph_module._global_model - if (hasattr(global_model,"_model")) : - add_update_function(gin, global_model._model, ROOT.TMVA.Experimental.SOFIE.GraphType.GraphIndependent, - ROOT.TMVA.Experimental.SOFIE.FunctionTarget.GLOBALS) - - graph_independent_model = ROOT.TMVA.Experimental.SOFIE.RModel_GraphIndependent(gin) - blas_routines = ROOT.std.vector['std::string']() - blas_routines.push_back("Gemm") - graph_independent_model.AddBlasRoutines(blas_routines) - graph_independent_model.AddNeededStdLib("algorithm") - graph_independent_model.AddNeededStdLib("cmath") - return graph_independent_model - - -@pythonization("RModel_GNN", ns="TMVA::Experimental::SOFIE") -def pythonize_gnn_parse(klass): - setattr(klass, "ParseFromMemory", RModel_GNN.ParseFromMemory) - -@pythonization("RModel_GraphIndependent", ns="TMVA::Experimental::SOFIE") -def pythonize_graph_independent_parse(klass): - setattr(klass, "ParseFromMemory", RModel_GraphIndependent.ParseFromMemory) - 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 b902e4028c898..0000000000000 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/conv.py +++ /dev/null @@ -1,85 +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])) - padding_height = max((outputHeight - 1) * fAttrStrides[0] + fAttrKernelShape[0] - inputHeight, 0) - padding_width = max((outputWidth - 1) * fAttrStrides[1] + fAttrKernelShape[1] - inputWidth, 0) - padding_top = math.floor(padding_height / 2) - padding_bottom = padding_height - padding_top - padding_left = math.floor(padding_width / 2) - padding_right = padding_width - padding_left - fAttrPads = [padding_top, padding_bottom, padding_left, padding_right] - else: - raise RuntimeError( - "TMVA::SOFIE - RModel Keras Parser doesn't yet supports Convolution layer with padding " + fKerasPadding - ) - if 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 b7cdc2e3bf5fa..c58805807e501 100644 --- a/bindings/pyroot/pythonizations/test/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/test/CMakeLists.txt @@ -132,38 +132,6 @@ if (dataframe) ROOT_ADD_PYUNITTEST(pyroot_pyz_rdataframe_misc rdataframe_misc.py PYTHON_DEPS numpy) endif() -# SOFIE-GNN pythonizations -if (tmva) - ROOT_FIND_PYTHON_MODULE(sonnet) - ROOT_FIND_PYTHON_MODULE(graph_nets) - if (ROOT_SONNET_FOUND AND ROOT_GRAPH_NETS_FOUND) - ROOT_ADD_PYUNITTEST(pyroot_pyz_sofie_gnn sofie_gnn.py PYTHON_DEPS numpy sonnet graph_nets) - 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 9788e89e42105..0000000000000 --- a/bindings/pyroot/pythonizations/test/generate_keras_functional.py +++ /dev/null @@ -1,237 +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_valid - inp = layers.Input(shape=(8, 8, 3)) - out = layers.Conv2D(4, (3, 3), padding='valid', data_format='channels_last', activation='elu')(inp) - model = models.Model(inp, out) - train_and_save(model, "Conv2D_padding_valid") - - # Dense - inp = layers.Input(shape=(10,)) - out = layers.Dense(5, activation='tanh')(inp) - model = models.Model(inp, out) - train_and_save(model, "Dense") - - # ELU - inp = layers.Input(shape=(10,)) - out = layers.ELU(alpha=0.5)(inp) - model = models.Model(inp, out) - train_and_save(model, "ELU") - - # Flatten - inp = layers.Input(shape=(4, 5)) - out = layers.Flatten()(inp) - model = models.Model(inp, out) - train_and_save(model, "Flatten") - - # GlobalAveragePooling2D channels first - if (is_channels_first_supported): - inp = layers.Input(shape=(3, 4, 6)) - out = layers.GlobalAveragePooling2D(data_format='channels_first')(inp) - model = models.Model(inp, out) - train_and_save(model, "GlobalAveragePooling2D_channels_first") - - # GlobalAveragePooling2D channels last - inp = layers.Input(shape=(4, 6, 3)) - out = layers.GlobalAveragePooling2D(data_format='channels_last')(inp) - model = models.Model(inp, out) - train_and_save(model, "GlobalAveragePooling2D_channels_last") - - # LayerNorm - inp = layers.Input(shape=(10, 3, 5)) - out = layers.LayerNormalization(axis=-1)(inp) - model = models.Model(inp, out) - train_and_save(model, "LayerNorm") - - # LeakyReLU - inp = layers.Input(shape=(10,)) - out = layers.LeakyReLU()(inp) - model = models.Model(inp, out) - train_and_save(model, "LeakyReLU") - - # MaxPooling2D channels_first - if (is_channels_first_supported): - inp = layers.Input(shape=(3, 8, 8)) - out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last')(inp) - model = models.Model(inp, out) - train_and_save(model, "MaxPool2D_channels_first") - - # MaxPooling2D channels_last - inp = layers.Input(shape=(8, 8, 3)) - out = layers.MaxPooling2D(pool_size=(2, 2), data_format='channels_last')(inp) - model = models.Model(inp, out) - train_and_save(model, "MaxPool2D_channels_last") - - # Multiply - in1 = layers.Input(shape=(8,)) - in2 = layers.Input(shape=(8,)) - out = layers.Multiply()([in1, in2]) - model = models.Model([in1, in2], out) - train_and_save(model, "Multiply") - - # Permute - inp = layers.Input(shape=(3, 4, 5)) - out = layers.Permute((2, 1, 3))(inp) - model = models.Model(inp, out) - train_and_save(model, "Permute") - - # ReLU - inp = layers.Input(shape=(10,)) - out = layers.ReLU()(inp) - model = models.Model(inp, out) - train_and_save(model, "ReLU") - - # Reshape - inp = layers.Input(shape=(4, 5)) - out = layers.Reshape((2, 10))(inp) - model = models.Model(inp, out) - train_and_save(model, "Reshape") - - # Softmax - inp = layers.Input(shape=(10,)) - out = layers.Softmax()(inp) - model = models.Model(inp, out) - train_and_save(model, "Softmax") - - # Subtract - in1 = layers.Input(shape=(8,)) - in2 = layers.Input(shape=(8,)) - out = layers.Subtract()([in1, in2]) - model = models.Model([in1, in2], out) - train_and_save(model, "Subtract") - - # Layer Combination - - inp = layers.Input(shape=(32, 32, 3)) - x = layers.Conv2D(8, (3,3), padding="same", activation="relu", data_format='channels_last')(inp) - x = layers.MaxPooling2D((2,2))(x) - x = layers.Reshape((16, 16, 8))(x) - x = layers.Permute((3, 1, 2))(x) - x = layers.Flatten()(x) - out = layers.Dense(10, activation="softmax")(x) - model = models.Model(inp, out) - train_and_save(model, "Layer_Combination_1") - - inp = layers.Input(shape=(20,)) - x = layers.Dense(32, activation="tanh")(inp) - x = layers.Dense(16)(x) - x = layers.ELU()(x) - x = layers.LayerNormalization()(x) - out = layers.Dense(5, activation="sigmoid")(x) - model = models.Model(inp, out) - train_and_save(model, "Layer_Combination_2") - - inp1 = layers.Input(shape=(16,)) - inp2 = layers.Input(shape=(16,)) - d1 = layers.Dense(16, activation="relu")(inp1) - d2 = layers.Dense(16, activation="selu")(inp2) - add = layers.Add()([d1, d2]) - sub = layers.Subtract()([d1, d2]) - mul = layers.Multiply()([d1, d2]) - merged = layers.Concatenate()([add, sub, mul]) - # `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 aed88f4b8b4e1..0000000000000 --- a/bindings/pyroot/pythonizations/test/generate_keras_sequential.py +++ /dev/null @@ -1,232 +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_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/sofie_gnn.py b/bindings/pyroot/pythonizations/test/sofie_gnn.py deleted file mode 100644 index 5f06fbaa6098f..0000000000000 --- a/bindings/pyroot/pythonizations/test/sofie_gnn.py +++ /dev/null @@ -1,355 +0,0 @@ -import unittest -import ROOT - -# Load system openblas library explicitly if available. This avoids pulling in -# NumPys builtin openblas later, which will conflict with the system openblas. -ROOT.gInterpreter.Load("libopenblaso.so") - -import numpy as np -from numpy.testing import assert_almost_equal - -if np.__version__ >= "1.20" and np.__version__ < "1.24": - raise RuntimeError(f"This test requires NumPy version <=1.19 or >=1.24") - -import graph_nets as gn -from graph_nets import utils_tf -import sonnet as snt -import os - - -# for generating input data for graph nets, -# from https://github.com/deepmind/graph_nets/blob/master/graph_nets/demos/graph_nets_basics.ipynb -def get_graph_data_dict(num_nodes, num_edges, GLOBAL_FEATURE_SIZE=2, NODE_FEATURE_SIZE=2, EDGE_FEATURE_SIZE=2): - return { - "globals": np.random.rand(GLOBAL_FEATURE_SIZE).astype(np.float32), - "nodes": np.random.rand(num_nodes, NODE_FEATURE_SIZE).astype(np.float32), - "edges": np.random.rand(num_edges, EDGE_FEATURE_SIZE).astype(np.float32), - "senders": np.random.randint(num_nodes, size=num_edges, dtype=np.int32), - "receivers": np.random.randint(num_nodes, size=num_edges, dtype=np.int32), - } - - -def resize_graph_data(input_data, GLOBAL_FEATURE_SIZE=2, NODE_FEATURE_SIZE=2, EDGE_FEATURE_SIZE=2): - n = input_data["nodes"] - e = input_data["edges"] - s = input_data["senders"] - r = input_data["receivers"] - return { - "globals": np.zeros((GLOBAL_FEATURE_SIZE)), - "nodes": np.zeros((n.shape[0], NODE_FEATURE_SIZE)), - "edges": np.zeros((e.shape[0], EDGE_FEATURE_SIZE)), - "senders": s, - "receivers": r, - } - - -LATENT_SIZE = 2 - - -def make_mlp_model(): - """Instantiates a new MLP, followed by LayerNorm. - - The parameters of each new MLP are not shared with others generated by - this function. - - Returns: - A Sonnet module which contains the MLP and LayerNorm. - """ - return snt.Sequential( - [ - snt.nets.MLP([LATENT_SIZE] * 2, activate_final=True), - snt.LayerNorm(axis=-1, create_offset=True, create_scale=True), - ] - ) - - -def CopyData(input_data): - output_data = ROOT.TMVA.Experimental.SOFIE.Copy(input_data) - return output_data - - -class MLPGraphIndependent(snt.Module): - """GraphIndependent with MLP edge, node, and global models.""" - - def __init__(self, name="MLPGraphIndependent"): - super(MLPGraphIndependent, self).__init__(name=name) - self._network = gn.modules.GraphIndependent( - edge_model_fn=lambda: snt.nets.MLP([LATENT_SIZE] * 2, activate_final=True), - node_model_fn=lambda: snt.nets.MLP([LATENT_SIZE] * 2, activate_final=True), - global_model_fn=lambda: snt.nets.MLP([LATENT_SIZE] * 2, activate_final=True), - ) - - def __call__(self, inputs): - return self._network(inputs) - - -class MLPGraphNetwork(snt.Module): - """GraphNetwork with MLP edge, node, and global models.""" - - def __init__(self, name="MLPGraphNetwork"): - super(MLPGraphNetwork, self).__init__(name=name) - self._network = gn.modules.GraphNetwork( - edge_model_fn=make_mlp_model, node_model_fn=make_mlp_model, global_model_fn=make_mlp_model - ) - - def __call__(self, inputs): - return self._network(inputs) - - -def PrintGData(data, printShape=True): - n = data.nodes.numpy() - e = data.edges.numpy() - g = data.globals.numpy() - if printShape: - print("GNet data ... shapes", n.shape, e.shape, g.shape) - print( - " node data", - n.reshape( - n.size, - ), - ) - print( - " edge data", - e.reshape( - e.size, - ), - ) - print( - " global data", - g.reshape( - g.size, - ), - ) - - -class EncodeProcessDecode(snt.Module): - - def __init__(self, name="EncodeProcessDecode"): - super(EncodeProcessDecode, self).__init__(name=name) - self._encoder = MLPGraphIndependent() - self._core = MLPGraphNetwork() - self._decoder = MLPGraphIndependent() - self._output_transform = MLPGraphIndependent() - - def __call__(self, input_op, num_processing_steps): - latent = self._encoder(input_op) - latent0 = latent - output_ops = [] - for _ in range(num_processing_steps): - core_input = utils_tf.concat([latent0, latent], axis=1) - latent = self._core(core_input) - decoded_op = self._decoder(latent) - output_ops.append(self._output_transform(decoded_op)) - return output_ops - - -class SOFIE_GNN(unittest.TestCase): - """ - Tests for the pythonizations of ParseFromMemory method of SOFIE GNN. - """ - - def test_a_parse_gnn(self): - """ - Test that parsed GNN model from a graphnets model generates correct - inference code - """ - - print("\nRun Graph parsing test") - - GraphModule = gn.modules.GraphNetwork( - edge_model_fn=lambda: snt.nets.MLP([2, 2], activate_final=True), - node_model_fn=lambda: snt.nets.MLP([2, 2], activate_final=True), - global_model_fn=lambda: snt.nets.MLP([2, 2], activate_final=True), - ) - - GraphData = get_graph_data_dict(2, 1, 2, 2, 2) - input_graphs = utils_tf.data_dicts_to_graphs_tuple([GraphData]) - output = GraphModule(input_graphs) - - # Parsing model to RModel_GNN - model = ROOT.TMVA.Experimental.SOFIE.RModel_GNN.ParseFromMemory(GraphModule, GraphData) - model.Generate() - model.OutputGenerated() - - ROOT.gInterpreter.Declare('#include "gnn_network.hxx"') - input_data = ROOT.TMVA.Experimental.SOFIE.GNN_Data() - - input_data.node_data = ROOT.TMVA.Experimental.AsRTensor(GraphData["nodes"]) - input_data.edge_data = ROOT.TMVA.Experimental.AsRTensor(GraphData["edges"]) - input_data.global_data = ROOT.TMVA.Experimental.AsRTensor(GraphData["globals"]) - input_data.edge_index = ROOT.TMVA.Experimental.AsRTensor( - np.stack((GraphData["receivers"], GraphData["senders"])) - ) - - session = ROOT.TMVA_SOFIE_gnn_network.Session() - session.infer(input_data) - - output_node_data = output.nodes.numpy() - output_edge_data = output.edges.numpy() - output_global_data = output.globals.numpy().flatten() - - assert_almost_equal(output_node_data, np.asarray(input_data.node_data), 5) - assert_almost_equal(output_edge_data, np.asarray(input_data.edge_data), 5) - assert_almost_equal(output_global_data, np.asarray(input_data.global_data), 5) - - def test_b_parse_graph_independent(self): - """ - Test that parsed GraphIndependent model from a graphnets model generates correct - inference code - """ - - print("\nRun Graph Independent parsing test") - - GraphModule = gn.modules.GraphIndependent( - edge_model_fn=lambda: snt.nets.MLP([2, 2], activate_final=True), - node_model_fn=lambda: snt.nets.MLP([2, 2], activate_final=True), - global_model_fn=lambda: snt.nets.MLP([2, 2], activate_final=True), - ) - - GraphData = get_graph_data_dict(2, 1, 2, 2, 2) - input_graphs = utils_tf.data_dicts_to_graphs_tuple([GraphData]) - output = GraphModule(input_graphs) - - # Parsing model to RModel_GraphIndependent - model = ROOT.TMVA.Experimental.SOFIE.RModel_GraphIndependent.ParseFromMemory(GraphModule, GraphData) - model.Generate() - model.OutputGenerated() - - ret = ROOT.gInterpreter.Declare('#include "graph_independent_network.hxx"') - - input_data = ROOT.TMVA.Experimental.SOFIE.GNN_Data() - - input_data.node_data = ROOT.TMVA.Experimental.AsRTensor(GraphData["nodes"]) - input_data.edge_data = ROOT.TMVA.Experimental.AsRTensor(GraphData["edges"]) - input_data.global_data = ROOT.TMVA.Experimental.AsRTensor(GraphData["globals"]) - input_data.edge_index = ROOT.TMVA.Experimental.AsRTensor( - np.stack((GraphData["receivers"], GraphData["senders"])) - ) - - session = ROOT.TMVA_SOFIE_graph_independent_network.Session() - session.infer(input_data) - - output_node_data = output.nodes.numpy() - output_edge_data = output.edges.numpy() - output_global_data = output.globals.numpy().flatten() - - assert_almost_equal(output_node_data, np.asarray(input_data.node_data), 5) - assert_almost_equal(output_edge_data, np.asarray(input_data.edge_data), 5) - assert_almost_equal(output_global_data, np.asarray(input_data.global_data), 5) - - def test_c_lhcb_toy_inference(self): - """ - Test that parsed stack of SOFIE GNN and GraphIndependent modules generate the correct - inference code - """ - - print("\nRun LHCb test") - - # Instantiating EncodeProcessDecode Model - - # number of features for node. edge, globals - nsize = 3 - esize = 3 - gsize = 2 - lsize = LATENT_SIZE # hard-coded latent size in definition of GNET model (for node edge and globals) - - ep_model = EncodeProcessDecode() - - # Initializing randomized input data - InputGraphData = get_graph_data_dict(2, 1, gsize, nsize, esize) - input_graphs = utils_tf.data_dicts_to_graphs_tuple([InputGraphData]) - - # Make data for core networks (number of features for node/edge global is 2 * lsize) - CoreGraphData = resize_graph_data(InputGraphData, 2 * lsize, 2 * lsize, 2 * lsize) - - OutputGraphData = resize_graph_data(InputGraphData, lsize, lsize, lsize) - - # Collecting output from GraphNets model stack - output_gn = ep_model(input_graphs, 2) - - print("senders and receivers ", InputGraphData["senders"], InputGraphData["receivers"]) - - # Declaring sofie models - encoder = ROOT.TMVA.Experimental.SOFIE.RModel_GraphIndependent.ParseFromMemory( - ep_model._encoder._network, InputGraphData, filename="encoder" - ) - encoder.Generate() - encoder.OutputGenerated() - - core = ROOT.TMVA.Experimental.SOFIE.RModel_GNN.ParseFromMemory( - ep_model._core._network, CoreGraphData, filename="core" - ) - core.Generate() - core.OutputGenerated() - - decoder = ROOT.TMVA.Experimental.SOFIE.RModel_GraphIndependent.ParseFromMemory( - ep_model._decoder._network, OutputGraphData, filename="decoder" - ) - decoder.Generate() - decoder.OutputGenerated() - - output_transform = ROOT.TMVA.Experimental.SOFIE.RModel_GraphIndependent.ParseFromMemory( - ep_model._output_transform._network, OutputGraphData, filename="output_transform" - ) - output_transform.Generate() - output_transform.OutputGenerated() - - # Including the sofie generated models - ROOT.gInterpreter.Declare('#include "encoder.hxx"') - ROOT.gInterpreter.Declare('#include "core.hxx"') - ROOT.gInterpreter.Declare('#include "decoder.hxx"') - ROOT.gInterpreter.Declare('#include "output_transform.hxx"') - - encoder_session = ROOT.TMVA_SOFIE_encoder.Session() - core_session = ROOT.TMVA_SOFIE_core.Session() - decoder_session = ROOT.TMVA_SOFIE_decoder.Session() - output_transform_session = ROOT.TMVA_SOFIE_output_transform.Session() - - # Preparing the input data for running inference on sofie - input_data = ROOT.TMVA.Experimental.SOFIE.GNN_Data() - input_data.node_data = ROOT.TMVA.Experimental.AsRTensor(InputGraphData["nodes"]) - input_data.edge_data = ROOT.TMVA.Experimental.AsRTensor(InputGraphData["edges"]) - input_data.global_data = ROOT.TMVA.Experimental.AsRTensor(InputGraphData["globals"]) - input_data.edge_index = ROOT.TMVA.Experimental.AsRTensor( - np.stack((InputGraphData["receivers"], InputGraphData["senders"])) - ) - - output_gn = ep_model(input_graphs, 2) - - # running inference on sofie - data = CopyData(input_data) - - encoder_session.infer(data) - latent0 = CopyData(data) - latent = data - output_ops = [] - for _ in range(2): - core_input = ROOT.TMVA.Experimental.SOFIE.Concatenate(latent0, latent, axis=1) - core_session.infer(core_input) - latent = CopyData(core_input) - decoder_session.infer(core_input) - output_transform_session.infer(core_input) - output = CopyData(core_input) - output_ops.append(output) - - for i in range(0, len(output_ops)): - output_node_data = output_gn[i].nodes.numpy() - output_edge_data = output_gn[i].edges.numpy() - output_global_data = output_gn[i].globals.numpy().flatten() - - assert_almost_equal(output_node_data, np.asarray(output_ops[i].node_data), 4) - - assert_almost_equal(output_edge_data, np.asarray(output_ops[i].edge_data), 4) - - assert_almost_equal(output_global_data, np.asarray(output_ops[i].global_data), 4) - - @classmethod - def tearDownClass(self): - filesToRemove = ["core", "encoder", "decoder", "output_transform", "gnn_network", "graph_independent_network"] - for fname in filesToRemove: - os.remove(fname + ".hxx") - os.remove(fname + ".dat") - - -if __name__ == "__main__": - unittest.main() 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 b123ee2d356dd..0000000000000 --- a/bindings/pyroot/pythonizations/test/sofie_keras_parser.py +++ /dev/null @@ -1,88 +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_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 c108474d199e1..0000000000000 --- a/bindings/pyroot/pythonizations/test/sofie_keras_parser_models.py +++ /dev/null @@ -1,272 +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 - ) - - # 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_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()