# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
###############################################################################
import warnings
import numpy as np
from onnx import helper, defs as onnx_defs, onnx_pb as onnx_proto
from onnx.mapping import NP_TYPE_TO_TENSOR_TYPE
DEFAULT_OPSET_NUMBER = 13 # The maximum opset supported by the converter in the code branch.
# From https://github.com/onnx/onnx/blob/master/docs/Versioning.md
OPSET_TO_IR_VERSION = {
1: 3, 2: 3, 3: 3, 4: 3, 5: 3, 6: 3,
7: 3, 8: 3, 9: 4, 10: 5, 11: 6, 12: 7,
13: 7, 14: 7, 15: 8, 16: 8, 17: 8
}
if hasattr(helper, 'VERSION_TABLE'):
OPSET_TO_IR_VERSION = {row[2]: row[1] for row in helper.VERSION_TABLE}
def _get_main_opset_version(model):
"""
Returns the main opset version.
"""
for op in model.opset_import:
if op.domain == '' or op.domain == 'ai.onnx':
return op.version
return None
def onnx_builtin_opset_version():
return onnx_defs.onnx_opset_version()
def get_maximum_opset_supported():
return min(DEFAULT_OPSET_NUMBER, onnx_builtin_opset_version())
def make_model_ex(graph, imported_opset_pairs, target_default_opset, **kwargs):
onnx_model = helper.make_model(graph, **kwargs)
# Merge operator sets for the same domain, the largest version number would be kept
purified_operator_set = dict()
for op_domain, op_version in imported_opset_pairs:
if op_domain not in purified_operator_set:
if op_domain == '' or op_domain == 'ai.onnx':
# Initializers are a subset of graph inputs for IR_VERSION <= 3 (target opset < 8).
# Need upgrade opv since initializers are separate for IR_VERSION >= 4 to pass onnx.checker.
if op_version < 8 and target_default_opset is not None and target_default_opset >= 8:
op_version = 8
purified_operator_set[op_domain] = op_version
else:
purified_operator_set[op_domain] = max(purified_operator_set[op_domain], op_version)
# Fill operator sets
i = 0
for op_domain, op_version in purified_operator_set.items():
if i == 0 and len(onnx_model.opset_import) == 1:
# Overwrite the default operator set created by helper.make_model(...)
op_set = onnx_model.opset_import[0]
else:
# Just create one ONNX element in opset_import
op_set = onnx_model.opset_import.add()
op_set.domain = op_domain
op_set.version = op_version
i += 1
if op_domain == '' or op_domain == 'ai.onnx':
if target_default_opset < op_version:
raise RuntimeError(('The specified opset %d is too low to convert this model, ' +
'which requires at least opset %d.') % (target_default_opset, op_version))
elif target_default_opset > op_version:
warnings.warn('The maximum opset needed by this model is only %d.' % op_version)
else:
pass
opv = _get_main_opset_version(onnx_model) or target_default_opset
irv = OPSET_TO_IR_VERSION.get(opv, onnx_proto.IR_VERSION)
onnx_model.ir_version = irv
return onnx_model
class _ONNXModelOperator:
def __init__(self, name, model, input, output):
self.name = name
self.model = model
self.input = input
self.output = output
def __repr__(self):
"""
without this method, it's too slow for the debugging.
:return:
"""
return "name: {}, input: {}, output: {}".format(self.name, self.input, self.output)
@property
def op_type(self):
return 'ModelOp'
class ONNXElementContainer:
opdict_counter = {}
def __init__(self, target_opset, parent=None):
"""
:param target_opset: number, for example, 7 for ONNX 1.2, and 8 for ONNX 1.3.
"""
self.inputs = []
self.outputs = []
self.initializers = []
self.value_info = []
self.nodes = []
self.node_domain_version_pair_sets = set()
self.target_opset = target_opset
self.enable_optimizer = True
self.parent = parent
# the following property make this container be compatible with onnx.GraphProto
@property
def initializer(self):
return self.initializers
@property
def input(self):
return self.inputs
@property
def output(self):
return self.outputs
@staticmethod
def _make_value_info(variable):
value_info = helper.ValueInfoProto()
value_info.name = variable.full_name
value_info.type.CopyFrom(variable.type.to_onnx_type())
if variable.type.doc_string:
value_info.doc_string = variable.type.doc_string
return value_info
def add_input(self, variable):
"""
Add our Variable object defined _parser.py into the the input list of the final ONNX model
:param variable: The Variable object to be added
"""
self.inputs.append(self._make_value_info(variable))
def add_output(self, variable):
"""1
Add our Variable object defined _parser.py into the the output list of the final ONNX model
:param variable: The Variable object to be added
"""
self.outputs.append(self._make_value_info(variable))
def add_initializer(self, name, onnx_type, shape, content):
"""
Add a TensorProto into the initializer list of the final ONNX model
:param name: Variable name in the produced ONNX model.
:param onnx_type: Element types allowed in ONNX tensor, e.g., TensorProto.FLOAT and TensorProto.STRING.
:param shape: Tensor shape, a list of integers.
:param content: Flattened tensor values (i.e., a float list or a float array).
"""
if any(d is None for d in shape):
raise ValueError('Shape of initializer cannot contain None')
tensor = helper.make_tensor(name, onnx_type, shape, content)
self.initializers.append(tensor)
def add_value_info(self, variable):
self.value_info.append(self._make_value_info(variable))
def add_node(self, op_type, inputs, outputs, op_domain='', op_version=1, **attrs):
"""
Add a NodeProto into the node list of the final ONNX model. If the input operator's domain-version information
cannot be found in our domain-version pool (a Python set), we may add it.
:param op_type: A string (e.g., Pool and Conv) indicating the type of the NodeProto
:param inputs: A list of strings. They are the input variables' names of the considered NodeProto
:param outputs: A list of strings. They are the output variables' names of the considered NodeProto
:param op_domain: The domain name (e.g., ai.onnx.ml) of the operator we are trying to add.
:param op_version: The version number (e.g., 0 and 1) of the operator we are trying to add.
:param attrs: A Python dictionary. Keys and values are attributes' names and attributes' values, respectively.
"""
if isinstance(inputs, str):
inputs = [inputs]
if isinstance(outputs, str):
outputs = [outputs]
if not isinstance(inputs, (list, tuple)) or not all(isinstance(s, str) for s in inputs):
type_list = ','.join(list(str(type(s)) for s in inputs))
raise ValueError('Inputs must be a list of string but get [%s]' % type_list)
if not isinstance(outputs, (list, tuple)) or not all(isinstance(s, str) for s in outputs):
type_list = ','.join(list(str(type(s)) for s in outputs))
raise ValueError('Outputs must be a list of string but get [%s]' % type_list)
for k, v in attrs.items():
if v is None:
raise ValueError('Failed to create ONNX node. Undefined attribute pair (%s, %s) found' % (k, v))
node = helper.make_node(op_type, inputs, outputs, **attrs)
node.domain = op_domain
self.node_domain_version_pair_sets.add((op_domain, op_version))
self.nodes.append(node)
def add_model_node(self, inputs, outputs, name, model):
self.nodes.append(_ONNXModelOperator(name=name, model=model, input=inputs, output=outputs))
@classmethod
def get_unique_operator_name(cls, op_type: str):
name = op_type.lower()
nn = cls.opdict_counter.get(name, 0)
cls.opdict_counter[name] = nn + 1
return name if nn == 0 else "{}_{}".format(name, nn+1)
def _create_name_or_use_existing_one(container, op_type, name):
return name or container.get_unique_operator_name(op_type)
class _OpSchema:
_ox = None # will be assigned by ONNXModelBuilder.
def __init__(self, *args, **kwargs):
# self.op_builder = None
self.apply_fn = args[0]
self.inputs = kwargs['inputs'] if 'inputs' in kwargs else []
self.outputs = kwargs['outputs'] if 'outputs' in kwargs else []
def __call__(self, *args, **kwargs):
assert self._ox is not None, 'no builder instance was created'
return self.apply_fn(self._ox, *args, **kwargs)
# def __get__(self, instance, owner):
# if owner.__name__ == '_ONNXModelBuilder':
# self.op_builder = instance
# return self
def schema(apply_fn=None, *args, **kwargs):
if apply_fn is None:
def wrapper(fn):
return _OpSchema(fn, *args, **kwargs)
return wrapper
else:
# used as a function.
return _OpSchema(apply_fn, *args, **kwargs)
class _ONNXOperatorAPI:
_dt = onnx_proto.TensorProto
def get_unique_tensor_name(self, base): pass # implemented by the model builder
def _apply_unary_operation(self, op_type, input_name, output_name, container, operator_name, **attrs):
name = _create_name_or_use_existing_one(container, op_type, operator_name)
attrs['name'] = name
if container.target_opset < 6:
attrs['consumed_inputs'] = [0]
op_version = 1
else:
op_version = 6
container.add_node(op_type, input_name, output_name, op_version=op_version, **attrs)
def _apply_basic_numerical_operation(self, op_type, input_names, output_name, container, operator_name,
axis, broadcast):
name = _create_name_or_use_existing_one(container, op_type, operator_name)
attrs = {}
if container.target_opset < 7:
# Before ONNX-1.2 (opset 7), broadcasting behavior is Caffe2-like.
if axis is not None:
attrs['axis'] = axis
if broadcast is not None:
attrs['broadcast'] = broadcast
if container.target_opset < 6:
attrs['consumed_inputs'] = [0, 0]
op_version = 1
else:
op_version = 6
else:
# Since ONNX-1.2 (opset 7), broadcasting behavior is Numpy-like, so we don't need to specify any attributes
op_version = 7
container.add_node(op_type, input_names, output_name, op_version=op_version, name=name, **attrs)
def _apply_pointwise_operation(self, op_type, input_names, output_name, container, operator_name):
name = _create_name_or_use_existing_one(container, op_type, operator_name)
attrs = {}
if container.target_opset < 6:
attrs['consumed_inputs'] = [0] * len(input_names)
op_version = 1
elif container.target_opset < 8:
op_version = 6
else:
if container.target_opset < 12 or op_type == 'Mean':
op_version = 8
else:
op_version = 12
container.add_node(op_type, input_names, output_name, op_version=op_version, name=name, **attrs)
def abs(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Abs', input_name, output_name, container, operator_name=operator_name)
return output_name
def add(self, input_names, output_name, container, operator_name=None, axis=None, broadcast=None):
self._apply_basic_numerical_operation('Add', input_names, output_name, container, operator_name=operator_name,
axis=axis, broadcast=broadcast)
return output_name
def argmax(self, input_name, output_name, container, operator_name=None, axis=0, keepdims=1,
select_last_index=0):
name = _create_name_or_use_existing_one(container, 'ArgMax', operator_name)
attrs = {'axis': axis, 'keepdims': keepdims}
if container.target_opset < 11:
op_version = 1
elif container.target_opset < 12:
op_version = 11
else:
op_version = 12
attrs['select_last_index'] = select_last_index
container.add_node('ArgMax', input_name, output_name, op_version=op_version, name=name, **attrs)
return output_name
def argmin(self, input_name, output_name, container, operator_name=None, axis=0, keepdims=1,
select_last_index=0):
name = _create_name_or_use_existing_one(container, 'ArgMin', operator_name)
attrs = {'axis': axis, 'keepdims': keepdims}
if container.target_opset < 11:
op_version = 1
elif container.target_opset < 12:
op_version = 11
else:
op_version = 12
attrs['select_last_index'] = select_last_index
container.add_node('ArgMin', input_name, output_name, op_version=op_version, name=name, **attrs)
return output_name
def affine(self, input_name, output_name, container, operator_name=None, alpha=1., beta=0.):
if container.target_opset < 9:
op_type = 'Affine'
name = _create_name_or_use_existing_one(container, 'Affine', operator_name)
attrs = {'name': name, 'alpha': alpha, 'beta': beta}
container.add_node(op_type, input_name, output_name, **attrs)
else:
name = _create_name_or_use_existing_one(container, 'Affine', operator_name)
# Define a and b.
aName = self.get_unique_tensor_name(name + '_alpha')
container.add_initializer(aName, onnx_proto.TensorProto.FLOAT, [1], [alpha])
bName = self.get_unique_tensor_name(name + '_beta')
container.add_initializer(bName, onnx_proto.TensorProto.FLOAT, [1], [beta])
# Compute Z = a * X, where X is the original input.
zName = self.get_unique_tensor_name(name + '_scaled')
self.mul([aName, input_name], zName, container)
# Compute Y = Z + b, where Y is the final output.
self.add(self, [zName, bName], output_name, container)
return output_name
def batch_norm(self, input_names, output_names, container, operator_name=None,
epsilon=None, is_test=None, momentum=None, spatial=None):
name = _create_name_or_use_existing_one(container, 'BatchNormalization', operator_name)
attrs = {'name': name, 'epsilon': epsilon, 'momentum': momentum}
if container.target_opset < 9:
attrs['spatial'] = spatial
if container.target_opset < 7:
attrs['is_test'] = is_test
if container.target_opset < 6:
attrs['consumed_inputs'] = [0] * len(input_names)
if len(input_names) > 3:
attrs['consumed_inputs'][3] = 1
if len(input_names) > 4:
attrs['consumed_inputs'][4] = 2
op_version = 1
elif container.target_opset < 7:
op_version = 6
elif container.target_opset < 9:
op_version = 7
else:
op_version = 9
container.add_node('BatchNormalization', input_names, output_names, op_version=op_version, **attrs)
return output_names
def cast(self, input_name, output_name, container, operator_name=None, to=None):
"""
:param to: enum defined in ONNX TensorProto.DataType, for example, TensorProto.FLOAT and TensorProto.INT64.
"""
name = _create_name_or_use_existing_one(container, 'Cast', operator_name)
attrs = {'name': name}
d = onnx_proto.TensorProto.DataType.DESCRIPTOR
allowed_type_name_and_type_enum_pairs = {v.number: k for k, v in d.values_by_name.items()}
if to not in allowed_type_name_and_type_enum_pairs:
raise ValueError('Attribute "to" must be one of %s' % allowed_type_name_and_type_enum_pairs.keys())
if container.target_opset < 9:
if to in [onnx_proto.TensorProto.STRING, onnx_proto.TensorProto.COMPLEX64, onnx_proto.TensorProto.COMPLEX128]:
raise ValueError('Attribute "to" cannot correspond to a String or Complex TensorProto type.')
if container.target_opset < 6:
# Convert enum to string, for example, TensorProto.INT64 to 'INT64'
attrs['to'] = allowed_type_name_and_type_enum_pairs[to]
op_version = 1
else:
# Enum, for example, TensorProto.INT64
attrs['to'] = to
op_version = 6
else:
# Enum value, for example, TensorProto.INT64
# String casting is supported in opset 9
if to in [onnx_proto.TensorProto.COMPLEX64, onnx_proto.TensorProto.COMPLEX128]:
raise ValueError('Attribute "to" cannot correspond to a Complex TensorProto type.')
attrs['to'] = to
op_version = 9
container.add_node('Cast', input_name, output_name, op_version=op_version, **attrs)
return output_name
def clip(self, input_name, output_name, container, operator_name=None, max=None, min=None):
name = _create_name_or_use_existing_one(container, 'Clip', operator_name)
attrs = {'name': name}
if container.target_opset < 11:
if max is not None:
attrs['max'] = float(max)
if min is not None:
attrs['min'] = float(min)
if container.target_opset < 6:
attrs['consumed_inputs'] = [0]
op_version = 1
else:
op_version = 6
container.add_node('Clip', input_name, output_name, op_version=op_version, **attrs)
else:
if container.target_opset < 12:
op_version = 11
else:
op_version = 12
if min is None and max is not None:
raise RuntimeError("Operator 'Clip': min must be specified if max is.")
inputs = [input_name]
if min is not None:
if isinstance(min, (np.ndarray, float, int)):
# add initializer
if isinstance(min, np.ndarray):
if len(min.shape) == 0:
min = [min]
elif min.shape == (1,):
min = list(min[0]) if hasattr(min[0], '__iter__') else list(min)
else:
raise RuntimeError("min must be an array of one element.")
else:
min = [min]
# container in sklearn-onnx stores the computation type in
# container.dtype.
min_name = self.get_unique_tensor_name('clip_min')
if op_version < 12:
min = np.array(min, dtype=getattr(container, 'dtype', np.float32))
container.add_initializer(min_name, getattr(container, 'proto_dtype',
onnx_proto.TensorProto.FLOAT), [], [min[0]])
else:
min = np.array(min)
container.add_initializer(min_name, NP_TYPE_TO_TENSOR_TYPE[min.dtype], [], [min[0]])
min = min_name
if isinstance(min, str):
inputs.append(min)
else:
raise RuntimeError("Parameter 'min' must be a string or a float.")
if max is not None:
if min is None:
raise RuntimeError("Parameter 'min' must be specified if 'max' is.")
if isinstance(max, (np.ndarray, float, int)):
# add initializer
if isinstance(max, np.ndarray):
if len(max.shape) == 0:
max = [max]
elif max.shape == (1,):
max = list(max[0]) if hasattr(max[0], '__iter__') else list(max)
else:
raise RuntimeError("max must be an array of one element.")
else:
max = [max]
max_name = self.get_unique_tensor_name('clip_max')
if op_version < 12:
max = np.array(max, dtype=getattr(container, 'dtype', np.float32))
container.add_initializer(max_name, getattr(container, 'proto_dtype',
onnx_proto.TensorProto.FLOAT), [], [max[0]])
else:
max = np.array(max)
container.add_initializer(max_name, NP_TYPE_TO_TENSOR_TYPE[max.dtype], [], [max[0]])
max = max_name
if isinstance(max, str):
inputs.append(max)
else:
raise RuntimeError("Parameter 'max' must be a string or a float.")
container.add_node('Clip', inputs, output_name, op_version=op_version,
**attrs)
return output_name
def concat(self, input_names, output_name, container, operator_name=None, axis=0):
name = _create_name_or_use_existing_one(container, 'Concat', operator_name)
if container.target_opset < 4:
op_version = 1
elif container.target_opset < 11:
op_version = 4
else:
op_version = 11
container.add_node('Concat', input_names, output_name, op_version=op_version, name=name, axis=axis)
return output_name
def concat_from_sequence(self, input_names, output_name, container, operator_name=None, axis=0, new_axis=None):
name = _create_name_or_use_existing_one(container, 'Concat', operator_name)
attrs = {'axis': axis}
if new_axis is not None:
attrs['new_axis'] = new_axis
container.add_node('ConcatFromSequence', input_names, output_name, op_version=11, name=name, **attrs)
return output_name
def constant(self, input_names, output_name, container, operator_name=None, value=None):
assert len(input_names) == 0 # only a placeholder to standardize the argument list.
name = _create_name_or_use_existing_one(container, 'Constant', operator_name)
if value is None:
raise ValueError('Attribute "value" is a required argument.')
if container.target_opset < 9:
op_version = 1
elif container.target_opset < 11:
op_version = 9
elif container.target_opset < 12:
op_version = 11
else:
op_version = 12
if op_version < 12:
attrs = {'name': name, 'value': value}
else:
if isinstance(value, float):
attrs = {'name': name, 'value_float': value}
elif isinstance(value, int):
attrs = {'name': name, 'value_int': value}
elif isinstance(value, str):
attrs = {'name': name, 'value_string': value}
else:
attrs = {'name': name, 'value': value}
container.add_node('Constant', [], output_name, op_version=op_version, **attrs)
return output_name
def constant_of_shape(self, input_names, output_name, container, operator_name=None, value=None):
attrs = {}
if value is not None:
attrs['value'] = value
name = _create_name_or_use_existing_one(container, 'ConstantOfShape', operator_name)
container.add_node('ConstantOfShape', input_names, output_name, name=name, op_version=9, **attrs)
return output_name
def conv(self, input_names, output_name, container, operator_name=None, **attrs):
name = _create_name_or_use_existing_one(container, 'Conv', operator_name)
if container.target_opset < 11:
op_version = 1
else:
op_version = 11
container.add_node('Conv', input_names, output_name, name=name, op_version=op_version, **attrs)
return output_name
def crop_height_width(self, input_name, output_name, container, operator_name=None,
top_border=0, bottom_border=0, left_border=0, right_border=0):
name = container.get_unique_operator_name('CropHeightWidth')
if container.target_opset < 9:
# If operator set < 9, we can use the experimental Crop in ONNX.
attrs = {'name': name, 'border': [left_border, top_border, right_border, bottom_border]}
container.add_node('Crop', input_name, output_name, **attrs)
else:
# The experimental Crop in ONNX is removed after operator set 9, so we
# switch to ONNX DynamicSlice operator.
# CoreML only crops H- and W-axes.
axes = [2, 3]
axes_name = self.get_unique_tensor_name(name + '_axes')
container.add_initializer(axes_name, onnx_proto.TensorProto.INT64,
[len(axes)], axes)
# Number of cropped pixels is the starting index of the remained region.
starts = [top_border, left_border]
starts_name = self.get_unique_tensor_name(name + '_starts')
container.add_initializer(starts_name, onnx_proto.TensorProto.INT64,
[len(starts)], starts)
# First we assume no cropping is needed at the end of those axes.
# We will change this right below depending on Crop's configuration.
ends = [np.iinfo(np.int64).max] * 2
# Crop n pixel means the end index (exclusive) is -n. Note that indexing
# system is zero-based.
if bottom_border > 0:
ends[0] = -bottom_border
if right_border > 0:
ends[1] = -right_border
# Add the adjusted ends.
ends_name = self.get_unique_tensor_name(name + '_ends')
container.add_initializer(ends_name, onnx_proto.TensorProto.INT64,
[len(ends)], ends)
# Collect all input names as a list because DynamicSlice has multiple inputs.
input_list = [input_name, starts_name, ends_name, axes_name]
container.add_node('DynamicSlice', input_list, output_name, op_version=9)
return output_name
def cumsum(self, input_names, output_names, container, operator_name=None, axis=None):
name = _create_name_or_use_existing_one(container, 'cumsum', operator_name)
assert axis is not None, "Axis in Op CumSum must be provided."
axis_name = self.get_unique_tensor_name(name+'_dim')
container.add_initializer(axis_name,
onnx_proto.TensorProto.INT64,
[1], [axis])
container.add_node('CumSum', input_names + [axis_name], output_names, op_version=11, name=name)
return output_names
def div(self, input_names, output_name, container, operator_name=None, axis=None, broadcast=None):
self._apply_basic_numerical_operation('Div', input_names, output_name,
container, operator_name,
axis, broadcast)
return output_name
def elu(self, input_name, output_name, container, operator_name=None, alpha=1.0):
self._apply_unary_operation('Elu', input_name, output_name, container, operator_name, alpha=alpha)
return output_name
def equal(self, input_names, output_name, container, operator_name=None):
name = _create_name_or_use_existing_one(container, 'equal', operator_name)
if container.target_opset < 7:
op_version = 1
elif container.target_opset < 9:
op_version = 7
else:
op_version = 9
container.add_node('Equal', input_names, output_name, name=name, op_version=op_version)
return output_name
def exp(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Exp', input_name, output_name, container, operator_name=operator_name)
return output_name
def floor(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Floor', input_name, output_name, container, operator_name=operator_name)
return output_name
def flatten(self, input_name, output_name, container, operator_name=None, axis=1):
name = _create_name_or_use_existing_one(container, 'Flatten', operator_name)
if container.target_opset < 9:
op_version = 1
elif container.target_opset < 11:
op_version = 9
else:
op_version = 11
container.add_node('Flatten', input_name, output_name, name=name, op_version=op_version, axis=axis)
return output_name
def gather(self, input_names, output_name, container, operator_name=None, axis=0):
name = _create_name_or_use_existing_one(container, 'Gather', operator_name)
if container.target_opset < 11:
op_version = 1
else:
op_version = 11
container.add_node('Gather', input_names, output_name, name=name, op_version=op_version, axis=axis)
return output_name
def gemm(self, input_name, output_name, container, operator_name=None, alpha=1.0, beta=1.0,
transA=0, transB=0):
"""
Applies operator `gemm <https://github.com/onnx/onnx/blob/master/docs/Operators.md#gemm>`.
"""
name = _create_name_or_use_existing_one(container, 'Gemm', operator_name)
attrs = {'alpha': alpha, 'beta': beta, 'transA': transA, 'transB': transB}
if container.target_opset < 5:
attrs['op_version'] = 1
attrs['broadcast'] = 1
elif container.target_opset < 7:
attrs['op_version'] = 6
attrs['broadcast'] = 1
elif container.target_opset < 11:
attrs['op_version'] = 7
else:
attrs['op_version'] = 11
container.add_node('Gemm', input_name, output_name, name=name, **attrs)
return output_name
@schema(outputs=((_dt.BOOL, []), ),)
def greater(self, input_names, output_name, container, operator_name=None):
name = _create_name_or_use_existing_one(container, 'Greater', operator_name)
if container.target_opset < 7:
op_version = 1
elif container.target_opset < 9:
op_version = 7
else:
op_version = 9
container.add_node('Greater', input_names, output_name, name=name, op_version=op_version)
return output_name
def _apply_convert_compare_equal(self, input_names, output_name, container, operator_name,
tf_op_string, onnx_op_string_rev, onnx_op_string):
if container.target_opset < 7:
raise ValueError(tf_op_string + " op is not supported for opset < 7")
elif container.target_opset < 9:
op_version = 7
elif container.target_opset < 12:
op_version = 9
else:
op_version = 12
name = _create_name_or_use_existing_one(container, tf_op_string, operator_name)
if op_version < 9:
compare_input_0 = self.get_unique_tensor_name(name + '_input_0_cast')
container.add_node('Cast', [input_names[0]], compare_input_0, name=name + '_input_0_cast', to=1)
compare_input_1 = self.get_unique_tensor_name(name + '_input_1_cast')
container.add_node('Cast', [input_names[1]], compare_input_1, name=name + '_input_1_cast', to=1)
less_out = self.get_unique_tensor_name(name + '_less_out')
container.add_node(onnx_op_string_rev, [compare_input_0, compare_input_1], less_out,
name=name + '_' + onnx_op_string_rev.lower(),
op_version=op_version)
container.add_node('Not', less_out, output_name, name=name + '_not')
elif op_version < 12:
compare_node = self.get_unique_tensor_name(name + '_compare_node')
container.add_node(onnx_op_string_rev, input_names, compare_node,
name=name + '_' + onnx_op_string_rev.lower(),
op_version=op_version)
container.add_node('Not', [compare_node], output_name, name=name)
else:
container.add_node(onnx_op_string, input_names, output_name,
name=name + '_' + onnx_op_string_rev.lower(), op_version=op_version)
def greater_or_equal(self, input_names, output_name, container, operator_name=None):
self._apply_convert_compare_equal(input_names, output_name, container, operator_name,
'GreaterEqual', 'Less', 'GreaterOrEqual')
return output_name
def less_or_equal(self, input_names, output_name, container, operator_name=None):
self._apply_convert_compare_equal(input_names, output_name, container,
operator_name, 'LessEqual', 'Greater', 'LessOrEqual')
return output_name
def gru(self, input_names, output_names, container, operator_name=None, output_seq=0, reset_after=0, **attrs):
name = _create_name_or_use_existing_one(container, 'GRU', operator_name)
if container.target_opset < 3:
op_version = 1
attrs['output_sequence'] = 1 if output_seq else 0
else:
attrs['linear_before_reset'] = 1 if reset_after else 0
if container.target_opset <= 5:
attrs['output_sequence'] = 1 if output_seq else 0
op_version = 3
else:
op_version = 7
container.add_node('GRU', input_names, output_names, name=name, op_version=op_version, **attrs)
return output_names
def hard_sigmoid(self, input_name, output_name, container, operator_name=None, alpha=None, beta=None):
self._apply_unary_operation('HardSigmoid', input_name, output_name, container, operator_name,
alpha=alpha, beta=beta)
return output_name
def identity(self, input_name, output_name, container, operator_name=None):
name = _create_name_or_use_existing_one(container, 'Identity', operator_name)
container.add_node('Identity', input_name, output_name, name=name)
return output_name
def instance_norm(self, input_names, output_name, container, operator_name=None, epsilon=1e-5):
name = _create_name_or_use_existing_one(container, 'InstanceNormalization', operator_name)
attrs = {'name': name, 'epsilon': epsilon}
if container.target_opset < 2:
attrs['consumed_inputs'] = [0] * len(input_names)
op_version = 1
else:
op_version = 6
container.add_node('InstanceNormalization', input_names, output_name, op_version=op_version, **attrs)
return output_name
def leaky_relu(self, input_name, output_name, container, operator_name=None, alpha=0.01):
self._apply_unary_operation('LeakyRelu', input_name, output_name, container, operator_name, alpha=alpha)
return output_name
def less(self, input_names, output_name, container, operator_name=None):
name = _create_name_or_use_existing_one(container, 'Less', operator_name)
if container.target_opset < 7:
op_version = 1
elif container.target_opset < 9:
op_version = 7
else:
op_version = 9
container.add_node('Less', input_names, output_name, name=name, op_version=op_version)
return output_name
def log(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Log', input_name, output_name, container, operator_name=operator_name)
return output_name
def lstm(self, input_names, output_names, container, operator_name=None, output_seq=0, **attrs):
name = _create_name_or_use_existing_one(container, 'LSTM', operator_name)
if container.target_opset <= 6:
attrs['output_sequence'] = 1 if output_seq else 0
op_version = 1
else:
op_version = 7
container.add_node('LSTM', input_names, output_names, name=name, op_version=op_version, **attrs)
return output_names
def matmul(self, input_names, output_name, container, operator_name=None):
op_type = 'MatMul'
name = _create_name_or_use_existing_one(container, op_type, operator_name)
if container.target_opset <= 9:
op_version = 1
else:
op_version = 9
container.add_node(op_type, input_names, output_name, op_version=op_version, name=name)
return output_name
def max(self, input_names, output_name, container, operator_name=None):
self._apply_pointwise_operation('Max', input_names, output_name, container, operator_name)
return output_name
def mean(self, input_names, output_name, container, operator_name=None):
self._apply_pointwise_operation('Mean', input_names, output_name, container, operator_name)
return output_name
def min(self, input_names, output_name, container, operator_name=None):
self._apply_pointwise_operation('Min', input_names, output_name, container, operator_name)
return output_name
def mul(self, input_names, output_name, container, operator_name=None, axis=None, broadcast=None):
self._apply_basic_numerical_operation('Mul', input_names, output_name,
container, operator_name=operator_name,
axis=axis, broadcast=broadcast)
return output_name
def neg(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Neg', input_name, output_name, container, operator_name)
return output_name
def lpnormalization(self, input_name, output_name, container, operator_name=None, axis=1, p=2):
name = _create_name_or_use_existing_one(container, 'LpNormalization', operator_name)
container.add_node('LpNormalization', input_name, output_name, name=name, p=p, axis=axis)
return output_name
def not_op(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Not', input_name, output_name, container, operator_name)
return output_name
def or_op(self, input_names, output_names, container, operator_name=None):
name = _create_name_or_use_existing_one(container, 'or', operator_name)
container.add_node('Or', input_names, output_names, op_version=7, name=name)
return output_names
def pad(self, input_name, output_name, container, operator_name=None, mode=None, pads=None, value=None,
onnx_type=onnx_proto.TensorProto.FLOAT):
name = _create_name_or_use_existing_one(container, 'Pad', operator_name)
attrs = {'name': name}
inputs = input_name if isinstance(input_name, list) else [input_name]
if mode is not None:
attrs['mode'] = mode
if container.target_opset < 11:
if isinstance(pads, str):
raise ValueError("Dynamic pad is not supported for opset < 11.")
if value is not None:
attrs['value'] = value
if container.target_opset < 2:
attrs['paddings'] = pads
op_version = 1
else:
attrs['pads'] = pads
op_version = 2
else:
op_version = 11
if isinstance(pads, str):
inputs.append(pads)
else:
pads_name = self.get_unique_tensor_name(name + '_pads')
container.add_initializer(pads_name, onnx_proto.TensorProto.INT64, [len(pads)], pads)
inputs.append(pads_name)
if value is not None:
value_name = self.get_unique_tensor_name(name + '_value')
container.add_initializer(value_name, onnx_type, [], [value])
inputs.append(value_name)
container.add_node('Pad', inputs, output_name, op_version=op_version, **attrs)
return output_name
def parametric_softplus(self, input_name, output_name, container, operator_name=None, alpha=None, beta=None):
if alpha is None:
alpha = [1.0]
if beta is None:
beta = [0.]
name = _create_name_or_use_existing_one(container, 'ParametricSoftplus', operator_name)
if container.target_opset < 9:
if len(alpha) != 1 or len(beta) != 1:
raise ValueError('alpha and beta must be 1-element lists')
op_type = 'ParametricSoftplus'
attrs = {'name': name, 'alpha': alpha[0], 'beta': beta[0]}
container.add_node(op_type, input_name, output_name, **attrs)
else:
# Define three scalars: a, b, 1.
aName = self.get_unique_tensor_name(name + '_alpha')
aShape = [len(alpha)] if len(alpha) == 1 else [len(alpha), 1, 1]
container.add_initializer(aName, onnx_proto.TensorProto.FLOAT, aShape, alpha)
bShape = [len(beta)] if len(beta) == 1 else [len(beta), 1, 1]
bName = self.get_unique_tensor_name(name + '_beta')
container.add_initializer(bName, onnx_proto.TensorProto.FLOAT, bShape, beta)
oneName = self.get_unique_tensor_name(name + '_one')
container.add_initializer(oneName, onnx_proto.TensorProto.FLOAT, [1], [1.])
# c = b * x
cName = self.get_unique_tensor_name(name + '_c')
self.mul([input_name, bName], cName, container)
# d = exp(c)
dName = self.get_unique_tensor_name(name + '_d')
self.exp(cName, dName, container)
# e = 1 + d
eName = self.get_unique_tensor_name(name + '_e')
self.add([dName, oneName], eName, container)
# f = log(e)
fName = self.get_unique_tensor_name(name + '_f')
self.log(eName, fName, container)
# g = a * f
self.mul([fName, aName], output_name, container)
return output_name
def pow(self, input_names, output_name, container, operator_name=None, axis=None, broadcast=None):
name = _create_name_or_use_existing_one(container, 'Pow', operator_name)
attrs = {'name': name}
if container.target_opset < 7:
# Before ONNX-1.2, broadcasting behavior is Caffe2-like.
if axis is not None:
attrs['axis'] = axis
if broadcast is not None:
attrs['broadcast'] = broadcast
op_version = 1
elif container.target_opset < 12:
# Since ONNX-1.2, broadcasting behavior is Numpy-like, so we don't need to specify any attributes
op_version = 7
else:
op_version = 12
container.add_node('Pow', input_names, output_name, op_version=op_version, **attrs)
return output_name
def prelu(self, input_name, output_name, container, operator_name=None, slp_rate=None):
name = _create_name_or_use_existing_one(container, 'PRelu', operator_name)
slp_rate_tensor_name = self.get_unique_tensor_name('slp_rate')
s_shape = slp_rate.shape
if container.target_opset < 7:
s_shape = [len(slp_rate.flatten())]
container.add_initializer(slp_rate_tensor_name, onnx_proto.TensorProto.FLOAT, s_shape, slp_rate.flatten())
if container.target_opset < 6:
container.add_node('PRelu', [input_name, slp_rate_tensor_name], output_name, op_version=1, name=name,
consumed_inputs=[0, 0])
else:
if container.target_opset < 7:
op_version = 6
elif container.target_opset < 9:
op_version = 7
else:
# opset 9 supports unidirectional broadcasting
op_version = 9
container.add_node('PRelu', [input_name, slp_rate_tensor_name], output_name, op_version=op_version, name=name)
return output_name
def range(self, input_name, output_name, container, operator_name=None):
name = _create_name_or_use_existing_one(container, 'Range', operator_name)
container.add_node('Range', input_name, output_name, op_version=11, name=name)
return output_name
def reciprocal(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Reciprocal', input_name, output_name, container, operator_name=operator_name)
return output_name
# Some old ORT supports axis < 0 case, so put rank=0 as default.
def reducesum(self, input_name, output_name, container, operator_name=None, axes=None, keepdims=1, rank=0):
name = _create_name_or_use_existing_one(container, 'ReduceSum', operator_name)
if axes is None:
axes = []
if container.target_opset < 13:
if container.target_opset < 11:
op_version = 1
axes = [axis if axis >= 0 else axis + rank for axis in axes]
else:
op_version = 11
container.add_node('ReduceSum', input_name, output_name, name=name,
op_version=op_version, axes=axes, keepdims=keepdims)
else:
if not isinstance(input_name, list):
input_name = [input_name]
op_version = 13
if isinstance(axes, str):
container.add_node('ReduceSum', input_name + [axes], output_name,
op_version=op_version, name=name, keepdims=keepdims)
elif axes is None or len(axes) == 0:
container.add_node('ReduceSum', input_name, output_name,
op_version=op_version, name=name, keepdims=keepdims)
else:
axes_name = self.get_unique_tensor_name(name + '_reducesum')
container.add_initializer(axes_name, onnx_proto.TensorProto.INT64, [len(axes)], axes)
container.add_node('ReduceSum', input_name + [axes_name], output_name,
op_version=op_version, name=name, keepdims=keepdims)
return output_name
def reducemin(self, input_name, output_name, container, operator_name=None, axes=None, keepdims=1, rank=0):
name = _create_name_or_use_existing_one(container, 'ReduceMin', operator_name)
if axes is None:
axes = []
if container.target_opset < 13:
if container.target_opset < 11:
op_version = 1
axes = [axis if axis >= 0 else axis + rank for axis in axes]
else:
op_version = 11
container.add_node('ReduceMin', input_name, output_name, name=name,
op_version=op_version, axes=axes, keepdims=keepdims)
else:
if not isinstance(input_name, list):
input_name = [input_name]
op_version = 13
if isinstance(axes, str):
container.add_node('ReduceMin', input_name + [axes], output_name,
op_version=op_version, name=name, keepdims=keepdims)
elif axes is None or len(axes) == 0:
container.add_node('ReduceMin', input_name, output_name,
op_version=op_version, name=name, keepdims=keepdims)
else:
axes_name = self.get_unique_tensor_name(name + '_reducemin')
container.add_initializer(axes_name, onnx_proto.TensorProto.INT64, [len(axes)], axes)
container.add_node('ReduceMin', input_name + [axes_name], output_name,
op_version=op_version, name=name, keepdims=keepdims)
return output_name
def relu(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Relu', input_name, output_name, container, operator_name)
return output_name
def relu_6(self, input_name, output_name, container, operator_name=None, zero_value=0.0):
name_relu = _create_name_or_use_existing_one(container, 'relu', operator_name)
name_relu_op = _create_name_or_use_existing_one(container, 'relu6', operator_name)
self.relu(input_name, name_relu, container, name_relu_op+'_relu')
self.clip(name_relu, output_name, container, name_relu_op + '_clip', zero_value+6, zero_value)
def reshape(self, input_name, output_name, container, operator_name=None, desired_shape=None):
if not isinstance(desired_shape, str) and len(list(i for i in desired_shape if i is not None and i < 0)) > 1:
raise ValueError('There can only be one -1 in the targeted shape of a Reshape but got %s' % desired_shape)
name = _create_name_or_use_existing_one(container, 'Reshape', operator_name)
if container.target_opset < 5:
container.add_node('Reshape', input_name, output_name, op_version=1, name=name, shape=desired_shape,
consumed_inputs=[0])
else:
if isinstance(desired_shape, str):
desired_shape_name = desired_shape
else:
desired_shape_name = self.get_unique_tensor_name('shape_tensor')
container.add_initializer(desired_shape_name, onnx_proto.TensorProto.INT64, [len(desired_shape)],
desired_shape)
# Create ONNX Reshape operator
if isinstance(input_name, list):
input_name.append(desired_shape_name)
else:
input_name = [input_name, desired_shape_name]
container.add_node('Reshape', input_name, output_name, op_version=5, name=name)
return output_name
def resize(self, input_name, output_name, container, operator_name=None, mode='nearest',
coordinate_transformation_mode='asymmetric', scales=None):
"""
:param mode: "nearest" or "linear"
:param scales: a float tensor for scaling (upsampling or downsampling) all input dimensions
"""
name = _create_name_or_use_existing_one(container, 'Resize', operator_name)
attrs = {'name': name}
attrs['mode'] = mode.lower()
inputs = [input_name]
if container.target_opset < 11:
op_version = 10
else:
op_version = 11
roi_tensor_name = self.get_unique_tensor_name(name + '_roi')
roi = [0.0] * len(scales) + [1.0] * len(scales)
container.add_initializer(roi_tensor_name, onnx_proto.TensorProto.FLOAT, [2 * len(scales)], roi)
inputs.append(roi_tensor_name)
attrs['coordinate_transformation_mode'] = coordinate_transformation_mode
if attrs['mode'] == 'nearest':
attrs['nearest_mode'] = 'floor'
scales_tensor_name = self.get_unique_tensor_name(name + '_scales')
container.add_initializer(scales_tensor_name, onnx_proto.TensorProto.FLOAT, [len(scales)], scales)
inputs.append(scales_tensor_name)
container.add_node('Resize', inputs, output_name, op_version=op_version, **attrs)
return output_name
def rnn(self, input_names, output_names, container, operator_name=None, output_seq=0, **attrs):
name = _create_name_or_use_existing_one(container, 'RNN', operator_name)
if container.target_opset <= 6:
attrs['output_sequence'] = 1 if output_seq else 0
op_version = 1
else:
op_version = 7
container.add_node('RNN', input_names, output_names, name=name, op_version=op_version, **attrs)
return output_names
def shape(self, input_name, output_name, container, operator_name=None):
name = _create_name_or_use_existing_one(container, 'Shape', operator_name)
container.add_node('Shape', input_name, output_name, name=name, op_version=1)
return output_name
def sigmoid(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Sigmoid', input_name, output_name, container, operator_name)
return output_name
def softsign(self, input_name, output_name, container, operator_name=None):
name = _create_name_or_use_existing_one(container, 'Softsign', operator_name)
container.add_node('Softsign', input_name, output_name, name=name, op_version=1)
return output_name
# See alpha and gamma at https://github.com/keras-team/keras/blob/master/keras/activations.py#L80-L81
def selu(self, input_name, output_name, container, operator_name=None, alpha=1.673263, gamma=1.050701):
self._apply_unary_operation('Selu', input_name, output_name, container, operator_name, alpha=alpha, gamma=gamma)
return output_name
def softmax(self, input_name, output_name, container, operator_name=None, axis=None):
name = _create_name_or_use_existing_one(container, 'Softmax', operator_name)
if axis is None:
axis = 1 if container.target_opset < 13 else -1
container.add_node('Softmax', input_name, output_name, name=name, axis=axis)
return output_name
def scaled_tanh(self, input_name, output_name, container, operator_name=None, alpha=None, beta=None):
if alpha is None:
alpha = [1.0]
if beta is None:
beta = [1.0]
if len(alpha) != 1 or len(beta) != 1:
raise ValueError('alpha and beta must be 1-element lists')
name = _create_name_or_use_existing_one(container, 'ScaledTanh', operator_name)
if container.target_opset < 9:
attrs = {'name': name, 'alpha': alpha[0], 'beta': beta[0]}
container.add_node('ScaledTanh', input_name, output_name, **attrs)
else:
# Define scalar a, initialize with parameter alpha.
aName = self.get_unique_tensor_name(name + '_alpha')
aShape = [len(alpha)] if len(alpha) == 1 else [len(alpha), 1, 1]
container.add_initializer(aName, onnx_proto.TensorProto.FLOAT, aShape, alpha)
# Define scalar b, initialize with parameter beta.
bShape = [len(beta)] if len(beta) == 1 else [len(beta), 1, 1]
bName = self.get_unique_tensor_name(name + '_beta')
container.add_initializer(bName, onnx_proto.TensorProto.FLOAT, bShape, beta)
# c = b * x
cName = self.get_unique_tensor_name(name + '_c')
self.mul([input_name, bName], cName, container)
# d = tanh(c)
dName = self.get_unique_tensor_name(name + '_d')
self.tanh(cName, dName, container)
# output = a * d
self.mul([aName, dName], output_name, container)
return output_name
def slice(self, input_name, output_name, container,
operator_name=None, starts=None, ends=None, axes=None, steps=None):
assert starts is not None, 'the starts in slice op cannot be None'
assert ends is not None, 'the ends in slice op cannot be None'
name = _create_name_or_use_existing_one(container, 'Slice', operator_name)
if container.target_opset < 10:
if axes is None:
container.add_node('Slice', input_name, output_name, name=name,
starts=starts, ends=ends, op_version=1)
else:
container.add_node('Slice', input_name, output_name, name=name,
starts=starts, ends=ends, axes=axes, op_version=1)
else:
if container.target_opset == 10:
op_version = 10
else:
op_version = 11
inputs = input_name if isinstance(input_name, list) else [input_name]
if isinstance(starts, str):
starts_name = starts
else:
starts_name = self.get_unique_tensor_name('starts')
container.add_initializer(starts_name, onnx_proto.TensorProto.INT64,
[len(starts)], starts)
if isinstance(ends, str):
ends_name = ends
else:
ends_name = self.get_unique_tensor_name('ends')
container.add_initializer(ends_name, onnx_proto.TensorProto.INT64,
[len(ends)], ends)
inputs.append(starts_name)
inputs.append(ends_name)
if axes:
if isinstance(axes, str):
axes_name = axes
else:
axes_name = self.get_unique_tensor_name('axes')
container.add_initializer(axes_name, onnx_proto.TensorProto.INT64,
[len(axes)], axes)
inputs.append(axes_name)
if steps:
if not axes:
inputs.append('')
if isinstance(steps, str):
steps_name = steps
else:
steps_name = self.get_unique_tensor_name('steps')
container.add_initializer(steps_name, onnx_proto.TensorProto.INT64,
[len(steps)], steps)
inputs.append(steps_name)
container.add_node('Slice', inputs, output_name, name=name,
op_version=op_version)
return output_name
def split(self, input_name, output_names, container, operator_name=None, split=None, axis=0):
name = _create_name_or_use_existing_one(container, 'Split', operator_name)
if container.target_opset <= 1:
op_version = 1
elif container.target_opset < 11:
op_version = 2
elif container.target_opset < 13:
op_version = 11
else:
op_version = 13
attrs = {'name': name}
if split is not None:
if container.target_opset < 13:
attrs['split'] = split
else:
if not isinstance(input_name, list):
input_name = [input_name]
if isinstance(split, str):
split_name = split
else:
split_name = self.get_unique_tensor_name(name + '_split')
container.add_initializer(split_name, onnx_proto.TensorProto.INT64, [len(split)], split)
input_name = input_name + [split_name]
if axis is not None:
attrs['axis'] = axis
container.add_node('Split', input_name, output_names, op_version=op_version, **attrs)
return output_names
def sqrt(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Sqrt', input_name, output_name, container, operator_name=operator_name)
return output_name
def _apply_squeeze_unsqueeze(self, input_name, output_name, container, squeeze_str, operator_name=None, axes=None,
rank=0):
name = _create_name_or_use_existing_one(container, squeeze_str, operator_name)
if container.target_opset < 13:
if container.target_opset < 11:
op_version = 1
axes = [axis if axis >= 0 else axis + rank for axis in axes]
else:
op_version = 11
container.add_node(squeeze_str, input_name, output_name, name=name, op_version=op_version, axes=axes)
else:
op_version = 13
if not isinstance(input_name, list):
input_name = [input_name]
if isinstance(axes, str):
container.add_node(squeeze_str, input_name + [axes], output_name, op_version=op_version, name=name)
elif len(axes) == 0:
container.add_node(squeeze_str, input_name, output_name, op_version=op_version, name=name)
else:
axes_name = self.get_unique_tensor_name(name + '_axes')
container.add_initializer(axes_name, onnx_proto.TensorProto.INT64, [len(axes)], axes)
container.add_node(squeeze_str, input_name + [axes_name], output_name, op_version=op_version, name=name)
return output_name
def squeeze(self, input_name, output_name, container, operator_name=None, axes=None, rank=0):
if axes is None:
axes = []
self._apply_squeeze_unsqueeze(input_name, output_name, container, 'Squeeze', operator_name, axes, rank)
return output_name
def sub(self, input_names, output_name, container, operator_name=None, axis=None, broadcast=0):
self._apply_basic_numerical_operation('Sub', input_names, output_name, container, operator_name=operator_name,
axis=axis, broadcast=broadcast)
return output_name
def sum(self, input_names, output_name, container, operator_name=None):
name = _create_name_or_use_existing_one(container, 'Sum', operator_name)
if container.target_opset < 6:
op_version = 1
else:
op_version = 6
container.add_node('Sum', input_names, output_name, op_version=op_version, name=name)
return output_name
def tanh(self, input_name, output_name, container, operator_name=None):
self._apply_unary_operation('Tanh', input_name, output_name, container, operator_name)
return output_name
def thresholded_relu(self, input_name, output_name, container, operator_name=None, alpha=None):
if alpha is None:
alpha = [1.0]
name = _create_name_or_use_existing_one(container, 'ThresholdedRelu', operator_name)
attrs = {'name': name, 'alpha': alpha[0]}
if container.target_opset < 10:
# ThresholdedRelu graduated from an experimental op to a full op in opset 10
# onnxruntime maintains support in the ONNX domain for ThresholdedRelu as a contrib op
attrs['op_domain'] = "ai.onnx"
op_version = 1
else:
op_version = 10
container.add_node('ThresholdedRelu', input_name, output_name, op_version=op_version, **attrs)
return output_name
def tile(self, input_name, output_name, container, operator_name=None, repeats=None):
name = _create_name_or_use_existing_one(container, 'Tile', operator_name)
if repeats is None or (not isinstance(repeats, str) and all(repeat_count == 1 for repeat_count in repeats)):
container.add_node('Identity', input_name, output_name, name=name)
return output_name
if container.target_opset < 6:
intermediate_input_name = input_name
intermediate_output_name = None
if isinstance(repeats, str):
raise ValueError('repeats cannot be string type before opset 6')
for axis, repeat_count in enumerate(repeats):
if repeat_count == 1:
continue
# Create the 2nd input of Tile
tile_tensor_name = self.get_unique_tensor_name(name + '_tile')
container.add_initializer(tile_tensor_name, onnx_proto.TensorProto.FLOAT, [1], [float(repeat_count)])
# Create the 3rd input of Tile
axis_tensor_name = self.get_unique_tensor_name(name + '_axis')
container.add_initializer(axis_tensor_name, onnx_proto.TensorProto.FLOAT, [1], [float(axis)])
# Create tile for duplicating along one axis. After ONNX-1.2, we can duplicate along multiple axes,
# so we don't have to iterate through all axes.
intermediate_output_name = self.get_unique_tensor_name(name + '_input')
container.add_node('Tile', [intermediate_input_name, tile_tensor_name, axis_tensor_name],
intermediate_output_name, name=name)
# Use the output produced by this round as the input in the next iteration
intermediate_input_name = intermediate_output_name
# Create a new name for next Tile
name = container.get_unique_operator_name('Tile')
# Use the last Tile name for the name of an Identity
container.add_node('Identity', intermediate_output_name, output_name, op_version=1, name=name)
else:
# ONNX-1.2 has a new Tile and we use it here
if isinstance(repeats, str):
container.add_node('Tile', input_name + [repeats], output_name, op_version=6, name=name)
else:
repeat_tensor_name = self.get_unique_tensor_name(name + '_repeats')
container.add_initializer(repeat_tensor_name, onnx_proto.TensorProto.INT64, [len(repeats)], repeats)
container.add_node('Tile', [input_name, repeat_tensor_name], output_name, op_version=6, name=name)
return output_name
def topk(self, input_name, output_names, container, k, operator_name=None):
name = _create_name_or_use_existing_one(container, 'TopK', operator_name)
if container.target_opset < 10:
if isinstance(k, str):
raise ValueError('topk k cannot be string type before opset 10')
container.add_node('TopK', input_name, output_names, name=name, k=k, op_version=1)
else:
if container.target_opset == 10:
op_version = 10
else:
op_version = 11
if isinstance(k, str):
k_value_name = k
else:
k_value_name = self.get_unique_tensor_name('k_value')
container.add_initializer(k_value_name, onnx_proto.TensorProto.INT64, [1], [k])
container.add_node('TopK', input_name + [k_value_name], output_names, name=name, op_version=op_version)
return output_names
def transpose(self, input_name, output_name, container, operator_name=None, perm=None):
name = _create_name_or_use_existing_one(container, 'Transpose', operator_name)
container.add_node('Transpose', input_name, output_name, name=name, perm=perm)
return output_name
def upsample(self, input_name, output_name, container, operator_name=None, mode='nearest',
coordinate_transformation_mode='asymmetric', scales=None):
"""
:param input_name:
:param output_name:
:param container:
:param operator_name:
:param mode: nearest or linear
:param coordinate_transformation_mode:
:param scales: an integer list of scaling-up rate of all input dimensions
:return:
"""
if container.target_opset < 10:
name = _create_name_or_use_existing_one(container, 'Upsample', operator_name)
inputs = [input_name]
attrs = {'name': name}
if container.target_opset < 7:
if len(scales) != 4:
raise ValueError('Need to specify a 4-element list the the scales of N-, C-, H-, and W-axes')
attrs['height_scale'] = float(scales[2])
attrs['width_scale'] = float(scales[3])
attrs['mode'] = mode.upper()
op_version = 1
else:
attrs['mode'] = mode.lower()
if container.target_opset < 9:
attrs['scales'] = list(map(float, scales))
op_version = 7
else:
# scales moved from attribute to input in opset 9
scales_tensor_name = self.get_unique_tensor_name(name + '_scales')
container.add_initializer(scales_tensor_name, onnx_proto.TensorProto.FLOAT, [len(scales)], scales)
inputs = [input_name, scales_tensor_name]
op_version = 9
container.add_node('Upsample', inputs, output_name, op_version=op_version, **attrs)
else:
# Upsample op is deprecated in ONNX opset 10
# We implement Upsample through Resize instead
self.resize(input_name, output_name, container, operator_name, mode, coordinate_transformation_mode,
scales)
return output_name
def unsqueeze(self, input_name, output_name, container, operator_name=None, axes=None, rank=0):
if axes is None:
axes = [0]
self._apply_squeeze_unsqueeze(input_name, output_name, container, 'Unsqueeze', operator_name, axes, rank)
return output_name
def where(self, input_names, output_names, container, operator_name=None):
name = _create_name_or_use_existing_one(container, 'where', operator_name)
container.add_node('Where', input_names, output_names, op_version=9, name=name)
return output_names
def loop(self, input_names, output_names, container, operator_name=None, body=None):
name = _create_name_or_use_existing_one(container, 'loop', operator_name)
trip_count, cond, *states = tuple(input_names)
trip_count = '' if trip_count is None else trip_count
cond_name = '' if cond is None else cond
container.add_node(
'Loop', [trip_count, cond_name] + states, output_names, op_version=11, name=name, body=body)
return output_names
def model_call(self, input_name, output_name, container, operator_name=None, oxml=None):
name = operator_name
if name is None:
name = container.get_unique_operator_name('og')
# The tensor name replacement happens on unfolding ONNX model.
for idx, nm_ in enumerate(input_name):
nvi = oxml.graph.input[idx]
self.identity([nm_], ["{}_{}".format(name, nvi.name)], container)
container.value_info.append(nvi)
for idx, nm_ in enumerate(output_name):
self.identity(["{}_{}".format(name, oxml.graph.output[idx].name)], [nm_], container)
container.value_info.extend(oxml.graph.output)
container.add_model_node(input_name, output_name, name=name, model=oxml)
return output_name
class _ONNXModelBuilder(_ONNXOperatorAPI):
def __init__(self):
_OpSchema._ox = self
self._id_count = 0
self.opdict_counter = {}
def get_unique_tensor_name(self, hint):
self._id_count += 1
return "v{}_{}".format(hint, str(self._id_count))
def make_tensor(self, dtype, dims, vals):
return helper.make_tensor(self.get_unique_tensor_name('ts'), dtype, dims, vals)
def get_unique_operator_type_name(self, op_type):
nn = self.opdict_counter.get(op_type, 0)
self.opdict_counter[op_type] = nn + 1
return "_Op{}".format(op_type) if nn == 0 else "_Op{}_{}".format(op_type, nn+1)
@classmethod
def is_raw(cls, func): # without any schema decorator
return not isinstance(func, _OpSchema)
# Singleton
ox = _ONNXModelBuilder()microsoft/onnxruntime-extensions
Publicmirrored fromhttps://github.com/microsoft/onnxruntime-extensionsAvailable
onnxruntime_extensions/pnp/_onnx_ops.py
1544lines · modepreview