Coverage for mlprodict/onnx_tools/onnx_export.py: 94%
352 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-04 02:28 +0100
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-04 02:28 +0100
1"""
2@file
3@brief Exports an ONNX graph in a way it can we created again
4with a python script. It relies on :epkg:`jinja2` and :epkg:`autopep8`.
6.. versionadded:: 0.7
7"""
8import textwrap
9import numpy
10import onnx
11from onnx.helper import printable_graph, make_node
12from onnx import numpy_helper, ModelProto
13from onnx.mapping import TENSOR_TYPE_TO_NP_TYPE
14from .onnx2py_helper import (
15 _var_as_dict, guess_proto_dtype, guess_proto_dtype_name,
16 get_tensor_shape, get_tensor_elem_type)
17from .onnx_export_templates import (
18 get_onnx_template, get_tf2onnx_template, get_numpy_template,
19 get_xop_template, get_cpp_template, get_python_template)
20from .exports.numpy_helper import make_numpy_code
21from .exports.tf2onnx_helper import make_tf2onnx_code
24_keywords = {
25 'False', 'await', 'else', 'import', 'pass',
26 'None', 'break', 'except', 'in', 'raise',
27 'True', 'class', 'finally', 'is', 'return',
28 'and', 'continue', 'for', 'lambda', 'try',
29 'as', 'def', 'from', 'nonlocal', 'while',
30 'assert', 'del', 'global', 'not', 'with',
31 'async', 'elif', 'if', 'or', 'yield'}
34def _rename_var(var, empty='None'):
35 if var in _keywords:
36 return 'r_' + var
37 if var == '':
38 return empty
39 return var
42def select_attribute(ens, att, sort=False, unique=False, skip=None):
43 """
44 Returns the list of the same attribute.
45 `[el.att for el in ens]`.
47 :param ens: list
48 :param att: attribute name
49 :param sort: sort the array
50 :param unique: returns the unique values
51 :param skip: to skip some names
52 :return: something like `[el.att for el in ens]`
53 """
54 if len(ens) == 0:
55 return []
56 if isinstance(ens[0], dict):
57 atts = [el[att] for el in ens]
58 else:
59 atts = [getattr(el, att) for el in ens]
60 if unique:
61 atts = list(set(atts))
62 if sort:
63 atts.sort()
64 if skip is None:
65 return atts
66 return [a for a in atts if a not in skip]
69def _nodes(graph, rename_name, used, output_names, use_onnx_tensor,
70 templates, verbose, opset, rename, autopep_options, name,
71 subgraphs, unique_operators, opsets=None):
72 if opsets is None:
73 raise ValueError( # pragma: no cover
74 "opsets cannot be None.")
75 if unique_operators is not None:
76 from ..npy.xop import loadop
77 nodes = []
78 for node in list(graph.node):
79 if (unique_operators is not None and
80 node.domain in ('', 'ai.onnx.ml')):
81 clname = loadop((node.domain, node.op_type))
82 unique_operators.add(
83 (node.domain, node.op_type, clname.__name__))
84 for i_raw_name in node.input:
85 if len(i_raw_name) == 0:
86 i = 'None'
87 else:
88 i = rename_name(i_raw_name, out=False)
89 if i not in used:
90 used[i] = []
91 used[i].append(node)
92 attributes = []
93 for at in node.attribute:
94 temp = _var_as_dict(at)
95 value = temp['value']
96 if node.op_type in {'Scan', 'Loop'} and at.name == 'body':
97 if "{{ inputs[0][0] }}" in str(templates):
98 attributes.append((at.name, at.g))
99 continue
100 fname = "_create_" + node.op_type + "_" + node.name + "_body"
101 body = export_template(
102 value, templates, opset=opset, verbose=verbose,
103 name=name, rename=rename,
104 use_onnx_tensor=use_onnx_tensor,
105 autopep_options=autopep_options,
106 function_name=fname, opsets=opsets)
107 subgraphs.append(
108 (body, node.op_type + "_" + node.name + "_body"))
109 attributes.append((at.name, fname + "()"))
110 continue
111 if node.op_type == 'If' and at.name in {'then_branch', 'else_branch'}:
112 if "{{ inputs[0][0] }}" in str(templates):
113 attributes.append((at.name, at.g))
114 continue
115 fname = "_create_if_" + node.name + "_" + at.name
116 body = export_template(
117 value, templates, opset=opset, verbose=verbose,
118 name=name, rename=rename,
119 use_onnx_tensor=use_onnx_tensor,
120 autopep_options=autopep_options,
121 function_name=fname, opsets=opsets)
122 subgraphs.append((body, "if_" + node.name + "_" + at.name))
123 attributes.append((at.name, fname + "()"))
124 continue
125 if use_onnx_tensor:
126 if node.op_type == 'Cast' and at.name == 'to':
127 attributes.append(
128 (at.name, guess_proto_dtype_name(int(value))))
129 continue
130 if isinstance(value, str):
131 attributes.append((at.name, f"{value!r}"))
132 else:
133 if isinstance(value, numpy.ndarray):
134 if use_onnx_tensor and at.name == 'value':
135 onnx_dtype = guess_proto_dtype_name(
136 guess_proto_dtype(value.dtype))
137 value = (
138 'make_tensor("value", %s, dims=%r, vals=%r)'
139 '' % (onnx_dtype, list(value.shape),
140 value.tolist()))
141 attributes.append((at.name, value))
142 else:
143 attributes.append((at.name, repr(value.tolist())))
144 else:
145 attributes.append((at.name, repr(value)))
147 attributes_str = ", ".join(f"{k}={v}" for k, v in attributes)
148 d = dict(name=node.name, op_type=node.op_type,
149 domain=node.domain, onnx_node=node,
150 inputs=[rename_name(n, out=False)
151 for n in node.input if len(n) > 0],
152 outputs=[rename_name(n, out=True) for n in node.output],
153 output_names=[rename_name(n, out=True) for n in node.output
154 if n in output_names],
155 attributes=attributes, attributes_str=attributes_str)
156 nodes.append(d)
157 return nodes
160def _xop_make_node_name(domain, name):
161 from ..npy.xop import _domain_to_class_name
162 class_name = "Onnx" + _domain_to_class_name(domain) + name
163 return class_name
166def _python_make_node_name(domain, version, name, node=False):
167 if node:
168 if version is None:
169 version = 1
170 if not isinstance(version, int):
171 raise TypeError( # pragma: no cover
172 "version must be an integer not %r for domain=%r and name=%r." % (
173 version, domain, name))
174 if domain == '':
175 return "opset%d.%s" % (version, name)
176 return "%s%d.%s" % (domain.replace(".", "_"), version, name)
177 return name
180def _python_make_node_graph(graph, opsets, indent=0, output_names=None):
181 """
182 Translates a GraphProto into python.
183 """
184 code = []
185 sindent = ' ' * indent
186 for init in graph.initializer:
187 node = make_node('Constant', [], [_rename_var(init.name)], value=init)
188 code.append(_python_make_node(node, opsets, indent=indent))
189 if len(graph.sparse_initializer) > 0:
190 raise NotImplementedError( # pragma: no cover
191 "Unable to convert sparse_initilizer into python.")
192 for node in list(graph.node):
193 code.append(_python_make_node(node, opsets, indent=indent))
194 if output_names is not None:
195 for fr, to in zip(graph.output, output_names):
196 code.append(f"{sindent}{_rename_var(to)} = {_rename_var(fr.name)}")
197 return "\n".join(code)
200def _python_make_node_make_attribute_str(node):
201 attributes = []
202 for at in node.attribute:
203 temp = _var_as_dict(at)
204 value = temp['value']
205 if isinstance(value, str):
206 attributes.append((at.name, f"{value.decode('utf-8')!r}"))
207 continue
208 if isinstance(value, numpy.ndarray):
209 if at.name == 'value':
210 onnx_dtype = guess_proto_dtype_name(
211 guess_proto_dtype(value.dtype))
212 value = (
213 'make_tensor("value", %s, dims=%r, vals=%r)'
214 '' % (onnx_dtype, list(value.shape),
215 value.ravel().tolist()))
216 attributes.append((at.name, value))
217 continue
218 attributes.append((at.name, repr(value.tolist())))
219 continue
220 attributes.append((at.name, repr(value)))
222 return ", ".join(f"{k}={v}" for k, v in attributes)
225def _python_make_node_if(node, opsets, indent=0):
226 """
227 Translates a node If into python.
228 """
229 sindent = ' ' * indent
230 code = [f"{sindent}if {node.input[0]}:"]
231 if len(node.attribute) != 2:
232 raise RuntimeError( # pragma: no cover
233 f"Node {node.op_type!r} expected two attributes not {len(node.attribute)}.")
234 atts = node.attribute
235 if atts[0].name == 'else_branch':
236 else_branch, then_branch = atts[0].g, atts[1].g
237 else:
238 else_branch, then_branch = atts[1].g, atts[0].g
239 code.append(_python_make_node_graph(
240 then_branch, opsets, indent=indent + 1,
241 output_names=node.output))
242 code.append(f"{sindent}else:")
243 code.append(_python_make_node_graph(
244 else_branch, opsets, indent=indent + 1,
245 output_names=node.output))
246 return "\n".join(code)
249def _python_make_node_loop(node, opsets, indent=0):
250 """
251 Translates a node Loop into python.
252 """
253 raise NotImplementedError() # pragma: no cover
256def _python_make_node_scan(node, opsets, indent=0):
257 """
258 Translates a node Scan into python.
259 """
260 raise NotImplementedError() # pragma: no cover
263def _python_make_node(onnx_node, opsets, indent=0):
264 if isinstance(onnx_node, dict):
265 node = onnx_node['onnx_node']
266 else:
267 node = onnx_node
268 version = opsets[node.domain]
269 if node.op_type in {'If', 'Loop', 'Scan'}:
270 # If, Loop, Scan
271 if node.op_type == 'If':
272 return _python_make_node_if(node, opsets, indent=indent)
273 if node.op_type == 'Loop':
274 return _python_make_node_loop(node, opsets, indent=indent)
275 if node.op_type == 'Scan':
276 return _python_make_node_scan(node, opsets, indent=indent)
277 raise RuntimeError( # pragma: no cover
278 f"Unable to export node type {node.op_type!r} into python.")
279 # pragma: no cover
280 if any(map(lambda att: hasattr(att, 'g') and att.g and att.g.ByteSize() > 0,
281 node.attribute)):
282 raise RuntimeError( # pragma: no cover
283 f"Unable to export node type {node.op_type!r} into python.")
284 ops = {'Add': '+', 'Sub': '-', 'Mul': '*', 'MatMul': '@',
285 'Div': '/', 'Pow': '**',
286 'And': '&', 'Or': '|', 'Greater': '>', 'Equal': '==',
287 'Lesser': '<', 'GreaterOrEqual': '>=', 'LessOrEqual': '<='}
288 sindent = " " * indent
289 if node.op_type in ops:
290 return "%s%s = %s" % (sindent, _rename_var(node.output[0], empty='_'),
291 (" %s " % ops[node.op_type]).join(
292 map(_rename_var, node.input)))
293 name = _python_make_node_name(
294 node.domain, version, node.op_type, node=True)
295 attributes_str = _python_make_node_make_attribute_str(node)
296 if len(node.input) > 0 and len(attributes_str) > 0:
297 attributes_str = ", " + attributes_str
298 output = ", ".join(map(lambda s: _rename_var(s, empty='_'), node.output))
299 text = [sindent, output, " = ", name,
300 '(', ', '.join(map(_rename_var, node.input)), attributes_str, ')']
301 return "".join(text)
304def export_template(model_onnx, templates, opset=None, # pylint: disable=R0914
305 verbose=True, name=None,
306 rename=False, use_onnx_tensor=False,
307 autopep_options=None, function_name='create_model',
308 clean_code=True, opsets=None):
309 """
310 Exports an ONNX model to the onnx syntax.
312 :param model_onnx: string or ONNX graph
313 :param templates: exporting templates
314 :param opset: opset to export to
315 (None to select the one from the graph)
316 :param opsets: nodes uses these opsets
317 :param verbose: insert prints
318 :param name: to overwrite onnx name
319 :param rename: rename the names to get shorter names
320 :param use_onnx_tensor: when an attribute is an array
321 and its name is `'value'`, it converts that array into an
322 ONNX tensor to avoid type mismatch, (operator *ConstantOfShape*, ...)
323 :param autopep_options: :epkg:`autopep8` options
324 :param function_name: main function name in the code
325 :param clean_code: clean the code
326 :return: python code
327 """
328 # delayed import to avoid raising an exception if not installed.
329 import autopep8
331 def number2name(n):
332 n += 1
333 seq = []
334 while n >= 1:
335 r = n % 26
336 seq.append(r)
337 n = (n - r) // 26
338 return "".join(chr(65 + i) for i in reversed(seq))
340 def rename_name(name, out):
341 if len(name) == 0:
342 if out:
343 return '__'
344 return "_"
345 if name in dict_names:
346 return dict_names[name]
347 if rename:
348 i = 0
349 new_name = number2name(i)
350 while new_name in dict_names:
351 i += 1
352 new_name = number2name(i)
353 if len(new_name) == 0:
354 raise ValueError( # pragma: no cover
355 "Unable to rename name=%r i=%d." % (name, i))
356 dict_names[name] = new_name
357 dict_names[new_name] = new_name
358 return new_name
359 return name
361 # unique_function_domain_version
362 unique_function_domain_version = set()
363 if hasattr(model_onnx, 'functions'):
364 for f in model_onnx.functions:
365 unique_function_domain_version.add((f.domain, 1))
366 unique_function_domain_version = list(
367 sorted(unique_function_domain_version))
369 # containers
370 context = {'main_model': model_onnx,
371 'printable_graph': printable_graph,
372 'xop_make_node_name': _xop_make_node_name,
373 'python_make_node': _python_make_node,
374 'python_make_node_name': _python_make_node_name,
375 'unique_function_domain_version': unique_function_domain_version,
376 'rename_var': _rename_var}
377 used = {}
379 # opset
380 if hasattr(model_onnx, 'opset_import'):
381 if opsets is None:
382 opsets = {}
383 else:
384 opsets = opsets.copy()
385 for oimp in model_onnx.opset_import:
386 if oimp.domain == '' and opset is None:
387 opsets[oimp.domain] = oimp.version
388 opset = oimp.version
389 else:
390 opsets[oimp.domain] = opset
391 context['opsets'] = opsets
392 context['target_opset'] = opset
393 else:
394 context['opsets'] = opsets
395 if opsets is None:
396 raise ValueError("opsets cannot be None.")
398 if hasattr(model_onnx, 'graph'):
399 graph = model_onnx.graph
400 else:
401 graph = model_onnx
402 dict_names = {}
403 if rename:
404 for o in graph.input:
405 dict_names[o.name] = o.name
406 for o in graph.output:
407 dict_names[o.name] = o.name
409 # inits
410 unique_operators = set()
411 initializers = []
412 for init in graph.initializer:
413 init_name = rename_name(init.name, out=False)
414 value = numpy_helper.to_array(init)
415 initializers.append((init_name, value))
416 context['initializers'] = initializers
417 context['initializers_dict'] = {k: v for k, v in initializers}
419 # functions
420 functions = []
421 fct_dict = {}
422 if hasattr(model_onnx, 'functions'):
423 from ..npy.xop import OnnxOperatorFunction
424 for fct in model_onnx.functions:
425 used = {}
426 opsets_fct = {}
427 for oimp in fct.opset_import:
428 if oimp.domain == '' and opset is None:
429 opsets_fct[oimp.domain] = oimp.version
430 else:
431 opsets_fct[oimp.domain] = opset
432 functions.append(
433 (fct.domain, fct.name,
434 {'proto': fct,
435 'opsets': opsets_fct,
436 'nodes': _nodes(fct, rename_name, used, fct.output,
437 use_onnx_tensor, templates, verbose,
438 opset, rename, autopep_options,
439 fct.name, [], unique_operators,
440 opsets=opsets)}))
441 if fct.name in fct_dict:
442 fct_dict[fct.name].append(fct)
443 else:
444 fct_dict[fct.name] = [fct]
445 context['OnnxOperatorFunction'] = OnnxOperatorFunction
446 context['functions'] = functions
447 context['functions_dict'] = fct_dict
449 # inputs
450 inputs = []
451 for inp in graph.input:
452 try:
453 elem_type = get_tensor_elem_type(inp)
454 except TypeError:
455 # not a tensor
456 inputs.append((inp.name, None, None))
457 continue
458 shape = get_tensor_shape(inp)
459 inputs.append((inp.name, elem_type, shape))
460 context['inputs'] = inputs
462 # outputs
463 outputs = []
464 for inp in graph.output:
465 try:
466 elem_type = get_tensor_elem_type(inp)
467 except TypeError:
468 # not a tensor
469 outputs.append((inp.name, None, None))
470 continue
471 shape = get_tensor_shape(inp)
472 outputs.append((inp.name, elem_type, shape))
473 context['outputs'] = outputs
475 # node
476 output_names = set(o.name for o in graph.output)
477 subgraphs = []
478 context['graph'] = graph
479 context['nodes'] = _nodes(
480 graph, rename_name, used, output_names, use_onnx_tensor,
481 templates, verbose, opset, rename, autopep_options, name,
482 subgraphs, unique_operators, opsets=opsets)
484 # graph
485 context['name'] = (name or graph.name).replace("(", "_").replace(")", "")
486 context['function_name'] = function_name
487 context['indent'] = textwrap.indent
488 if hasattr(model_onnx, 'graph'):
489 context['ir_version'] = model_onnx.ir_version
490 context['producer_name'] = model_onnx.producer_name
491 context['domain'] = model_onnx.domain
492 context['model_version'] = model_onnx.model_version
493 context['doc_string'] = model_onnx.doc_string
494 context['metadata'] = {
495 p.key: p.value for p in model_onnx.metadata_props}
496 else:
497 # subgraph
498 context['ir_version'] = None
499 context['producer_name'] = None
500 context['domain'] = None
501 context['model_version'] = None
502 context['doc_string'] = ""
503 context['metadata'] = {}
505 # common context
506 context['unique_operators'] = [dict(domain=o[0], name=o[1], classname=o[2])
507 for o in sorted(unique_operators)]
508 context['skip_inits'] = {}
509 context['subgraphs'] = subgraphs
511 mark_inits = {}
513 # First rendering to detect any unused or replaced initializer.
514 from jinja2 import Template # delayed import
515 template = Template(templates)
516 final = template.render(
517 enumerate=enumerate, sorted=sorted, len=len, map=map,
518 select_attribute=select_attribute, repr=repr,
519 TENSOR_TYPE_TO_NP_TYPE=TENSOR_TYPE_TO_NP_TYPE,
520 make_numpy_code=lambda *args, **kwargs: make_numpy_code(
521 *args, context=context, used=used, mark_inits=mark_inits,
522 **kwargs),
523 make_tf2onnx_code=lambda *args, **kwargs: make_tf2onnx_code(
524 *args, context=context, used=used, mark_inits=mark_inits,
525 **kwargs),
526 verbose=verbose, **context)
528 skip_inits = set()
529 for k, v in mark_inits.items():
530 if len(v) == len(used[k]):
531 # One initializers was removed.
532 skip_inits.add(k)
534 if len(skip_inits) > 0:
535 # Second rendering if needed when an initializer was replaced
536 # or removed.
537 context['skip_inits'] = skip_inits
538 # Again with skip_inits.
539 final = template.render(
540 enumerate=enumerate, sorted=sorted, len=len,
541 make_numpy_code=lambda *args, **kwargs: make_numpy_code(
542 *args, context=context, used=used, mark_inits=mark_inits,
543 **kwargs),
544 make_tf2onnx_code=lambda *args, **kwargs: make_tf2onnx_code(
545 *args, context=context, used=used, mark_inits=mark_inits,
546 **kwargs),
547 verbose=verbose, **context)
549 final += "\n"
550 if not verbose:
551 rows = final.split("\n")
552 final = "\n".join(_ for _ in rows if not _.endswith("# verbose"))
553 if clean_code:
554 return autopep8.fix_code(final, options=autopep_options)
555 return final
558def export2onnx(model_onnx, opset=None, verbose=True, name=None, rename=False,
559 autopep_options=None):
560 """
561 Exports an ONNX model to the :epkg:`onnx` syntax.
563 :param model_onnx: string or ONNX graph
564 :param opset: opset to export to
565 (None to select the one from the graph)
566 :param verbose: inserts prints
567 :param name: to overwrite onnx name
568 :param rename: rename the names to get shorter names
569 :param autopep_options: :epkg:`autopep8` options
570 :return: python code
572 The following example shows what a python code creating a graph
573 implementing the KMeans would look like.
575 .. runpython::
576 :showcode:
577 :process:
579 import numpy
580 from sklearn.cluster import KMeans
581 from mlprodict.onnx_conv import to_onnx
582 from mlprodict.onnx_tools.onnx_export import export2onnx
584 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32)
585 tr = KMeans(n_clusters=2)
586 tr.fit(X)
588 onx = to_onnx(tr, X, target_opset=14)
589 code = export2onnx(onx)
591 print(code)
592 """
593 if isinstance(model_onnx, str):
594 model_onnx = onnx.load(model_onnx)
596 if not isinstance(model_onnx, ModelProto):
597 raise TypeError( # pragma: no cover
598 f"The function expects a ModelProto not {type(model_onnx)!r}.")
599 code = export_template(model_onnx, templates=get_onnx_template(),
600 opset=opset, verbose=verbose, name=name,
601 rename=rename, use_onnx_tensor=True,
602 autopep_options=autopep_options)
603 return code
606def export2tf2onnx(model_onnx, opset=None, verbose=True, name=None,
607 rename=False, autopep_options=None):
608 """
609 Exports an ONNX model to the :epkg:`tensorflow-onnx` syntax.
611 :param model_onnx: string or ONNX graph
612 :param opset: opset to export to
613 (None to select the one from the graph)
614 :param verbose: inserts prints
615 :param name: to overwrite onnx name
616 :param rename: rename the names to get shorter names
617 :param autopep_options: :epkg:`autopep8` options
618 :return: python code
620 .. runpython::
621 :showcode:
622 :process:
624 import numpy
625 from sklearn.cluster import KMeans
626 from mlprodict.onnx_conv import to_onnx
627 from mlprodict.onnx_tools.onnx_export import export2tf2onnx
629 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32)
630 tr = KMeans(n_clusters=2)
631 tr.fit(X)
633 onx = to_onnx(tr, X, target_opset=14)
634 code = export2tf2onnx(onx)
636 print(code)
637 """
638 if isinstance(model_onnx, str):
639 model_onnx = onnx.load(model_onnx)
641 if not isinstance(model_onnx, ModelProto):
642 raise TypeError( # pragma: no cover
643 f"The function expects a ModelProto not {type(model_onnx)!r}.")
644 code = export_template(model_onnx, templates=get_tf2onnx_template(),
645 opset=opset, verbose=verbose, name=name,
646 rename=rename, use_onnx_tensor=True,
647 autopep_options=autopep_options)
648 code = code.replace("], ]", "]]")
649 return code
652def export2numpy(model_onnx, opset=None, verbose=True, name=None,
653 rename=False, autopep_options=None):
654 """
655 Exports an ONNX model to the :epkg:`numpy` syntax.
656 The exports does not work with all operators.
658 :param model_onnx: string or ONNX graph
659 :param opset: opset to export to
660 (None to select the one from the graph)
661 :param verbose: inserts prints
662 :param name: to overwrite onnx name
663 :param rename: rename the names to get shorter names
664 :param autopep_options: :epkg:`autopep8` options
665 :return: python code
667 .. runpython::
668 :showcode:
669 :process:
671 import numpy
672 from sklearn.cluster import KMeans
673 from mlprodict.onnx_conv import to_onnx
674 from mlprodict.onnx_tools.onnx_export import export2numpy
676 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32)
677 tr = KMeans(n_clusters=2)
678 tr.fit(X)
680 onx = to_onnx(tr, X, target_opset=14)
681 code = export2numpy(onx)
683 print(code)
685 This can be applied to the decomposition of an einsum
686 equation into simple matrix operations.
688 .. runpython::
689 :showcode:
690 :process:
692 import numpy
693 from mlprodict.testing.einsum import decompose_einsum_equation
694 from mlprodict.onnx_tools.onnx_export import export2numpy
696 x1 = numpy.arange(8).reshape(2, 2, 2).astype(numpy.float32)
697 x2 = numpy.arange(4).reshape(2, 2).astype(numpy.float32)
698 r = numpy.einsum("bac,cd->ad", x1, x2)
700 seq_clean = decompose_einsum_equation(
701 "bac,cd->ad", strategy='numpy', clean=True)
702 onx = seq_clean.to_onnx("Y", "X1", "X2", dtype=numpy.float32)
703 code = export2numpy(onx, name="einsum")
704 print(code)
705 """
706 if isinstance(model_onnx, str):
707 model_onnx = onnx.load(model_onnx)
709 if not isinstance(model_onnx, ModelProto):
710 raise TypeError( # pragma: no cover
711 f"The function expects a ModelProto not {type(model_onnx)!r}.")
712 code = export_template(model_onnx, templates=get_numpy_template(),
713 opset=opset, verbose=verbose, name=name,
714 rename=rename, autopep_options=autopep_options)
715 for i in range(-6, 6):
716 code = code.replace("axis=tuple([%d])" % i, "axis=%d" % i)
717 code = code.replace("tuple([%d])" % i, "(%d, )" % i)
718 return code
721def export2cpp(model_onnx, opset=None, verbose=True, name=None, rename=False,
722 autopep_options=None):
723 """
724 Exports an ONNX model to the :epkg:`c` syntax.
726 :param model_onnx: string or ONNX graph
727 :param opset: opset to export to
728 (None to select the one from the graph)
729 :param verbose: inserts prints
730 :param name: to overwrite onnx name
731 :param rename: rename the names to get shorter names
732 :param autopep_options: :epkg:`autopep8` options
733 :return: python code
735 The following example shows what a python code creating a graph
736 implementing the KMeans would look like.
738 .. runpython::
739 :showcode:
740 :process:
742 import numpy
743 from sklearn.cluster import KMeans
744 from mlprodict.onnx_conv import to_onnx
745 from mlprodict.onnx_tools.onnx_export import export2cpp
747 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32)
748 tr = KMeans(n_clusters=2)
749 tr.fit(X)
751 onx = to_onnx(tr, X, target_opset=14)
752 code = export2cpp(onx)
754 print(code)
755 """
756 if isinstance(model_onnx, str):
757 model_onnx = onnx.load(model_onnx)
759 if not isinstance(model_onnx, ModelProto):
760 raise TypeError( # pragma: no cover
761 f"The function expects a ModelProto not {type(model_onnx)!r}.")
762 code = export_template(model_onnx, templates=get_cpp_template(),
763 opset=opset, verbose=verbose, name=name,
764 rename=rename, use_onnx_tensor=True,
765 autopep_options=autopep_options,
766 clean_code=False)
767 return code
770def export2xop(model_onnx, opset=None, verbose=True, name=None, rename=False,
771 autopep_options=None):
772 """
773 Exports an ONNX model to the XOP syntax.
775 :param model_onnx: string or ONNX graph
776 :param opset: opset to export to
777 (None to select the one from the graph)
778 :param verbose: inserts prints
779 :param name: to overwrite onnx name
780 :param rename: rename the names to get shorter names
781 :param autopep_options: :epkg:`autopep8` options
782 :return: python code
784 The following example shows what a python code creating a graph
785 implementing the KMeans would look like.
787 .. runpython::
788 :showcode:
789 :process:
791 import numpy
792 from sklearn.cluster import KMeans
793 from mlprodict.onnx_conv import to_onnx
794 from mlprodict.onnx_tools.onnx_export import export2xop
796 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32)
797 tr = KMeans(n_clusters=2)
798 tr.fit(X)
800 onx = to_onnx(tr, X, target_opset=14)
801 code = export2xop(onx)
803 print(code)
804 """
805 if isinstance(model_onnx, str):
806 model_onnx = onnx.load(model_onnx)
808 if not isinstance(model_onnx, ModelProto):
809 raise TypeError( # pragma: no cover
810 f"The function expects a ModelProto not {type(model_onnx)!r}.")
811 code = export_template(model_onnx, templates=get_xop_template(),
812 opset=opset, verbose=verbose, name=name,
813 rename=rename, use_onnx_tensor=True,
814 autopep_options=autopep_options)
815 return code
818def export2python(model_onnx, opset=None, verbose=True, name=None, rename=False,
819 autopep_options=None, function_name='main'):
820 """
821 Exports an ONNX model to the *python* syntax.
823 :param model_onnx: string or ONNX graph
824 :param opset: opset to export to
825 (None to select the one from the graph)
826 :param verbose: inserts prints
827 :param name: to overwrite onnx name
828 :param rename: rename the names to get shorter names
829 :param autopep_options: :epkg:`autopep8` options
830 :param function_name: main function name
831 :return: python code
833 The following example shows what a python code creating a graph
834 implementing the KMeans would look like.
836 .. runpython::
837 :showcode:
838 :process:
840 import numpy
841 from sklearn.cluster import KMeans
842 from mlprodict.onnx_conv import to_onnx
843 from mlprodict.onnx_tools.onnx_export import export2python
845 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32)
846 tr = KMeans(n_clusters=2)
847 tr.fit(X)
849 onx = to_onnx(tr, X, target_opset=14)
850 code = export2python(onx)
852 print(code)
853 """
854 if isinstance(model_onnx, str):
855 model_onnx = onnx.load(model_onnx)
857 if not isinstance(model_onnx, ModelProto):
858 raise TypeError( # pragma: no cover
859 f"The function expects a ModelProto not {type(model_onnx)!r}.")
860 code = export_template(model_onnx, templates=get_python_template(),
861 opset=opset, verbose=verbose, name=name,
862 rename=rename, use_onnx_tensor=True,
863 autopep_options=autopep_options,
864 clean_code=True, function_name=function_name)
865 return code