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

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`. 

5 

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 

22 

23 

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'} 

32 

33 

34def _rename_var(var, empty='None'): 

35 if var in _keywords: 

36 return 'r_' + var 

37 if var == '': 

38 return empty 

39 return var 

40 

41 

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]`. 

46 

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] 

67 

68 

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))) 

146 

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 

158 

159 

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 

164 

165 

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 

178 

179 

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) 

198 

199 

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))) 

221 

222 return ", ".join(f"{k}={v}" for k, v in attributes) 

223 

224 

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) 

247 

248 

249def _python_make_node_loop(node, opsets, indent=0): 

250 """ 

251 Translates a node Loop into python. 

252 """ 

253 raise NotImplementedError() # pragma: no cover 

254 

255 

256def _python_make_node_scan(node, opsets, indent=0): 

257 """ 

258 Translates a node Scan into python. 

259 """ 

260 raise NotImplementedError() # pragma: no cover 

261 

262 

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) 

302 

303 

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. 

311 

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 

330 

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)) 

339 

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 

360 

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)) 

368 

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 = {} 

378 

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.") 

397 

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 

408 

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} 

418 

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 

448 

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 

461 

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 

474 

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) 

483 

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'] = {} 

504 

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 

510 

511 mark_inits = {} 

512 

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) 

527 

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) 

533 

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) 

548 

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 

556 

557 

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. 

562 

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 

571 

572 The following example shows what a python code creating a graph 

573 implementing the KMeans would look like. 

574 

575 .. runpython:: 

576 :showcode: 

577 :process: 

578 

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 

583 

584 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32) 

585 tr = KMeans(n_clusters=2) 

586 tr.fit(X) 

587 

588 onx = to_onnx(tr, X, target_opset=14) 

589 code = export2onnx(onx) 

590 

591 print(code) 

592 """ 

593 if isinstance(model_onnx, str): 

594 model_onnx = onnx.load(model_onnx) 

595 

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 

604 

605 

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. 

610 

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 

619 

620 .. runpython:: 

621 :showcode: 

622 :process: 

623 

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 

628 

629 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32) 

630 tr = KMeans(n_clusters=2) 

631 tr.fit(X) 

632 

633 onx = to_onnx(tr, X, target_opset=14) 

634 code = export2tf2onnx(onx) 

635 

636 print(code) 

637 """ 

638 if isinstance(model_onnx, str): 

639 model_onnx = onnx.load(model_onnx) 

640 

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 

650 

651 

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. 

657 

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 

666 

667 .. runpython:: 

668 :showcode: 

669 :process: 

670 

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 

675 

676 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32) 

677 tr = KMeans(n_clusters=2) 

678 tr.fit(X) 

679 

680 onx = to_onnx(tr, X, target_opset=14) 

681 code = export2numpy(onx) 

682 

683 print(code) 

684 

685 This can be applied to the decomposition of an einsum 

686 equation into simple matrix operations. 

687 

688 .. runpython:: 

689 :showcode: 

690 :process: 

691 

692 import numpy 

693 from mlprodict.testing.einsum import decompose_einsum_equation 

694 from mlprodict.onnx_tools.onnx_export import export2numpy 

695 

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) 

699 

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) 

708 

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 

719 

720 

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. 

725 

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 

734 

735 The following example shows what a python code creating a graph 

736 implementing the KMeans would look like. 

737 

738 .. runpython:: 

739 :showcode: 

740 :process: 

741 

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 

746 

747 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32) 

748 tr = KMeans(n_clusters=2) 

749 tr.fit(X) 

750 

751 onx = to_onnx(tr, X, target_opset=14) 

752 code = export2cpp(onx) 

753 

754 print(code) 

755 """ 

756 if isinstance(model_onnx, str): 

757 model_onnx = onnx.load(model_onnx) 

758 

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 

768 

769 

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. 

774 

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 

783 

784 The following example shows what a python code creating a graph 

785 implementing the KMeans would look like. 

786 

787 .. runpython:: 

788 :showcode: 

789 :process: 

790 

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 

795 

796 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32) 

797 tr = KMeans(n_clusters=2) 

798 tr.fit(X) 

799 

800 onx = to_onnx(tr, X, target_opset=14) 

801 code = export2xop(onx) 

802 

803 print(code) 

804 """ 

805 if isinstance(model_onnx, str): 

806 model_onnx = onnx.load(model_onnx) 

807 

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 

816 

817 

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. 

822 

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 

832 

833 The following example shows what a python code creating a graph 

834 implementing the KMeans would look like. 

835 

836 .. runpython:: 

837 :showcode: 

838 :process: 

839 

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 

844 

845 X = numpy.arange(20).reshape(10, 2).astype(numpy.float32) 

846 tr = KMeans(n_clusters=2) 

847 tr.fit(X) 

848 

849 onx = to_onnx(tr, X, target_opset=14) 

850 code = export2python(onx) 

851 

852 print(code) 

853 """ 

854 if isinstance(model_onnx, str): 

855 model_onnx = onnx.load(model_onnx) 

856 

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