From f2444ff95d77ccae6545780bfcf1229f68232e75 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Wed, 19 Jun 2019 16:28:31 -0700 Subject: [PATCH 01/16] Add initial implementation of the export to ONNX functionality. --- src/DotNetBridge/Bridge.cs | 2 +- src/DotNetBridge/DotNetBridge.csproj | 1 + src/Platforms/build.csproj | 1 + src/python/nimbusml/base_predictor.py | 17 ++++ src/python/nimbusml/base_transform.py | 19 +++++ .../nimbusml/internal/utils/entrypoints.py | 2 +- src/python/nimbusml/pipeline.py | 84 +++++++++++++++++++ 7 files changed, 124 insertions(+), 2 deletions(-) diff --git a/src/DotNetBridge/Bridge.cs b/src/DotNetBridge/Bridge.cs index 96100247..103b31ff 100644 --- a/src/DotNetBridge/Bridge.cs +++ b/src/DotNetBridge/Bridge.cs @@ -326,7 +326,7 @@ private static unsafe int GenericExec(EnvironmentBlock* penv, sbyte* psz, int cd //env.ComponentCatalog.RegisterAssembly(typeof(AutoInference).Assembly); // ML.PipelineInference env.ComponentCatalog.RegisterAssembly(typeof(DataViewReference).Assembly); env.ComponentCatalog.RegisterAssembly(typeof(ImageLoadingTransformer).Assembly); - //env.ComponentCatalog.RegisterAssembly(typeof(SaveOnnxCommand).Assembly); + env.ComponentCatalog.RegisterAssembly(typeof(OnnxExportExtensions).Assembly); //env.ComponentCatalog.RegisterAssembly(typeof(TimeSeriesProcessingEntryPoints).Assembly); //env.ComponentCatalog.RegisterAssembly(typeof(ParquetLoader).Assembly); env.ComponentCatalog.RegisterAssembly(typeof(SsaChangePointDetector).Assembly); diff --git a/src/DotNetBridge/DotNetBridge.csproj b/src/DotNetBridge/DotNetBridge.csproj index b7afdc3e..7632e23b 100644 --- a/src/DotNetBridge/DotNetBridge.csproj +++ b/src/DotNetBridge/DotNetBridge.csproj @@ -39,6 +39,7 @@ + diff --git a/src/Platforms/build.csproj b/src/Platforms/build.csproj index cb7f2445..bd9606f6 100644 --- a/src/Platforms/build.csproj +++ b/src/Platforms/build.csproj @@ -18,6 +18,7 @@ + diff --git a/src/python/nimbusml/base_predictor.py b/src/python/nimbusml/base_predictor.py index bfa2813f..dc228033 100644 --- a/src/python/nimbusml/base_predictor.py +++ b/src/python/nimbusml/base_predictor.py @@ -157,3 +157,20 @@ def summary(self): pipeline = Pipeline([self], model=self.model_) self.model_summary_ = pipeline.summary() return self.model_summary_ + + @trace + def export_to_onnx(self, *args, **kwargs): + """ + Export the model to the ONNX format. + + See :py:meth:`nimbusml.Pipeline.export_to_onnx` for accepted arguments. + """ + if not hasattr(self, 'model_') \ + or self.model_ is None \ + or not os.path.isfile(self.model_): + + raise ValueError("Model is not fitted. Train or load a model before " + "export_to_onnx().") + + pipeline = Pipeline(model=self.model_) + pipeline.export_to_onnx(*args, **kwargs) diff --git a/src/python/nimbusml/base_transform.py b/src/python/nimbusml/base_transform.py index 73e54abf..65eb5cb8 100644 --- a/src/python/nimbusml/base_transform.py +++ b/src/python/nimbusml/base_transform.py @@ -8,6 +8,8 @@ __all__ = ["BaseTransform"] +import os + from sklearn.base import BaseEstimator from . import Pipeline @@ -89,3 +91,20 @@ def transform(self, X, as_binary_data_stream=False, **params): data = pipeline.transform( X, as_binary_data_stream=as_binary_data_stream, **params) return data + + @trace + def export_to_onnx(self, *args, **kwargs): + """ + Export the model to the ONNX format. + + See :py:meth:`nimbusml.Pipeline.export_to_onnx` for accepted arguments. + """ + if not hasattr(self, 'model_') \ + or self.model_ is None \ + or not os.path.isfile(self.model_): + + raise ValueError("Model is not fitted. Train or load a model before " + "export_to_onnx().") + + pipeline = Pipeline(model=self.model_) + pipeline.export_to_onnx(*args, **kwargs) diff --git a/src/python/nimbusml/internal/utils/entrypoints.py b/src/python/nimbusml/internal/utils/entrypoints.py index 0a292866..e49a3242 100644 --- a/src/python/nimbusml/internal/utils/entrypoints.py +++ b/src/python/nimbusml/internal/utils/entrypoints.py @@ -405,7 +405,7 @@ def remove_multi_level_index(c): self.inputs['input_data'] = X._filename elif 'data' in self.inputs: self.inputs['data'] = X._filename - elif not summary: + elif not (summary or params.get('is_onnx_export')): raise RuntimeError( "data should be a dataframe, FileDataStream or DataView") diff --git a/src/python/nimbusml/pipeline.py b/src/python/nimbusml/pipeline.py index 5a15bac4..7691e9de 100644 --- a/src/python/nimbusml/pipeline.py +++ b/src/python/nimbusml/pipeline.py @@ -36,6 +36,7 @@ from .internal.entrypoints.models_regressionevaluator import \ models_regressionevaluator from .internal.entrypoints.models_summarizer import models_summarizer +from .internal.entrypoints.models_onnxconverter import models_onnxconverter from .internal.entrypoints.transforms_datasetscorer import \ transforms_datasetscorer from .internal.entrypoints.transforms_featurecombiner import \ @@ -2265,6 +2266,89 @@ def load_model(self, src): self.model = src self.steps = [] + @trace + def export_to_onnx(self, + dst, + domain, + dst_json=None, + name=None, + data_file=None, + inputs_to_drop=None, + outputs_to_drop=None, + onnx_version="Stable", + verbose=0): + """ + Export the model to the ONNX format. + + :param str dst: The path to write the output ONNX to. + :param str domain: A reverse-DNS name to indicate the model + namespace or domain, for example, 'org.onnx'. + :param str dst_json: The path to write the output ONNX to + in JSON format. + :param name: The 'graph.name' property in the output ONNX. By default + this will be the ONNX extension-less name. (inputs). + :param data_file: The data file (inputs). + :param inputs_to_drop: Array of input column names to drop + (inputs). + :param outputs_to_drop: Array of output column names to drop + (inputs). + :param onnx_version: The targeted ONNX version. It can be either + "Stable" or "Experimental". If "Experimental" is used, + produced model can contain components that is not officially + supported in ONNX standard. (inputs). + """ + if not domain: + raise ValueError("domain argument must be specified and not empty.") + + if not self._is_fitted: + raise ValueError("Model is not fitted. Train or load a model before " + "export_to_onnx().") + + # start the clock! + start_time = time.time() + + onnx_converter_node = models_onnxconverter( + onnx=dst, + json=dst_json, + model="$model", + domain=domain, + name=name, + data_file=data_file, + inputs_to_drop=inputs_to_drop, + outputs_to_drop=outputs_to_drop, + onnx_version=onnx_version) + + inputs = dict([('model', self.model)]) + outputs = dict() + + graph = Graph( + inputs, + outputs, + False, + onnx_converter_node) + + class_name = type(self).__name__ + method_name = inspect.currentframe().f_code.co_name + telemetry_info = ".".join([class_name, method_name]) + + try: + graph.run( + X=None, + y=None, + random_state=self.random_state, + model=self.model, + verbose=verbose, + is_summary=False, + is_onnx_export=True, + telemetry_info=telemetry_info) + except RuntimeError as e: + self._run_time = time.time() - start_time + raise e + + # stop the clock + self._run_time = time.time() - start_time + self._write_csv_time = graph._write_csv_time + @trace def score( self, From 52c923b7871d40d8162c91094e60cc73d2e503b7 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Fri, 12 Jul 2019 15:39:00 -0700 Subject: [PATCH 02/16] Update the Microsoft.ML.OnnxConverter version in Platforms/build.csproj --- src/Platforms/build.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Platforms/build.csproj b/src/Platforms/build.csproj index bd9606f6..29de80f8 100644 --- a/src/Platforms/build.csproj +++ b/src/Platforms/build.csproj @@ -18,7 +18,7 @@ - + From 23e933a2cea88f0728fa79d3e4e394d4cce25964 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Fri, 12 Jul 2019 16:12:44 -0700 Subject: [PATCH 03/16] Add test for verifying onnx export support. --- src/python/nimbusml.pyproj | 1 + .../tests_extended/test_export_to_onnx.py | 386 ++++++++++++++++++ 2 files changed, 387 insertions(+) create mode 100644 src/python/tests_extended/test_export_to_onnx.py diff --git a/src/python/nimbusml.pyproj b/src/python/nimbusml.pyproj index a97e8b14..47bc4b74 100644 --- a/src/python/nimbusml.pyproj +++ b/src/python/nimbusml.pyproj @@ -609,6 +609,7 @@ + diff --git a/src/python/tests_extended/test_export_to_onnx.py b/src/python/tests_extended/test_export_to_onnx.py new file mode 100644 index 00000000..0eda44d9 --- /dev/null +++ b/src/python/tests_extended/test_export_to_onnx.py @@ -0,0 +1,386 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# -------------------------------------------------------------------------------------------- +""" +Verify onnx export support +""" +import contextlib +import io +import json +import os +import pandas +import sys +import tempfile +import numpy as np +import pandas as pd + +from nimbusml import Pipeline +from nimbusml.cluster import KMeansPlusPlus +from nimbusml.datasets import get_dataset +from nimbusml.datasets.image import get_RevolutionAnalyticslogo, get_Microsoftlogo +from nimbusml.decomposition import PcaTransformer, PcaAnomalyDetector +from nimbusml.ensemble import FastForestBinaryClassifier, LightGbmRanker +from nimbusml.feature_extraction.categorical import OneHotVectorizer, OneHotHashVectorizer +from nimbusml.feature_extraction.image import Loader, Resizer, PixelExtractor +from nimbusml.feature_extraction.text import NGramFeaturizer +from nimbusml.feature_extraction.text.extractor import Ngram +from nimbusml.feature_selection import CountSelector, MutualInformationSelector +from nimbusml.linear_model import FastLinearBinaryClassifier +from nimbusml.naive_bayes import NaiveBayesClassifier +from nimbusml.preprocessing import TensorFlowScorer, FromKey, ToKey +from nimbusml.preprocessing.filter import SkipFilter, TakeFilter, RangeFilter +from nimbusml.preprocessing.missing_values import Handler, Indicator +from nimbusml.preprocessing.normalization import Binner, GlobalContrastRowScaler +from nimbusml.preprocessing.schema import (ColumnConcatenator, TypeConverter, + ColumnDuplicator, ColumnSelector) +from nimbusml.preprocessing.text import CharTokenizer +from nimbusml.timeseries import (IidSpikeDetector, IidChangePointDetector, + SsaSpikeDetector, SsaChangePointDetector, + SsaForecaster) + + +SHOW_ONNX_JSON = False + +script_path = os.path.realpath(__file__) +script_dir = os.path.dirname(script_path) + +# Sepal_Length Sepal_Width Petal_Length Petal_Width Label Species Setosa +# 0 5.1 3.5 1.4 0.2 0 setosa 1.0 +# 1 4.9 3.0 1.4 0.2 0 setosa 1.0 +iris_df = get_dataset("iris").as_df() +iris_df.drop(['Species'], axis=1, inplace=True) + +iris_no_label_df = iris_df.drop(['Label'], axis=1) +iris_binary_df = iris_no_label_df.rename(columns={'Setosa': 'Label'}) +iris_regression_df = iris_no_label_df.drop(['Setosa'], axis=1).rename(columns={'Petal_Width': 'Label'}) + +# Unnamed: 0 education age parity induced case spontaneous stratum pooled.stratum education_str +# 0 1 0.0 26.0 6.0 1.0 1.0 2.0 1.0 3.0 0-5yrs +# 1 2 0.0 42.0 1.0 1.0 1.0 0.0 2.0 1.0 0-5yrs +infert_df = get_dataset("infert").as_df() +infert_df.columns = [i.replace(': ', '') for i in infert_df.columns] +infert_df.rename(columns={'case': 'Label'}, inplace=True) + +infert_onehot_df = (OneHotVectorizer() << 'education_str').fit_transform(infert_df) + +# rank group carrier price Class dep_day nbr_stops duration +# 0 2 1 AA 240 3 1 0 12.0 +# 1 1 1 AA 300 3 0 1 15.0 +file_path = get_dataset("gen_tickettrain").as_filepath() +gen_tt_df = pd.read_csv(file_path) +gen_tt_df['group'] = gen_tt_df['group'].astype(np.uint32) + +# Sentiment SentimentText +# 0 1 ==RUDE== Dude, you are rude upload that carl... +# 1 1 == OK! == IM GOING TO VANDALIZE WILD ONES W... +file_path = get_dataset("wiki_detox_train").as_filepath() +wiki_detox_df = pd.read_csv(file_path, sep='\t') + +# Path Label +# 0 C:\repo\src\python... True +# 1 C:\repo\src\python... False +image_paths_df = pd.DataFrame(data=dict( + Path=[get_RevolutionAnalyticslogo(), get_Microsoftlogo()], + Label=[True, False])) + + +SKIP = { + 'LightLda', + 'OneVsRestClassifier', + 'Sentiment', + 'TensorFlowScorer', + 'TreeFeaturizer', + 'WordEmbedding' +} + +INSTANCES = { + 'Binner': Binner(num_bins=3), + 'CharTokenizer': CharTokenizer(columns={'SentimentText_Transform': 'SentimentText'}), + 'ColumnConcatenator': ColumnConcatenator(columns={'Features': [ + 'Sepal_Length', + 'Sepal_Width', + 'Petal_Length', + 'Petal_Width', + 'Setosa']}), + 'ColumnSelector': ColumnSelector(columns=['Sepal_Width', 'Sepal_Length']), + 'ColumnDuplicator': ColumnDuplicator(columns={'dup': 'Sepal_Width'}), + 'CountSelector': CountSelector(count=5, columns=['Sepal_Width']), + 'FastForestBinaryClassifier': FastForestBinaryClassifier(feature=['Sepal_Width', 'Sepal_Length'], + label='Setosa'), + 'FastLinearBinaryClassifier': FastLinearBinaryClassifier(feature=['Sepal_Width', 'Sepal_Length'], + label='Setosa'), + 'FromKey': Pipeline([ + ToKey(columns=['Setosa']), + FromKey(columns=['Setosa']) + ]), + # GlobalContrastRowScaler currently requires a vector input to work + 'GlobalContrastRowScaler': Pipeline([ + ColumnConcatenator() << { + 'concated_columns': [ + 'Petal_Length', + 'Sepal_Width', + 'Sepal_Length']}, + GlobalContrastRowScaler(columns={'normed_columns': 'concated_columns'}) + ]), + 'Handler': Handler(replace_with='Mean', columns={'NewVals': 'Sepal_Length'}), + 'IidSpikeDetector': IidSpikeDetector(columns=['Sepal_Length']), + 'IidChangePointDetector': IidChangePointDetector(columns=['Sepal_Length']), + 'Indicator': Indicator(columns={'Has_Nan': 'Petal_Length'}), + 'KMeansPlusPlus': KMeansPlusPlus(n_clusters=3, feature=['Sepal_Width', 'Sepal_Length']), + 'LightGbmRanker': LightGbmRanker(feature=['Class', 'dep_day', 'duration'], + label='rank', + group_id='group'), + 'Loader': Loader(columns={'ImgPath': 'Path'}), + 'MutualInformationSelector': Pipeline([ + ColumnConcatenator(columns={'Features': ['Sepal_Width', 'Sepal_Length', 'Petal_Width']}), + MutualInformationSelector( + columns='Features', + label='Label', + slots_in_output=2) # only accept one column + ]), + 'NaiveBayesClassifier': NaiveBayesClassifier(feature=['Sepal_Width', 'Sepal_Length']), + 'NGramFeaturizer': NGramFeaturizer(word_feature_extractor=Ngram(), + columns={ 'features': ['SentimentText']}), + 'OneHotHashVectorizer': OneHotHashVectorizer(columns=['education_str']), + 'OneHotVectorizer': OneHotVectorizer(columns=['education_str']), + 'PcaAnomalyDetector': PcaAnomalyDetector(rank=3), + 'PcaTransformer': PcaTransformer(rank=3), + 'PixelExtractor': Pipeline([ + Loader(columns={'ImgPath': 'Path'}), + PixelExtractor(columns={'ImgPixels': 'ImgPath'}), + ]), + 'Resizer': Pipeline([ + Loader(columns={'ImgPath': 'Path'}), + Resizer(image_width=227, image_height=227, + columns={'ImgResize': 'ImgPath'}) + ]), + 'SkipFilter': SkipFilter(count=5), + 'SsaSpikeDetector': SsaSpikeDetector(columns=['Sepal_Length'], + seasonal_window_size=2), + 'SsaChangePointDetector': SsaChangePointDetector(columns=['Sepal_Length'], + seasonal_window_size=2), + 'SsaForecaster': SsaForecaster(columns=['Sepal_Length'], + window_size=2, + series_length=5, + train_size=5, + horizon=1), + 'RangeFilter': RangeFilter(min=5.0, max=5.1, columns=['Sepal_Length']), + 'TakeFilter': TakeFilter(count=100), + 'TensorFlowScorer': TensorFlowScorer( + model_location=os.path.join( + script_dir, + '..', + 'nimbusml', + 'examples', + 'frozen_saved_model.pb'), + columns={'c': ['a', 'b']}), + 'ToKey': ToKey(columns={'edu_1': 'education'}), + 'TypeConverter': TypeConverter(columns=['age'], result_type='R4') +} + +DATASETS = { + 'AveragedPerceptronBinaryClassifier': infert_onehot_df, + 'Binner': iris_no_label_df, + 'BootstrapSampler': infert_df, + 'CharTokenizer': wiki_detox_df, + 'FactorizationMachineBinaryClassifier': iris_binary_df, + 'FastForestBinaryClassifier': iris_no_label_df, + 'FastForestRegressor': iris_regression_df, + 'FastLinearBinaryClassifier': iris_no_label_df, + 'FastLinearClassifier': iris_binary_df, + 'FastLinearRegressor': iris_regression_df, + 'FastTreesBinaryClassifier': iris_binary_df, + 'FastTreesRegressor': iris_regression_df, + 'FastTreesTweedieRegressor': iris_regression_df, + 'GamBinaryClassifier': iris_binary_df, + 'GamRegressor': iris_regression_df, + 'GlobalContrastRowScaler': iris_df.astype(np.float32), + 'LightGbmRanker': gen_tt_df, + 'Loader': image_paths_df, + 'LogisticRegressionBinaryClassifier': iris_binary_df, + 'LogisticRegressionClassifier': iris_binary_df, + 'LogMeanVarianceScaler': iris_no_label_df, + 'MeanVarianceScaler': iris_no_label_df, + 'MinMaxScaler': iris_no_label_df, + 'NGramFeaturizer': wiki_detox_df, + 'OneHotHashVectorizer': infert_df, + 'OneHotVectorizer': infert_df, + 'OnlineGradientDescentRegressor': iris_regression_df, + 'OrdinaryLeastSquaresRegressor': iris_regression_df, + 'PcaAnomalyDetector': iris_no_label_df, + 'PcaTransformer': iris_no_label_df, + 'PixelExtractor': image_paths_df, + 'PoissonRegressionRegressor': iris_regression_df, + 'Resizer': image_paths_df, + 'SgdBinaryClassifier': iris_binary_df, + 'SymSgdBinaryClassifier': iris_binary_df, + 'ToKey': infert_df, + 'TypeConverter': infert_onehot_df +} + +REQUIRES_EXPERIMENTAL = { + 'TypeConverter', + 'MeanVarianceScaler', + 'MinMaxScaler' +} + +SUPPORTED_ESTIMATORS = { + 'ColumnConcatenator', + 'OneHotVectorizer', + 'MeanVarianceScaler', + 'MinMaxScaler', + 'TypeConverter' +} + + +class CaptureOutputContext(): + """ + Context which can be used for + capturing stdout and stderr. + """ + def __enter__(self): + self.orig_stdout = sys.stdout + self.orig_stderr = sys.stderr + self.stdout_capturer = io.StringIO() + self.stderr_capturer = io.StringIO() + sys.stdout = self.stdout_capturer + sys.stderr = self.stderr_capturer + return self + + def __exit__(self, *args): + sys.stdout = self.orig_stdout + sys.stderr = self.orig_stderr + self.stdout = self.stdout_capturer.getvalue() + self.stderr = self.stderr_capturer.getvalue() + + if self.stdout: + print(self.stdout) + + if self.stderr: + print(self.stderr) + + # free up some memory + del self.stdout_capturer + del self.stderr_capturer + + +def get_tmp_file(suffix=None): + fd, file_name = tempfile.mkstemp(suffix=suffix) + fl = os.fdopen(fd, 'w') + fl.close() + return file_name + + +def get_file_size(file_path): + file_size = 0 + try: + file_size = os.path.getsize(file_path) + except: + pass + return file_size + + +def load_json(file_path): + with open(file_path) as f: + lines = f.readlines() + lines = [l for l in lines if not l.strip().startswith('#')] + content_without_comments = '\n'.join(lines) + return json.loads(content_without_comments) + + +def test_export_to_onnx(estimator, class_name): + """ + Fit and test an estimator and determine + if it supports exporting to the ONNX format. + """ + onnx_path = get_tmp_file('.onnx') + onnx_json_path = get_tmp_file('.onnx.json') + + output = None + exported = False + + try: + dataset = DATASETS.get(class_name, iris_df) + estimator.fit(dataset) + + onnx_version = 'Experimental' if class_name in REQUIRES_EXPERIMENTAL else 'Stable' + + with CaptureOutputContext() as output: + estimator.export_to_onnx(onnx_path, + 'com.microsoft.ml', + dst_json=onnx_json_path, + onnx_version=onnx_version) + except Exception as e: + print(e) + + onnx_file_size = get_file_size(onnx_path) + onnx_json_file_size = get_file_size(onnx_json_path) + + if (output and + (onnx_file_size != 0) and + (onnx_json_file_size != 0) and + (not 'cannot save itself as ONNX' in output.stdout)): + exported = True + + if exported and SHOW_ONNX_JSON: + with open(onnx_json_path) as f: + print(json.dumps(json.load(f), indent=4)) + + os.remove(onnx_path) + os.remove(onnx_json_path) + return exported + + +manifest_diff = os.path.join(script_dir, '..', 'tools', 'manifest_diff.json') +entry_points = load_json(manifest_diff)['EntryPoints'] +entry_points = sorted(entry_points, key=lambda ep: ep['NewName']) + +exportable_estimators = set() +exportable_experimental_estimators = set() +unexportable_estimators = set() + +for entry_point in entry_points: + class_name = entry_point['NewName'] + + print('\n===========> %s' % class_name) + + if class_name in SKIP: + print("skipped") + continue + + mod = __import__('nimbusml.' + entry_point['Module'], + fromlist=[str(class_name)]) + + if class_name in INSTANCES: + estimator = INSTANCES[class_name] + else: + the_class = getattr(mod, class_name) + estimator = the_class() + + result = test_export_to_onnx(estimator, class_name) + + if result: + if class_name in REQUIRES_EXPERIMENTAL: + exportable_experimental_estimators.add(class_name) + else: + exportable_estimators.add(class_name) + + print('Estimator successfully exported to ONNX.') + + else: + unexportable_estimators.add(class_name) + print('Estimator could NOT be exported to ONNX.') + +print('\nThe following estimators were skipped: ', sorted(SKIP)) +print('\nThe following estimators were successfully exported to ONNX: ', sorted(exportable_estimators)) +print('\nThe following estimators were successfully exported to experimental ONNX: ', sorted(exportable_experimental_estimators)) +print('\nThe following estimators could not be exported to ONNX: ', sorted(unexportable_estimators)) + +failed_estimators = SUPPORTED_ESTIMATORS.difference(exportable_estimators) \ + .difference(exportable_experimental_estimators) + +if len(failed_estimators) > 0: + print("The following tests failed exporting to onnx:", sorted(failed_estimators)) + raise RuntimeError("onnx export checks failed") + From 02645e5858b32d83e9bc494cee9551f06443d678 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Wed, 17 Jul 2019 16:35:11 -0700 Subject: [PATCH 04/16] Update the onnx conversion to be compatible with the latest changes in pull quest https://github.com/dotnet/machinelearning/pull/3986. --- src/python/nimbusml/base_predictor.py | 2 +- src/python/nimbusml/base_transform.py | 2 +- .../entrypoints/models_onnxconverter.py | 10 +++++-- src/python/nimbusml/pipeline.py | 27 ++++++++++++------- src/python/tools/manifest.json | 14 ++++++++-- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/python/nimbusml/base_predictor.py b/src/python/nimbusml/base_predictor.py index dc228033..dbebca99 100644 --- a/src/python/nimbusml/base_predictor.py +++ b/src/python/nimbusml/base_predictor.py @@ -172,5 +172,5 @@ def export_to_onnx(self, *args, **kwargs): raise ValueError("Model is not fitted. Train or load a model before " "export_to_onnx().") - pipeline = Pipeline(model=self.model_) + pipeline = Pipeline([self], model=self.model_) pipeline.export_to_onnx(*args, **kwargs) diff --git a/src/python/nimbusml/base_transform.py b/src/python/nimbusml/base_transform.py index 65eb5cb8..1462f9e9 100644 --- a/src/python/nimbusml/base_transform.py +++ b/src/python/nimbusml/base_transform.py @@ -106,5 +106,5 @@ def export_to_onnx(self, *args, **kwargs): raise ValueError("Model is not fitted. Train or load a model before " "export_to_onnx().") - pipeline = Pipeline(model=self.model_) + pipeline = Pipeline([self], model=self.model_) pipeline.export_to_onnx(*args, **kwargs) diff --git a/src/python/nimbusml/internal/entrypoints/models_onnxconverter.py b/src/python/nimbusml/internal/entrypoints/models_onnxconverter.py index 70bef2a8..3c080eb6 100644 --- a/src/python/nimbusml/internal/entrypoints/models_onnxconverter.py +++ b/src/python/nimbusml/internal/entrypoints/models_onnxconverter.py @@ -10,14 +10,15 @@ def models_onnxconverter( onnx, - model, data_file=None, json=None, name=None, domain=None, inputs_to_drop=None, outputs_to_drop=None, + model=None, onnx_version='Stable', + predictive_model=None, **params): """ **Description** @@ -40,6 +41,8 @@ def models_onnxconverter( "Stable" or "Experimental". If "Experimental" is used, produced model can contain components that is not officially supported in ONNX standard. (inputs). + :param predictive_model: Predictor model that needs to be + converted to ONNX format. (inputs). """ entrypoint_name = 'Models.OnnxConverter' @@ -85,7 +88,7 @@ def models_onnxconverter( if model is not None: inputs['Model'] = try_set( obj=model, - none_acceptable=False, + none_acceptable=True, is_of_type=str) if onnx_version is not None: inputs['OnnxVersion'] = try_set( @@ -95,6 +98,9 @@ def models_onnxconverter( values=[ 'Stable', 'Experimental']) + if predictive_model is not None: + inputs['PredictiveModel'] = try_set( + obj=predictive_model, none_acceptable=True, is_of_type=str) input_variables = { x for x in unlist(inputs.values()) diff --git a/src/python/nimbusml/pipeline.py b/src/python/nimbusml/pipeline.py index 7691e9de..7d1291b0 100644 --- a/src/python/nimbusml/pipeline.py +++ b/src/python/nimbusml/pipeline.py @@ -2307,16 +2307,23 @@ def export_to_onnx(self, # start the clock! start_time = time.time() - onnx_converter_node = models_onnxconverter( - onnx=dst, - json=dst_json, - model="$model", - domain=domain, - name=name, - data_file=data_file, - inputs_to_drop=inputs_to_drop, - outputs_to_drop=outputs_to_drop, - onnx_version=onnx_version) + onnx_converter_args = { + 'onnx': dst, + 'json': dst_json, + 'domain': domain, + 'name': name, + 'data_file': data_file, + 'inputs_to_drop': inputs_to_drop, + 'outputs_to_drop': outputs_to_drop, + 'onnx_version': onnx_version + } + + if (len(self.steps) > 0) and (self.last_node.type != "transform"): + onnx_converter_args['predictive_model'] = "$model" + else: + onnx_converter_args['model'] = "$model" + + onnx_converter_node = models_onnxconverter(**onnx_converter_args) inputs = dict([('model', self.model)]) outputs = dict() diff --git a/src/python/tools/manifest.json b/src/python/tools/manifest.json index 35ebb09d..43bee571 100644 --- a/src/python/tools/manifest.json +++ b/src/python/tools/manifest.json @@ -2275,9 +2275,10 @@ "Name": "Model", "Type": "TransformModel", "Desc": "Model that needs to be converted to ONNX format.", - "Required": true, + "Required": false, "SortOrder": 10.0, - "IsNullable": false + "IsNullable": false, + "Default": null }, { "Name": "OnnxVersion", @@ -2293,6 +2294,15 @@ "SortOrder": 11.0, "IsNullable": false, "Default": "Stable" + }, + { + "Name": "PredictiveModel", + "Type": "PredictorModel", + "Desc": "Predictor model that needs to be converted to ONNX format.", + "Required": false, + "SortOrder": 12.0, + "IsNullable": false, + "Default": null } ], "Outputs": [] From a445f0d45ea8d9b7110e42695a9d1a5064506216 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Wed, 16 Oct 2019 12:35:22 -0700 Subject: [PATCH 05/16] Fix a few of the issues with test_export_to_onnx. --- .../tests_extended/test_export_to_onnx.py | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/python/tests_extended/test_export_to_onnx.py b/src/python/tests_extended/test_export_to_onnx.py index e90c0f42..299cc69f 100644 --- a/src/python/tests_extended/test_export_to_onnx.py +++ b/src/python/tests_extended/test_export_to_onnx.py @@ -9,7 +9,6 @@ import io import json import os -import pandas import sys import tempfile import numpy as np @@ -20,7 +19,7 @@ from nimbusml.datasets import get_dataset from nimbusml.datasets.image import get_RevolutionAnalyticslogo, get_Microsoftlogo from nimbusml.decomposition import PcaTransformer, PcaAnomalyDetector -from nimbusml.ensemble import FastForestBinaryClassifier, LightGbmRanker +from nimbusml.ensemble import FastForestBinaryClassifier, FastTreesTweedieRegressor, LightGbmRanker from nimbusml.feature_extraction.categorical import OneHotVectorizer, OneHotHashVectorizer from nimbusml.feature_extraction.image import Loader, Resizer, PixelExtractor from nimbusml.feature_extraction.text import NGramFeaturizer @@ -30,11 +29,11 @@ from nimbusml.naive_bayes import NaiveBayesClassifier from nimbusml.preprocessing import TensorFlowScorer, FromKey, ToKey from nimbusml.preprocessing.filter import SkipFilter, TakeFilter, RangeFilter -from nimbusml.preprocessing.missing_values import Handler, Indicator -from nimbusml.preprocessing.normalization import Binner, GlobalContrastRowScaler +from nimbusml.preprocessing.missing_values import Filter, Handler, Indicator +from nimbusml.preprocessing.normalization import Binner, GlobalContrastRowScaler, LpScaler from nimbusml.preprocessing.schema import (ColumnConcatenator, TypeConverter, ColumnDuplicator, ColumnSelector) -from nimbusml.preprocessing.text import CharTokenizer +from nimbusml.preprocessing.text import CharTokenizer, WordTokenizer from nimbusml.timeseries import (IidSpikeDetector, IidChangePointDetector, SsaSpikeDetector, SsaChangePointDetector, SsaForecaster) @@ -71,11 +70,18 @@ gen_tt_df = pd.read_csv(file_path) gen_tt_df['group'] = gen_tt_df['group'].astype(np.uint32) +# Unnamed: 0 Label Solar_R Wind Temp Month Day +# 0 1 41.0 190.0 7.4 67 5 1 +# 1 2 36.0 118.0 8.0 72 5 2 +airquality_df = get_dataset("airquality").as_df().fillna(0) +airquality_df = airquality_df[airquality_df.Ozone.notnull()] + # Sentiment SentimentText # 0 1 ==RUDE== Dude, you are rude upload that carl... # 1 1 == OK! == IM GOING TO VANDALIZE WILD ONES W... file_path = get_dataset("wiki_detox_train").as_filepath() wiki_detox_df = pd.read_csv(file_path, sep='\t') +wiki_detox_df = wiki_detox_df.head(10) # Path Label # 0 C:\repo\src\python... True @@ -111,6 +117,8 @@ label='Setosa'), 'FastLinearBinaryClassifier': FastLinearBinaryClassifier(feature=['Sepal_Width', 'Sepal_Length'], label='Setosa'), + 'FastTreesTweedieRegressor': FastTreesTweedieRegressor(label='Ozone'), + 'Filter': Filter(columns=[ 'Petal_Length', 'Petal_Width']), 'FromKey': Pipeline([ ToKey(columns=['Setosa']), FromKey(columns=['Setosa']) @@ -133,6 +141,14 @@ label='rank', group_id='group'), 'Loader': Loader(columns={'ImgPath': 'Path'}), + 'LpScaler': Pipeline([ + ColumnConcatenator() << { + 'concated_columns': [ + 'Petal_Length', + 'Sepal_Width', + 'Sepal_Length']}, + LpScaler(columns={'normed_columns': 'concated_columns'}) + ]), 'MutualInformationSelector': Pipeline([ ColumnConcatenator(columns={'Features': ['Sepal_Width', 'Sepal_Length', 'Petal_Width']}), MutualInformationSelector( @@ -146,7 +162,7 @@ 'OneHotHashVectorizer': OneHotHashVectorizer(columns=['education_str']), 'OneHotVectorizer': OneHotVectorizer(columns=['education_str']), 'PcaAnomalyDetector': PcaAnomalyDetector(rank=3), - 'PcaTransformer': PcaTransformer(rank=3), + 'PcaTransformer': PcaTransformer(rank=2), 'PixelExtractor': Pipeline([ Loader(columns={'ImgPath': 'Path'}), PixelExtractor(columns={'ImgPixels': 'ImgPath'}), @@ -177,7 +193,8 @@ 'frozen_saved_model.pb'), columns={'c': ['a', 'b']}), 'ToKey': ToKey(columns={'edu_1': 'education'}), - 'TypeConverter': TypeConverter(columns=['age'], result_type='R4') + 'TypeConverter': TypeConverter(columns=['age'], result_type='R4'), + 'WordTokenizer': WordTokenizer(char_array_term_separators=[" "]) << {'wt': 'SentimentText'} } DATASETS = { @@ -185,6 +202,7 @@ 'Binner': iris_no_label_df, 'BootstrapSampler': infert_df, 'CharTokenizer': wiki_detox_df, + 'EnsembleRegressor': iris_regression_df, 'FactorizationMachineBinaryClassifier': iris_binary_df, 'FastForestBinaryClassifier': iris_no_label_df, 'FastForestRegressor': iris_regression_df, @@ -193,15 +211,18 @@ 'FastLinearRegressor': iris_regression_df, 'FastTreesBinaryClassifier': iris_binary_df, 'FastTreesRegressor': iris_regression_df, - 'FastTreesTweedieRegressor': iris_regression_df, + 'FastTreesTweedieRegressor': airquality_df, + 'Filter': iris_no_label_df, 'GamBinaryClassifier': iris_binary_df, 'GamRegressor': iris_regression_df, 'GlobalContrastRowScaler': iris_df.astype(np.float32), 'LightGbmRanker': gen_tt_df, + 'LinearSvmBinaryClassifier': iris_binary_df, 'Loader': image_paths_df, 'LogisticRegressionBinaryClassifier': iris_binary_df, 'LogisticRegressionClassifier': iris_binary_df, 'LogMeanVarianceScaler': iris_no_label_df, + 'LpScaler': iris_no_label_df.drop(['Setosa'], axis=1).astype(np.float32), 'MeanVarianceScaler': iris_no_label_df, 'MinMaxScaler': iris_no_label_df, 'NGramFeaturizer': wiki_detox_df, @@ -210,14 +231,15 @@ 'OnlineGradientDescentRegressor': iris_regression_df, 'OrdinaryLeastSquaresRegressor': iris_regression_df, 'PcaAnomalyDetector': iris_no_label_df, - 'PcaTransformer': iris_no_label_df, + 'PcaTransformer': iris_regression_df, 'PixelExtractor': image_paths_df, 'PoissonRegressionRegressor': iris_regression_df, 'Resizer': image_paths_df, 'SgdBinaryClassifier': iris_binary_df, 'SymSgdBinaryClassifier': iris_binary_df, 'ToKey': infert_df, - 'TypeConverter': infert_onehot_df + 'TypeConverter': infert_onehot_df, + 'WordTokenizer': wiki_detox_df } REQUIRES_EXPERIMENTAL = { From d11fffda7bcf1af02d096554d1531f1803b37afd Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Wed, 30 Oct 2019 11:15:47 -0700 Subject: [PATCH 06/16] Add onnxruntime.dll to the NimbusML python package. It is already included in the Linux and Mac builds. --- build/libs_win.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/build/libs_win.txt b/build/libs_win.txt index 7ef9cca7..c1ba4b51 100644 --- a/build/libs_win.txt +++ b/build/libs_win.txt @@ -13,3 +13,4 @@ TensorFlow.NET.dll NumSharp.Core.dll System.Drawing.Common.dll Microsoft.ML.* +onnxruntime.dll From b3c6d6211f7ae2b8f773186296d973341a6957cb Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Wed, 30 Oct 2019 16:10:39 -0700 Subject: [PATCH 07/16] Initial implementation of the OnnxRunner transform. --- src/DotNetBridge/Entrypoints.cs | 54 +++++++++++ src/python/nimbusml.pyproj | 3 + .../internal/core/preprocessing/onnxrunner.py | 71 ++++++++++++++ .../entrypoints/models_onnxtransformer.py | 96 +++++++++++++++++++ src/python/nimbusml/preprocessing/__init__.py | 4 +- .../nimbusml/preprocessing/onnxrunner.py | 82 ++++++++++++++++ src/python/tools/manifest.json | 84 ++++++++++++++++ src/python/tools/manifest_diff.json | 6 ++ 8 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 src/python/nimbusml/internal/core/preprocessing/onnxrunner.py create mode 100644 src/python/nimbusml/internal/entrypoints/models_onnxtransformer.py create mode 100644 src/python/nimbusml/preprocessing/onnxrunner.py diff --git a/src/DotNetBridge/Entrypoints.cs b/src/DotNetBridge/Entrypoints.cs index 9be84e67..d0bcb9db 100644 --- a/src/DotNetBridge/Entrypoints.cs +++ b/src/DotNetBridge/Entrypoints.cs @@ -178,5 +178,59 @@ public static ScoringTransformOutput Score(IHostEnvironment env, ScoringTransfor }; } + + public sealed class OnnxTransformInput : TransformInputBase + { + [Argument(ArgumentType.Required, HelpText = "Path to the onnx model file.", ShortName = "model", SortOrder = 0)] + public string ModelFile; + + [Argument(ArgumentType.Multiple, HelpText = "Name of the input column.", SortOrder = 1)] + public string[] InputColumns; + + [Argument(ArgumentType.Multiple, HelpText = "Name of the output column.", SortOrder = 2)] + public string[] OutputColumns; + + [Argument(ArgumentType.AtMostOnce, HelpText = "GPU device id to run on (e.g. 0,1,..). Null for CPU. Requires CUDA 9.1.", SortOrder = 3)] + public int? GpuDeviceId = null; + + [Argument(ArgumentType.AtMostOnce, HelpText = "If true, resumes execution on CPU upon GPU error. If false, will raise the GPU execption.", SortOrder = 4)] + public bool FallbackToCpu = false; + } + + public sealed class OnnxTransformOutput + { + [TlcModule.Output(Desc = "ONNX transformed dataset", SortOrder = 1)] + public IDataView OutputData; + + [TlcModule.Output(Desc = "Transform model", SortOrder = 2)] + public TransformModel Model; + } + + [TlcModule.EntryPoint(Name = "Models.OnnxTransformer", + Desc = "Applies an ONNX model to a dataset.", + UserName = "Onnx Transformer", + ShortName = "onnx-xf")] + public static OnnxTransformOutput ApplyOnnxModel(IHostEnvironment env, OnnxTransformInput input) + { + var host = EntryPointUtils.CheckArgsAndCreateHost(env, "OnnxTransform", input); + + var inputColumns = input.InputColumns ?? (Array.Empty()); + var outputColumns = input.OutputColumns ?? (Array.Empty()); + + var transformsCatalog = new TransformsCatalog(host); + var onnxScoringEstimator = OnnxCatalog.ApplyOnnxModel(transformsCatalog, + outputColumns, + inputColumns, + input.ModelFile, + input.GpuDeviceId, + input.FallbackToCpu); + + var view = onnxScoringEstimator.Fit(input.Data).Transform(input.Data); + return new OnnxTransformOutput() + { + Model = new TransformModelImpl(host, view, input.Data), + OutputData = view + }; + } } } diff --git a/src/python/nimbusml.pyproj b/src/python/nimbusml.pyproj index 3a7a9328..079c7440 100644 --- a/src/python/nimbusml.pyproj +++ b/src/python/nimbusml.pyproj @@ -309,6 +309,7 @@ + @@ -342,6 +343,7 @@ + @@ -648,6 +650,7 @@ + diff --git a/src/python/nimbusml/internal/core/preprocessing/onnxrunner.py b/src/python/nimbusml/internal/core/preprocessing/onnxrunner.py new file mode 100644 index 00000000..34ed46ba --- /dev/null +++ b/src/python/nimbusml/internal/core/preprocessing/onnxrunner.py @@ -0,0 +1,71 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# -------------------------------------------------------------------------------------------- +# - Generated by tools/entrypoint_compiler.py: do not edit by hand +""" +OnnxRunner +""" + +__all__ = ["OnnxRunner"] + + +from ...entrypoints.models_onnxtransformer import models_onnxtransformer +from ...utils.utils import trace +from ..base_pipeline_item import BasePipelineItem, DefaultSignature + + +class OnnxRunner(BasePipelineItem, DefaultSignature): + """ + **Description** + Applies an ONNX model to a dataset. + + :param model_file: Path to the onnx model file. + + :param input_columns: Name of the input column. + + :param output_columns: Name of the output column. + + :param gpu_device_id: GPU device id to run on (e.g. 0,1,..). Null for CPU. + Requires CUDA 9.1. + + :param fallback_to_cpu: If true, resumes execution on CPU upon GPU error. + If false, will raise the GPU execption. + + :param params: Additional arguments sent to compute engine. + + """ + + @trace + def __init__( + self, + model_file, + input_columns=None, + output_columns=None, + gpu_device_id=None, + fallback_to_cpu=False, + **params): + BasePipelineItem.__init__( + self, type='transform', **params) + + self.model_file = model_file + self.input_columns = input_columns + self.output_columns = output_columns + self.gpu_device_id = gpu_device_id + self.fallback_to_cpu = fallback_to_cpu + + @property + def _entrypoint(self): + return models_onnxtransformer + + @trace + def _get_node(self, **all_args): + algo_args = dict( + model_file=self.model_file, + input_columns=self.input_columns, + output_columns=self.output_columns, + gpu_device_id=self.gpu_device_id, + fallback_to_cpu=self.fallback_to_cpu) + + all_args.update(algo_args) + return self._entrypoint(**all_args) diff --git a/src/python/nimbusml/internal/entrypoints/models_onnxtransformer.py b/src/python/nimbusml/internal/entrypoints/models_onnxtransformer.py new file mode 100644 index 00000000..173c976a --- /dev/null +++ b/src/python/nimbusml/internal/entrypoints/models_onnxtransformer.py @@ -0,0 +1,96 @@ +# - Generated by tools/entrypoint_compiler.py: do not edit by hand +""" +Models.OnnxTransformer +""" + +import numbers + +from ..utils.entrypoints import EntryPoint +from ..utils.utils import try_set, unlist + + +def models_onnxtransformer( + model_file, + data, + output_data=None, + model=None, + input_columns=None, + output_columns=None, + gpu_device_id=None, + fallback_to_cpu=False, + **params): + """ + **Description** + Applies an ONNX model to a dataset. + + :param model_file: Path to the onnx model file. (inputs). + :param input_columns: Name of the input column. (inputs). + :param data: Input dataset (inputs). + :param output_columns: Name of the output column. (inputs). + :param gpu_device_id: GPU device id to run on (e.g. 0,1,..). Null + for CPU. Requires CUDA 9.1. (inputs). + :param fallback_to_cpu: If true, resumes execution on CPU upon + GPU error. If false, will raise the GPU execption. (inputs). + :param output_data: ONNX transformed dataset (outputs). + :param model: Transform model (outputs). + """ + + entrypoint_name = 'Models.OnnxTransformer' + inputs = {} + outputs = {} + + if model_file is not None: + inputs['ModelFile'] = try_set( + obj=model_file, + none_acceptable=False, + is_of_type=str) + if input_columns is not None: + inputs['InputColumns'] = try_set( + obj=input_columns, + none_acceptable=True, + is_of_type=list, + is_column=True) + if data is not None: + inputs['Data'] = try_set( + obj=data, + none_acceptable=False, + is_of_type=str) + if output_columns is not None: + inputs['OutputColumns'] = try_set( + obj=output_columns, + none_acceptable=True, + is_of_type=list, + is_column=True) + if gpu_device_id is not None: + inputs['GpuDeviceId'] = try_set( + obj=gpu_device_id, + none_acceptable=True, + is_of_type=numbers.Real) + if fallback_to_cpu is not None: + inputs['FallbackToCpu'] = try_set( + obj=fallback_to_cpu, + none_acceptable=True, + is_of_type=bool) + if output_data is not None: + outputs['OutputData'] = try_set( + obj=output_data, + none_acceptable=False, + is_of_type=str) + if model is not None: + outputs['Model'] = try_set( + obj=model, + none_acceptable=False, + is_of_type=str) + + input_variables = { + x for x in unlist(inputs.values()) + if isinstance(x, str) and x.startswith("$")} + output_variables = { + x for x in unlist(outputs.values()) + if isinstance(x, str) and x.startswith("$")} + + entrypoint = EntryPoint( + name=entrypoint_name, inputs=inputs, outputs=outputs, + input_variables=input_variables, + output_variables=output_variables) + return entrypoint diff --git a/src/python/nimbusml/preprocessing/__init__.py b/src/python/nimbusml/preprocessing/__init__.py index 26b41b8e..58296bd6 100644 --- a/src/python/nimbusml/preprocessing/__init__.py +++ b/src/python/nimbusml/preprocessing/__init__.py @@ -2,10 +2,12 @@ from .tokey import ToKey from .tensorflowscorer import TensorFlowScorer from .datasettransformer import DatasetTransformer +from .onnxrunner import OnnxRunner __all__ = [ 'FromKey', 'ToKey', 'TensorFlowScorer', - 'DatasetTransformer' + 'DatasetTransformer', + 'OnnxRunner' ] diff --git a/src/python/nimbusml/preprocessing/onnxrunner.py b/src/python/nimbusml/preprocessing/onnxrunner.py new file mode 100644 index 00000000..2df2ac75 --- /dev/null +++ b/src/python/nimbusml/preprocessing/onnxrunner.py @@ -0,0 +1,82 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +# -------------------------------------------------------------------------------------------- +# - Generated by tools/entrypoint_compiler.py: do not edit by hand +""" +OnnxRunner +""" + +__all__ = ["OnnxRunner"] + + +from sklearn.base import TransformerMixin + +from ..base_transform import BaseTransform +from ..internal.core.preprocessing.onnxrunner import OnnxRunner as core +from ..internal.utils.utils import trace + + +class OnnxRunner(core, BaseTransform, TransformerMixin): + """ + **Description** + Applies an ONNX model to a dataset. + + :param columns: see `Columns `_. + + :param model_file: Path to the onnx model file. + + :param input_columns: Name of the input column. + + :param output_columns: Name of the output column. + + :param gpu_device_id: GPU device id to run on (e.g. 0,1,..). Null for CPU. + Requires CUDA 9.1. + + :param fallback_to_cpu: If true, resumes execution on CPU upon GPU error. + If false, will raise the GPU execption. + + :param params: Additional arguments sent to compute engine. + + """ + + @trace + def __init__( + self, + model_file, + input_columns=None, + output_columns=None, + gpu_device_id=None, + fallback_to_cpu=False, + columns=None, + **params): + + if columns: + params['columns'] = columns + if columns: + input_columns = sum( + list( + columns.values()), + []) if isinstance( + list( + columns.values())[0], + list) else list( + columns.values()) + if columns: + output_columns = list(columns.keys()) + BaseTransform.__init__(self, **params) + core.__init__( + self, + model_file=model_file, + input_columns=input_columns, + output_columns=output_columns, + gpu_device_id=gpu_device_id, + fallback_to_cpu=fallback_to_cpu, + **params) + self._columns = columns + + def get_params(self, deep=False): + """ + Get the parameters for this operator. + """ + return core.get_params(self) diff --git a/src/python/tools/manifest.json b/src/python/tools/manifest.json index 06779641..a4236688 100644 --- a/src/python/tools/manifest.json +++ b/src/python/tools/manifest.json @@ -2307,6 +2307,90 @@ ], "Outputs": [] }, + { + "Name": "Models.OnnxTransformer", + "Desc": "Applies an ONNX model to a dataset.", + "FriendlyName": "Onnx Transformer", + "ShortName": "onnx-xf", + "Inputs": [ + { + "Name": "ModelFile", + "Type": "String", + "Desc": "Path to the onnx model file.", + "Aliases": [ + "model" + ], + "Required": true, + "SortOrder": 0.0, + "IsNullable": false + }, + { + "Name": "InputColumns", + "Type": { + "Kind": "Array", + "ItemType": "String" + }, + "Desc": "Name of the input column.", + "Required": false, + "SortOrder": 1.0, + "IsNullable": false, + "Default": null + }, + { + "Name": "Data", + "Type": "DataView", + "Desc": "Input dataset", + "Required": true, + "SortOrder": 1.0, + "IsNullable": false + }, + { + "Name": "OutputColumns", + "Type": { + "Kind": "Array", + "ItemType": "String" + }, + "Desc": "Name of the output column.", + "Required": false, + "SortOrder": 2.0, + "IsNullable": false, + "Default": null + }, + { + "Name": "GpuDeviceId", + "Type": "Int", + "Desc": "GPU device id to run on (e.g. 0,1,..). Null for CPU. Requires CUDA 9.1.", + "Required": false, + "SortOrder": 3.0, + "IsNullable": true, + "Default": null + }, + { + "Name": "FallbackToCpu", + "Type": "Bool", + "Desc": "If true, resumes execution on CPU upon GPU error. If false, will raise the GPU execption.", + "Required": false, + "SortOrder": 4.0, + "IsNullable": false, + "Default": false + } + ], + "Outputs": [ + { + "Name": "OutputData", + "Type": "DataView", + "Desc": "ONNX transformed dataset" + }, + { + "Name": "Model", + "Type": "TransformModel", + "Desc": "Transform model" + } + ], + "InputKind": [ + "ITransformInput" + ] + }, { "Name": "Models.OvaModelCombiner", "Desc": "Combines a sequence of PredictorModels into a single model", diff --git a/src/python/tools/manifest_diff.json b/src/python/tools/manifest_diff.json index 68ab2fa5..4263762e 100644 --- a/src/python/tools/manifest_diff.json +++ b/src/python/tools/manifest_diff.json @@ -330,6 +330,12 @@ "Module": "preprocessing", "Type": "Transform" }, + { + "Name": "Models.OnnxTransformer", + "NewName": "OnnxRunner", + "Module": "preprocessing", + "Type": "Transform" + }, { "Name": "Trainers.FieldAwareFactorizationMachineBinaryClassifier", "NewName": "FactorizationMachineBinaryClassifier", From 3aa18715409d9897beff3252aad32c17d8dfb9a2 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Wed, 30 Oct 2019 16:22:14 -0700 Subject: [PATCH 08/16] Fix missing reference to models_onnxconverter in nimbusml.pyproj. --- src/python/nimbusml.pyproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/python/nimbusml.pyproj b/src/python/nimbusml.pyproj index 3a7a9328..35eb7b5a 100644 --- a/src/python/nimbusml.pyproj +++ b/src/python/nimbusml.pyproj @@ -342,6 +342,7 @@ + From cc87fbb99fd7c0930b9b2819dacb747b35f0aa63 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Thu, 31 Oct 2019 14:48:39 -0700 Subject: [PATCH 09/16] Exclude OnnxRunner from the test_export_to_onnx tests. --- src/python/tests_extended/test_export_to_onnx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/python/tests_extended/test_export_to_onnx.py b/src/python/tests_extended/test_export_to_onnx.py index 299cc69f..6dd54606 100644 --- a/src/python/tests_extended/test_export_to_onnx.py +++ b/src/python/tests_extended/test_export_to_onnx.py @@ -98,7 +98,8 @@ 'Sentiment', 'TensorFlowScorer', 'TreeFeaturizer', - 'WordEmbedding' + 'WordEmbedding', + 'OnnxRunner' } INSTANCES = { From 2a804940ba17aa772f4e0db3d65c575c28ba9bee Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Thu, 31 Oct 2019 14:57:11 -0700 Subject: [PATCH 10/16] Remove OnnxRunner from test_estimator_checks. --- src/python/tests/test_estimator_checks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/python/tests/test_estimator_checks.py b/src/python/tests/test_estimator_checks.py index d8a19e1f..bff69380 100644 --- a/src/python/tests/test_estimator_checks.py +++ b/src/python/tests/test_estimator_checks.py @@ -256,7 +256,8 @@ 'TreeFeaturizer', # skip SymSgdBinaryClassifier for now, because of crashes. 'SymSgdBinaryClassifier', - 'DatasetTransformer' + 'DatasetTransformer', + 'OnnxRunner' ]) From 6331e938d83d1cdc99921b2db8707fe67676fdde Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Fri, 6 Dec 2019 11:55:08 -0800 Subject: [PATCH 11/16] Add back in OnnxConverter reference which was accidentally removed in merge. --- src/DotNetBridge/DotNetBridge.csproj | 1 + src/Platforms/build.csproj | 1 + 2 files changed, 2 insertions(+) diff --git a/src/DotNetBridge/DotNetBridge.csproj b/src/DotNetBridge/DotNetBridge.csproj index 38fcba37..ba1f61d6 100644 --- a/src/DotNetBridge/DotNetBridge.csproj +++ b/src/DotNetBridge/DotNetBridge.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Platforms/build.csproj b/src/Platforms/build.csproj index f027dc30..fcefc4f1 100644 --- a/src/Platforms/build.csproj +++ b/src/Platforms/build.csproj @@ -17,6 +17,7 @@ + From f1b2c9c19b0a6b95e49fe49726d522f966038941 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Fri, 6 Dec 2019 13:05:57 -0800 Subject: [PATCH 12/16] Update onnx export test. TypeConverter, MeanVarianceScaler, MinMaxScaler no longer require experimental flag. --- src/python/tests_extended/test_export_to_onnx.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/python/tests_extended/test_export_to_onnx.py b/src/python/tests_extended/test_export_to_onnx.py index 6dd54606..6e8375a9 100644 --- a/src/python/tests_extended/test_export_to_onnx.py +++ b/src/python/tests_extended/test_export_to_onnx.py @@ -244,9 +244,6 @@ } REQUIRES_EXPERIMENTAL = { - 'TypeConverter', - 'MeanVarianceScaler', - 'MinMaxScaler' } SUPPORTED_ESTIMATORS = { From a988faf96245de44a9c9a03d6bb6c7ff1eabfd48 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Fri, 6 Dec 2019 13:36:47 -0800 Subject: [PATCH 13/16] Pretty print the output of test_export_to_onnx. --- src/python/tests_extended/test_export_to_onnx.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/python/tests_extended/test_export_to_onnx.py b/src/python/tests_extended/test_export_to_onnx.py index 6e8375a9..c2feed7f 100644 --- a/src/python/tests_extended/test_export_to_onnx.py +++ b/src/python/tests_extended/test_export_to_onnx.py @@ -13,6 +13,7 @@ import tempfile import numpy as np import pandas as pd +import pprint from nimbusml import Pipeline from nimbusml.cluster import KMeansPlusPlus @@ -393,10 +394,17 @@ def test_export_to_onnx(estimator, class_name): unexportable_estimators.add(class_name) print('Estimator could NOT be exported to ONNX.') -print('\nThe following estimators were skipped: ', sorted(SKIP)) -print('\nThe following estimators were successfully exported to ONNX: ', sorted(exportable_estimators)) -print('\nThe following estimators were successfully exported to experimental ONNX: ', sorted(exportable_experimental_estimators)) -print('\nThe following estimators could not be exported to ONNX: ', sorted(unexportable_estimators)) +print('\nThe following estimators were skipped: ') +pprint.pprint(sorted(SKIP)) + +print('\nThe following estimators were successfully exported to ONNX:') +pprint.pprint(sorted(exportable_estimators)) + +print('\nThe following estimators were successfully exported to experimental ONNX: ') +pprint.pprint(sorted(exportable_experimental_estimators)) + +print('\nThe following estimators could not be exported to ONNX: ') +pprint.pprint(sorted(unexportable_estimators)) failed_estimators = SUPPORTED_ESTIMATORS.difference(exportable_estimators) \ .difference(exportable_experimental_estimators) From 5dc5a91cfaef999d2c1545289dfcdf50cd5ace18 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Fri, 27 Dec 2019 15:52:46 -0800 Subject: [PATCH 14/16] Update to the latest version of ML.Net. --- src/DotNetBridge/DotNetBridge.csproj | 24 ++++++++++++------------ src/Platforms/build.csproj | 24 ++++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/DotNetBridge/DotNetBridge.csproj b/src/DotNetBridge/DotNetBridge.csproj index 81022313..97408c6c 100644 --- a/src/DotNetBridge/DotNetBridge.csproj +++ b/src/DotNetBridge/DotNetBridge.csproj @@ -32,18 +32,18 @@ all runtime; build; native; contentfiles; analyzers - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/Platforms/build.csproj b/src/Platforms/build.csproj index 7dc0c4a2..c2a0cf40 100644 --- a/src/Platforms/build.csproj +++ b/src/Platforms/build.csproj @@ -11,18 +11,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + From 301894e8b5f0930931eef99bcb30bd86b7b79952 Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Mon, 30 Dec 2019 16:34:33 -0800 Subject: [PATCH 15/16] Update supported estimators in test_export_to_onnx. --- .../tests_extended/test_export_to_onnx.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/python/tests_extended/test_export_to_onnx.py b/src/python/tests_extended/test_export_to_onnx.py index c2feed7f..3cf5fdb4 100644 --- a/src/python/tests_extended/test_export_to_onnx.py +++ b/src/python/tests_extended/test_export_to_onnx.py @@ -249,10 +249,32 @@ SUPPORTED_ESTIMATORS = { 'ColumnConcatenator', - 'OneHotVectorizer', + 'ColumnDuplicator', + 'CountSelector', + 'EnsembleClassifier', + 'EnsembleRegressor', + 'FastForestRegressor', + 'FastLinearRegressor', + 'FastTreesRegressor', + 'FastTreesTweedieRegressor', + 'GamRegressor', + 'Indicator', + 'KMeansPlusPlus', + 'LightGbmBinaryClassifier', + 'LightGbmClassifier', + 'LightGbmRegressor', + 'LpScaler', 'MeanVarianceScaler', 'MinMaxScaler', - 'TypeConverter' + 'NaiveBayesClassifier', + 'OneHotVectorizer', + 'OnlineGradientDescentRegressor', + 'OrdinaryLeastSquaresRegressor', + 'PcaAnomalyDetector', + 'PoissonRegressionRegressor', + 'PrefixColumnConcatenator', + 'TypeConverter', + 'WordTokenizer' } From 79cb2b59a97571eae35877c4bd5a6dd8ca1d8e6f Mon Sep 17 00:00:00 2001 From: "pieths.dev@gmail.com" Date: Mon, 27 Jan 2020 20:28:06 -0800 Subject: [PATCH 16/16] Use the latest nightly builds for the ML.Net packages. --- nuget.config | 1 + src/DotNetBridge/DotNetBridge.csproj | 24 +++++++++---------- src/Platforms/build.csproj | 24 +++++++++---------- .../internal/core/timeseries/ssaforecaster.py | 2 +- ...iesprocessingentrypoints_ssaforecasting.py | 2 +- ...eneralizedadditivemodelbinaryclassifier.py | 2 +- ...iners_generalizedadditivemodelregressor.py | 2 +- .../trainers_logisticregressionclassifier.py | 2 +- .../transforms_missingvaluehandler.py | 2 +- .../nimbusml/timeseries/ssaforecaster.py | 2 +- src/python/tools/manifest.json | 10 ++++---- 11 files changed, 37 insertions(+), 36 deletions(-) diff --git a/nuget.config b/nuget.config index cedba361..c0efdcaa 100644 --- a/nuget.config +++ b/nuget.config @@ -5,6 +5,7 @@ + diff --git a/src/DotNetBridge/DotNetBridge.csproj b/src/DotNetBridge/DotNetBridge.csproj index 274183cc..77fb025c 100644 --- a/src/DotNetBridge/DotNetBridge.csproj +++ b/src/DotNetBridge/DotNetBridge.csproj @@ -32,18 +32,18 @@ all runtime; build; native; contentfiles; analyzers - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/Platforms/build.csproj b/src/Platforms/build.csproj index c2a0cf40..8d8642a0 100644 --- a/src/Platforms/build.csproj +++ b/src/Platforms/build.csproj @@ -11,18 +11,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/python/nimbusml/internal/core/timeseries/ssaforecaster.py b/src/python/nimbusml/internal/core/timeseries/ssaforecaster.py index ce9064b5..f1ee5f6b 100644 --- a/src/python/nimbusml/internal/core/timeseries/ssaforecaster.py +++ b/src/python/nimbusml/internal/core/timeseries/ssaforecaster.py @@ -38,7 +38,7 @@ class SsaForecaster(BasePipelineItem, DefaultSignature): :param series_length: The length of series that is kept in buffer for modeling (parameter N). - :param train_size: The length of series from the begining used for + :param train_size: The length of series from the beginning used for training. :param horizon: The number of values to forecast. diff --git a/src/python/nimbusml/internal/entrypoints/timeseriesprocessingentrypoints_ssaforecasting.py b/src/python/nimbusml/internal/entrypoints/timeseriesprocessingentrypoints_ssaforecasting.py index f02da3a7..1684783c 100644 --- a/src/python/nimbusml/internal/entrypoints/timeseriesprocessingentrypoints_ssaforecasting.py +++ b/src/python/nimbusml/internal/entrypoints/timeseriesprocessingentrypoints_ssaforecasting.py @@ -43,7 +43,7 @@ def timeseriesprocessingentrypoints_ssaforecasting( building the trajectory matrix (parameter L). (inputs). :param series_length: The length of series that is kept in buffer for modeling (parameter N). (inputs). - :param train_size: The length of series from the begining used + :param train_size: The length of series from the beginning used for training. (inputs). :param horizon: The number of values to forecast. (inputs). :param confidence_level: The confidence level in [0, 1) for diff --git a/src/python/nimbusml/internal/entrypoints/trainers_generalizedadditivemodelbinaryclassifier.py b/src/python/nimbusml/internal/entrypoints/trainers_generalizedadditivemodelbinaryclassifier.py index e5b62a23..5c281338 100644 --- a/src/python/nimbusml/internal/entrypoints/trainers_generalizedadditivemodelbinaryclassifier.py +++ b/src/python/nimbusml/internal/entrypoints/trainers_generalizedadditivemodelbinaryclassifier.py @@ -36,7 +36,7 @@ def trainers_generalizedadditivemodelbinaryclassifier( **Description** Trains a gradient boosted stump per feature, on all features simultaneously, to fit target values using least-squares. It - mantains no interactions between features. + maintains no interactions between features. :param number_of_iterations: Total number of iterations over all features (inputs). diff --git a/src/python/nimbusml/internal/entrypoints/trainers_generalizedadditivemodelregressor.py b/src/python/nimbusml/internal/entrypoints/trainers_generalizedadditivemodelregressor.py index 1c56a706..2b9334f8 100644 --- a/src/python/nimbusml/internal/entrypoints/trainers_generalizedadditivemodelregressor.py +++ b/src/python/nimbusml/internal/entrypoints/trainers_generalizedadditivemodelregressor.py @@ -36,7 +36,7 @@ def trainers_generalizedadditivemodelregressor( **Description** Trains a gradient boosted stump per feature, on all features simultaneously, to fit target values using least-squares. It - mantains no interactions between features. + maintains no interactions between features. :param number_of_iterations: Total number of iterations over all features (inputs). diff --git a/src/python/nimbusml/internal/entrypoints/trainers_logisticregressionclassifier.py b/src/python/nimbusml/internal/entrypoints/trainers_logisticregressionclassifier.py index 5db498b1..61759e4d 100644 --- a/src/python/nimbusml/internal/entrypoints/trainers_logisticregressionclassifier.py +++ b/src/python/nimbusml/internal/entrypoints/trainers_logisticregressionclassifier.py @@ -33,7 +33,7 @@ def trainers_logisticregressionclassifier( **params): """ **Description** - Maximum entrypy classification is a method in statistics used to + Maximum entropy classification is a method in statistics used to predict the probabilities of parallel events. The model predicts the probabilities of parallel events by fitting data to a softmax function. diff --git a/src/python/nimbusml/internal/entrypoints/transforms_missingvaluehandler.py b/src/python/nimbusml/internal/entrypoints/transforms_missingvaluehandler.py index 1f1a3870..121115b4 100644 --- a/src/python/nimbusml/internal/entrypoints/transforms_missingvaluehandler.py +++ b/src/python/nimbusml/internal/entrypoints/transforms_missingvaluehandler.py @@ -21,7 +21,7 @@ def transforms_missingvaluehandler( **Description** Handle missing values by replacing them with either the default value or the mean/min/max value (for non-text columns only). An - indicator column can optionally be concatenated, if theinput + indicator column can optionally be concatenated, if the input column type is numeric. :param column: New column definition(s) (optional form: diff --git a/src/python/nimbusml/timeseries/ssaforecaster.py b/src/python/nimbusml/timeseries/ssaforecaster.py index 3cbe540f..35516d15 100644 --- a/src/python/nimbusml/timeseries/ssaforecaster.py +++ b/src/python/nimbusml/timeseries/ssaforecaster.py @@ -41,7 +41,7 @@ class SsaForecaster(core, BaseTransform, TransformerMixin): :param series_length: The length of series that is kept in buffer for modeling (parameter N). - :param train_size: The length of series from the begining used for + :param train_size: The length of series from the beginning used for training. :param horizon: The number of values to forecast. diff --git a/src/python/tools/manifest.json b/src/python/tools/manifest.json index a4236688..a1a8b3a5 100644 --- a/src/python/tools/manifest.json +++ b/src/python/tools/manifest.json @@ -4178,7 +4178,7 @@ { "Name": "TrainSize", "Type": "Int", - "Desc": "The length of series from the begining used for training.", + "Desc": "The length of series from the beginning used for training.", "Required": true, "SortOrder": 2.0, "IsNullable": false, @@ -10615,7 +10615,7 @@ }, { "Name": "Trainers.GeneralizedAdditiveModelBinaryClassifier", - "Desc": "Trains a gradient boosted stump per feature, on all features simultaneously, to fit target values using least-squares. It mantains no interactions between features.", + "Desc": "Trains a gradient boosted stump per feature, on all features simultaneously, to fit target values using least-squares. It maintains no interactions between features.", "FriendlyName": "Generalized Additive Model for Binary Classification", "ShortName": "gam", "Inputs": [ @@ -10915,7 +10915,7 @@ }, { "Name": "Trainers.GeneralizedAdditiveModelRegressor", - "Desc": "Trains a gradient boosted stump per feature, on all features simultaneously, to fit target values using least-squares. It mantains no interactions between features.", + "Desc": "Trains a gradient boosted stump per feature, on all features simultaneously, to fit target values using least-squares. It maintains no interactions between features.", "FriendlyName": "Generalized Additive Model for Regression", "ShortName": "gamr", "Inputs": [ @@ -13936,7 +13936,7 @@ }, { "Name": "Trainers.LogisticRegressionClassifier", - "Desc": "Maximum entrypy classification is a method in statistics used to predict the probabilities of parallel events. The model predicts the probabilities of parallel events by fitting data to a softmax function.", + "Desc": "Maximum entropy classification is a method in statistics used to predict the probabilities of parallel events. The model predicts the probabilities of parallel events by fitting data to a softmax function.", "FriendlyName": "Multi-class Logistic Regression", "ShortName": "mlr", "Inputs": [ @@ -20834,7 +20834,7 @@ }, { "Name": "Transforms.MissingValueHandler", - "Desc": "Handle missing values by replacing them with either the default value or the mean/min/max value (for non-text columns only). An indicator column can optionally be concatenated, if theinput column type is numeric.", + "Desc": "Handle missing values by replacing them with either the default value or the mean/min/max value (for non-text columns only). An indicator column can optionally be concatenated, if the input column type is numeric.", "FriendlyName": "NA Handle Transform", "ShortName": "NAHandle", "Inputs": [