From 2b7f4471e764dd4b1a924837b6f515ad7e8a294a Mon Sep 17 00:00:00 2001 From: Gitesh Dawer Date: Wed, 18 Sep 2019 16:13:10 -0700 Subject: [PATCH] This PR includes: correcting conversion function of log_sum_exp Integrate flexible utils in the converter + support for optional inputs + bug fixes bug fixes in ssa_converter + shaper Fixing type inference pass for slice, equal & notequal + bunch of fixes in shapers for multiple layers --- coremltools/converters/nnssa/coreml/shapes.py | 14 +- .../converters/nnssa/coreml/ssa_converter.py | 204 ++++++++++++------ .../frontend/graph_pass/type_inference.py | 59 +++-- .../converters/tensorflow/_tf_converter.py | 14 +- .../converters/tensorflow/test/test_base.py | 41 ++-- .../tensorflow/test/test_custom_layer.py | 2 +- coremltools/test/test_numpy_nn_layers.py | 22 +- 7 files changed, 240 insertions(+), 116 deletions(-) diff --git a/coremltools/converters/nnssa/coreml/shapes.py b/coremltools/converters/nnssa/coreml/shapes.py index 651905308..ad1ff3949 100644 --- a/coremltools/converters/nnssa/coreml/shapes.py +++ b/coremltools/converters/nnssa/coreml/shapes.py @@ -34,6 +34,8 @@ def _slice_static(layer_spec, input_shapes): begin = 0 if params.beginMasks[idx] else begin_indices[idx] end = dim if params.endMasks[idx] else end_indices[idx] output_shape[idx] = (end - begin) // params.strides[idx] + if (end - begin) % params.strides[idx] != 0: + output_shape[idx] += 1 return [output_shape] @@ -45,6 +47,8 @@ def _slice_dynamic(layer_spec, input_shapes): def _squeeze(layer_spec, input_shapes): + if layer_spec.squeeze.squeezeAll: + return [[1]] axes = list(layer_spec.squeeze.axes) input_shape = input_shapes[0] rank = len(input_shape) @@ -56,7 +60,7 @@ def _squeeze(layer_spec, input_shapes): for dim in range(rank): if dim not in axes: output_shape.append(input_shape[dim]) - elif input_shape[dim] != 1: + elif input_shape[dim] > 0 and input_shape[dim] != 1: raise ValueError( '[Shaper] Cannot squeeze on index %d of shape %s' % (dim, str(input_shape))) return [output_shape] if output_shape else [[1]] @@ -319,6 +323,9 @@ def _reduce_general(params, input_shapes): return [output_shape] if output_shape else [[1]] +def _reduce_logsumexp(layer_spec, input_shapes): + return _reduce_general(layer_spec.reduceLogSumExp, input_shapes) + def _reduce_prod(layer_spec, input_shapes): return _reduce_general(layer_spec.reduceProd, input_shapes) @@ -355,7 +362,7 @@ def _argmax(layer_spec, input_shapes): def _argmin(layer_spec, input_shapes): - params = layer_spec.argMax + params = layer_spec.argMin axis = params.axis keepdims = not params.removeDim @@ -427,7 +434,7 @@ def _reorganize_data(layer_spec, input_shapes): elif 'DepthToSpace' in layer_spec.name or 'BatchToSpaceND' in layer_spec.name: output_shape[2] *= block_size output_shape[3] *= block_size - output_shape[1] = input_shape[2] // (block_size * block_size) + output_shape[1] = input_shape[1] // (block_size * block_size) return [output_shape] @@ -490,6 +497,7 @@ def _reorganize_data(layer_spec, input_shapes): 'reduce': _reduce, 'argMax': _argmax, 'argMin': _argmin, + 'reduceLogSumExp': _reduce_logsumexp, 'reduceProd': _reduce_prod, 'reduceMean': _reduce_mean, 'reduceSum': _reduce_sum, diff --git a/coremltools/converters/nnssa/coreml/ssa_converter.py b/coremltools/converters/nnssa/coreml/ssa_converter.py index 893b49a63..1f47f8e5e 100644 --- a/coremltools/converters/nnssa/coreml/ssa_converter.py +++ b/coremltools/converters/nnssa/coreml/ssa_converter.py @@ -3,8 +3,9 @@ from six import string_types as _string_types from coremltools.models import datatypes -from coremltools.proto import NeuralNetwork_pb2 +from coremltools.proto import NeuralNetwork_pb2, Model_pb2 from coremltools.models.neural_network import NeuralNetworkBuilder +from coremltools.models.neural_network.flexible_shape_utils import set_multiarray_ndshape_range from collections import Iterable import coremltools @@ -44,7 +45,10 @@ def ssa_convert(ssa, predicted_probabilities_output='', add_custom_layers=False, custom_conversion_functions={}, - custom_shape_functions={}): + custom_shape_functions={}, + optional_inputs = [] + ): + """ Convert NNSSA into CoreML spec. ssa : NetworkEnsemble @@ -52,7 +56,7 @@ def ssa_convert(ssa, NNSSA to be converted to CoreML spec. top_func : str or 'main' Function entry point - inputs : list of str or None + inputs : dict of str -> list/tuple or None Input features of CoreML specs. Must be a dictionary with name as key and shape as value {name: shape}, where name is the input's name, shape is the @@ -130,7 +134,9 @@ def ssa_convert(ssa, neural_network_type=neural_network_type, add_custom_layers=add_custom_layers, custom_conversion_functions=custom_conversion_functions, - custom_shape_functions=custom_shape_functions) + custom_shape_functions=custom_shape_functions, + optional_inputs = optional_inputs) + converter.convert() builder = converter._get_builder(func=top_func) @@ -176,6 +182,29 @@ def ssa_convert(ssa, mlmodel_spec = converter.get_spec() + + # Required if an output node produces multiple outputs + # Generate new output features + modified_output_features_list = [] + for idx, output_feature in enumerate(mlmodel_spec.description.output): + if output_feature.name in converter.op_tensor_map: + atype = mlmodel_spec.description.output[idx].type + for aname in converter.op_tensor_map[output_feature.name]: + new_feature = Model_pb2.FeatureDescription() + new_feature.name = aname + new_feature.type.CopyFrom(atype) + if aname not in [feature.name for feature in modified_output_features_list]: + modified_output_features_list.append(new_feature) + else: + modified_output_features_list.append(output_feature) + + + # delete the existing output feature + mlmodel_spec.description.ClearField('output') + + # creating new output features description + mlmodel_spec.description.output.extend(modified_output_features_list) + # MLModel passes mlmodel_passes = [remove_disconnected_constants] for p in mlmodel_passes: @@ -192,12 +221,13 @@ class SSAConverter(object): def __init__(self, net_ensemble, # type: NetworkEnsemble top_func='main', # type: str - inputs=None, # type: List[str] + inputs=None, # type: Dict[str, tuple] outputs=None, # type: List[str] neural_network_type=None, # type: str add_custom_layers=False, # type: bool custom_conversion_functions={}, # type: Dict[Text, Any] - custom_shape_functions={} # type: Dict[Text, Any] + custom_shape_functions={}, # type: Dict[Text, Any] + optional_inputs = [] # type: List[str] ): self.net_ensemble = net_ensemble self.top_func = top_func # string indicating the top level function @@ -234,25 +264,24 @@ def __init__(self, if name not in inputs: raise ValueError( 'Input "%s" is required by SSAConverter, but not passed in argument "inputs"' % name) - if not shapes.is_static_shape(inputs[name]): - raise ValueError( - 'Supplied input "%s" has non-static shape %s' % (name, inputs[name])) - # Now that inputs[name] is deterministic, check whether it's a match for node's shape - if not shapes.is_a_shape_of(inputs[name], shape): + if shapes.is_static_shape(inputs[name]) and not shapes.is_a_shape_of(inputs[name], shape): raise ValueError( 'Input "%s" expects a shape compatible to %s, but is given %s' % (name, str(shape), inputs[name])) # Now that we can use the shape to create top_input_shapes shape = inputs[name] if inputs[name] else [1, ] - else: - # If input is None, use whatever there is - if not shapes.is_static_shape(shape): - raise ValueError( - 'NNSSA input "%s" has non-static shape %s, please provide in argument "inputs"' - % (name, str(shape))) top_input_shapes.append(shape) - top_input_types = [datatypes.Array(*dim) for dim in top_input_shapes] + top_input_types = [] + is_input_optional = [True if name in optional_inputs else False for name in top_input_names] + is_input_dynamic = [True if not shapes.is_static_shape(shape) else False for shape in top_input_shapes] + for idx, dims in enumerate(top_input_shapes): + if is_input_dynamic[idx]: + static_shape = [dim_size if dim_size > 0 else 1 for dim_size in dims] + else: + static_shape = dims + + top_input_types.append(datatypes.Array(*static_shape)) top_input_features = list(zip(top_input_names, top_input_types)) # TODO - verify outputs @@ -279,6 +308,23 @@ def __init__(self, self.spec = self.top_builder.spec + for idx, input in enumerate(self.spec.description.input): + if is_input_dynamic[idx]: + input_name = top_input_names[idx] + dynamic_shape = top_input_shapes[idx] + lower_bounds, upper_bounds = [], [] + for dim_size in dynamic_shape: + if dim_size > 0: + lower_bounds.append(dim_size) + upper_bounds.append(dim_size) + else: + lower_bounds.append(1) + upper_bounds.append(-1) + set_multiarray_ndshape_range(self.spec, input_name, lower_bounds=lower_bounds, upper_bounds=upper_bounds) + + if is_input_optional[idx]: + self.spec.description.input[idx].type.isOptional = True + self.CONVERT_FUNCTION_MAP = { 'Placeholder': self._convert_input, 'Const': self._convert_const, @@ -395,6 +441,7 @@ def __init__(self, 'BatchToSpaceND': self._convert_batch_to_space_nd, 'BatchNorm': self._convert_batchnorm, 'LRN': self._convert_lrn, + 'ClipByValue': self._convert_clip, } # converter state variables @@ -511,6 +558,11 @@ def _get_tensor_shape_from_type(self, type_): shape = (1,) elif builtins.is_tensor(type_): shape = type_.get_shape() + elif builtins.is_list(type_): + element_shape = type_.T[0].get_shape() + for ashape in type_.T: + assert ashape.get_shape() == element_shape + shape = [-1] + list(element_shape) else: shape = None return shape @@ -686,14 +738,12 @@ def _convert_slice(self, node): builder = self._get_builder() - # For simple RNN, node.attr always has a 'slice' - # This means slicing is always static + rank = len(self._get_tensor_shape_from_type(input_types[0])) + begin_masks = [True if i in node.attr['begin_masks'] else False for i in range(rank)] + end_masks = [True if i in node.attr['end_masks'] else False for i in range(rank)] if 'slice' not in node.attr: assert node.attr["new_axis_mask"] == 0 assert len(input_names) >= 4 - rank = len(self._get_tensor_shape_from_type(input_nodes[0].datatype)) - begin_masks = [True if i in node.attr['begin_masks'] else False for i in range(rank)] - end_masks = [True if i in node.attr['end_masks'] else False for i in range(rank)] layer = builder.add_slice_dynamic(name=slice_output_name, input_names=input_names[:4], output_name=slice_output_name, @@ -706,6 +756,8 @@ def _convert_slice(self, node): shapes.propagate_single_layer(layer, self.tensor_shapes) else: + # For simple RNN, node.attr always has a 'slice' + # This means slicing is always static # each slice is [begin, end, step] slices = node.attr['slice'] begin_indices, end_indices, strides = [], [], [] @@ -721,18 +773,21 @@ def _convert_slice(self, node): begin_ids=begin_indices, end_ids=end_indices, strides=strides, - begin_masks=[False] * len(slices), - end_masks=[True if id == 2147483647 else False for id in - end_indices]) # NNSSA uses 2147483647 to include all the remaining elements from that dimension + begin_masks=begin_masks, + end_masks=end_masks) shapes.propagate_single_layer(layer, self.tensor_shapes) if has_squeeze: + input_shape = self._get_tensor_shape_from_type(input_types[0]) + input_rank = len(input_shape) + squeeze_all = (input_rank == len(axes)) layer = builder.add_squeeze( name=node.name, input_name=slice_output_name, output_name=node.name, - axes=axes) + axes= axes if not squeeze_all else None, + squeeze_all = squeeze_all) shapes.propagate_single_layer(layer, self.tensor_shapes) def _convert_range(self, node): @@ -782,24 +837,26 @@ def _convert_tensorarray_alloc(self, node): shapes.propagate_single_layer(layer, self.tensor_shapes) elif has_static_element_shape: # Load element shape into network - node_es_name = node.name + '__element_shape' builder = self._get_builder() - layer = builder.add_load_constant_nd( - name=node_es_name, - output_name=node_es_name, - constant_value=np.array(element_shape, dtype='float'), - shape=[len(element_shape)]) - shapes.propagate_single_layer(layer, self.tensor_shapes) + if element_shape: + node_es_name = node.name + '__element_shape' + layer = builder.add_load_constant_nd( + name=node_es_name, + output_name=node_es_name, + constant_value=np.array(element_shape, dtype='float'), + shape=[len(element_shape)]) + shapes.propagate_single_layer(layer, self.tensor_shapes) - # Concatenate list length (the input, should be a constant vector - # of size 1) with element shape - node_arr_shape_name = node.name + '__arr_shape' - layer = builder.add_concat_nd( - name=node_arr_shape_name, - input_names=input_names + [node_es_name], - output_name=node_arr_shape_name, - axis=0) - shapes.propagate_single_layer(layer, self.tensor_shapes) + # Concatenate list length (the input, should be a constant vector of size 1) with element shape + node_arr_shape_name = node.name + '__arr_shape' + layer = builder.add_concat_nd( + name=node_arr_shape_name, + input_names=input_names + [node_es_name], + output_name=node_arr_shape_name, + axis=0) + shapes.propagate_single_layer(layer, self.tensor_shapes) + else: + node_arr_shape_name = input_names[0] # Now allocate required shape layer = builder.add_fill_dynamic( @@ -926,16 +983,17 @@ def _convert_set_global(self, node): output_name = node.attr["variable"] builder = self._get_builder() - layer = builder.add_copy(name=node.name, - input_name=input_names[0], - output_name=output_name) - - shapes.propagate_single_layer(layer, self.tensor_shapes) if len(node.outputs) > 0: + self.op_tensor_map[node.name] = [input_names[0]] + + if input_nodes[0].op == "Const" and input_nodes[0].value.val.size == 0: + return + + if output_name != input_names[0]: layer = builder.add_copy(name=node.name, input_name=input_names[0], - output_name=node.name) + output_name=output_name) shapes.propagate_single_layer(layer, self.tensor_shapes) @@ -1173,6 +1231,10 @@ def _convert_concat_nd(self, node): input_types = input_types if node.op == 'ConcatV2' else input_types[1:] input_names = [name for i, name in enumerate(input_names) if self._get_tensor_shape_from_type(input_types[i])[axis] != 0] + if len(input_names) == 1: + self.op_tensor_map[node.name] = input_names + return + if node.attr.get('data_format', None) == 'NHWC_format_inserted' and (axis == 1 or axis == -3): layer = self._get_builder().add_elementwise(node.name, input_names, node.name, 'CONCAT') else: @@ -1222,7 +1284,7 @@ def _convert_split(self, node): input_nodes, input_names, input_types = self._get_input_tensors(node) # Split output is a tuple. We need to split them into a list of tensors - output_names = [(node.name + '_' + str(i) + '_') for i in range(num_splits)] + output_names = [(node.name + '_' + str(i)) for i in range(num_splits)] if node.name in self.op_tensor_map: raise ValueError( '[SSAConverter] split node %s should not be visited twice.' % node.name) @@ -1252,7 +1314,6 @@ def _convert_split(self, node): def _convert_identity(self, node): input_nodes, input_names, input_types = self._get_input_tensors(node) - layer = self._get_builder().add_activation( name=node.name, non_linearity='LINEAR', @@ -1536,7 +1597,7 @@ def _convert_argmax(self, node): input_name=input_names[0], output_name=node.name, axis=axis, - keepdims=False) + keepdims=node.attr.get("keep_dims", False)) shapes.propagate_single_layer(layer, self.tensor_shapes) def _convert_argmin(self, node): @@ -1547,7 +1608,7 @@ def _convert_argmin(self, node): input_name=input_names[0], output_name=node.name, axis=axis, - keepdims=False) + keepdims=node.attr.get("keep_dims", False)) shapes.propagate_single_layer(layer, self.tensor_shapes) def _convert_reverse(self, node): @@ -1812,7 +1873,7 @@ def _convert_topk(self, node): input_nodes, input_names, input_types = self._get_input_tensors(node) k = input_nodes[1].value.val - output_names = [node.name, node.name + '_indices'] + output_names = [(node.name + '_' + str(i)) for i in range(2)] layer = self._get_builder().add_topk( name=node.name, input_names=[input_names[0]], @@ -1827,19 +1888,23 @@ def _convert_unary_log_softmax(self, node): assert len(node.inputs) == 1 input_nodes, input_names, input_types = self._get_input_tensors(node) axis = -1 if 'axis' not in node.attr else node.attr['axis'] - layer = self._get_builder().add_softmax_nd( - name=node.name + '_softmax', + + layer = self._get_builder().add_reduce_logsumexp( + name=node.name + "_logsumexp", input_name=input_names[0], - output_name=node.name + '_softmax', - axis=axis + output_name=node.name + "_logsumexp", + axes=[axis], + keepdims=True, + reduce_all=False ) shapes.propagate_single_layer(layer, self.tensor_shapes) - layer = self._get_builder().add_unary( + + layer = self._get_builder().add_subtract_broadcastable( name=node.name, - input_name=node.name + '_softmax', - output_name=node.name, - mode='log' + input_names=input_names + [node.name + "_logsumexp"], + output_name=node.name ) + shapes.propagate_single_layer(layer, self.tensor_shapes) def _convert_batchnorm(self, node): @@ -2357,3 +2422,18 @@ def _convert_lrn(self, node): k=bias ) shapes.propagate_single_layer(layer, self.tensor_shapes) + + def _convert_clip(self, node): + + input_nodes, input_names, input_types = self._get_input_tensors(node) + + min_value = input_nodes[1].value.val + max_value = input_nodes[2].value.val + + layer = self._get_builder().add_clip(name = node.name, + input_name=input_names[0], + output_name=node.name, + min_value=min_value, + max_value=max_value) + + self.tensor_shapes[node.name] = self._get_tensor_shape_from_type(node.datatype) diff --git a/coremltools/converters/nnssa/frontend/graph_pass/type_inference.py b/coremltools/converters/nnssa/frontend/graph_pass/type_inference.py index 6fab0b01c..b19cad191 100644 --- a/coremltools/converters/nnssa/frontend/graph_pass/type_inference.py +++ b/coremltools/converters/nnssa/frontend/graph_pass/type_inference.py @@ -177,7 +177,7 @@ def visit_elementwiseBinary(self, node): elif node.op == 'FloorDiv': node.attr['symbolic_value'] = rettype() node.attr['symbolic_value'].val = self.gdict[node.inputs[0]].attr[ - 'symbolic_value'].val / self.gdict[node.inputs[1]].attr['symbolic_value'].val + 'symbolic_value'].val // self.gdict[node.inputs[1]].attr['symbolic_value'].val elif node.op == 'RealDiv': node.attr['symbolic_value'] = rettype() node.attr['symbolic_value'].val = self.gdict[node.inputs[0]].attr[ @@ -186,6 +186,12 @@ def visit_elementwiseBinary(self, node): node.attr['symbolic_value'] = rettype() node.attr['symbolic_value'].val = sm.functions.Max(self.gdict[node.inputs[0]].attr['symbolic_value'].val, self.gdict[node.inputs[1]].attr['symbolic_value'].val) + elif node.op == 'Equal': + node.attr['symbolic_value'] = rettype() + node.attr['symbolic_value'].val = (vala.val == valb.val) + elif node.op == 'NotEqual': + node.attr['symbolic_value'] = rettype() + node.attr['symbolic_value'].val = (vala.val != valb.val) return rettype def visit_reduction_op(self, node): @@ -551,35 +557,10 @@ def visit_AvgPool(self, node): return self.visit_pooling(node) def visit_Equal(self, node): - assert (len(node.inputs) == 2) - inputtype = self.visit(node.inputs[0]) - self.visit(node.inputs[1]) - if not builtins.is_tensor(inputtype): - rettype = builtins.bool - else: - rettype = builtins.tensor(builtins.bool, inputtype.get_shape()) - - vala = self.gdict[node.inputs[0]].attr['symbolic_value'] - valb = self.gdict[node.inputs[1]].attr['symbolic_value'] - if vala is not None and valb is not None: - node.attr['symbolic_value'] = rettype() - node.attr['symbolic_value'].val = (vala.val == valb.val) - return rettype + return self.visit_broadcast_op(node) def visit_NotEqual(self, node): - assert (len(node.inputs) == 2) - inputtype = self.visit(node.inputs[0]) - self.visit(node.inputs[1]) - if not builtins.is_tensor(inputtype): - return builtins.bool - rettype = builtins.tensor(builtins.bool, inputtype.get_shape()) - - vala = self.gdict[node.inputs[0]].attr['symbolic_value'] - valb = self.gdict[node.inputs[1]].attr['symbolic_value'] - if vala is not None and valb is not None: - node.attr['symbolic_value'] = rettype() - node.attr['symbolic_value'].val = (vala.val == valb.val) - return rettype + return self.visit_broadcast_op(node) def visit_ExpandDims(self, node): assert (len(node.inputs) == 2) @@ -1203,6 +1184,8 @@ def visit_Slice(self, node): ] slices = [[int(begin[i]), int(end[i]), 1] for i in range(len(begin))] node.attr['slice'] = slices + node.attr['begin_masks'] = [idx for idx, value in enumerate(begin) if value == 0] + node.attr['end_masks'] = [idx for idx, value in enumerate(end) if value == 2147483647] output_value = None if input_value is not None: slices = [slice(*i) for i in slices] @@ -1511,11 +1494,13 @@ def visit_StridedSlice(self, node): # if we have a complete value, we can force it slicesv = [[begin[i], end[i], stride_value.val[i]] for i in range(len(begin))] - for s in slicesv: + for idx, s in enumerate(slicesv): if s[0] is None: s[0] = 0 + begin_mask.append(idx) if s[1] is None: s[1] = 2147483647 + end_mask.append(idx) if s[2] is None: s[2] = 1 s[0] = int(s[0]) @@ -1524,8 +1509,14 @@ def visit_StridedSlice(self, node): # insert missing slices for i in range(len(slicesv), len(input_shape)): slicesv.append([0, 2147483647, 1]) + if i not in begin_mask: + begin_mask.append(i) + if i not in end_mask: + end_mask.append(i) node.attr['slice'] = slicesv node.attr['squeeze'] = list(int(i) for i in shrink_axes) + node.attr['begin_masks'] = list(int(i) for i in begin_mask) + node.attr['end_masks'] = list(int(i) for i in end_mask) if isscalar(res): rettype = input_type.get_primitive() output_value = rettype() @@ -1615,11 +1606,13 @@ def visit_StridedSlice(self, node): retshape.append(thisslicelen) slices = [[begin[i], end[i], stride_value[i]] for i in range(len(begin))] has_symbolic_slices = False - for s in slices: + for idx, s in enumerate(slices): if s[0] is None: s[0] = 0 + begin_mask.append(idx) if s[1] is None: s[1] = 2147483647 + end_mask.append(idx) if s[2] is None: s[2] = 1 try: @@ -1641,6 +1634,11 @@ def visit_StridedSlice(self, node): for i in range(len(slices), len(input_shape)): slices.append([0, 2147483647, 1]) retshape.append(input_shape[i]) + if i not in begin_mask: + begin_mask.append(i) + if i not in end_mask: + end_mask.append(i) + if not has_symbolic_slices: node.attr['slice'] = slices node.attr['squeeze'] = list(int(i) for i in shrink_axes) @@ -1654,6 +1652,7 @@ def visit_StridedSlice(self, node): rettype = input_type.get_primitive() else: rettype = builtins.tensor(input_type.get_primitive(), retshape) + node.attr['symbolic_value'] = output_value return rettype diff --git a/coremltools/converters/tensorflow/_tf_converter.py b/coremltools/converters/tensorflow/_tf_converter.py index 5f6019ac6..fb270cbdb 100644 --- a/coremltools/converters/tensorflow/_tf_converter.py +++ b/coremltools/converters/tensorflow/_tf_converter.py @@ -24,6 +24,13 @@ def convert(filename, custom_conversion_functions={}, # type: Dict[Text, Any] custom_shape_functions={}, # type: Dict[Text, Any] **kwargs): + + use_cpu_only = kwargs.get('use_cpu_only') + use_cpu_only = use_cpu_only if use_cpu_only is not None else False + + optional_inputs = kwargs.get('optional_inputs') + optional_inputs = optional_inputs if optional_inputs is not None else [] + if not filename or not isinstance(filename, str) or not os.path.exists(filename) or not os.path.isfile(filename): raise ValueError('invalid input tf_model_path: {}.'.format(filename)) @@ -55,10 +62,11 @@ def convert(filename, predicted_probabilities_output=predicted_probabilities_output, add_custom_layers=add_custom_layers, custom_conversion_functions=custom_conversion_functions, - custom_shape_functions=custom_shape_functions) + custom_shape_functions=custom_shape_functions, + optional_inputs = optional_inputs + ) except ImportError as err: raise ImportError("Backend converter not found! Error message:\n%s" % err) - use_cpu_only = kwargs.get('use_cpu_only') - use_cpu_only = use_cpu_only if use_cpu_only is not None else False + return MLModel(mlmodelspec, useCPUOnly=use_cpu_only) diff --git a/coremltools/converters/tensorflow/test/test_base.py b/coremltools/converters/tensorflow/test/test_base.py index 52bbce520..998e458bb 100644 --- a/coremltools/converters/tensorflow/test/test_base.py +++ b/coremltools/converters/tensorflow/test/test_base.py @@ -250,7 +250,10 @@ def _test_tf_model_constant( # initialize sess.run(tf.global_variables_initializer()) # run the result - fetches = [graph.get_operation_by_name(name).outputs[0] for name in output_node_names] + fetches = [] + for name in output_node_names: + fetches += graph.get_operation_by_name(name).outputs + result = sess.run(fetches, feed_dict=feed_dict) output_graph_def = tf.graph_util.convert_variables_to_constants( @@ -285,21 +288,27 @@ def _test_tf_model_constant( # Run predict in CoreML coreml_output = mlmodel.predict(coreml_inputs, useCPUOnly=use_cpu_only) - for idx, out_name in enumerate(output_node_names): - tf_out = result[idx] - if len(tf_out.shape) == 0: - tf_out = np.array([tf_out]) - tp = tf_out.flatten() - coreml_out = coreml_output[out_name] - cp = coreml_out.flatten() - - self.assertTrue(tf_out.shape == coreml_out.shape, msg=(tf_out.shape, 'vs.', coreml_out.shape)) - - if validate_bool_only: - cp = np.logical_and(cp, cp) - for i in range(len(tp)): - max_den = max(1.0, tp[i], cp[i]) - self.assertAlmostEqual(tp[i] / max_den, cp[i] / max_den, delta=delta) + idx = 0 + for node_name in output_node_names: + num_outputs = len(graph.get_operation_by_name(node_name).outputs) + for out_id in range(num_outputs): + tf_out = result[idx] + if len(tf_out.shape) == 0: + tf_out = np.array([tf_out]) + tp = tf_out.flatten() + out_name = node_name if num_outputs == 1 else node_name + '_' + str(out_id) + coreml_out = coreml_output[out_name] + cp = coreml_out.flatten() + + self.assertTrue(tf_out.shape == coreml_out.shape, msg=(tf_out.shape, 'vs.', coreml_out.shape)) + + if validate_bool_only: + cp = np.logical_and(cp, cp) + for i in range(len(tp)): + max_den = max(1.0, tp[i], cp[i]) + self.assertAlmostEqual(tp[i] / max_den, cp[i] / max_den, delta=delta) + + idx += 1 # Cleanup files - models on disk no longer useful if os.path.exists(model_dir): diff --git a/coremltools/converters/tensorflow/test/test_custom_layer.py b/coremltools/converters/tensorflow/test/test_custom_layer.py index ebaab037d..b17550663 100644 --- a/coremltools/converters/tensorflow/test/test_custom_layer.py +++ b/coremltools/converters/tensorflow/test/test_custom_layer.py @@ -159,4 +159,4 @@ def _shape_acos(layer_spec, input_shapes): self.assertEqual('Acos', layers[2].custom.className) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/coremltools/test/test_numpy_nn_layers.py b/coremltools/test/test_numpy_nn_layers.py index 76794863f..1f6d5d75b 100644 --- a/coremltools/test/test_numpy_nn_layers.py +++ b/coremltools/test/test_numpy_nn_layers.py @@ -972,6 +972,26 @@ def test_embedding_nd_half_precision_GPU(self): self.test_embedding_nd_cpu( model_precision=_MLMODEL_HALF_PRECISION, use_cpu_only=False) + def test_softmax_nan_bug_cpu(self): + input_shape = [2,2] + input_features = [('data', datatypes.Array(*input_shape))] + output_features = [('output', None)] + for axis in [0,1]: + builder = neural_network.NeuralNetworkBuilder( + input_features, output_features, + disable_rank5_shape_mapping=True) + + builder.add_softmax_nd(name='softmax_nd', input_name='data', + output_name='output', axis=axis) + + x = np.array([[0.5, 0.5],[1e8, 1e8]]) + input = {'data': x} + y = np.exp(x - np.max(x, axis=axis, keepdims=True)) + y = y / np.sum(y, axis=axis, keepdims=True) + expected = {'output': y} + + self._test_model(builder.spec, input, expected, useCPUOnly=True) + def test_softmax_nd_cpu(self, cpu_only=True): for rank in range(1, 6): for axis in range(-rank, rank): @@ -4546,6 +4566,6 @@ def test_power_iteration_cpu(self): if __name__ == '__main__': unittest.main() # suite = unittest.TestSuite() - # suite.addTest(NewLayersSimpleTest("test_nms_cpu")) + # suite.addTest(NewLayersSimpleTest("test_softmax_nan_bug_cpu")) # #suite.addTest(SimpleNetworkTest("test_power_iteration_cpu")) # unittest.TextTestRunner().run(suite)