From 371644c9b40ba88e2b61f71953882932e54a5283 Mon Sep 17 00:00:00 2001 From: joshuahaddad Date: Wed, 9 Feb 2022 13:28:56 -0500 Subject: [PATCH 01/17] Renaming onnx.py to onnx_reader.py to avoid import issues with global onnx package --- src/omlt/io/__init__.py | 2 +- src/omlt/io/{onnx.py => onnx_reader.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/omlt/io/{onnx.py => onnx_reader.py} (100%) diff --git a/src/omlt/io/__init__.py b/src/omlt/io/__init__.py index 0b45abf7..67a5feac 100644 --- a/src/omlt/io/__init__.py +++ b/src/omlt/io/__init__.py @@ -1,2 +1,2 @@ -from omlt.io.onnx import load_onnx_neural_network, write_onnx_model_with_bounds, load_onnx_neural_network_with_bounds +from omlt.io.onnx_reader import load_onnx_neural_network, write_onnx_model_with_bounds, load_onnx_neural_network_with_bounds from omlt.io.keras_reader import load_keras_sequential diff --git a/src/omlt/io/onnx.py b/src/omlt/io/onnx_reader.py similarity index 100% rename from src/omlt/io/onnx.py rename to src/omlt/io/onnx_reader.py From f5270e75ad2e0584e4e229dee20e314f624b7bd2 Mon Sep 17 00:00:00 2001 From: joshuahaddad Date: Wed, 9 Feb 2022 13:31:11 -0500 Subject: [PATCH 02/17] Initial sklearn reader --- src/omlt/io/sklearn-reader.py | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/omlt/io/sklearn-reader.py diff --git a/src/omlt/io/sklearn-reader.py b/src/omlt/io/sklearn-reader.py new file mode 100644 index 00000000..313c79c3 --- /dev/null +++ b/src/omlt/io/sklearn-reader.py @@ -0,0 +1,42 @@ +import skl2onnx +import onnx +import sklearn +from sklearn.linear_model import LogisticRegression +import numpy +import onnxruntime as rt +from skl2onnx.common.data_types import FloatTensorType +from skl2onnx import convert_sklearn +from sklearn.neural_network import MLPRegressor +from sklearn.datasets import make_regression +from sklearn.model_selection import train_test_split + +from onnx_reader import load_onnx_neural_network + +def load_sklearn_MLP(model, scaling_object=None, input_bounds=None, initial_types=None): + + # Assume float inputs if no types are supplied to the model + if initial_types is None: + initial_types = [('float_input', FloatTensorType([None, model.n_features_in_]))] + + onx = convert_sklearn(model, initial_types=initial_types, target_opset=12) + onx_model = onx.SerializeToString() + + return load_onnx_neural_network(onx_model, scaling_object, input_bounds) + + + +X, y = make_regression(n_samples=200, n_features=42, random_state=1) +X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1) +regr = MLPRegressor(random_state=1, max_iter=500).fit(X_train, y_train) +print(regr) + +initial_type = [('float_input', FloatTensorType([None, regr.n_features_in_]))] +onx = convert_sklearn(regr, initial_types=initial_type) +onx_model = onx.SerializeToString() + +sess = rt.InferenceSession(onx_model) +input_name = sess.get_inputs()[0].name +label_name = sess.get_outputs()[0].name +pred_onx = sess.run([label_name], {input_name: X_test.astype(numpy.float32)})[0].transpose()[0] +pred_sklearn = regr.predict(X_test.astype(numpy.float32)) +print(max(abs(pred_onx - pred_sklearn)/pred_sklearn*100)) From 8048baf53017489320dad69afe1b058c6f8e4944 Mon Sep 17 00:00:00 2001 From: joshuahaddad Date: Wed, 9 Feb 2022 14:08:32 -0500 Subject: [PATCH 03/17] Adding sklearn scaling object conversion to OffsetScaling object --- src/omlt/io/sklearn-reader.py | 44 +++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/omlt/io/sklearn-reader.py b/src/omlt/io/sklearn-reader.py index 313c79c3..66803557 100644 --- a/src/omlt/io/sklearn-reader.py +++ b/src/omlt/io/sklearn-reader.py @@ -1,17 +1,49 @@ -import skl2onnx +from skl2onnx.common.data_types import FloatTensorType +from skl2onnx import convert_sklearn +from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler, StandardScaler +from omlt.scaling import OffsetScaling + import onnx import sklearn from sklearn.linear_model import LogisticRegression import numpy import onnxruntime as rt -from skl2onnx.common.data_types import FloatTensorType -from skl2onnx import convert_sklearn from sklearn.neural_network import MLPRegressor from sklearn.datasets import make_regression from sklearn.model_selection import train_test_split + from onnx_reader import load_onnx_neural_network +def get_sklearn_scaling_params(sklearn_scaler): + + if isinstance(sklearn_scaler, StandardScaler): + offset = sklearn_scaler.mean_ + factor = sklearn_scaler.var_ + + elif isinstance(sklearn_scaler, MaxAbsScaler): + factor = sklearn_scaler.scale_ + offset = factor*0 + + elif isinstance(sklearn_scaler, MinMaxScaler): + factor = sklearn_scaler.data_max_ - sklearn_scaler.data_min_ + offset = sklearn_scaler.data_min_ + + else: + raise(ValueError("Scaling object provided is not currently supported. " + "Supported objects include StandardScaler, MinMaxScaler, and MaxAbsScaler")) + + return offset, factor + +def parse_sklearn_scaler(sklearn_input_scaler, sklearn_output_scaler): + + offset_inputs, factor_inputs = get_sklearn_scaling_params(sklearn_input_scaler) + offset_outputs, factor_ouputs = get_sklearn_scaling_params(sklearn_output_scaler) + + return OffsetScaling(offset_inputs=offset_inputs, factor_inputs=factor_inputs, + offset_outputs=offset_outputs, factor_outputs=factor_ouputs) + + def load_sklearn_MLP(model, scaling_object=None, input_bounds=None, initial_types=None): # Assume float inputs if no types are supplied to the model @@ -25,7 +57,9 @@ def load_sklearn_MLP(model, scaling_object=None, input_bounds=None, initial_type -X, y = make_regression(n_samples=200, n_features=42, random_state=1) + + +"""X, y = make_regression(n_samples=200, n_features=42, random_state=1) X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1) regr = MLPRegressor(random_state=1, max_iter=500).fit(X_train, y_train) print(regr) @@ -39,4 +73,4 @@ def load_sklearn_MLP(model, scaling_object=None, input_bounds=None, initial_type label_name = sess.get_outputs()[0].name pred_onx = sess.run([label_name], {input_name: X_test.astype(numpy.float32)})[0].transpose()[0] pred_sklearn = regr.predict(X_test.astype(numpy.float32)) -print(max(abs(pred_onx - pred_sklearn)/pred_sklearn*100)) +print(max(abs(pred_onx - pred_sklearn)/pred_sklearn*100))""" From 90110f80756ce94410239044e9aea3c784efc67f Mon Sep 17 00:00:00 2001 From: joshuahaddad Date: Wed, 9 Feb 2022 14:08:56 -0500 Subject: [PATCH 04/17] Removing test code --- src/omlt/io/sklearn-reader.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/omlt/io/sklearn-reader.py b/src/omlt/io/sklearn-reader.py index 66803557..e00d223d 100644 --- a/src/omlt/io/sklearn-reader.py +++ b/src/omlt/io/sklearn-reader.py @@ -54,23 +54,3 @@ def load_sklearn_MLP(model, scaling_object=None, input_bounds=None, initial_type onx_model = onx.SerializeToString() return load_onnx_neural_network(onx_model, scaling_object, input_bounds) - - - - - -"""X, y = make_regression(n_samples=200, n_features=42, random_state=1) -X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1) -regr = MLPRegressor(random_state=1, max_iter=500).fit(X_train, y_train) -print(regr) - -initial_type = [('float_input', FloatTensorType([None, regr.n_features_in_]))] -onx = convert_sklearn(regr, initial_types=initial_type) -onx_model = onx.SerializeToString() - -sess = rt.InferenceSession(onx_model) -input_name = sess.get_inputs()[0].name -label_name = sess.get_outputs()[0].name -pred_onx = sess.run([label_name], {input_name: X_test.astype(numpy.float32)})[0].transpose()[0] -pred_sklearn = regr.predict(X_test.astype(numpy.float32)) -print(max(abs(pred_onx - pred_sklearn)/pred_sklearn*100))""" From 9b87324967612666f3032fd41858a08a47340c40 Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Fri, 11 Feb 2022 13:07:21 -0500 Subject: [PATCH 05/17] Renaming onnx.py to onnx_reader.py to prevent global import errors --- src/omlt/io/{sklearn-reader.py => sklearn_reader.py} | 3 +-- tests/io/test_onnx_parser.py | 2 +- tests/neuralnet/test_onnx.py | 2 +- tests/neuralnet/test_relu.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) rename src/omlt/io/{sklearn-reader.py => sklearn_reader.py} (97%) diff --git a/src/omlt/io/sklearn-reader.py b/src/omlt/io/sklearn_reader.py similarity index 97% rename from src/omlt/io/sklearn-reader.py rename to src/omlt/io/sklearn_reader.py index e00d223d..ebfd5483 100644 --- a/src/omlt/io/sklearn-reader.py +++ b/src/omlt/io/sklearn_reader.py @@ -12,8 +12,7 @@ from sklearn.datasets import make_regression from sklearn.model_selection import train_test_split - -from onnx_reader import load_onnx_neural_network +from omlt.io.onnx_reader import load_onnx_neural_network def get_sklearn_scaling_params(sklearn_scaler): diff --git a/tests/io/test_onnx_parser.py b/tests/io/test_onnx_parser.py index 402c7e39..257a0186 100644 --- a/tests/io/test_onnx_parser.py +++ b/tests/io/test_onnx_parser.py @@ -2,7 +2,7 @@ import onnx import numpy as np -from omlt.io.onnx import load_onnx_neural_network +from omlt.io.onnx_reader import load_onnx_neural_network def test_linear_131(datadir): diff --git a/tests/neuralnet/test_onnx.py b/tests/neuralnet/test_onnx.py index 2258b975..73e040d7 100644 --- a/tests/neuralnet/test_onnx.py +++ b/tests/neuralnet/test_onnx.py @@ -1,5 +1,5 @@ import tempfile -from omlt.io.onnx import load_onnx_neural_network, load_onnx_neural_network_with_bounds, write_onnx_model_with_bounds +from omlt.io.onnx_reader import load_onnx_neural_network, load_onnx_neural_network_with_bounds, write_onnx_model_with_bounds import onnx import onnxruntime as ort import numpy as np diff --git a/tests/neuralnet/test_relu.py b/tests/neuralnet/test_relu.py index 354dd322..3f6e9669 100644 --- a/tests/neuralnet/test_relu.py +++ b/tests/neuralnet/test_relu.py @@ -2,7 +2,7 @@ import numpy as np from omlt.block import OmltBlock -from omlt.io.onnx import load_onnx_neural_network_with_bounds +from omlt.io.onnx_reader import load_onnx_neural_network_with_bounds from omlt.neuralnet import FullSpaceNNFormulation, ReluBigMFormulation, ReluComplementarityFormulation, ReluPartitionFormulation from omlt.neuralnet.activations import ComplementarityReLUActivation From 5cd9512a26cfebab0fa081945a0f763634150f94 Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Fri, 11 Feb 2022 13:35:41 -0500 Subject: [PATCH 06/17] Support for all offset sklearn scalers --- src/omlt/io/sklearn_reader.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/omlt/io/sklearn_reader.py b/src/omlt/io/sklearn_reader.py index ebfd5483..1a7822a5 100644 --- a/src/omlt/io/sklearn_reader.py +++ b/src/omlt/io/sklearn_reader.py @@ -1,6 +1,6 @@ from skl2onnx.common.data_types import FloatTensorType from skl2onnx import convert_sklearn -from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler, StandardScaler +from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler, StandardScaler, RobustScaler from omlt.scaling import OffsetScaling import onnx @@ -11,7 +11,7 @@ from sklearn.neural_network import MLPRegressor from sklearn.datasets import make_regression from sklearn.model_selection import train_test_split - +from sklearn.preprocessing import RobustScaler from omlt.io.onnx_reader import load_onnx_neural_network def get_sklearn_scaling_params(sklearn_scaler): @@ -28,6 +28,10 @@ def get_sklearn_scaling_params(sklearn_scaler): factor = sklearn_scaler.data_max_ - sklearn_scaler.data_min_ offset = sklearn_scaler.data_min_ + elif isinstance(sklearn_scaler, RobustScaler): + factor = sklearn_scaler.scale_ + offset = sklearn_scaler.center_ + else: raise(ValueError("Scaling object provided is not currently supported. " "Supported objects include StandardScaler, MinMaxScaler, and MaxAbsScaler")) From c2c5b35f4a7c428323a0a0c7efd7386efc2777c5 Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Fri, 11 Feb 2022 13:37:11 -0500 Subject: [PATCH 07/17] Support for all offset sklearn scalers - error msg update --- src/omlt/io/sklearn_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/omlt/io/sklearn_reader.py b/src/omlt/io/sklearn_reader.py index 1a7822a5..368a7e6b 100644 --- a/src/omlt/io/sklearn_reader.py +++ b/src/omlt/io/sklearn_reader.py @@ -33,8 +33,8 @@ def get_sklearn_scaling_params(sklearn_scaler): offset = sklearn_scaler.center_ else: - raise(ValueError("Scaling object provided is not currently supported. " - "Supported objects include StandardScaler, MinMaxScaler, and MaxAbsScaler")) + raise(ValueError("Scaling object provided is not currently supported. Only linear scalers are supported." + "Supported scalers include StandardScaler, MinMaxScaler, MaxAbsScaler, and RobustScaler")) return offset, factor From ab4e61bbc7c856ec0a9cee0f02683b1b4ee205aa Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Fri, 11 Feb 2022 16:30:44 -0500 Subject: [PATCH 08/17] Cleaning up imports --- src/omlt/io/sklearn_reader.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/omlt/io/sklearn_reader.py b/src/omlt/io/sklearn_reader.py index 368a7e6b..16c158d4 100644 --- a/src/omlt/io/sklearn_reader.py +++ b/src/omlt/io/sklearn_reader.py @@ -1,20 +1,11 @@ from skl2onnx.common.data_types import FloatTensorType from skl2onnx import convert_sklearn from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler, StandardScaler, RobustScaler -from omlt.scaling import OffsetScaling - -import onnx -import sklearn -from sklearn.linear_model import LogisticRegression -import numpy -import onnxruntime as rt -from sklearn.neural_network import MLPRegressor -from sklearn.datasets import make_regression -from sklearn.model_selection import train_test_split from sklearn.preprocessing import RobustScaler +from omlt.scaling import OffsetScaling from omlt.io.onnx_reader import load_onnx_neural_network -def get_sklearn_scaling_params(sklearn_scaler): +def parse_sklearn_scaler(sklearn_scaler): if isinstance(sklearn_scaler, StandardScaler): offset = sklearn_scaler.mean_ @@ -38,10 +29,12 @@ def get_sklearn_scaling_params(sklearn_scaler): return offset, factor -def parse_sklearn_scaler(sklearn_input_scaler, sklearn_output_scaler): +def convert_sklearn_scalers(sklearn_input_scaler, sklearn_output_scaler): + + #Todo: support only scaling input or output? - offset_inputs, factor_inputs = get_sklearn_scaling_params(sklearn_input_scaler) - offset_outputs, factor_ouputs = get_sklearn_scaling_params(sklearn_output_scaler) + offset_inputs, factor_inputs = parse_sklearn_scaler(sklearn_input_scaler) + offset_outputs, factor_ouputs = parse_sklearn_scaler(sklearn_output_scaler) return OffsetScaling(offset_inputs=offset_inputs, factor_inputs=factor_inputs, offset_outputs=offset_outputs, factor_outputs=factor_ouputs) From af582c987497be811d31a6d321a790d78b9c8cf0 Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Fri, 11 Feb 2022 16:42:13 -0500 Subject: [PATCH 09/17] Finished tests for scaling object conversion --- src/omlt/io/sklearn_reader.py | 3 +- tests/io/test_sklearn.py | 127 ++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/io/test_sklearn.py diff --git a/src/omlt/io/sklearn_reader.py b/src/omlt/io/sklearn_reader.py index 16c158d4..bc05b0d4 100644 --- a/src/omlt/io/sklearn_reader.py +++ b/src/omlt/io/sklearn_reader.py @@ -9,7 +9,7 @@ def parse_sklearn_scaler(sklearn_scaler): if isinstance(sklearn_scaler, StandardScaler): offset = sklearn_scaler.mean_ - factor = sklearn_scaler.var_ + factor = sklearn_scaler.scale_ elif isinstance(sklearn_scaler, MaxAbsScaler): factor = sklearn_scaler.scale_ @@ -39,7 +39,6 @@ def convert_sklearn_scalers(sklearn_input_scaler, sklearn_output_scaler): return OffsetScaling(offset_inputs=offset_inputs, factor_inputs=factor_inputs, offset_outputs=offset_outputs, factor_outputs=factor_ouputs) - def load_sklearn_MLP(model, scaling_object=None, input_bounds=None, initial_types=None): # Assume float inputs if no types are supplied to the model diff --git a/tests/io/test_sklearn.py b/tests/io/test_sklearn.py new file mode 100644 index 00000000..e450b76f --- /dev/null +++ b/tests/io/test_sklearn.py @@ -0,0 +1,127 @@ +import omlt.scaling as scaling +from omlt.scaling import OffsetScaling +from omlt.block import OmltBlock +from omlt.io.sklearn_reader import convert_sklearn_scalers +from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler, StandardScaler, RobustScaler +import numpy as np +from scipy.stats import iqr +import pyomo.environ as pyo + +def test_sklearn_scaler_conversion(): + X = np.array( + [[42, 10, 29], + [12, 19, 15]] + ) + + Y = np.array( + [[1, 2], + [3, 4]] + ) + + # Create sklearn scalers + xMinMax = MinMaxScaler() + xMaxAbs = MaxAbsScaler() + xStandard = StandardScaler() + xRobust = RobustScaler() + + yMinMax = MinMaxScaler() + yMaxAbs = MaxAbsScaler() + yStandard = StandardScaler() + yRobust = RobustScaler() + + sklearn_scalers = [(xMinMax, yMinMax), (xMaxAbs, yMaxAbs), (xStandard, yStandard), (xRobust, yRobust)] + for scalers in sklearn_scalers: + scalers[0].fit(X) + scalers[1].fit(Y) + + # Create OMLT scalers using OMLT function + MinMaxOMLT = convert_sklearn_scalers(xMinMax, yMinMax) + MaxAbsOMLT = convert_sklearn_scalers(xMaxAbs, yMaxAbs) + StandardOMLT = convert_sklearn_scalers(xStandard, yStandard) + RobustOMLT = convert_sklearn_scalers(xRobust, yRobust) + + omlt_scalers = [MinMaxOMLT, MaxAbsOMLT, StandardOMLT, RobustOMLT] + + # Generate test data + x = {0: 10, 1: 29, 2: 42} + y = {0: 2, 1: 1} + + # Test Scalers + for i in range(len(omlt_scalers)): + x_s_omlt = omlt_scalers[i].get_scaled_input_expressions(x) + y_s_omlt = omlt_scalers[i].get_scaled_output_expressions(y) + + x_s_sklearn = sklearn_scalers[i][0].transform([list(x.values())])[0] + y_s_sklearn = sklearn_scalers[i][1].transform([list(y.values())])[0] + + np.testing.assert_almost_equal(list(x_s_omlt.values()), list(x_s_sklearn)) + np.testing.assert_almost_equal(list(y_s_omlt.values()), list(y_s_sklearn)) + +def test_sklearn_offset_equivalence(): + X = np.array( + [[42, 10, 29], + [12, 19, 15]] + ) + + Y = np.array( + [[1, 2], + [3, 4]] + ) + + # Get scaling factors for OffsetScaler + xmean = X.mean(axis=0) + xstd = X.std(axis=0) + xmax = X.max(axis=0) + absxmax = abs(X).max(axis=0) + xmin = X.min(axis=0) + xminmax = xmax-xmin + xmedian = np.median(X, axis=0) + xiqr = iqr(X, axis=0) + + ymean = Y.mean(axis=0) + ystd = Y.std(axis=0) + ymax = Y.max(axis=0) + absymax = abs(Y).max(axis=0) + ymin = Y.min(axis=0) + yminmax = ymax-ymin + ymedian = np.median(Y, axis=0) + yiqr = iqr(Y, axis=0) + + # Create sklearn scalers + xMinMax = MinMaxScaler() + xMaxAbs = MaxAbsScaler() + xStandard = StandardScaler() + xRobust = RobustScaler() + + yMinMax = MinMaxScaler() + yMaxAbs = MaxAbsScaler() + yStandard = StandardScaler() + yRobust = RobustScaler() + + sklearn_scalers = [(xMinMax, yMinMax), (xMaxAbs, yMaxAbs), (xStandard, yStandard), (xRobust, yRobust)] + for scalers in sklearn_scalers: + scalers[0].fit(X) + scalers[1].fit(Y) + + # Create OMLT scalers manually + MinMaxOMLT = OffsetScaling(offset_inputs=xmin, factor_inputs=xminmax, offset_outputs=ymin, factor_outputs=yminmax) + MaxAbsOMLT = OffsetScaling(offset_inputs=[0]*3, factor_inputs=absxmax, offset_outputs=[0]*2, factor_outputs=absymax) + StandardOMLT = OffsetScaling(offset_inputs=xmean, factor_inputs=xstd, offset_outputs=ymean, factor_outputs=ystd) + RobustOMLT = OffsetScaling(offset_inputs=xmedian, factor_inputs=xiqr, offset_outputs=ymedian, factor_outputs=yiqr) + + omlt_scalers = [MinMaxOMLT, MaxAbsOMLT, StandardOMLT, RobustOMLT] + + # Generate test data + x = {0: 10, 1: 29, 2: 42} + y = {0: 2, 1: 1} + + # Test Scalers + for i in range(len(omlt_scalers)): + x_s_omlt = omlt_scalers[i].get_scaled_input_expressions(x) + y_s_omlt = omlt_scalers[i].get_scaled_output_expressions(y) + + x_s_sklearn = sklearn_scalers[i][0].transform([list(x.values())])[0] + y_s_sklearn = sklearn_scalers[i][1].transform([list(y.values())])[0] + + np.testing.assert_almost_equal(list(x_s_omlt.values()), list(x_s_sklearn)) + np.testing.assert_almost_equal(list(y_s_omlt.values()), list(y_s_sklearn)) \ No newline at end of file From 7384bfbc025398256cc260e0b273ed0004ed6e51 Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Wed, 16 Feb 2022 13:50:31 -0500 Subject: [PATCH 10/17] Minor changes for edge cases produced by sklearn2onnx models --- src/omlt/io/onnx_parser.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/omlt/io/onnx_parser.py b/src/omlt/io/onnx_parser.py index 34de5ec1..9ba3b92f 100644 --- a/src/omlt/io/onnx_parser.py +++ b/src/omlt/io/onnx_parser.py @@ -10,7 +10,7 @@ ) -_ACTIVATION_OP_TYPES = ["Relu", "Sigmoid", "LogSoftmax"] +_ACTIVATION_OP_TYPES = ["Relu", "Sigmoid", "LogSoftmax", "Tanh"] class NetworkParser: @@ -181,6 +181,11 @@ def _consume_dense_nodes(self, node, next_nodes): node_biases = self._initializers[in_1] assert len(node_weights.shape) == 2 + + # Converted sklearn models have biases with swapped shape + if node_weights.shape[1] != node_biases.shape[0]: + node_biases = node_biases.transpose() + assert node_weights.shape[1] == node_biases.shape[0] assert len(node.output) == 1 @@ -339,12 +344,21 @@ def _consume_reshape_nodes(self, node, next_nodes): assert len(node.input) == 2 [in_0, in_1] = list(node.input) input_layer = self._node_map[in_0] - new_shape = self._constants[in_1] + + if in_1 in self._constants: + new_shape = self._constants[in_1] + else: + new_shape = self._initializers[in_1] + output_size = np.empty(input_layer.output_size).reshape(new_shape).shape transformer = IndexMapper(input_layer.output_size, list(output_size)) self._node_map[node.output[0]] = (transformer, input_layer) return next_nodes + def _consume_cast_nodes(self, node, next_nodes): + """Parse Cast node.""" + assert node.op_type == "Cast" + def _node_input_and_transformer(self, node_name): maybe_layer = self._node_map[node_name] if isinstance(maybe_layer, tuple): From c655bee68e52984dc6cd1257e10ceee0c26babdd Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Wed, 16 Feb 2022 13:51:25 -0500 Subject: [PATCH 11/17] Testing code, needs signifiant cleaning --- src/omlt/io/sklearn_reader.py | 39 ++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/omlt/io/sklearn_reader.py b/src/omlt/io/sklearn_reader.py index bc05b0d4..ef26e372 100644 --- a/src/omlt/io/sklearn_reader.py +++ b/src/omlt/io/sklearn_reader.py @@ -1,9 +1,12 @@ -from skl2onnx.common.data_types import FloatTensorType +from skl2onnx.common.data_types import FloatTensorType, Int64TensorType from skl2onnx import convert_sklearn from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler, StandardScaler, RobustScaler from sklearn.preprocessing import RobustScaler from omlt.scaling import OffsetScaling from omlt.io.onnx_reader import load_onnx_neural_network +import onnxruntime as rt +import numpy as np +import onnx def parse_sklearn_scaler(sklearn_scaler): @@ -46,6 +49,36 @@ def load_sklearn_MLP(model, scaling_object=None, input_bounds=None, initial_type initial_types = [('float_input', FloatTensorType([None, model.n_features_in_]))] onx = convert_sklearn(model, initial_types=initial_types, target_opset=12) - onx_model = onx.SerializeToString() - return load_onnx_neural_network(onx_model, scaling_object, input_bounds) + x = np.array([[1.0] * 8]) + print(x) + x = x.astype(np.float32) + print(x.shape) + print(model.predict(x)) + + # REMOVE INITIAL CAST LAYER + graph = onx.graph + node1 = graph.node[0] + graph.node.remove(node1) + new_node = onnx.helper.make_node( + 'MatMul', + name="MatMul", + inputs=['float_input', 'coefficient'], + outputs=['mul_result'] + ) + graph.node.insert(0, new_node) + node2 = graph.node[1] + + graph.node.remove(node2) + + # onx_model = onx.SerializeToString() + with open("test.onnx", "wb") as f: + f.write(onx.SerializeToString()) + + sess = rt.InferenceSession("test.onnx") + input_name = sess.get_inputs()[0].name + label_name = sess.get_outputs()[0].name + outputs = sess.run([label_name], {input_name: x})[0][0] + + print(outputs-model.predict(x)) + return load_onnx_neural_network(onx, scaling_object, input_bounds) From 6a756bb73c3959b989aac033944f93139228a46a Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Fri, 18 Feb 2022 12:20:58 -0500 Subject: [PATCH 12/17] Handling different API bias reading --- src/omlt/io/onnx_parser.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/omlt/io/onnx_parser.py b/src/omlt/io/onnx_parser.py index 9ba3b92f..76c6d8e4 100644 --- a/src/omlt/io/onnx_parser.py +++ b/src/omlt/io/onnx_parser.py @@ -182,9 +182,8 @@ def _consume_dense_nodes(self, node, next_nodes): assert len(node_weights.shape) == 2 - # Converted sklearn models have biases with swapped shape - if node_weights.shape[1] != node_biases.shape[0]: - node_biases = node_biases.transpose() + # Flatten biases array as some APIs (scikit) store biases as (1, n) instead of (n,) + node_biases = node_biases.flatten() assert node_weights.shape[1] == node_biases.shape[0] assert len(node.output) == 1 From 85a4f92749839a5a8ea333f95541637c1710e9a1 Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Fri, 18 Feb 2022 12:23:33 -0500 Subject: [PATCH 13/17] Graph modification documnetation and cleaning code --- src/omlt/io/sklearn_reader.py | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/src/omlt/io/sklearn_reader.py b/src/omlt/io/sklearn_reader.py index ef26e372..717a08fa 100644 --- a/src/omlt/io/sklearn_reader.py +++ b/src/omlt/io/sklearn_reader.py @@ -1,11 +1,9 @@ -from skl2onnx.common.data_types import FloatTensorType, Int64TensorType +from skl2onnx.common.data_types import FloatTensorType from skl2onnx import convert_sklearn -from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler, StandardScaler, RobustScaler +from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler, StandardScaler from sklearn.preprocessing import RobustScaler from omlt.scaling import OffsetScaling from omlt.io.onnx_reader import load_onnx_neural_network -import onnxruntime as rt -import numpy as np import onnx def parse_sklearn_scaler(sklearn_scaler): @@ -50,13 +48,7 @@ def load_sklearn_MLP(model, scaling_object=None, input_bounds=None, initial_type onx = convert_sklearn(model, initial_types=initial_types, target_opset=12) - x = np.array([[1.0] * 8]) - print(x) - x = x.astype(np.float32) - print(x.shape) - print(model.predict(x)) - - # REMOVE INITIAL CAST LAYER + # Remove initial cast layer created by sklearn2onnx graph = onx.graph node1 = graph.node[0] graph.node.remove(node1) @@ -67,18 +59,9 @@ def load_sklearn_MLP(model, scaling_object=None, input_bounds=None, initial_type outputs=['mul_result'] ) graph.node.insert(0, new_node) - node2 = graph.node[1] + # Replace old MatMul node with new node with correct input name + node2 = graph.node[1] graph.node.remove(node2) - # onx_model = onx.SerializeToString() - with open("test.onnx", "wb") as f: - f.write(onx.SerializeToString()) - - sess = rt.InferenceSession("test.onnx") - input_name = sess.get_inputs()[0].name - label_name = sess.get_outputs()[0].name - outputs = sess.run([label_name], {input_name: x})[0][0] - - print(outputs-model.predict(x)) return load_onnx_neural_network(onx, scaling_object, input_bounds) From d7d4c56c47ecc488caeeed2d7465d5ba8d461fec Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Mon, 21 Feb 2022 22:24:09 -0500 Subject: [PATCH 14/17] Adding test models --- tests/models/sklearn_identity_131.pkl | Bin 0 -> 8360 bytes tests/models/sklearn_identity_131_bounds | 1 + tests/models/sklearn_logistic_131.pkl | Bin 0 -> 8368 bytes tests/models/sklearn_logistic_131_bounds | 1 + tests/models/sklearn_tanh_131.pkl | Bin 0 -> 8365 bytes tests/models/sklearn_tanh_131_bounds | 1 + 6 files changed, 3 insertions(+) create mode 100644 tests/models/sklearn_identity_131.pkl create mode 100644 tests/models/sklearn_identity_131_bounds create mode 100644 tests/models/sklearn_logistic_131.pkl create mode 100644 tests/models/sklearn_logistic_131_bounds create mode 100644 tests/models/sklearn_tanh_131.pkl create mode 100644 tests/models/sklearn_tanh_131_bounds diff --git a/tests/models/sklearn_identity_131.pkl b/tests/models/sklearn_identity_131.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f5889b2d61cc75fe8684d071e258d3a0f932f167 GIT binary patch literal 8360 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 literal 0 HcmV?d00001 diff --git a/tests/models/sklearn_identity_131_bounds b/tests/models/sklearn_identity_131_bounds new file mode 100644 index 00000000..007637c7 --- /dev/null +++ b/tests/models/sklearn_identity_131_bounds @@ -0,0 +1 @@ +{"0": [-3.2412673400690726, 2.3146585666735087], "1": [-1.9875689146008928, 3.852731490654721]} \ No newline at end of file diff --git a/tests/models/sklearn_logistic_131.pkl b/tests/models/sklearn_logistic_131.pkl new file mode 100644 index 0000000000000000000000000000000000000000..9cb2b3d252abd3fc635cf318a44ecc3d9380e1ae GIT binary patch literal 8368 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 literal 0 HcmV?d00001 diff --git a/tests/models/sklearn_tanh_131_bounds b/tests/models/sklearn_tanh_131_bounds new file mode 100644 index 00000000..007637c7 --- /dev/null +++ b/tests/models/sklearn_tanh_131_bounds @@ -0,0 +1 @@ +{"0": [-3.2412673400690726, 2.3146585666735087], "1": [-1.9875689146008928, 3.852731490654721]} \ No newline at end of file From af3a21c96565b4e4fa21febc22b8b98fee75a448 Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Mon, 21 Feb 2022 22:24:26 -0500 Subject: [PATCH 15/17] Adding tests for sklearn models --- tests/io/test_sklearn.py | 45 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/tests/io/test_sklearn.py b/tests/io/test_sklearn.py index e450b76f..3e6f23dd 100644 --- a/tests/io/test_sklearn.py +++ b/tests/io/test_sklearn.py @@ -1,11 +1,14 @@ -import omlt.scaling as scaling from omlt.scaling import OffsetScaling from omlt.block import OmltBlock from omlt.io.sklearn_reader import convert_sklearn_scalers +from omlt.io.sklearn_reader import load_sklearn_MLP +from omlt.neuralnet import FullSpaceNNFormulation from sklearn.preprocessing import MinMaxScaler, MaxAbsScaler, StandardScaler, RobustScaler -import numpy as np from scipy.stats import iqr -import pyomo.environ as pyo +from pyomo.environ import * +import numpy as np +import json +import pickle def test_sklearn_scaler_conversion(): X = np.array( @@ -124,4 +127,38 @@ def test_sklearn_offset_equivalence(): y_s_sklearn = sklearn_scalers[i][1].transform([list(y.values())])[0] np.testing.assert_almost_equal(list(x_s_omlt.values()), list(x_s_sklearn)) - np.testing.assert_almost_equal(list(y_s_omlt.values()), list(y_s_sklearn)) \ No newline at end of file + np.testing.assert_almost_equal(list(y_s_omlt.values()), list(y_s_sklearn)) + +def test_sklearn_model(datadir): + nn_names = ["sklearn_identity_131", "sklearn_logistic_131", "sklearn_tanh_131"] + + # Test each nn + for nn_name in nn_names: + nn = pickle.load(open(datadir.file(nn_name+".pkl"), 'rb')) + + with open(datadir.file(nn_name+"_bounds"), 'r') as f: + bounds = json.load(f) + + # Convert to omlt format + xbounds = {int(i): tuple(bounds[i]) for i in bounds} + + net = load_sklearn_MLP(nn, input_bounds=xbounds) + formulation = FullSpaceNNFormulation(net) + + model = ConcreteModel() + model.nn = OmltBlock() + model.nn.build_formulation(formulation) + + @model.Objective() + def obj(mdl): + return 1 + + x = [(xbounds[i][0]+xbounds[i][1])/2.0 for i in range(2)] + for i in range(len(x)): + model.nn.inputs[i].fix(x[i]) + + result = SolverFactory("ipopt").solve(model, tee=False) + yomlt = [value(model.nn.outputs[0]), value(model.nn.outputs[1])] + + ysklearn = nn.predict([x])[0] + np.testing.assert_almost_equal(list(yomlt), list(ysklearn)) \ No newline at end of file From b4c402a6d714670181ed44e3d11c9158f50f2ae4 Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Mon, 21 Feb 2022 22:47:56 -0500 Subject: [PATCH 16/17] removing cast node code --- src/omlt/io/onnx_parser.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/omlt/io/onnx_parser.py b/src/omlt/io/onnx_parser.py index 76c6d8e4..6ac7a9d1 100644 --- a/src/omlt/io/onnx_parser.py +++ b/src/omlt/io/onnx_parser.py @@ -354,10 +354,6 @@ def _consume_reshape_nodes(self, node, next_nodes): self._node_map[node.output[0]] = (transformer, input_layer) return next_nodes - def _consume_cast_nodes(self, node, next_nodes): - """Parse Cast node.""" - assert node.op_type == "Cast" - def _node_input_and_transformer(self, node_name): maybe_layer = self._node_map[node_name] if isinstance(maybe_layer, tuple): From 83f96bcdfa6a8c3b7a70929c320b90006ebecb80 Mon Sep 17 00:00:00 2001 From: Joshua Haddad Date: Mon, 21 Feb 2022 22:58:36 -0500 Subject: [PATCH 17/17] Renaming io.onnx to io.onnx_reader in notebooks --- docs/notebooks/neuralnet/mnist_example_convolutional.ipynb | 4 ++-- docs/notebooks/neuralnet/mnist_example_dense.ipynb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/notebooks/neuralnet/mnist_example_convolutional.ipynb b/docs/notebooks/neuralnet/mnist_example_convolutional.ipynb index 8365ade9..e4e5fc02 100644 --- a/docs/notebooks/neuralnet/mnist_example_convolutional.ipynb +++ b/docs/notebooks/neuralnet/mnist_example_convolutional.ipynb @@ -53,7 +53,7 @@ "#omlt for interfacing our neural network with pyomo\n", "from omlt import OmltBlock\n", "from omlt.neuralnet import FullSpaceNNFormulation\n", - "from omlt.io.onnx import write_onnx_model_with_bounds, load_onnx_neural_network_with_bounds" + "from omlt.io.onnx_reader import write_onnx_model_with_bounds, load_onnx_neural_network_with_bounds" ] }, { @@ -659,4 +659,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/notebooks/neuralnet/mnist_example_dense.ipynb b/docs/notebooks/neuralnet/mnist_example_dense.ipynb index 052059bb..e7f5b79d 100644 --- a/docs/notebooks/neuralnet/mnist_example_dense.ipynb +++ b/docs/notebooks/neuralnet/mnist_example_dense.ipynb @@ -52,7 +52,7 @@ "#omlt for interfacing our neural network with pyomo\n", "from omlt import OmltBlock\n", "from omlt.neuralnet import FullSpaceNNFormulation\n", - "from omlt.io.onnx import write_onnx_model_with_bounds, load_onnx_neural_network_with_bounds" + "from omlt.io.onnx_reader import write_onnx_model_with_bounds, load_onnx_neural_network_with_bounds" ] }, { @@ -742,4 +742,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file