Skip to content

Commit c894415

Browse files
authored
Merge branch 'master' into implement_isinf
2 parents 6975b75 + d1d0802 commit c894415

18 files changed

+347
-119
lines changed

README.md

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
tf2onnx - convert TensorFlow models to ONNX models.
22
========
33

4-
![Build Status](https://dev.azure.com/tensorflow-onnx/tensorflow-onnx/_apis/build/status/unit_test?branchName=master)
4+
[![Build Status](https://dev.azure.com/tensorflow-onnx/tensorflow-onnx/_apis/build/status/unit_test?branchName=master)](https://dev.azure.com/tensorflow-onnx/tensorflow-onnx/_build?definitionId=16&branchName=master)
55

66
# Supported ONNX version
7-
tensorflow-onnx will use the onnx version installed on your system and installs the latest onnx version if none is found.
7+
tensorflow-onnx will use the ONNX version installed on your system and installs the latest ONNX version if none is found.
88

9-
By default we use opset 7 for the resulting onnx graph since most runtimes will support opset 7. Opset 7 was introduced in onnx-1.2.
9+
By default we use opset 7 for the resulting ONNX graph since most runtimes will support opset 7. Opset 7 was introduced in onnx-1.2.
1010

11-
With the release of onnx-1.3 there is now opset 8 - to create an onnx graph for opset 8 use in the command line ```--opset 8```.
11+
Newer releases of ONNX support higher opsets. For example, to create an ONNX graph for opset 8 use in the command line ```--opset 8```.
1212

1313
# Status
1414
We support many TensorFlow models. Support for Fully Connected and Convolutional networks is mature. Dynamic LSTM/GRU/Attention networks should work but the code for this is evolving.
@@ -73,17 +73,16 @@ python -m tf2onnx.convert
7373
--graphdef SOURCE_GRAPHDEF_PB
7474
--checkpoint SOURCE_CHECKPOINT
7575
--saved-model SOURCE_SAVED_MODEL
76+
[--output TARGET_ONNX_MODEL]
7677
[--inputs GRAPH_INPUTS]
7778
[--outputs GRAPH_OUTPUS]
7879
[--inputs-as-nchw inputs_provided_as_nchw]
80+
[--opset OPSET]
7981
[--target TARGET]
80-
[--output TARGET_ONNX_GRAPH]
81-
[--target TARGET]
82-
[--continue_on_error]
83-
[--verbose]
8482
[--custom-ops list-of-custom-ops]
85-
[--opset OPSET]
8683
[--fold_const]
84+
[--continue_on_error]
85+
[--verbose]
8786
```
8887

8988
## Parameters
@@ -100,10 +99,10 @@ the target onnx file path.
10099
Tensorflow model's input/output names, which can be found with [summarize graph tool](#summarize_graph). Those names typically end on ```:0```, for example ```--inputs input0:0,input1:0```. inputs and outputs are ***not*** needed for models in saved-model format.
101100
### --inputs-as-nchw
102101
By default we preserve the image format of inputs (nchw or nhwc) as given in the TensorFlow model. If your hosts (for example windows) native format nchw and the model is written for nhwc, ```--inputs-as-nchw``` tensorflow-onnx will transpose the input. Doing so is convinient for the application and the converter in many cases can optimize the transpose away. For example ```--inputs input0:0,input1:0 --inputs-as-nchw input0:0``` assumes that images are passed into ```input0:0``` as nchw while the TensorFlow model given uses nhwc.
103-
### --target
104-
Some runtimes need workarounds, for example they don't support all types given in the onnx spec. We'll workaround it in some cases by generating a different graph. Those workarounds are activated with ```--target TARGET```.
105102
### --opset
106-
by default we uses the newest opset 7 to generate the graph. By specifieing ```--opset``` the user can override the default to generate a graph with the desired opset. For example ```--opset 5``` would create a onnx graph that uses only ops available in opset 5. Because older opsets have in most cases fewer ops, some models might not convert on a older opset.
103+
By default we use the opset 7 to generate the graph. By specifying ```--opset``` the user can override the default to generate a graph with the desired opset. For example ```--opset 5``` would create a onnx graph that uses only ops available in opset 5. Because older opsets have in most cases fewer ops, some models might not convert on a older opset.
104+
### --target
105+
Some models require special handling to run on some runtimes. In particular, the model may use unsupported data types. Workarounds are activated with ```--target TARGET```. Currently supported values are listed on this [wiki](https://github.com/onnx/tensorflow-onnx/wiki/target). If your model will be run on Windows ML, you should specify the appropriate target value.
107106
### --custom-ops
108107
the runtime may support custom ops that are not defined in onnx. A user can asked the converter to map to custom ops by listing them with the --custom-ops option. Tensorflow ops listed here will be mapped to a custom op with the same name as the tensorflow op but in the onnx domain ai.onnx.converters.tensorflow. For example: ```--custom-ops Print``` will insert a op ```Print``` in the onnx domain ```ai.onnx.converters.tensorflow``` into the graph. We also support a python api for custom ops documented later in this readme.
109108
### --fold_const
@@ -163,7 +162,7 @@ optional arguments:
163162
--config yaml config file
164163
--verbose verbose output, option is additive
165164
--opset OPSET target opset to use
166-
--perf csv-file capture performance numbers or tensorflow and onnx runtime
165+
--perf csv-file capture performance numbers for tensorflow and onnx runtime
167166
--debug dump generated graph with shape info
168167
--fold_const when set, TensorFlow fold_constants transformation will be applied before conversion. This will benefit features including Transpose optimization (e.g. Transpose operations introduced during tf-graph-to-onnx-graph conversion will be removed), and RNN unit conversion (for example LSTM).
169168
```

ci_build/azure_pipelines/pretrained_model_test-matrix.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,17 @@ jobs:
55
parameters:
66
platforms: ['linux', 'windows', 'mac']
77
python_versions: ['3.6', '3.5']
8-
tf_versions: ['1.13.1', '1.12', '1.11', '1.10', '1.9', '1.8', '1.7', '1.6', '1.5']
8+
tf_versions: ['1.12', '1.11', '1.10', '1.9', '1.8', '1.7', '1.6', '1.5']
9+
onnx_opsets: ['9', '8', '7']
10+
job:
11+
steps:
12+
- template: 'pretrained_model_test.yml'
13+
14+
- template: 'templates/job_generator.yml'
15+
parameters:
16+
platforms: ['linux', 'windows', 'mac']
17+
python_versions: ['3.7', '3.6', '3.5']
18+
tf_versions: ['1.13.1']
919
onnx_opsets: ['9', '8', '7']
1020
job:
1121
steps:

ci_build/azure_pipelines/pretrained_model_test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
jobs:
44
- template: 'templates/job_generator.yml'
55
parameters:
6-
python_versions: ['3.6', '3.5']
6+
python_versions: ['3.7', '3.6', '3.5']
77
tf_versions: ['1.13.1']
88
onnx_opsets: ['9', '8', '7']
99
job:

ci_build/azure_pipelines/unit_test-matrix.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ jobs:
55
parameters:
66
platforms: ['linux', 'windows', 'mac']
77
python_versions: ['3.6', '3.5']
8-
tf_versions: ['1.13.1', '1.12', '1.11', '1.10', '1.9', '1.8', '1.7', '1.6', '1.5']
8+
tf_versions: ['1.12', '1.11', '1.10', '1.9', '1.8', '1.7', '1.6', '1.5']
9+
job:
10+
steps:
11+
- template: 'unit_test.yml'
12+
13+
- template: 'templates/job_generator.yml'
14+
parameters:
15+
platforms: ['linux', 'windows', 'mac']
16+
python_versions: ['3.7']
17+
tf_versions: ['1.13.1']
918
job:
1019
steps:
1120
- template: 'unit_test.yml'

ci_build/azure_pipelines/unit_test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
jobs:
44
- template: 'templates/job_generator.yml'
55
parameters:
6-
python_versions: ['3.6', '3.5']
6+
python_versions: ['3.7', '3.6', '3.5']
77
tf_versions: ['1.13.1']
88
job:
99
steps:

tests/test_backend.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,42 @@ def test_slice(self):
945945
_ = tf.identity(x_, name=_TFOUTPUT)
946946
self._run_test_case([_OUTPUT], {_INPUT: x_val})
947947

948+
@check_opset_min_version(10, "Slice in opset 10 can accept dymaic 'start' and 'ends'")
949+
def test_slice_with_non_const(self):
950+
x_val = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.float32)
951+
t1 = np.array([0, 1], dtype=np.int32)
952+
t2 = np.array([2, 2], dtype=np.int32)
953+
x0 = tf.placeholder(tf.float32, x_val.shape, name=_TFINPUT)
954+
t1_ = tf.placeholder(tf.int32, t1.shape, name=_TFINPUT1)
955+
t2_ = tf.placeholder(tf.int32, t2.shape, name=_TFINPUT2)
956+
x_ = tf.slice(x0, t1_, t2_)
957+
_ = tf.identity(x_, name=_TFOUTPUT)
958+
self._run_test_case([_OUTPUT], {_INPUT: x_val, _INPUT1: t1, _INPUT2: t2})
959+
960+
@check_opset_min_version(10, "Slice in opset 10 can accept dymaic 'start' and 'ends'")
961+
def test_slice_with_size_is_negative_one(self):
962+
x_val = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.float32)
963+
t1 = np.array([0, 1], dtype=np.int32)
964+
# input "size" contains -1
965+
t2 = np.array([2, -1], dtype=np.int32)
966+
x0 = tf.placeholder(tf.float32, x_val.shape, name=_TFINPUT)
967+
t1_ = tf.placeholder(tf.int32, t1.shape, name=_TFINPUT1)
968+
t2_ = tf.placeholder(tf.int32, t2.shape, name=_TFINPUT2)
969+
x_ = tf.slice(x0, t1_, t2_)
970+
_ = tf.identity(x_, name=_TFOUTPUT)
971+
self._run_test_case([_OUTPUT], {_INPUT: x_val, _INPUT1: t1, _INPUT2: t2})
972+
973+
@skip_caffe2_backend()
974+
def test_slice1(self):
975+
# FIXME: only 1 dimension supported by caffe2 and msrt
976+
x_val = np.array([[[1, 1, 1], [2, 2, 2]], [[3, 3, 3], [4, 4, 4]], [[5, 5, 5], [6, 6, 6]]], dtype=np.float32)
977+
t1 = tf.constant([1, 0, 0], dtype=tf.int32)
978+
t2 = tf.constant([1, 1, 3], dtype=tf.int32)
979+
x0 = tf.placeholder(tf.float32, x_val.shape, name=_TFINPUT)
980+
x_ = tf.slice(x0, t1, t2)
981+
_ = tf.identity(x_, name=_TFOUTPUT)
982+
self._run_test_case([_OUTPUT], {_INPUT: x_val})
983+
948984
def test_split(self):
949985
x_val = np.linspace(1.0, 5 * 30.0, 5 * 30).astype(np.float32).reshape(5, 30)
950986
x0 = tf.placeholder(tf.float32, x_val.shape, name=_TFINPUT)
@@ -1103,17 +1139,6 @@ def test_reducemean(self):
11031139
_ = tf.identity(x_, name=_TFOUTPUT)
11041140
self._run_test_case([_OUTPUT], {_INPUT: x_val})
11051141

1106-
@unittest.skip("")
1107-
def test_slice1(self):
1108-
# FIXME: only 1 dimension supported by caffe2 and msrt
1109-
x_val = np.array([[[1, 1, 1], [2, 2, 2]], [[3, 3, 3], [4, 4, 4]], [[5, 5, 5], [6, 6, 6]]], dtype=np.float32)
1110-
t1 = tf.constant([1, 0, 0], dtype=tf.int32)
1111-
t2 = tf.constant([1, 1, 3], dtype=tf.int32)
1112-
x0 = tf.placeholder(tf.float32, x_val.shape, name=_TFINPUT)
1113-
x_ = tf.slice(x0, t1, t2)
1114-
_ = tf.identity(x_, name=_TFOUTPUT)
1115-
self._run_test_case([_OUTPUT], {_INPUT: x_val})
1116-
11171142
@skip_caffe2_backend()
11181143
@check_onnxruntime_incompatibility("Pow")
11191144
def test_pow_scalar(self):

tf2onnx/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from __future__ import print_function
77
from __future__ import unicode_literals
88

9-
__all__ = ["utils", "graph_matcher", "graph", "loader", "tfonnx", "shape_inference", "schemas"]
9+
__all__ = ["utils", "graph_matcher", "graph", "graph_builder", "loader", "tfonnx", "shape_inference", "schemas"]
1010

1111
from .version import version as __version__
1212
from . import verbose_logging as logging
13-
from tf2onnx import tfonnx, utils, graph, graph_matcher, shape_inference, schemas # pylint: disable=wrong-import-order
13+
from tf2onnx import tfonnx, utils, graph, graph_builder, graph_matcher, shape_inference, schemas # pylint: disable=wrong-import-order

tf2onnx/graph.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from tf2onnx.utils import port_name, find_opset
2121
from tf2onnx import optimizer
2222
from tf2onnx.schemas import get_schema, infer_onnx_shape_dtype
23+
from tf2onnx import constants
2324

2425
logger = logging.getLogger(__name__)
2526

@@ -419,7 +420,8 @@ def make_const(self, name, np_val, skip_conversion=False, raw=True):
419420
return node
420421

421422
def make_node(self, op_type, inputs, attr=None, output_count=1, outputs=None, skip_conversion=True,
422-
op_name_scope=None, name=None, shapes=None, dtypes=None, domain=None, infer_shape_dtype=True):
423+
op_name_scope=None, name=None, shapes=None, dtypes=None, domain=constants.ONNX_DOMAIN,
424+
infer_shape_dtype=True):
423425
"""Make a new onnx node in the graph"""
424426
if attr is None:
425427
attr = {}

tf2onnx/graph_builder.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT license.
3+
4+
"""
5+
tf2onnx.graph_helper - class to help building graph, such as helping to make complex node
6+
"""
7+
8+
import numpy as np
9+
from tf2onnx import utils, logging
10+
11+
12+
# pylint: disable=missing-docstring
13+
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
class GraphBuilder(object):
19+
"""help to build graph"""
20+
def __init__(self, graph):
21+
self._g = graph
22+
23+
@property
24+
def graph(self):
25+
return self._g
26+
27+
def make_slice(self, kwargs, name=None, shapes=None, dtypes=None):
28+
"""
29+
slice changes its schema at opset 10: it treats some attributes as dynamic input
30+
so this function has to process inputs according to graph's opset version
31+
to get "inputs" and "attr" to feed "make_node"
32+
kwargs: key could be ["data", "starts", "ends", "axes", "steps", "outputs"].
33+
"""
34+
outputs = kwargs.pop("outputs", None)
35+
36+
if self.graph.opset < 10:
37+
# "data" is string
38+
# "starts", "ends" and "axes" are attributes, and "axes" is optional.
39+
inputs = [kwargs.pop("data")]
40+
starts = self.convert_to_attribute(kwargs.pop("starts"))
41+
ends = self.convert_to_attribute(kwargs.pop("ends"))
42+
axes = self.convert_to_attribute(kwargs.pop("axes", None), is_optional=True)
43+
attr = {"starts": starts, "ends": ends, "axes": axes}
44+
else:
45+
# slice-10 has 3 required inputs "data", "starts", "ends"l
46+
# and 2 optional inputs "axes", "steps"
47+
# input sequence should be "data", "starts", "ends", "axes", "steps"
48+
attr = {}
49+
data = self.convert_to_input(kwargs.pop("data"))
50+
starts = self.convert_to_input(kwargs.pop("starts"))
51+
ends = self.convert_to_input(kwargs.pop("ends"))
52+
axes = self.convert_to_input(kwargs.pop("axes", None), is_optional=True)
53+
steps = self.convert_to_input(kwargs.pop("steps", None), is_optional=True)
54+
inputs = [data, starts, ends, axes, steps]
55+
56+
# pro-process inputs and attr
57+
if kwargs:
58+
logger.warning("kwargs contains un-used key")
59+
60+
new_attr = {}
61+
for key, val in attr.items():
62+
if val is not None:
63+
new_attr[key] = val
64+
attr = new_attr
65+
66+
for ind, val in enumerate(inputs):
67+
if val is None:
68+
inputs[ind] = "" # empty string means no connection in ONNX
69+
# remove tailing ""
70+
while inputs[-1] == "":
71+
inputs = inputs[:-1]
72+
73+
return self.graph.make_node(op_type="Slice", inputs=inputs, attr=attr, name=name,
74+
outputs=outputs, shapes=shapes, dtypes=dtypes).output[0]
75+
76+
def convert_to_input(self, tensor, is_optional=False):
77+
"""in ONNX, input shold come from node, so it must be a string"""
78+
if is_optional and tensor is None:
79+
return None
80+
81+
utils.make_sure(tensor is not None, "input is required so it couldn't be None")
82+
83+
res = tensor
84+
if isinstance(tensor, list):
85+
res = self.graph.make_const(utils.make_name("const_slice"), np.array(tensor)).output[0]
86+
87+
utils.make_sure(isinstance(res, str), "input is a dynamic input, so a str is needed")
88+
89+
return res
90+
91+
def convert_to_attribute(self, tensor, is_optional=False):
92+
if is_optional and tensor is None:
93+
return None
94+
95+
utils.make_sure(tensor is not None, "input is required so it couldn't be None")
96+
97+
res = tensor
98+
if isinstance(tensor, str):
99+
const_node = self.graph.get_node_by_output(tensor)
100+
res = const_node.get_tensor_value(as_list=True)
101+
102+
utils.make_sure(isinstance(res, list), "input is an attr, so a list is needed")
103+
104+
return res

tf2onnx/onnx_opset/nn.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from onnx import onnx_pb
1616
from onnx.onnx_pb import TensorProto
1717
from tf2onnx import constants, utils
18+
from tf2onnx.graph_builder import GraphBuilder
1819
from tf2onnx.handler import tf_op
1920
from tf2onnx.onnx_opset import common, controlflow, tensor
2021

@@ -463,6 +464,14 @@ def version_7(cls, ctx, node, **kwargs):
463464

464465
@classmethod
465466
def version_9(cls, ctx, node, **kwargs):
467+
cls._convert_since_9(ctx, node, **kwargs)
468+
469+
@classmethod
470+
def version_10(cls, ctx, node, **kwargs):
471+
cls._convert_since_9(ctx, node, **kwargs)
472+
473+
@classmethod
474+
def _convert_since_9(cls, ctx, node, **kwargs):
466475
# float32 out = ResizeBilinear/ResizeNearestNeighbor(T images, int size)
467476
# https://www.tensorflow.org/api_docs/python/tf/image/resize_nearest_neighbor
468477
# wants the input to be NHWC - adjust target_shape to this.
@@ -481,8 +490,10 @@ def version_9(cls, ctx, node, **kwargs):
481490
scales = ctx.make_const(utils.make_name("scales"), scale_val, raw=False)
482491
else:
483492
ori_shape = ctx.make_node("Shape", [node.input[0]])
484-
ori_shape_hw = ctx.make_node("Slice", ori_shape.output, {"axes": [0], "starts": [1], "ends": [3]})
485-
ori_shape_hw_float = ctx.make_node("Cast", ori_shape_hw.output, attr={"to": onnx_pb.TensorProto.FLOAT})
493+
attr = {"axes": [0], "starts": [1], "ends": [3]}
494+
inputs_map = {"data": ori_shape.output[0], **attr}
495+
ori_shape_hw = GraphBuilder(ctx).make_slice(inputs_map)
496+
ori_shape_hw_float = ctx.make_node("Cast", [ori_shape_hw], attr={"to": onnx_pb.TensorProto.FLOAT})
486497

487498
target_hw = node.inputs[1]
488499
target_hw_float = ctx.make_node("Cast", target_hw.output, attr={"to": onnx_pb.TensorProto.FLOAT})
@@ -538,12 +549,13 @@ def version_7(cls, ctx, node, **kwargs):
538549
new_line = g.make_node(op_type="Concat", inputs=[const_zero_bool.output[0], "line"],
539550
attr={"axis": counter_axis},
540551
dtypes=[onnx_pb.TensorProto.BOOL])
541-
slice_node = g.make_node(op_type="Slice", inputs=[new_line.output[0]],
542-
attr={"axes": [counter_axis], "starts": [0], "ends": [-1]})
552+
attr = {"axes": [counter_axis], "starts": [0], "ends": [-1]}
553+
inputs_map = {"data": new_line.output[0], **attr}
554+
slice_node = GraphBuilder(g).make_slice(inputs_map)
543555

544556
g.make_node("Identity", ["cond"], outputs=["cond_out"])
545557
g.make_node("Identity", ["line"], outputs=["res"])
546-
g.make_node("Identity", [slice_node.output[0]], outputs=["line_out"])
558+
g.make_node("Identity", [slice_node], outputs=["line_out"])
547559

548560
g.add_graph_input("trip", onnx_pb.TensorProto.INT64, [])
549561
g.add_graph_input("cond", onnx_pb.TensorProto.BOOL, [])

0 commit comments

Comments
 (0)