Coverage for mlprodict/onnx_tools/onnx_export.py: 99%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

221 statements  

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

8from textwrap import indent 

9import numpy 

10import onnx 

11from onnx import numpy_helper 

12from onnx.mapping import TENSOR_TYPE_TO_NP_TYPE 

13from .onnx2py_helper import ( 

14 _var_as_dict, guess_proto_dtype, guess_proto_dtype_name) 

15from .onnx_export_templates import ( 

16 get_onnx_template, get_tf2onnx_template, get_numpy_template, 

17 get_xop_template) 

18from .exports.numpy_helper import make_numpy_code 

19from .exports.tf2onnx_helper import make_tf2onnx_code 

20 

21 

22def select_attribute(ens, att, sort=False, unique=False, skip=None): 

23 """ 

24 Returns the list of the same attribute. 

25 `[el.att for el in ens]`. 

26 

27 :param ens: list 

28 :param att: attribute name 

29 :param sort: sort the array 

30 :param unique: returns the unique values 

31 :param skip: to skip some names 

32 :return: something like `[el.att for el in ens]` 

33 """ 

34 if len(ens) == 0: 

35 return [] 

36 if isinstance(ens[0], dict): 

37 atts = [el[att] for el in ens] 

38 else: 

39 atts = [getattr(el, att) for el in ens] 

40 if unique: 

41 atts = list(set(atts)) 

42 if sort: 

43 atts.sort() 

44 if skip is None: 

45 return atts 

46 return [a for a in atts if a not in skip] 

47 

48 

49def _nodes(graph, rename_name, used, output_names, use_onnx_tensor, 

50 templates, verbose, opset, rename, autopep_options, name, 

51 subgraphs, unique_operators): 

52 from ..npy.xop import loadop 

53 nodes = [] 

54 for node in graph.node: 

55 if node.domain in ('', 'ai.onnx.ml'): 

56 clname = loadop((node.domain, node.op_type)) 

57 unique_operators.add( 

58 (node.domain, node.op_type, clname.__name__)) 

59 for index_input, i_raw_name in enumerate(node.input): 

60 if len(i_raw_name) == 0: 

61 # This means the input is optional. 

62 if any(map(lambda s: len(s) > 0, node.input[index_input:])): 

63 raise NotImplementedError( 

64 "Input cannot be placed after an unused optional input " 

65 "in node %r." % (node, )) 

66 break 

67 i = rename_name(i_raw_name) 

68 if i not in used: 

69 used[i] = [] 

70 used[i].append(node) 

71 attributes = [] 

72 for at in node.attribute: 

73 temp = _var_as_dict(at) 

74 value = temp['value'] 

75 if node.op_type == 'Scan' and at.name == 'body': 

76 fname = "_create_" + node.name + "_body" 

77 body = export_template( 

78 value, templates, opset=opset, verbose=verbose, 

79 name=name, rename=rename, 

80 use_onnx_tensor=use_onnx_tensor, 

81 autopep_options=autopep_options, 

82 function_name=fname) 

83 subgraphs.append((body, node.name + "_body")) 

84 attributes.append((at.name, fname + "()")) 

85 continue 

86 if node.op_type in {'Loop', 'If'}: 

87 raise NotImplementedError( 

88 "Subgraphs are not yet implemented (operator=%r)." 

89 "" % node.op_type) 

90 if use_onnx_tensor: 

91 if node.op_type == 'Cast' and at.name == 'to': 

92 attributes.append( 

93 (at.name, guess_proto_dtype_name(int(value)))) 

94 continue 

95 if isinstance(value, str): 

96 attributes.append((at.name, "%r" % value)) 

97 else: 

98 if isinstance(value, numpy.ndarray): 

99 if use_onnx_tensor and at.name == 'value': 

100 onnx_dtype = guess_proto_dtype_name( 

101 guess_proto_dtype(value.dtype)) 

102 value = ( 

103 'make_tensor("value", %s, dims=%r, vals=%r)' 

104 '' % (onnx_dtype, list(value.shape), 

105 value.tolist())) 

106 attributes.append((at.name, value)) 

107 else: 

108 attributes.append((at.name, repr(value.tolist()))) 

109 else: 

110 attributes.append((at.name, repr(value))) 

111 

112 attributes_str = ", ".join("%s=%s" % (k, v) for k, v in attributes) 

113 d = dict(name=node.name, op_type=node.op_type, 

114 domain=node.domain, 

115 inputs=[rename_name(n) for n in node.input if len(n) > 0], 

116 outputs=[rename_name(n) for n in node.output], 

117 output_names=[rename_name(n) for n in node.output 

118 if n in output_names], 

119 attributes=attributes, attributes_str=attributes_str) 

120 nodes.append(d) 

121 return nodes 

122 

123 

124def export_template(model_onnx, templates, opset=None, # pylint: disable=R0914 

125 verbose=True, name=None, 

126 rename=False, use_onnx_tensor=False, 

127 autopep_options=None, function_name='create_model'): 

128 """ 

129 Exports an ONNX model to the onnx syntax. 

130 

131 :param model_onnx: string or ONNX graph 

132 :param templates: exporting templates 

133 :param opset: opset to export to 

134 (None to select the one from the graph) 

135 :param verbose: insert prints 

136 :param name: to overwrite onnx name 

137 :param rename: rename the names to get shorter names 

138 :param use_onnx_tensor: when an attribute is an array 

139 and its name is `'value'`, it converts that array into an 

140 ONNX tensor to avoid type mismatch, (operator *ConstantOfShape*, ...) 

141 :param autopep_options: :epkg:`autopep8` options 

142 :param function_name: main function name in the code 

143 :return: python code 

144 """ 

145 # delayed import to avoid raising an exception if not installed. 

146 import autopep8 

147 

148 def number2name(n): 

149 n += 1 

150 seq = [] 

151 while n >= 1: 

152 r = n % 26 

153 seq.append(r) 

154 n = (n - r) // 26 

155 return "".join(chr(65 + i) for i in reversed(seq)) 

156 

157 def rename_name(name): 

158 if len(name) == 0: 

159 raise ValueError( # pragma: no cover 

160 "name is empty.") 

161 if name in dict_names: 

162 return dict_names[name] 

163 if rename: 

164 i = 0 

165 new_name = number2name(i) 

166 while new_name in dict_names: 

167 i += 1 

168 new_name = number2name(i) 

169 if len(new_name) == 0: 

170 raise ValueError( # pragma: no cover 

171 "Unable to rename name=%r i=%d." % (name, i)) 

172 dict_names[name] = new_name 

173 dict_names[new_name] = new_name 

174 return new_name 

175 return name 

176 

177 # containers 

178 context = {} 

179 used = {} 

180 

181 # opset 

182 if hasattr(model_onnx, 'opset_import'): 

183 opsets = {} 

184 for oimp in model_onnx.opset_import: 

185 if oimp.domain == '' and opset is None: 

186 opsets[oimp.domain] = oimp.version 

187 opset = oimp.version 

188 else: 

189 opsets[oimp.domain] = opset 

190 context['opsets'] = opsets 

191 context['target_opset'] = opset 

192 

193 if hasattr(model_onnx, 'graph'): 

194 graph = model_onnx.graph 

195 else: 

196 graph = model_onnx 

197 dict_names = {} 

198 if rename: 

199 for o in graph.input: 

200 dict_names[o.name] = o.name 

201 for o in graph.output: 

202 dict_names[o.name] = o.name 

203 

204 # inits 

205 unique_operators = set() 

206 initializers = [] 

207 for init in graph.initializer: 

208 init_name = rename_name(init.name) 

209 value = numpy_helper.to_array(init) 

210 initializers.append((init_name, value)) 

211 context['initializers'] = initializers 

212 context['initializers_dict'] = {k: v for k, v in initializers} 

213 

214 # functions 

215 functions = [] 

216 fct_dict = {} 

217 if hasattr(model_onnx, 'functions'): 

218 from ..npy.xop import OnnxOperatorFunction 

219 for fct in model_onnx.functions: 

220 used = {} 

221 functions.append( 

222 (fct.domain, fct.name, 

223 {'proto': fct, 

224 'nodes': _nodes(fct, rename_name, used, fct.output, 

225 use_onnx_tensor, templates, verbose, 

226 opset, rename, autopep_options, 

227 fct.name, [], unique_operators)})) 

228 if fct.name in fct_dict: 

229 fct_dict[fct.name].append(fct) 

230 else: 

231 fct_dict[fct.name] = [fct] 

232 context['OnnxOperatorFunction'] = OnnxOperatorFunction 

233 context['functions'] = functions 

234 context['functions_dict'] = fct_dict 

235 

236 # inputs 

237 inputs = [] 

238 for inp in graph.input: 

239 t = inp.type.tensor_type 

240 dims = [] 

241 for d in t.shape.dim: 

242 dd = d.dim_value 

243 if dd == 0: 

244 dd = None 

245 dims.append(dd) 

246 if len(dims) == 0: 

247 dims = None 

248 if 'dim_value' in str(dims): 

249 raise RuntimeError( # pragma: no cover 

250 "Unexpected issue in %r - %r." % (dims, t)) 

251 inputs.append((inp.name, t.elem_type, dims)) 

252 context['inputs'] = inputs 

253 

254 # outputs 

255 outputs = [] 

256 for inp in graph.output: 

257 t = inp.type.tensor_type 

258 dims = [] 

259 for d in t.shape.dim: 

260 dd = d.dim_value 

261 if dd == 0: 

262 dd = None 

263 dims.append(dd) 

264 if len(dims) == 0: 

265 dims = None 

266 outputs.append((inp.name, t.elem_type, dims)) 

267 context['outputs'] = outputs 

268 

269 # node 

270 output_names = set(o.name for o in graph.output) 

271 subgraphs = [] 

272 context['nodes'] = _nodes( 

273 graph, rename_name, used, output_names, use_onnx_tensor, 

274 templates, verbose, opset, rename, autopep_options, name, 

275 subgraphs, unique_operators) 

276 

277 # graph 

278 context['name'] = name or graph.name 

279 context['name'] = context['name'].replace("(", "_").replace(")", "") 

280 context['function_name'] = function_name 

281 context['indent'] = indent 

282 if hasattr(model_onnx, 'graph'): 

283 context['ir_version'] = model_onnx.ir_version 

284 context['producer_name'] = model_onnx.producer_name 

285 context['domain'] = model_onnx.domain 

286 context['model_version'] = model_onnx.model_version 

287 context['doc_string'] = model_onnx.doc_string 

288 context['metadata'] = { 

289 p.key: p.value for p in model_onnx.metadata_props} 

290 else: 

291 # subgraph 

292 context['ir_version'] = None 

293 context['producer_name'] = None 

294 context['domain'] = None 

295 context['model_version'] = None 

296 context['doc_string'] = "" 

297 context['metadata'] = {} 

298 

299 # common context 

300 context['unique_operators'] = [dict(domain=o[0], name=o[1], classname=o[2]) 

301 for o in sorted(unique_operators)] 

302 context['skip_inits'] = {} 

303 context['subgraphs'] = subgraphs 

304 

305 mark_inits = {} 

306 

307 # First rendering to detect any unused or replaced initializer. 

308 from jinja2 import Template # delayed import 

309 template = Template(templates) 

310 final = template.render( 

311 enumerate=enumerate, sorted=sorted, len=len, 

312 select_attribute=select_attribute, repr=repr, 

313 TENSOR_TYPE_TO_NP_TYPE=TENSOR_TYPE_TO_NP_TYPE, 

314 make_numpy_code=lambda *args, **kwargs: make_numpy_code( 

315 *args, context=context, used=used, mark_inits=mark_inits, 

316 **kwargs), 

317 make_tf2onnx_code=lambda *args, **kwargs: make_tf2onnx_code( 

318 *args, context=context, used=used, mark_inits=mark_inits, 

319 **kwargs), 

320 verbose=verbose, **context) 

321 

322 skip_inits = set() 

323 for k, v in mark_inits.items(): 

324 if len(v) == len(used[k]): 

325 # One initializers was removed. 

326 skip_inits.add(k) 

327 

328 if len(skip_inits) > 0: 

329 # Second rendering if needed when an initializer was replaced 

330 # or removed. 

331 context['skip_inits'] = skip_inits 

332 # Again with skip_inits. 

333 final = template.render( 

334 enumerate=enumerate, sorted=sorted, len=len, 

335 make_numpy_code=lambda *args, **kwargs: make_numpy_code( 

336 *args, context=context, used=used, mark_inits=mark_inits, 

337 **kwargs), 

338 make_tf2onnx_code=lambda *args, **kwargs: make_tf2onnx_code( 

339 *args, context=context, used=used, mark_inits=mark_inits, 

340 **kwargs), 

341 verbose=verbose, **context) 

342 

343 final += "\n" 

344 if not verbose: 

345 rows = final.split("\n") 

346 final = "\n".join(_ for _ in rows if not _.endswith("# verbose")) 

347 return autopep8.fix_code(final, options=autopep_options) 

348 

349 

350def export2onnx(model_onnx, opset=None, verbose=True, name=None, rename=False, 

351 autopep_options=None): 

352 """ 

353 Exports an ONNX model to the :epkg:`onnx` syntax. 

354 

355 :param model_onnx: string or ONNX graph 

356 :param opset: opset to export to 

357 (None to select the one from the graph) 

358 :param verbose: inserts prints 

359 :param name: to overwrite onnx name 

360 :param rename: rename the names to get shorter names 

361 :param autopep_options: :epkg:`autopep8` options 

362 :return: python code 

363 

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

365 implementing the KMeans would look like. 

366 

367 .. runpython:: 

368 :showcode: 

369 :process: 

370 

371 import numpy 

372 from sklearn.cluster import KMeans 

373 from mlprodict.onnx_conv import to_onnx 

374 from mlprodict.onnx_tools.onnx_export import export2onnx 

375 

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

377 tr = KMeans(n_clusters=2) 

378 tr.fit(X) 

379 

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

381 code = export2onnx(onx) 

382 

383 print(code) 

384 """ 

385 if isinstance(model_onnx, str): 

386 model_onnx = onnx.load(model_onnx) 

387 

388 code = export_template(model_onnx, templates=get_onnx_template(), 

389 opset=opset, verbose=verbose, name=name, 

390 rename=rename, use_onnx_tensor=True, 

391 autopep_options=autopep_options) 

392 return code 

393 

394 

395def export2tf2onnx(model_onnx, opset=None, verbose=True, name=None, 

396 rename=False, autopep_options=None): 

397 """ 

398 Exports an ONNX model to the :epkg:`tensorflow-onnx` syntax. 

399 

400 :param model_onnx: string or ONNX graph 

401 :param opset: opset to export to 

402 (None to select the one from the graph) 

403 :param verbose: inserts prints 

404 :param name: to overwrite onnx name 

405 :param rename: rename the names to get shorter names 

406 :param autopep_options: :epkg:`autopep8` options 

407 :return: python code 

408 

409 .. runpython:: 

410 :showcode: 

411 :process: 

412 

413 import numpy 

414 from sklearn.cluster import KMeans 

415 from mlprodict.onnx_conv import to_onnx 

416 from mlprodict.onnx_tools.onnx_export import export2tf2onnx 

417 

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

419 tr = KMeans(n_clusters=2) 

420 tr.fit(X) 

421 

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

423 code = export2tf2onnx(onx) 

424 

425 print(code) 

426 """ 

427 if isinstance(model_onnx, str): 

428 model_onnx = onnx.load(model_onnx) 

429 

430 code = export_template(model_onnx, templates=get_tf2onnx_template(), 

431 opset=opset, verbose=verbose, name=name, 

432 rename=rename, use_onnx_tensor=True, 

433 autopep_options=autopep_options) 

434 code = code.replace("], ]", "]]") 

435 return code 

436 

437 

438def export2numpy(model_onnx, opset=None, verbose=True, name=None, 

439 rename=False, autopep_options=None): 

440 """ 

441 Exports an ONNX model to the :epkg:`numpy` syntax. 

442 The exports does not work with all operators. 

443 

444 :param model_onnx: string or ONNX graph 

445 :param opset: opset to export to 

446 (None to select the one from the graph) 

447 :param verbose: inserts prints 

448 :param name: to overwrite onnx name 

449 :param rename: rename the names to get shorter names 

450 :param autopep_options: :epkg:`autopep8` options 

451 :return: python code 

452 

453 .. runpython:: 

454 :showcode: 

455 :process: 

456 

457 import numpy 

458 from sklearn.cluster import KMeans 

459 from mlprodict.onnx_conv import to_onnx 

460 from mlprodict.onnx_tools.onnx_export import export2numpy 

461 

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

463 tr = KMeans(n_clusters=2) 

464 tr.fit(X) 

465 

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

467 code = export2numpy(onx) 

468 

469 print(code) 

470 

471 This can be applied to the decomposition of an einsum 

472 equation into simple matrix operations. 

473 

474 .. runpython:: 

475 :showcode: 

476 :process: 

477 

478 import numpy 

479 from mlprodict.testing.einsum import decompose_einsum_equation 

480 from mlprodict.onnx_tools.onnx_export import export2numpy 

481 

482 x1 = numpy.arange(8).reshape(2, 2, 2).astype(numpy.float32) 

483 x2 = numpy.arange(4).reshape(2, 2).astype(numpy.float32) 

484 r = numpy.einsum("bac,cd->ad", x1, x2) 

485 

486 seq_clean = decompose_einsum_equation( 

487 "bac,cd->ad", strategy='numpy', clean=True) 

488 onx = seq_clean.to_onnx("Y", "X1", "X2", dtype=numpy.float32) 

489 code = export2numpy(onx, name="einsum") 

490 print(code) 

491 """ 

492 if isinstance(model_onnx, str): 

493 model_onnx = onnx.load(model_onnx) 

494 

495 code = export_template(model_onnx, templates=get_numpy_template(), 

496 opset=opset, verbose=verbose, name=name, 

497 rename=rename, autopep_options=autopep_options) 

498 for i in range(-6, 6): 

499 code = code.replace("axis=tuple([%d])" % i, "axis=%d" % i) 

500 code = code.replace("tuple([%d])" % i, "(%d, )" % i) 

501 return code 

502 

503 

504def export2xop(model_onnx, opset=None, verbose=True, name=None, rename=False, 

505 autopep_options=None): 

506 """ 

507 Exports an ONNX model to the :epkg:`onnx` syntax. 

508 

509 :param model_onnx: string or ONNX graph 

510 :param opset: opset to export to 

511 (None to select the one from the graph) 

512 :param verbose: inserts prints 

513 :param name: to overwrite onnx name 

514 :param rename: rename the names to get shorter names 

515 :param autopep_options: :epkg:`autopep8` options 

516 :return: python code 

517 

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

519 implementing the KMeans would look like. 

520 

521 .. runpython:: 

522 :showcode: 

523 :process: 

524 

525 import numpy 

526 from sklearn.cluster import KMeans 

527 from mlprodict.onnx_conv import to_onnx 

528 from mlprodict.onnx_tools.onnx_export import export2xop 

529 

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

531 tr = KMeans(n_clusters=2) 

532 tr.fit(X) 

533 

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

535 code = export2xop(onx) 

536 

537 print(code) 

538 """ 

539 if isinstance(model_onnx, str): 

540 model_onnx = onnx.load(model_onnx) 

541 

542 code = export_template(model_onnx, templates=get_xop_template(), 

543 opset=opset, verbose=verbose, name=name, 

544 rename=rename, use_onnx_tensor=True, 

545 autopep_options=autopep_options) 

546 return code