Repository: PaddlePaddle/Paddle2ONNX

Issue:
Enable the conversion of MobileNet & SE_ResNeXt model



Discussion:

Resolving patch:
diff --git a/convert.py b/convert.py
--- a/convert.py
+++ b/convert.py
@@ -15,6 +15,7 @@
 import os
 import argparse
 
+from fluid.utils import op_io_info
 from onnx import helper, checker
 import paddle.fluid as fluid
 
@@ -28,7 +29,11 @@ def parse_args():
     parser.add_argument(
         "--fluid_model", required=True, help="Input PaddlePaddle Fluid model.")
     parser.add_argument(
-        "--onnx_model", required=False, help="The path to save ONNX model.")
+        "--onnx_model", required=True, help="The path to save ONNX model.")
+    parser.add_argument(
+        "--to_print_model",
+        action='store_true',
+        help="To print converted ONNX model.")
     args = parser.parse_args()
     return args
 
@@ -68,15 +73,6 @@ def convert(args):
             for v in feed_target_names
         ]
 
-        # Create outputs
-        fetch_target_names = [
-            fetch_target.name for fetch_target in fetch_targets
-        ]
-        outputs = [
-            paddle_variable_to_onnx_tensor(v, global_block)
-            for v in fetch_target_names
-        ]
-
         # Create nodes
         for block in inference_program.blocks:
             for op in block.ops:
@@ -84,7 +80,7 @@ def convert(args):
                     # TODO(kuke): deal with the corner case that vars in 
                     #     different blocks have the same name
                     node_proto = ops.node_maker[op.type](operator=op,
-                                                         scope=inference_scope)
+                                                         block=block)
 
                     if isinstance(node_proto, tuple):
                         onnx_nodes.extend(list(node_proto))
@@ -95,6 +91,21 @@ def convert(args):
                         raise NotImplementedError("OP[%s] is not supported in "
                                                   "the converter!" % op.type)
 
+        # Create outputs
+        fetch_target_names = [
+            fetch_target.name for fetch_target in fetch_targets
+        ]
+        # Get the new names for outputs if they've renamed in nodes' making
+        renamed_outputs = op_io_info.get_all_renamed_outputs()
+        fetch_target_names = [
+            name if name not in renamed_outputs else renamed_outputs[name]
+            for name in fetch_target_names
+        ]
+        outputs = [
+            paddle_variable_to_onnx_tensor(v, global_block)
+            for v in fetch_target_names
+        ]
+
         # Make graph
         model_name = os.path.basename(args.fluid_model.strip('/')).split('.')[0]
         onnx_graph = helper.make_graph(onnx_nodes, model_name, inputs, outputs)
@@ -106,7 +117,8 @@ def convert(args):
         checker.check_model(onnx_model)
 
         # Print model
-        print("The converted model is:\n{}".format(onnx_model))
+        if args.to_print_model:
+            print("The converted model is:\n{}".format(onnx_model))
 
         # Save converted model
         if args.onnx_model is not None:
diff --git a/fluid/utils.py b/fluid/utils.py
--- a/fluid/utils.py
+++ b/fluid/utils.py
@@ -12,12 +12,66 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from compiler.ast import flatten
 
-def get_op_io_info(op):
-    inputs = dict([(name, op.input(name)) for name in op.input_names])
-    attrs = dict(
-        [(name, op.attr(name))
-         for name in op.attr_names]) if op.attr_names is not None else None
-    outputs = dict([(name, op.output(name)) for name in op.output_names])
 
-    return inputs, attrs, outputs
+class OpIOsInfo():
+    """Return inputs/outputs information for an operator, and resolve potential 
+       name conflicts in ONNX graph.
+    """
+
+    def __init__(self):
+        self._all_renamed_outputs = {}
+        self._renamed_cnt = 0
+
+    def _get_new_name(self, arg):
+        """Get the new name for an argument.
+        """
+
+        self._renamed_cnt += 1
+        return arg + '@dup_' + str(self._renamed_cnt)
+
+    def _rename_input_args(self):
+        """Rename input arguments if their previous output arugments have been 
+           renamed.
+        """
+
+        for in_name in self.inputs:
+            if self.inputs[in_name][0] in self._all_renamed_outputs:
+                self.inputs[in_name][0] = self._all_renamed_outputs[self.inputs[
+                    in_name][0]]
+
+    def _rename_output_args(self):
+        """Rename output arguments if they have same names with the input 
+           arguments.
+        """
+
+        input_args = flatten(self.inputs.values())
+        for out_name in self.outputs:
+            if self.outputs[out_name][0] in input_args:
+                new_name = self._get_new_name(self.outputs[out_name][0])
+                self._all_renamed_outputs[self.outputs[out_name][0]] = new_name
+                self.outputs[out_name][0] = new_name
+
+    def get_all_renamed_outputs(self):
+        """Get all the renamed outputs in history.
+        """
+
+        return self._all_renamed_outputs
+
+    def __call__(self, op):
+        self.inputs = dict([(name, op.input(name)) for name in op.input_names])
+        self.attrs = dict(
+            [(name, op.attr(name))
+             for name in op.attr_names]) if op.attr_names is not None else None
+        self.outputs = dict(
+            [(name, op.output(name)) for name in op.output_names])
+
+        self._rename_input_args()
+        self._rename_output_args()
+
+        return self.inputs, self.attrs, self.outputs
+
+
+# Instantiate the class to a callable object
+op_io_info = OpIOsInfo()
diff --git a/fluid_onnx/ops.py b/fluid_onnx/ops.py
--- a/fluid_onnx/ops.py
+++ b/fluid_onnx/ops.py
@@ -13,9 +13,10 @@
 # limitations under the License.
 
 import sys
+from onnx import TensorProto
 from onnx.helper import make_node, make_tensor
 from paddle.fluid.executor import fetch_var
-from fluid.utils import get_op_io_info
+from fluid.utils import op_io_info
 from fluid_onnx.variables import PADDLE_TO_ONNX_DTYPE
 """
 Priority of ops (uniques) to figure out support for.
@@ -59,16 +60,6 @@ def abs_op():
     pass
 
 
-def add_op(operator, scope):
-    inputs, attrs, outputs = get_op_io_info(operator)
-    return make_node(
-        'Add',
-        inputs=inputs['X'] + inputs['Y'],
-        outputs=outputs['Out'],
-        axis=attrs['axis'],
-        broadcast=1)
-
-
 def and_op():
     """
     Need to support broadcast.
@@ -91,17 +82,40 @@ def averagepool_op():
     pass
 
 
-def batchnorm_op(operator, scope):
-    inputs, attrs, outputs = get_op_io_info(operator)
-    bn_op = make_node(
+def batch_norm_op(operator, block):
+    inputs, attrs, outputs = op_io_info(operator)
+
+    x_shape = block.vars[inputs['X'][0]].shape
+    reshape_node = None
+    if len(x_shape) == 2:
+        reshaped_x = [inputs['X'][0] + '@reshape_0']
+        new_shape = [0, x_shape[1], 1, 1]
+        new_shape_name = [inputs['X'][0] + '@shape_tensor_0']
+        new_shape_node = make_node(
+            'Constant',
+            inputs=[],
+            outputs=new_shape_name,
+            value=make_tensor(
+                name=new_shape_name[0],
+                data_type=TensorProto.INT64,
+                dims=(4, ),
+                vals=new_shape))
+        reshape_node = make_node(
+            'Reshape', inputs=inputs['X'] + new_shape_name, outputs=reshaped_x)
+    else:
+        reshaped_x = inputs['X']
+
+    bn_node = make_node(
         'BatchNormalization',
-        inputs=inputs['X'] + inputs['Scale'] + inputs['Bias'] + inputs['Mean'] +
+        inputs=reshaped_x + inputs['Scale'] + inputs['Bias'] + inputs['Mean'] +
         inputs['Variance'],
         outputs=outputs['Y'],
         is_test=attrs['is_test'],
         epsilon=attrs['epsilon'],
         momentum=attrs['momentum'])
-    return bn_op
+
+    return bn_node if reshape_node is None else (new_shape_node, reshape_node,
+                                                 bn_node)
 
 
 def cast_op():
@@ -134,11 +148,10 @@ def constant_op(var, scope):
     return constant_node
 
 
-def conv2d_op(operator, scope):
-    inputs, attrs, outputs = get_op_io_info(operator)
-    kernel_shape = fetch_var(
-        operator.input('Filter')[0].decode('string_escape'), scope).shape
+def conv2d_op(operator, block):
+    inputs, attrs, outputs = op_io_info(operator)
 
+    kernel_shape = block.vars[inputs['Filter'][0]].shape
     conv2d = make_node(
         'Conv',
         inputs=inputs['Input'] + inputs['Filter'],
@@ -163,19 +176,50 @@ def div_op():
     pass
 
 
-def dropout_op():
-    pass
+def dropout_op(operator, block):
+    inputs, attrs, outputs = op_io_info(operator)
+    scale_input = [outputs['Out'][0] + '@dropout']
+    dropout_op = make_node(
+        'Dropout',
+        inputs=inputs['X'],
+        outputs=scale_input + outputs['Mask'],
+        is_test=attrs['is_test'],
+        ratio=attrs['dropout_prob'])
 
+    # Fluid and ONNX use different dropout formula
+    scale_op = make_node(
+        'Scale',
+        inputs=scale_input,
+        outputs=outputs['Out'],
+        scale=1.0 - attrs['dropout_prob'])
+    return (dropout_op, scale_op)
 
-def elu_op():
-    pass
 
+def elementwise_add_op(operator, block):
+    inputs, attrs, outputs = op_io_info(operator)
+    return make_node(
+        'Add',
+        inputs=inputs['X'] + inputs['Y'],
+        outputs=outputs['Out'],
+        axis=attrs['axis'],
+        broadcast=1)
 
-def equal_op():
+
+def elementwise_mul_op(operator, block):
+    inputs, attrs, outputs = op_io_info(operator)
+    return make_node(
+        'Mul',
+        inputs=inputs['X'] + inputs['Y'],
+        outputs=outputs['Out'],
+        axis=attrs['axis'],
+        broadcast=1)
+
+
+def elu_op():
     pass
 
 
-def dropout_op():
+def equal_op():
     pass
 
 
@@ -263,8 +307,8 @@ def lppool_op():
     pass
 
 
-def mul_op(operator, scope):
-    inputs, attrs, outputs = get_op_io_info(operator)
+def mul_op(operator, block):
+    inputs, attrs, outputs = op_io_info(operator)
 
     # Flatten input(X) and input(Y) into 2-D matries
     x_flat_out = [inputs['X'][0] + '@flatten_0']
@@ -371,8 +415,8 @@ def pad_op():
     pass
 
 
-def pool2d_op(operator, scope):
-    inputs, attrs, outputs = get_op_io_info(operator)
+def pool2d_op(operator, block):
+    inputs, attrs, outputs = op_io_info(operator)
     if attrs['global_pooling'] is False:
         op_type = {'max': 'MaxPool', 'avg': 'AveragePool'}
         pool2d = make_node(
@@ -459,8 +503,8 @@ def reducesumsquare_op():
     pass
 
 
-def relu_op(operator, scope):
-    inputs, _, outputs = get_op_io_info(operator)
+def relu_op(operator, block):
+    inputs, _, outputs = op_io_info(operator)
     return make_node('Relu', inputs=inputs['X'], outputs=outputs['Out'])
 
 
@@ -476,8 +520,9 @@ def shape_op():
     pass
 
 
-def sigmoid_op():
-    pass
+def sigmoid_op(operator, block):
+    inputs, _, outputs = op_io_info(operator)
+    return make_node('Sigmoid', inputs=inputs['X'], outputs=outputs['Out'])
 
 
 def size_op():
@@ -488,8 +533,8 @@ def slice_op():
     pass
 
 
-def softmax_op(operator, scope):
-    inputs, attrs, outputs = get_op_io_info(operator)
+def softmax_op(operator, block):
+    inputs, attrs, outputs = op_io_info(operator)
     return make_node('Softmax', inputs=inputs['X'], outputs=outputs['Out'])
 
 
@@ -525,8 +570,8 @@ def sum_op():
     pass
 
 
-def tanh_op(operator, scope):
-    inputs, attrs, outputs = get_op_io_info(operator)
+def tanh_op(operator, block):
+    inputs, attrs, outputs = op_io_info(operator)
     return make_node('Tanh', inputs=inputs['X'], outputs=outputs['Out'])
 
 
@@ -560,13 +605,14 @@ def xor_op():
 node_maker = {
     # Paddle op name : (ONNX op name, modifier)
     'abs': ('Abs', abs_op),
-    'elementwise_add': add_op,
+    'elementwise_add': elementwise_add_op,
+    'elementwise_mul': elementwise_mul_op,
 
     # '': 'And', # ?
     # 'ArgMax', NEEDS ATTENTION.
     # 'ArgMin', NEEDS ATTENTION.
     '': ('AveragePool', averagepool_op),
-    'batch_norm': batchnorm_op,
+    'batch_norm': batch_norm_op,
     'cast': ('Cast', cast_op),
     # 'Ceil', NEEDS ATTENTION.
     'cast': ('Clip', clip_op),
@@ -577,8 +623,9 @@ def xor_op():
     # Need to continue the mapping below.
     '': 'ConvTranspose',
     '': 'DepthToSpace',
+    'depthwise_conv2d': conv2d_op,
     '': 'Div',
-    '': 'Dropout',
+    'dropout': dropout_op,
     '': 'Elu',
     '': 'Equal',
     '': 'Exp',
@@ -636,7 +683,7 @@ def xor_op():
     '': 'Reshape',
     # 'Selu', NEEDS ATTENTION.
     '': 'Shape',
-    '': 'Sigmoid',
+    'sigmoid': sigmoid_op,
     '': 'Size',
     # 'Slice', NEEDS ATTENTION.
     'softmax': softmax_op,


Test patch:
diff --git a/tests/op_test.py b/tests/op_test.py
--- a/tests/op_test.py
+++ b/tests/op_test.py
@@ -28,7 +28,6 @@
 
 from fluid_onnx.ops import node_maker
 from fluid_onnx.variables import paddle_variable_to_onnx_tensor
-
 """
 NOTE (varunarora): Some of the code snippets below have been inspired from
 op_test.py in /python/paddle/fluid/tests/unittests/ in the original
@@ -37,6 +36,7 @@
 When in doubt, keep in sync with it's counterparts.
 """
 
+
 def append_input_output(block, op_proto, np_list, persistable_list, is_input):
     """Returns a list of Paddle variables associated with a block.
 
@@ -50,12 +50,14 @@ def append_input_output(block, op_proto, np_list, persistable_list, is_input):
     Returns:
         A dict of variable names -> Paddle variable instances.
     """
+
     # A list of expected inputs and outputs, as desired by Paddle's
     # C++ runtime.
     proto_list = op_proto.inputs if is_input else op_proto.outputs
 
     def create_var(block, name, np_list, var_proto):
-        """Creates a Paddle var in the given block and C++ proto type"""
+        """Creates a Paddle var in the given block and C++ proto type.
+        """
 
         # If the expected variable is not found is in the provided list
         # of variables, make an assertion. Else, determine the shape and
@@ -87,10 +89,9 @@ def create_var(block, name, np_list, var_proto):
         var_name = str(var_proto.name)
 
         # If these are inputs, and the expected input is not necessary
-        # are is not provided in the list of inputs, we move on to the next
-        # expected. input.
-        # If not, we make sure it the expected input is provided, or that it
-        # is unnecessary.
+        # and not provided in the list of inputs, we move on to the next
+        # expected input. If not, we make sure it the expected input is 
+        # provided, or that it is unnecessary.
         if is_input:
             if (var_name not in np_list) and var_proto.dispensable:
                 continue
@@ -113,6 +114,27 @@ def create_var(block, name, np_list, var_proto):
     return var_dict
 
 
+def create_tensor(np_value, place):
+    """Create a LoDTensor initialized by the numpy ndarray.
+
+    Args: 
+        np_value (ndarray|tuple): The numpy ndarry to initialize the tensor, 
+                                  in tuple (value, LoD) when LoD is given.
+        place (CPUPlace|CUDAPlace): The place for the tensor.
+    Return:
+        The created LoDTensor.
+    """
+
+    tensor = core.LoDTensor()
+    if isinstance(np_value, tuple):
+        tensor.set(np_value[0], place)
+        tensor.set_lod(np_value[1])
+    else:
+        tensor.set(np_value, place)
+
+    return tensor
+
+
 class OpTest(unittest.TestCase):
     """Evaluates an op maker's validity.
 
@@ -131,30 +153,22 @@ class OpTest(unittest.TestCase):
 
         Additionally, custom attributes to the op.
     """
+
     def feed_var(self, input_vars, place):
         """Returns a dictionary of variable names -> initialized tensors.
 
         It sets tensors' execution place set (CPU or GPU), and Level of
         Details (LoD) using this info from the numpy values.
         """
+
         feed_map = {}
         for var_name in input_vars:
             if isinstance(input_vars[var_name], list):
                 for name, np_value in self.inputs[var_name]:
-                    tensor = core.LoDTensor()
-                    if isinstance(np_value, tuple):
-                        tensor.set(np_value[0], place)
-                        tensor.set_lod(np_value[1])
-                    else:
-                        tensor.set(np_value, place)
+                    tensor = create_tensor(np_value, place)
                     feed_map[name] = tensor
             else:
-                tensor = core.LoDTensor()
-                if isinstance(self.inputs[var_name], tuple):
-                    tensor.set(self.inputs[var_name][0], place)
-                    tensor.set_lod(self.inputs[var_name][1])
-                else:
-                    tensor.set(self.inputs[var_name], place)
+                tensor = create_tensor(self.inputs[var_name], place)
                 feed_map[var_name] = tensor
 
         return feed_map
@@ -164,14 +178,15 @@ def eval_fluid_op(self):
 
         Returns the output values after running.
         """
+
         op_proto = OpProtoHolder.instance().get_op_proto(self.op_type)
 
         # Create a new paddle scope and program.
         place = core.CPUPlace()
         exe = Executor(place)
-        self.scope = core.Scope()
+        scope = core.Scope()
 
-        with scope_guard(self.scope):
+        with scope_guard(scope):
             program = Program()
             self.block = program.global_block()
 
@@ -212,7 +227,6 @@ def eval_fluid_op(self):
 
             outs = exe.run(program,
                            feed=self.feed_map,
-                           scope=self.scope,
                            fetch_list=self.fetch_list,
                            return_numpy=True)
         return outs
@@ -224,6 +238,7 @@ def eval_onnx_node(self):
         ONNX ops and prepare the inputs and output values based on ONNX
         compatibility.
         """
+
         # Convert inputs and outputs to ONNX tensors.
         # Use the Paddle fetch_list to prepare the outputs.
         inputs = [
@@ -239,7 +254,7 @@ def eval_onnx_node(self):
         ]
 
         # Construct the ONNX model using paddle-onnx.
-        onnx_node = node_maker[self.op_type](operator=self.op, scope=self.scope)
+        onnx_node = node_maker[self.op_type](operator=self.op, block=self.block)
         node_list = list(onnx_node) if isinstance(onnx_node,
                                                   tuple) else [onnx_node]
         for node in node_list:
@@ -261,6 +276,7 @@ def check_output(self, decimal=5):
 
         Compares accuracy at a precision of 5 decimal places by default.
         """
+
         fluid_result = self.eval_fluid_op()
         onnx_result = self.eval_onnx_node()
 
diff --git a/tests/test_conv2d_op.py b/tests/test_conv2d_op.py
--- a/tests/test_conv2d_op.py
+++ b/tests/test_conv2d_op.py
@@ -19,7 +19,7 @@
 
 class TestConv2dOp(OpTest):
     def setUp(self):
-        self.op_type = "conv2d"
+        self.init_conv_type()
         self.use_cudnn = False
         self.use_mkldnn = False
         self.dtype = np.float32
@@ -56,9 +56,17 @@ def setUp(self):
         output = np.zeros((1, 1, 1, 1))
         self.outputs = {'Output': output}
 
+    def init_conv_type(self):
+        self.op_type = "conv2d"
+
     def test_check_output(self):
         self.check_output(decimal=5)
 
 
+class TestDepthwiseConv2dOp(TestConv2dOp):
+    def init_conv_type(self):
+        self.op_type = "depthwise_conv2d"
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_dropout_op.py b/tests/test_dropout_op.py
new file mode 100644
--- /dev/null
+++ b/tests/test_dropout_op.py
@@ -0,0 +1,32 @@
+#   Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import numpy as np
+from op_test import OpTest
+
+
+class TestDropoutOp(OpTest):
+    def setUp(self):
+        self.op_type = "dropout"
+        self.inputs = {'X': np.random.random((32, 64, 2)).astype("float32")}
+        self.attrs = {'dropout_prob': 0.8, 'is_test': True}
+        self.outputs = {'Out': np.zeros((1, 1))}
+
+    def test_check_output(self):
+        self.check_output()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/test_elementwise_mul_op.py b/tests/test_elementwise_mul_op.py
new file mode 100644
--- /dev/null
+++ b/tests/test_elementwise_mul_op.py
@@ -0,0 +1,46 @@
+#  Copyright (c) 2018 PaddlePaddle Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import numpy as np
+from op_test import OpTest
+
+
+class TestElementwiseMulOp(OpTest):
+    def setUp(self):
+        self.op_type = "elementwise_mul"
+        self.inputs = {
+            'X': np.random.uniform(0.1, 1, [11, 13]).astype(np.float32),
+            'Y': np.random.uniform(0.1, 1, [11, 13]).astype(np.float32)
+        }
+        self.outputs = {'Out': np.zeros((1, 1))}
+
+    def test_check_output(self):
+        self.check_output()
+
+
+class TestElementwiseMulOp_broadcast(TestElementwiseMulOp):
+    def setUp(self):
+        self.op_type = "elementwise_mul"
+        self.inputs = {
+            'X': np.random.rand(2, 3, 4, 5).astype(np.float64),
+            'Y': np.random.rand(3, 4).astype(np.float64)
+        }
+
+        self.attrs = {'axis': 1}
+        self.outputs = {'Out': np.zeros((1, 1))}
+
+
+if __name__ == '__main__':
+    unittest.main()