Caffe2 - Python API
A deep learning, cross platform ML framework
frontend.py
1 ## @package onnx
2 # Module caffe2.python.onnx.frontend
3 
4 """Caffe2 Protobuf to ONNX converter
5 
6 To run this, you will need to have Caffe2 installed as well.
7 """
8 
9 from __future__ import absolute_import
10 from __future__ import division
11 from __future__ import print_function
12 from __future__ import unicode_literals
13 
14 import itertools
15 import collections
16 import logging
17 import re
18 
19 from caffe2.python import core as caffe2_core
20 from caffe2.proto import caffe2_legacy_pb2
21 from enum import Enum
22 from onnx import (defs, checker, helper, numpy_helper, mapping,
23  ModelProto, GraphProto, NodeProto, AttributeProto, TensorProto, OperatorSetIdProto)
24 from onnx.helper import make_tensor, make_tensor_value_info, make_attribute, make_model
25 import numpy as np
26 
27 from caffe2.python.onnx.helper import c2_native_run_net, dummy_name
28 from caffe2.python.onnx.error import Unsupported
29 
31 
32 logging.basicConfig(level=logging.INFO)
33 logger = logging.getLogger(__name__)
34 
35 
36 class Caffe2Frontend(object):
37  # This number controls the semantics of the operators we target. Whenever
38  # ONNX makes a BC breaking change to semantics of operators, having this set
39  # to an accurate number will prevent our models form exporting. However,
40  # we should strive to keep this up-to-date as much as possible.
41  target_opset_version = 6
42 
43  _renamed_operators = {
44  'SpatialBN': 'BatchNormalization',
45  'Conv1D': 'Conv',
46  'Conv2D': 'Conv',
47  'Conv3D': 'Conv',
48  'ConvTranspose1D': 'ConvTranspose',
49  'ConvTranspose2D': 'ConvTranspose',
50  'ConvTranspose3D': 'ConvTranspose',
51  'MaxPool1D': 'MaxPool',
52  'MaxPool2D': 'MaxPool',
53  'MaxPool3D': 'MaxPool',
54  'AveragePool1D': 'AveragePool',
55  'AveragePool2D': 'AveragePool',
56  'AveragePool3D': 'AveragePool',
57  }
58 
59  # caffe2 arguments that are completely removed in onnx
60  _blacklist_caffe2_args = {
61  'order': {b'NCHW'},
62  'cudnn_exhaustive_search': {0, 1},
63  'use_cudnn': {0, 1},
64  }
65 
66  _global_renamed_args = {
67  'kernels': 'kernel_shape',
68  }
69 
70  _per_op_renamed_args = {
71  'Squeeze': {'dims': 'axes'},
72  'Transpose': {'axes': 'perm'},
73  }
74 
75  _special_operators = {}
76 
77  @classmethod
78  def _common_caffe2_arg_to_onnx_attr(cls, op_def, arg):
79  # name
80  op_type = op_def.type
81  if op_type in cls._per_op_renamed_args:
82  name = cls._per_op_renamed_args[op_type].get(
83  arg.name, arg.name)
84  else:
85  name = cls._global_renamed_args.get(arg.name, arg.name)
86 
87  # value
88  if arg.HasField('f'):
89  value = arg.f
90  elif arg.HasField('i'):
91  value = arg.i
92  elif arg.HasField('s'):
93  value = arg.s
94  elif arg.floats:
95  value = arg.floats
96  elif arg.ints:
97  value = arg.ints
98  elif arg.strings:
99  value = arg.strings
100  else:
101  raise ValueError('Could not find data field in arg: {}'.format(arg))
102 
103  if name in cls._blacklist_caffe2_args:
104  assert value in cls._blacklist_caffe2_args[arg.name]
105  return None
106 
107  return helper.make_attribute(name, value)
108 
109  @classmethod
110  def caffe2_arg_to_onnx_attr(cls, op_def, arg):
111  return cls._common_caffe2_arg_to_onnx_attr(op_def, arg)
112 
113  @classmethod
114  def _common_caffe2_op_to_onnx_node(cls, op_def, shapes):
115  node_def = NodeProto()
116  node_def.name = op_def.name
117 
118  node_def.op_type = cls._renamed_operators.get(op_def.type, op_def.type)
119 
120  node_def.input.extend(op_def.input)
121  node_def.output.extend(op_def.output)
122 
123  attrs = filter(None, [cls.caffe2_arg_to_onnx_attr(op_def, arg)
124  for arg in op_def.arg])
125  node_def.attribute.extend(attrs)
126 
127  return node_def
128 
129  @classmethod
130  def caffe2_op_to_onnx_node(cls, op_def, shapes):
131  if C.support_onnx_export(op_def.type):
132  shape_list = list(shapes.values())
133  node_strs, tensor_strs = C.export_to_onnx(op_def.SerializeToString(), shapes)
134  nodes = []
135  for s in node_strs:
136  node = NodeProto()
137  node.ParseFromString(s)
138  nodes.append(node)
139  const_tensors = []
140  for s in tensor_strs:
141  tensor = TensorProto()
142  tensor.ParseFromString(s)
143  const_tensors.append(tensor)
144  return nodes, const_tensors
145  elif op_def.type in cls._special_operators:
146  translator = getattr(cls, cls._special_operators[op_def.type])
147  else:
148  translator = cls._common_caffe2_op_to_onnx_node
149  nodes = translator(op_def, shapes)
150  const_tensors = []
151  if isinstance(nodes, tuple):
152  nodes, const_tensors = nodes
153  if not isinstance(nodes, collections.Iterable):
154  nodes = [nodes]
155  return nodes, const_tensors
156 
157  @staticmethod
158  def _all_names_in_net(net):
159  if net is None:
160  return set()
161 
162  names = set()
163  names.update(net.external_input)
164  names.update(net.external_output)
165  for op in net.op:
166  names.update(op.input)
167  names.update(op.output)
168  return names
169 
170  @staticmethod
171  def _extract_value_info(tensor):
172  return make_tensor_value_info(
173  name=tensor.name,
174  elem_type=tensor.data_type,
175  shape=tensor.dims)
176 
177  @classmethod
178  def caffe2_net_to_onnx_graph(cls,
179  predict_net,
180  init_net=None,
181  value_info=None):
182  if value_info is None:
183  value_info = {}
184  if not isinstance(value_info, dict):
185  raise ValueError('Please pass value_info as a '
186  'name -> (type, shape) dictionary')
187 
188  cls._filter_fake_init(init_net, value_info)
189  cls._ssa_rewrite(predict_net, init_net, value_info)
190 
191  if init_net:
192  initializer = cls.caffe2_init_net_to_initializer(init_net)
193  value_info.update({init.name: (init.data_type, init.dims)
194  for init in initializer})
195  else:
196  initializer = []
197 
198  # Check whether we have got type shape info of all input
199  missing = (set(list(predict_net.external_input)) -
200  set(value_info.keys()))
201  if missing:
202  raise RuntimeError('Could not find value info of inputs: {}'.format(
203  ', '.join(missing)))
204 
205  inputs = {}
206  for name in predict_net.external_input:
207  elem_type, shape = value_info[name]
208  inputs[name] = np.random.randn(*shape).astype(
209  mapping.TENSOR_TYPE_TO_NP_TYPE[elem_type])
210 
211  ws, outputs = c2_native_run_net(
212  init_net,
213  predict_net,
214  inputs)
215 
216  for name in predict_net.external_output:
217  output = outputs[name]
218  elem_type = mapping.NP_TYPE_TO_TENSOR_TYPE[output.dtype]
219  shape = output.shape
220  value_info[name] = (elem_type, shape)
221 
222  graph_def = GraphProto()
223  graph_def.name = predict_net.name
224  graph_def.initializer.extend(initializer)
225  # This is a mapping from Caffe2 names to ONNX names
226  graph_def.input.extend(
227  make_tensor_value_info(
228  name=name,
229  elem_type=value_info[name][0],
230  shape=value_info[name][1])
231  for name in predict_net.external_input)
232 
233  dummy_name(cls._all_names_in_net(predict_net) |
234  cls._all_names_in_net(init_net))
235 
236  for op in predict_net.op:
237  shapes = {}
238  for name in itertools.chain(op.input, op.output):
239  blob = ws.FetchBlob(name)
240  if hasattr(blob, 'shape'):
241  shapes[name] = blob.shape
242  nodes, const_tensors = cls.caffe2_op_to_onnx_node(op, shapes=shapes)
243  graph_def.node.extend(nodes)
244  graph_def.initializer.extend(const_tensors)
245  graph_def.input.extend([cls._extract_value_info(tensor) for tensor in const_tensors])
246 
247  all_output = set(sum((list(node.output) for node in graph_def.node),
248  [init.name for init in graph_def.initializer]))
249  redundant_output = set(vi.name for vi in graph_def.output) - all_output
250  if redundant_output:
251  logger.warning(
252  'There are graph output not produced by any node or initializer: {}'
253  '! Will drop them.'.format(', '.join(redundant_output)))
254  graph_def.output.extend(
255  make_tensor_value_info(
256  name=name,
257  elem_type=value_info[name][0],
258  shape=value_info[name][1])
259  for name in predict_net.external_output
260  if name in all_output)
261 
262  checker.check_graph(graph_def)
263  return graph_def
264 
265  @classmethod
266  def caffe2_init_net_to_initializer(cls, init_net):
267  initializer = []
268  for op in init_net.op:
269  assert not op.input
270  try:
271  data_type, field_name = {
272  'GivenTensorFill': (TensorProto.FLOAT, 'floats'),
273  'GivenTensorInt64Fill': (TensorProto.INT64, 'ints'),
274  'GivenTensorIntFill': (TensorProto.INT32, 'ints'),
275  'GivenTensorBoolFill': (TensorProto.BOOL, 'ints'),
276  'GivenTensorStringFill': (TensorProto.STRING, 'strings'),
277  }[op.type]
278  except KeyError:
279  raise RuntimeError(
280  "Can not translate init_net with operator '{}' "
281  "to initializer".format(op.type)
282  )
283  raw = (data_type != TensorProto.STRING)
284  args = {a.name: a for a in op.arg}
285  vals = getattr(args['values'], field_name)
286  if raw:
287  vals = np.asarray(
288  vals,
289  dtype=mapping.TENSOR_TYPE_TO_NP_TYPE[data_type]).tobytes()
290  initializer.append(make_tensor(
291  name=op.output[0],
292  data_type=data_type,
293  dims=args['shape'].ints,
294  vals=vals,
295  raw=raw,
296  ))
297  return initializer
298 
299  @classmethod
300  def _filter_fake_init(cls, init_net, value_info):
301  if init_net:
302  fake_inits = [op for op in init_net.op
303  if len(op.output) == 1 and op.output[0] in value_info and
304  re.match('GivenTensor.*Fill|ConstantFill', op.type)]
305  for fake_init in fake_inits:
306  init_net.op.remove(fake_init)
307  del fake_inits[:]
308  del fake_inits
309 
310  @classmethod
311  def _ssa_rewrite(cls, net, init_net, value_info):
312  def ssa_name(name, version):
313  return '{}_{}'.format(name, version)
314 
315  if init_net:
316  for op in init_net.op:
317  assert re.match('GivenTensor.*Fill', op.type), "type is {}, \n{}".format(op.type, op)
318  assert len(op.output) == 1
319  op.output[0] = ssa_name(op.output[0], 0)
320  init_net.external_input[:] = [ssa_name(name, 0)
321  for name in init_net.external_input]
322  init_net.external_output[:] = [ssa_name(name, 0)
323  for name in init_net.external_output]
324  if value_info:
325  ssa_value_info = {ssa_name(name, 0): value
326  for name, value in value_info.items()}
327  value_info.clear()
328  value_info.update(ssa_value_info)
329  net.external_input[:] = [ssa_name(name, 0)
330  for name in net.external_input]
331  ssa, blob_versions = caffe2_core.get_ssa(net)
332  assert len(net.op) == len(ssa)
333  for op, (versioned_inputs, versioned_outputs) in zip(net.op, ssa):
334  op.input[:] = [ssa_name(name, version)
335  for name, version in versioned_inputs]
336  op.output[:] = [ssa_name(name, version)
337  for name, version in versioned_outputs]
338  net.external_output[:] = [ssa_name(name, blob_versions[name])
339  for name in net.external_output]
340 
341  @classmethod
342  def caffe2_net_to_onnx_model(cls, *args, **kwargs):
343  opset_id = OperatorSetIdProto()
344  opset_id.domain = '' # ONNX default domain
345  opset_id.version = cls.target_opset_version
346  model = make_model(cls.caffe2_net_to_onnx_graph(*args, **kwargs),
347  opset_imports=[opset_id], # current supported opset version
348  producer_name='onnx-caffe2', # producer name
349  )
350  checker.check_model(model)
351  return model
352 
353 
354 caffe2_net_to_onnx_graph = Caffe2Frontend.caffe2_net_to_onnx_graph
355 caffe2_net_to_onnx_model = Caffe2Frontend.caffe2_net_to_onnx_model
356 caffe2_init_net_to_initializer = Caffe2Frontend.caffe2_init_net_to_initializer
def _common_caffe2_op_to_onnx_node(cls, op_def, shapes)
Definition: frontend.py:114
def caffe2_op_to_onnx_node(cls, op_def, shapes)
Definition: frontend.py:130
def _filter_fake_init(cls, init_net, value_info)
Definition: frontend.py:300
def caffe2_net_to_onnx_graph(cls, predict_net, init_net=None, value_info=None)
Definition: frontend.py:181
def caffe2_init_net_to_initializer(cls, init_net)
Definition: frontend.py:266
def caffe2_arg_to_onnx_attr(cls, op_def, arg)
Definition: frontend.py:110
def _ssa_rewrite(cls, net, init_net, value_info)
Definition: frontend.py:311
def _common_caffe2_arg_to_onnx_attr(cls, op_def, arg)
Definition: frontend.py:78