import struct
from enum import Enum
import numpy as np
import pymoose as pm
from pymoose.predictors import predictor
from pymoose.predictors import predictor_utils
[docs]class Activation(Enum):
IDENTITY = 1
SIGMOID = 2
SOFTMAX = 3
RELU = 4
[docs]class NeuralNetwork(predictor.Predictor):
def __init__(self, weights, biases, activations):
super().__init__()
self.weights = weights
self.biases = biases
self.activations = activations
self.n_classes = np.shape(biases[-1])[0] # infer number of classes
[docs] def apply_layer(self, input, i, fixedpoint_dtype):
w = self.fixedpoint_constant(
self.weights[i], plc=self.mirrored, dtype=fixedpoint_dtype
)
b = self.fixedpoint_constant(
self.biases[i], plc=self.mirrored, dtype=fixedpoint_dtype
)
y = pm.dot(input, w)
z = pm.add(y, b)
return z
[docs] def activation_fn(self, z, i):
activation = self.activations[i]
if activation == Activation.SIGMOID:
activation_output = pm.sigmoid(z)
elif activation == Activation.RELU:
z_shape = pm.shape(z)
with self.bob:
zeros = pm.zeros(z_shape, dtype=predictor_utils.DEFAULT_FLOAT_DTYPE)
zeros = pm.cast(zeros, dtype=predictor_utils.DEFAULT_FIXED_DTYPE)
activation_output = pm.maximum([zeros, z])
elif activation == Activation.SOFTMAX:
activation_output = pm.softmax(z, axis=1, upmost_index=self.n_classes)
elif activation == Activation.IDENTITY:
activation_output = z
else:
raise ValueError("Invalid or unsupported activation function")
return activation_output
[docs] def predictor_fn(self, x, fixedpoint_dtype):
num_layers = len(self.weights)
for i in range(num_layers):
x = self.apply_layer(x, i, fixedpoint_dtype)
x = self.activation_fn(x, i)
return x
def __call__(self, x, fixedpoint_dtype=predictor_utils.DEFAULT_FIXED_DTYPE):
return self.predictor_fn(x, fixedpoint_dtype)
[docs] @classmethod
def from_onnx(cls, model_proto):
# extract activations from operations
operations = predictor_utils.find_op_types_in_model_proto(model_proto)
activations = []
for i in range(len(operations)):
if operations[i] == "Sigmoid":
activations.append(Activation.SIGMOID)
elif operations[i] == "Softmax":
activations.append(Activation.SOFTMAX)
elif operations[i] == "Relu":
activations.append(Activation.RELU)
# PyTorch
if i > 0:
if operations[i] == "Gemm" and operations[i - 1] == "Gemm":
activations.append(Activation.IDENTITY)
# TF Keras
if i > 2:
if (
operations[i] == "Add"
and operations[i - 1] == "MatMul"
and operations[i - 2] == "Add"
and operations[i - 3] == "MatMul"
):
activations.append(Activation.IDENTITY)
# PyTorch: weight, bias; TF Keras: MatMul, BiasAdd
weights_data = predictor_utils.find_parameters_in_model_proto(
model_proto, ["weight", "MatMul"], enforce=False
)
biases_data = predictor_utils.find_parameters_in_model_proto(
model_proto, ["bias", "BiasAdd"], enforce=False
)
weights = []
for weight in weights_data:
dimentions = weight.dims
assert weight is not None
if weight.data_type != 1: # FLOATS
raise ValueError(
"Neural Network Weights must be of type FLOATS, found other."
)
weight = weight.raw_data
# decode bytes object
weight = struct.unpack("f" * (dimentions[0] * dimentions[1]), weight)
weight = np.asarray(weight)
weight = weight.reshape(dimentions[0], dimentions[1]).T
weights.append(weight)
biases = []
for bias in biases_data:
dimentions = bias.dims
assert bias is not None
if bias.data_type != 1: # FLOATS
raise ValueError(
"Neural network biases must be of type FLOATS, found other."
)
bias = bias.raw_data
bias = struct.unpack("f" * dimentions[0], bias)
bias = np.asarray(bias)
biases.append(bias)
# TF Keras onnx graph stores weights and biases in reversed order
# I.e.: from last to first layer
if "tf" in model_proto.producer_name:
weights = weights[::-1]
biases = biases[::-1]
# TF Keras weights need to be transposed
weights = [item.T for item in weights]
# `n_features` arg
model_input = model_proto.graph.input[0]
input_shape = predictor_utils.find_input_shape(model_input)
assert len(input_shape) == 2
n_features = input_shape[1].dim_value
first_layer_weights_shape = weights[0].shape
if n_features != first_layer_weights_shape[0]:
raise ValueError(
f"In the ONNX file, the input shape has {n_features} "
"features and the shape of the weights for the first "
f"layer is: {first_layer_weights_shape}. Validate you set "
"correctly the `initial_types` when converting "
"your model to ONNX."
)
return cls(weights, biases, activations)