From 4f4f4bcd924f403618470045c158a02ed5443cdf Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 6 Feb 2020 16:46:07 -0800 Subject: [PATCH] Handle dynamic shapes and reshape the input according to the model (#2986) Put type validation in separate method. --- .../tools/featurizer_ops/data_frame_tool.py | 72 +++++++++++++------ 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/onnxruntime/python/tools/featurizer_ops/data_frame_tool.py b/onnxruntime/python/tools/featurizer_ops/data_frame_tool.py index 3566860c2b..4903483dd9 100644 --- a/onnxruntime/python/tools/featurizer_ops/data_frame_tool.py +++ b/onnxruntime/python/tools/featurizer_ops/data_frame_tool.py @@ -45,42 +45,68 @@ class DataFrameTool(): self._sess_options = sess_options self._sess = onnxrt.InferenceSession(self._model_path, self._sess_options) + def _reshape_input(self, input_array, expected_shape): + """ + :param - input_array numpy array. This one is obtained from DataFrame and expected to have + : a rank if 1. + :expected_shape - shape fetched from the model which may include dynamic elements. + : expected_shape may at most have one -1, None or zero which will be computed from + : the size of the input_array. We replace None and zeros to -1 and let np.ndarray.reshape deal with it. + """ + # expected_shape rank is one, we will let onnxruntime to deal with it + if len(expected_shape) == 1: + return input_array + + inferred_shape = [dim if dim else -1 for dim in expected_shape] + return input_array.reshape(inferred_shape) + + def _validate_type(self, input_meta, col_type): + """ + : input_meta - meta info obtained from the model for the given input + : col_type - dtype of the column + : throws if conditions are not met + + float16 and bool will always require exact match + We attempt to convert any type to a string if it is required. + With strings we always want to put this into a flat array, cast to np.object and then reshape as object + Any other type to qualify for casting must match either integer or floating point types + """ + expected_type = types_dict[input_meta.type] + if input_meta.type == 'tensor(string)': + return + elif expected_type == col_type: + return + elif expected_type in ort_float_set and str(col_type) in pd_float_set: + return + elif expected_type in ort_int_set and str(col_type) in pd_int_set: + return + + raise TypeError("Input {} requires type {} unable to cast column type {} ".format( + input_meta.name, expected_type, col_type)) + + def _process_input_list(self, df, input_metas, require): """ Return a dictionary of input_name : a typed and shaped np.array of values for a given input_meta The function does the heavy lifting for _get_input_feeds() - :param df: See :class:`pandas.DataFrame`. + :param df: See :class:`pandas.DataFrame`. :param input_metas: a list of name/type pairs :require is a boolean. If True this helper throws on a missing input. - + """ feeds = {} # Process mandadory inputs. Raise an error if anything is not present for input_meta in input_metas: - shape = [dim if dim else 1 for dim in input_meta.shape] # We fully expect all the types are in the above dictionary assert input_meta.type in types_dict, "Update types_dict for the new type" if input_meta.name in df.columns: - expected_type = types_dict[input_meta.type] - # float16 and bool will always require exact match - # We attempt to convert any type to a string if it is required. - # With strings we always want to put this into a flat array, cast to np.object and then reshape as object - if input_meta.type == 'tensor(string)': - #print('Col: {} processed as string type: {} '.format(input_meta.name, df[input_meta.name].dtype)) - feeds[input_meta.name] = np.array([df[input_meta.name][0]]).astype(expected_type).reshape(shape) - elif expected_type == df[input_meta.name].dtype: # If there is an exact match we take as is - #print('Col: {} processed exact match type: {} '.format(input_meta.name, df[input_meta.name].dtype)) - feeds[input_meta.name] = np.array([df[input_meta.name][0]]).astype(expected_type).reshape(shape) - elif expected_type in ort_float_set and str(df[input_meta.name].dtype) in pd_float_set: - #print('Col: {} processed as floating type: {} '.format(input_meta.name, df[input_meta.name].dtype)) - feeds[input_meta.name] = np.array([df[input_meta.name][0]]).astype(expected_type).reshape(shape) - elif expected_type in ort_int_set and str(df[input_meta.name].dtype) in pd_int_set: - #print('Col: {} processed as integer type: {} '.format(input_meta.name, df[input_meta.name].dtype)) - feeds[input_meta.name] = np.array([df[input_meta.name][0]]).astype(expected_type).reshape(shape) - else: - raise TypeError("Input {} expected to be of type: {} got {} ".format( - input_meta.name, expected_type, df[input_meta.name].dtype)) + self._validate_type(input_meta, df[input_meta.name].dtype) + # With strings we must cast first to np.object then then reshape + # so we do it for everything + input_array = np.array(df[input_meta.name]).astype(types_dict[input_meta.type]) + feeds[input_meta.name] = self._reshape_input(input_array, input_meta.shape) + elif require: raise RuntimeError("This model requires input {} of type {} but it is not found in the DataFrame".format( input_meta.name, types_dict[input_meta.type])) @@ -102,7 +128,7 @@ class DataFrameTool(): and feeds the data to the appropriate model inputs. :param sess: See :class:`onnxruntime.InferenceSession`. - + :: For example: pd.DataFrame([[0], [4],[20]],index=[0], columns=['A', 'B', 'C'])