Coverage for mlprodict/onnx_conv/operator_converters/conv_lightgbm.py: 95%

317 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-02-04 02:28 +0100

1""" 

2@file 

3@brief Modified converter from 

4`LightGbm.py <https://github.com/onnx/onnxmltools/blob/master/onnxmltools/convert/ 

5lightgbm/operator_converters/LightGbm.py>`_. 

6""" 

7from collections import Counter 

8import copy 

9import numbers 

10import pprint 

11import numpy 

12from onnx import TensorProto 

13from skl2onnx.common._apply_operation import apply_div, apply_reshape, apply_sub # pylint: disable=E0611 

14from skl2onnx.common.tree_ensemble import get_default_tree_classifier_attribute_pairs 

15from skl2onnx.proto import onnx_proto 

16from skl2onnx.common.shape_calculator import ( 

17 calculate_linear_regressor_output_shapes, 

18 calculate_linear_classifier_output_shapes) 

19from skl2onnx.common.data_types import guess_numpy_type 

20from skl2onnx.common.tree_ensemble import sklearn_threshold 

21from ..sklconv.tree_converters import _fix_tree_ensemble 

22from ..helpers.lgbm_helper import ( 

23 dump_lgbm_booster, modify_tree_for_rule_in_set) 

24 

25 

26def calculate_lightgbm_output_shapes(operator): 

27 """ 

28 Shape calculator for LightGBM Booster 

29 (see :epkg:`lightgbm`). 

30 """ 

31 op = operator.raw_operator 

32 if hasattr(op, "_model_dict"): 

33 objective = op._model_dict['objective'] # pragma: no cover 

34 elif hasattr(op, 'objective_'): 

35 objective = op.objective_ 

36 else: 

37 raise RuntimeError( # pragma: no cover 

38 "Unable to find attributes '_model_dict' or 'objective_' in " 

39 "instance of type %r (list of attributes=%r)." % ( 

40 type(op), dir(op))) 

41 if objective.startswith('binary') or objective.startswith('multiclass'): 

42 return calculate_linear_classifier_output_shapes(operator) 

43 if objective.startswith('regression'): # pragma: no cover 

44 return calculate_linear_regressor_output_shapes(operator) 

45 raise NotImplementedError( # pragma: no cover 

46 f"Objective '{objective}' is not implemented yet.") 

47 

48 

49def _translate_split_criterion(criterion): 

50 # If the criterion is true, LightGBM use the left child. Otherwise, right child is selected. 

51 if criterion == '<=': 

52 return 'BRANCH_LEQ' 

53 if criterion == '<': # pragma: no cover 

54 return 'BRANCH_LT' 

55 if criterion == '>=': # pragma: no cover 

56 return 'BRANCH_GTE' 

57 if criterion == '>': # pragma: no cover 

58 return 'BRANCH_GT' 

59 if criterion == '==': # pragma: no cover 

60 return 'BRANCH_EQ' 

61 if criterion == '!=': # pragma: no cover 

62 return 'BRANCH_NEQ' 

63 raise ValueError( # pragma: no cover 

64 'Unsupported splitting criterion: %s. Only <=, ' 

65 '<, >=, and > are allowed.') 

66 

67 

68def _create_node_id(node_id_pool): 

69 i = 0 

70 while i in node_id_pool: 

71 i += 1 

72 node_id_pool.add(i) 

73 return i 

74 

75 

76def _parse_tree_structure(tree_id, class_id, learning_rate, 

77 tree_structure, attrs): 

78 """ 

79 The pool of all nodes' indexes created when parsing a single tree. 

80 Different tree use different pools. 

81 """ 

82 node_id_pool = set() 

83 node_pyid_pool = dict() 

84 

85 node_id = _create_node_id(node_id_pool) 

86 node_pyid_pool[id(tree_structure)] = node_id 

87 

88 # The root node is a leaf node. 

89 if ('left_child' not in tree_structure or 

90 'right_child' not in tree_structure): 

91 _parse_node(tree_id, class_id, node_id, node_id_pool, node_pyid_pool, 

92 learning_rate, tree_structure, attrs) 

93 return 

94 

95 left_pyid = id(tree_structure['left_child']) 

96 right_pyid = id(tree_structure['right_child']) 

97 

98 if left_pyid in node_pyid_pool: 

99 left_id = node_pyid_pool[left_pyid] 

100 left_parse = False 

101 else: 

102 left_id = _create_node_id(node_id_pool) 

103 node_pyid_pool[left_pyid] = left_id 

104 left_parse = True 

105 

106 if right_pyid in node_pyid_pool: 

107 right_id = node_pyid_pool[right_pyid] 

108 right_parse = False 

109 else: 

110 right_id = _create_node_id(node_id_pool) 

111 node_pyid_pool[right_pyid] = right_id 

112 right_parse = True 

113 

114 attrs['nodes_treeids'].append(tree_id) 

115 attrs['nodes_nodeids'].append(node_id) 

116 

117 attrs['nodes_featureids'].append(tree_structure['split_feature']) 

118 mode = _translate_split_criterion(tree_structure['decision_type']) 

119 attrs['nodes_modes'].append(mode) 

120 

121 if isinstance(tree_structure['threshold'], str): 

122 try: # pragma: no cover 

123 th = float(tree_structure['threshold']) # pragma: no cover 

124 except ValueError as e: # pragma: no cover 

125 text = pprint.pformat(tree_structure) 

126 if len(text) > 99999: 

127 text = text[:99999] + "\n..." 

128 raise TypeError("threshold must be a number not '{}'" 

129 "\n{}".format(tree_structure['threshold'], text)) from e 

130 else: 

131 th = tree_structure['threshold'] 

132 if mode == 'BRANCH_LEQ': 

133 th2 = sklearn_threshold(th, numpy.float32, mode) 

134 else: 

135 # other decision criteria are not implemented 

136 th2 = th 

137 attrs['nodes_values'].append(th2) 

138 

139 # Assume left is the true branch and right is the false branch 

140 attrs['nodes_truenodeids'].append(left_id) 

141 attrs['nodes_falsenodeids'].append(right_id) 

142 if tree_structure['default_left']: 

143 # attrs['nodes_missing_value_tracks_true'].append(1) 

144 if (tree_structure["missing_type"] in ('None', None) and 

145 float(tree_structure['threshold']) < 0.0): 

146 attrs['nodes_missing_value_tracks_true'].append(0) 

147 else: 

148 attrs['nodes_missing_value_tracks_true'].append(1) 

149 else: 

150 attrs['nodes_missing_value_tracks_true'].append(0) 

151 attrs['nodes_hitrates'].append(1.) 

152 if left_parse: 

153 _parse_node( 

154 tree_id, class_id, left_id, node_id_pool, node_pyid_pool, 

155 learning_rate, tree_structure['left_child'], attrs) 

156 if right_parse: 

157 _parse_node( 

158 tree_id, class_id, right_id, node_id_pool, node_pyid_pool, 

159 learning_rate, tree_structure['right_child'], attrs) 

160 

161 

162def _parse_node(tree_id, class_id, node_id, node_id_pool, node_pyid_pool, 

163 learning_rate, node, attrs): 

164 """ 

165 Parses nodes. 

166 """ 

167 if ((hasattr(node, 'left_child') and hasattr(node, 'right_child')) or 

168 ('left_child' in node and 'right_child' in node)): 

169 

170 left_pyid = id(node['left_child']) 

171 right_pyid = id(node['right_child']) 

172 

173 if left_pyid in node_pyid_pool: 

174 left_id = node_pyid_pool[left_pyid] 

175 left_parse = False 

176 else: 

177 left_id = _create_node_id(node_id_pool) 

178 node_pyid_pool[left_pyid] = left_id 

179 left_parse = True 

180 

181 if right_pyid in node_pyid_pool: 

182 right_id = node_pyid_pool[right_pyid] 

183 right_parse = False 

184 else: 

185 right_id = _create_node_id(node_id_pool) 

186 node_pyid_pool[right_pyid] = right_id 

187 right_parse = True 

188 

189 attrs['nodes_treeids'].append(tree_id) 

190 attrs['nodes_nodeids'].append(node_id) 

191 

192 attrs['nodes_featureids'].append(node['split_feature']) 

193 attrs['nodes_modes'].append( 

194 _translate_split_criterion(node['decision_type'])) 

195 if isinstance(node['threshold'], str): 

196 try: # pragma: no cover 

197 attrs['nodes_values'].append( # pragma: no cover 

198 float(node['threshold'])) 

199 except ValueError as e: # pragma: no cover 

200 text = pprint.pformat(node) 

201 if len(text) > 99999: 

202 text = text[:99999] + "\n..." 

203 raise TypeError("threshold must be a number not '{}'" 

204 "\n{}".format(node['threshold'], text)) from e 

205 else: 

206 attrs['nodes_values'].append(node['threshold']) 

207 

208 # Assume left is the true branch and right is the false branch 

209 attrs['nodes_truenodeids'].append(left_id) 

210 attrs['nodes_falsenodeids'].append(right_id) 

211 if node['default_left']: 

212 # attrs['nodes_missing_value_tracks_true'].append(1) 

213 if (node['missing_type'] in ('None', None) and 

214 float(node['threshold']) < 0.0): 

215 attrs['nodes_missing_value_tracks_true'].append(0) 

216 else: 

217 attrs['nodes_missing_value_tracks_true'].append(1) 

218 else: 

219 attrs['nodes_missing_value_tracks_true'].append(0) 

220 attrs['nodes_hitrates'].append(1.) 

221 

222 # Recursively dive into the child nodes 

223 if left_parse: 

224 _parse_node( 

225 tree_id, class_id, left_id, node_id_pool, node_pyid_pool, 

226 learning_rate, node['left_child'], attrs) 

227 if right_parse: 

228 _parse_node( 

229 tree_id, class_id, right_id, node_id_pool, node_pyid_pool, 

230 learning_rate, node['right_child'], attrs) 

231 elif hasattr(node, 'left_child') or hasattr(node, 'right_child'): 

232 raise ValueError('Need two branches') # pragma: no cover 

233 else: 

234 # Node attributes 

235 attrs['nodes_treeids'].append(tree_id) 

236 attrs['nodes_nodeids'].append(node_id) 

237 attrs['nodes_featureids'].append(0) 

238 attrs['nodes_modes'].append('LEAF') 

239 # Leaf node has no threshold. A zero is appended but it will never be used. 

240 attrs['nodes_values'].append(0.) 

241 # Leaf node has no child. A zero is appended but it will never be used. 

242 attrs['nodes_truenodeids'].append(0) 

243 # Leaf node has no child. A zero is appended but it will never be used. 

244 attrs['nodes_falsenodeids'].append(0) 

245 # Leaf node has no split function. A zero is appended but it will never be used. 

246 attrs['nodes_missing_value_tracks_true'].append(0) 

247 attrs['nodes_hitrates'].append(1.) 

248 

249 # Leaf attributes 

250 attrs['class_treeids'].append(tree_id) 

251 attrs['class_nodeids'].append(node_id) 

252 attrs['class_ids'].append(class_id) 

253 attrs['class_weights'].append( 

254 float(node['leaf_value']) * learning_rate) 

255 

256 

257def _split_tree_ensemble_atts(attrs, split): 

258 """ 

259 Splits the attributes of a TreeEnsembleRegressor into 

260 multiple trees in order to do the summation in double instead of floats. 

261 """ 

262 trees_id = list(sorted(set(attrs['nodes_treeids']))) 

263 results = [] 

264 index = 0 

265 while index < len(trees_id): 

266 index2 = min(index + split, len(trees_id)) 

267 subset = set(trees_id[index: index2]) 

268 

269 indices_node = [] 

270 indices_target = [] 

271 for j, v in enumerate(attrs['nodes_treeids']): 

272 if v in subset: 

273 indices_node.append(j) 

274 for j, v in enumerate(attrs['target_treeids']): 

275 if v in subset: 

276 indices_target.append(j) 

277 

278 if (len(indices_node) >= len(attrs['nodes_treeids']) or 

279 len(indices_target) >= len(attrs['target_treeids'])): 

280 raise RuntimeError( # pragma: no cover 

281 "Initial attributes are not consistant." 

282 "\nindex=%r index2=%r subset=%r" 

283 "\nnodes_treeids=%r\ntarget_treeids=%r" 

284 "\nindices_node=%r\nindices_target=%r" % ( 

285 index, index2, subset, 

286 attrs['nodes_treeids'], attrs['target_treeids'], 

287 indices_node, indices_target)) 

288 

289 ats = {} 

290 for name, att in attrs.items(): 

291 if name == 'nodes_treeids': 

292 new_att = [att[i] for i in indices_node] 

293 new_att = [i - att[0] for i in new_att] 

294 elif name == 'target_treeids': 

295 new_att = [att[i] for i in indices_target] 

296 new_att = [i - att[0] for i in new_att] 

297 elif name.startswith("nodes_"): 

298 new_att = [att[i] for i in indices_node] 

299 assert len(new_att) == len(indices_node) 

300 elif name.startswith("target_"): 

301 new_att = [att[i] for i in indices_target] 

302 assert len(new_att) == len(indices_target) 

303 elif name == 'name': 

304 new_att = f"{att}{len(results)}" 

305 else: 

306 new_att = att 

307 ats[name] = new_att 

308 

309 results.append(ats) 

310 index = index2 

311 

312 return results 

313 

314 

315def _select_close_float(x): 

316 """ 

317 Selects the closest float to `x`. 

318 It returns always `numpy.float32(x)`. 

319 """ 

320 if isinstance(x, (numpy.float32, numpy.float16)): 

321 return x 

322 if not isinstance(x, (float, numpy.float64)): 

323 raise TypeError(f"Unexpected type for x ({type(x)}), " 

324 f"it should be a double.") 

325 eps = numpy.finfo(numpy.float32).eps 

326 x64 = numpy.float64(x) 

327 r = numpy.float32(x64) 

328 if numpy.float64(r) == x64: 

329 return r 

330 delta = r - x64 

331 direction = (eps) if delta < 0 else (-eps) 

332 diff1 = abs(delta) 

333 nr64 = r + direction 

334 nr = numpy.float32(nr64) 

335 diff2 = abs(nr - x64) 

336 return r if diff1 <= diff2 else nr 

337 

338 

339def convert_lightgbm(scope, operator, container): # pylint: disable=R0914 

340 """ 

341 This converters reuses the code from 

342 `LightGbm.py <https://github.com/onnx/onnxmltools/blob/master/onnxmltools/convert/ 

343 lightgbm/operator_converters/LightGbm.py>`_ and makes 

344 some modifications. It implements converters 

345 for models in :epkg:`lightgbm`. 

346 """ 

347 verbose = getattr(container, 'verbose', 0) 

348 gbm_model = operator.raw_operator 

349 if hasattr(gbm_model, '_model_dict_info'): 

350 gbm_text, info = gbm_model._model_dict_info 

351 else: 

352 if verbose >= 2: 

353 print("[convert_lightgbm] dump_model") # pragma: no cover 

354 gbm_text, info = dump_lgbm_booster(gbm_model.booster_, verbose=verbose) 

355 opsetml = container.target_opset_all.get('ai.onnx.ml', None) 

356 if opsetml is None: 

357 opsetml = 3 if container.target_opset >= 16 else 1 

358 if verbose >= 2: 

359 print( # pragma: no cover 

360 "[convert_lightgbm] modify_tree_for_rule_in_set") 

361 modify_tree_for_rule_in_set(gbm_text, use_float=True, verbose=verbose, 

362 info=info) 

363 

364 attrs = get_default_tree_classifier_attribute_pairs() 

365 attrs['name'] = operator.full_name 

366 

367 # Create different attributes for classifier and 

368 # regressor, respectively 

369 post_transform = None 

370 if gbm_text['objective'].startswith('binary'): 

371 n_classes = 1 

372 attrs['post_transform'] = 'LOGISTIC' 

373 elif gbm_text['objective'].startswith('multiclass'): 

374 n_classes = gbm_text['num_class'] 

375 attrs['post_transform'] = 'SOFTMAX' 

376 elif gbm_text['objective'].startswith('regression'): 

377 n_classes = 1 # Regressor has only one output variable 

378 attrs['post_transform'] = 'NONE' 

379 attrs['n_targets'] = n_classes 

380 elif gbm_text['objective'].startswith(('poisson', 'gamma')): 

381 n_classes = 1 # Regressor has only one output variable 

382 attrs['n_targets'] = n_classes 

383 # 'Exp' is not a supported post_transform value in the ONNX spec yet, 

384 # so we need to add an 'Exp' post transform node to the model 

385 attrs['post_transform'] = 'NONE' 

386 post_transform = "Exp" 

387 else: 

388 raise RuntimeError( # pragma: no cover 

389 "LightGBM objective should be cleaned already not '{}'.".format( 

390 gbm_text['objective'])) 

391 

392 # Use the same algorithm to parse the tree 

393 if verbose >= 2: # pragma: no cover 

394 from tqdm import tqdm 

395 loop = tqdm(gbm_text['tree_info']) 

396 loop.set_description("parse") 

397 else: 

398 loop = gbm_text['tree_info'] 

399 for i, tree in enumerate(loop): 

400 tree_id = i 

401 class_id = tree_id % n_classes 

402 # tree['shrinkage'] --> LightGbm provides figures with it already. 

403 learning_rate = 1. 

404 _parse_tree_structure( 

405 tree_id, class_id, learning_rate, tree['tree_structure'], attrs) 

406 

407 if verbose >= 2: 

408 print("[convert_lightgbm] onnx") # pragma: no cover 

409 # Sort nodes_* attributes. For one tree, its node indexes 

410 # should appear in an ascent order in nodes_nodeids. Nodes 

411 # from a tree with a smaller tree index should appear 

412 # before trees with larger indexes in nodes_nodeids. 

413 node_numbers_per_tree = Counter(attrs['nodes_treeids']) 

414 tree_number = len(node_numbers_per_tree.keys()) 

415 accumulated_node_numbers = [0] * tree_number 

416 for i in range(1, tree_number): 

417 accumulated_node_numbers[i] = ( 

418 accumulated_node_numbers[i - 1] + node_numbers_per_tree[i - 1]) 

419 global_node_indexes = [] 

420 for i in range(len(attrs['nodes_nodeids'])): 

421 tree_id = attrs['nodes_treeids'][i] 

422 node_id = attrs['nodes_nodeids'][i] 

423 global_node_indexes.append( 

424 accumulated_node_numbers[tree_id] + node_id) 

425 for k, v in attrs.items(): 

426 if k.startswith('nodes_'): 

427 merged_indexes = zip( 

428 copy.deepcopy(global_node_indexes), v) 

429 sorted_list = [pair[1] 

430 for pair in sorted(merged_indexes, 

431 key=lambda x: x[0])] 

432 attrs[k] = sorted_list 

433 

434 dtype = guess_numpy_type(operator.inputs[0].type) 

435 if dtype != numpy.float64: 

436 dtype = numpy.float32 

437 

438 if dtype == numpy.float64: 

439 for key in ['nodes_values', 'nodes_hitrates', 'target_weights', 

440 'class_weights', 'base_values']: 

441 if key not in attrs: 

442 continue 

443 attrs[key] = numpy.array(attrs[key], dtype=dtype) 

444 

445 # Create ONNX object 

446 if (gbm_text['objective'].startswith('binary') or 

447 gbm_text['objective'].startswith('multiclass')): 

448 # Prepare label information for both of TreeEnsembleClassifier 

449 # and ZipMap 

450 class_type = onnx_proto.TensorProto.STRING # pylint: disable=E1101 

451 zipmap_attrs = {'name': scope.get_unique_variable_name('ZipMap')} 

452 if all(isinstance(i, (numbers.Real, bool, numpy.bool_)) 

453 for i in gbm_model.classes_): 

454 class_type = onnx_proto.TensorProto.INT64 # pylint: disable=E1101 

455 class_labels = [int(i) for i in gbm_model.classes_] 

456 attrs['classlabels_int64s'] = class_labels 

457 zipmap_attrs['classlabels_int64s'] = class_labels 

458 elif all(isinstance(i, str) for i in gbm_model.classes_): 

459 class_labels = [str(i) for i in gbm_model.classes_] 

460 attrs['classlabels_strings'] = class_labels 

461 zipmap_attrs['classlabels_strings'] = class_labels 

462 else: 

463 raise ValueError( # pragma: no cover 

464 'Only string and integer class labels are allowed') 

465 

466 # Create tree classifier 

467 probability_tensor_name = scope.get_unique_variable_name( 

468 'probability_tensor') 

469 label_tensor_name = scope.get_unique_variable_name('label_tensor') 

470 

471 if dtype == numpy.float64 and opsetml < 3: 

472 container.add_node('TreeEnsembleClassifierDouble', operator.input_full_names, 

473 [label_tensor_name, probability_tensor_name], 

474 op_domain='mlprodict', op_version=1, **attrs) 

475 else: 

476 container.add_node('TreeEnsembleClassifier', operator.input_full_names, 

477 [label_tensor_name, probability_tensor_name], 

478 op_domain='ai.onnx.ml', op_version=1, **attrs) 

479 

480 prob_tensor = probability_tensor_name 

481 

482 if gbm_model.boosting_type == 'rf': 

483 col_index_name = scope.get_unique_variable_name('col_index') 

484 first_col_name = scope.get_unique_variable_name('first_col') 

485 zeroth_col_name = scope.get_unique_variable_name('zeroth_col') 

486 denominator_name = scope.get_unique_variable_name('denominator') 

487 modified_first_col_name = scope.get_unique_variable_name( 

488 'modified_first_col') 

489 unit_float_tensor_name = scope.get_unique_variable_name( 

490 'unit_float_tensor') 

491 merged_prob_name = scope.get_unique_variable_name('merged_prob') 

492 predicted_label_name = scope.get_unique_variable_name( 

493 'predicted_label') 

494 classes_name = scope.get_unique_variable_name('classes') 

495 final_label_name = scope.get_unique_variable_name('final_label') 

496 

497 container.add_initializer( 

498 col_index_name, onnx_proto.TensorProto.INT64, [], [1]) # pylint: disable=E1101 

499 container.add_initializer( 

500 unit_float_tensor_name, onnx_proto.TensorProto.FLOAT, [], [1.0]) # pylint: disable=E1101 

501 container.add_initializer( 

502 denominator_name, onnx_proto.TensorProto.FLOAT, [], [100.0]) # pylint: disable=E1101 

503 container.add_initializer(classes_name, class_type, 

504 [len(class_labels)], class_labels) 

505 

506 container.add_node( 

507 'ArrayFeatureExtractor', 

508 [probability_tensor_name, col_index_name], 

509 first_col_name, 

510 name=scope.get_unique_operator_name( 

511 'ArrayFeatureExtractor'), 

512 op_domain='ai.onnx.ml') 

513 apply_div(scope, [first_col_name, denominator_name], 

514 modified_first_col_name, container, broadcast=1) 

515 apply_sub( 

516 scope, [unit_float_tensor_name, modified_first_col_name], 

517 zeroth_col_name, container, broadcast=1) 

518 container.add_node( 

519 'Concat', [zeroth_col_name, modified_first_col_name], 

520 merged_prob_name, 

521 name=scope.get_unique_operator_name('Concat'), axis=1) 

522 container.add_node( 

523 'ArgMax', merged_prob_name, 

524 predicted_label_name, 

525 name=scope.get_unique_operator_name('ArgMax'), axis=1) 

526 container.add_node( 

527 'ArrayFeatureExtractor', [classes_name, predicted_label_name], 

528 final_label_name, 

529 name=scope.get_unique_operator_name('ArrayFeatureExtractor'), 

530 op_domain='ai.onnx.ml') 

531 apply_reshape(scope, final_label_name, 

532 operator.outputs[0].full_name, 

533 container, desired_shape=[-1, ]) 

534 prob_tensor = merged_prob_name 

535 else: 

536 container.add_node('Identity', label_tensor_name, 

537 operator.outputs[0].full_name, 

538 name=scope.get_unique_operator_name('Identity')) 

539 

540 # Convert probability tensor to probability map 

541 # (keys are labels while values are the associated probabilities) 

542 container.add_node('Identity', prob_tensor, 

543 operator.outputs[1].full_name) 

544 else: 

545 # Create tree regressor 

546 output_name = scope.get_unique_variable_name('output') 

547 

548 keys_to_be_renamed = list( 

549 k for k in attrs if k.startswith('class_')) 

550 

551 for k in keys_to_be_renamed: 

552 # Rename class_* attribute to target_* 

553 # because TreeEnsebmleClassifier 

554 # and TreeEnsembleClassifier have different ONNX attributes 

555 attrs['target' + k[5:]] = copy.deepcopy(attrs[k]) 

556 del attrs[k] 

557 

558 options = container.get_options(gbm_model, dict(split=-1)) 

559 split = options['split'] 

560 if split == -1: 

561 if dtype == numpy.float64 and opsetml < 3: 

562 container.add_node( 

563 'TreeEnsembleRegressorDouble', operator.input_full_names, 

564 output_name, op_domain='mlprodict', op_version=1, **attrs) 

565 else: 

566 container.add_node( 

567 'TreeEnsembleRegressor', operator.input_full_names, 

568 output_name, op_domain='ai.onnx.ml', op_version=1, **attrs) 

569 else: 

570 tree_attrs = _split_tree_ensemble_atts(attrs, split) 

571 tree_nodes = [] 

572 for i, ats in enumerate(tree_attrs): 

573 tree_name = scope.get_unique_variable_name('tree%d' % i) 

574 if dtype == numpy.float64: 

575 container.add_node( 

576 'TreeEnsembleRegressorDouble', operator.input_full_names, 

577 tree_name, op_domain='mlprodict', op_version=1, **ats) 

578 tree_nodes.append(tree_name) 

579 else: 

580 container.add_node( 

581 'TreeEnsembleRegressor', operator.input_full_names, 

582 tree_name, op_domain='ai.onnx.ml', op_version=1, **ats) 

583 cast_name = scope.get_unique_variable_name('dtree%d' % i) 

584 container.add_node( 

585 'Cast', tree_name, cast_name, to=TensorProto.DOUBLE, # pylint: disable=E1101 

586 name=scope.get_unique_operator_name("dtree%d" % i)) 

587 tree_nodes.append(cast_name) 

588 if dtype == numpy.float64: 

589 container.add_node( 

590 'Sum', tree_nodes, output_name, 

591 name=scope.get_unique_operator_name(f"sumtree{len(tree_nodes)}")) 

592 else: 

593 cast_name = scope.get_unique_variable_name('ftrees') 

594 container.add_node( 

595 'Sum', tree_nodes, cast_name, 

596 name=scope.get_unique_operator_name(f"sumtree{len(tree_nodes)}")) 

597 container.add_node( 

598 'Cast', cast_name, output_name, to=TensorProto.FLOAT, # pylint: disable=E1101 

599 name=scope.get_unique_operator_name("dtree%d" % i)) 

600 

601 if gbm_model.boosting_type == 'rf': 

602 denominator_name = scope.get_unique_variable_name('denominator') 

603 

604 container.add_initializer( 

605 denominator_name, onnx_proto.TensorProto.FLOAT, # pylint: disable=E1101 

606 [], [100.0]) 

607 

608 apply_div(scope, [output_name, denominator_name], 

609 operator.output_full_names, container, broadcast=1) 

610 elif post_transform: 

611 container.add_node( 

612 post_transform, output_name, 

613 operator.output_full_names, 

614 name=scope.get_unique_operator_name( 

615 post_transform)) 

616 else: 

617 container.add_node('Identity', output_name, 

618 operator.output_full_names, 

619 name=scope.get_unique_operator_name('Identity')) 

620 if opsetml >= 3: 

621 _fix_tree_ensemble(scope, container, opsetml, dtype) 

622 if verbose >= 2: 

623 print("[convert_lightgbm] end") # pragma: no cover