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

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

301 statements  

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

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 "Objective '{}' is not implemented yet.".format(objective)) 

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 = "%s%d" % (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 convert_lightgbm(scope, operator, container): # pylint: disable=R0914 

316 """ 

317 This converters reuses the code from 

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

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

320 some modifications. It implements converters 

321 for models in :epkg:`lightgbm`. 

322 """ 

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

324 gbm_model = operator.raw_operator 

325 if hasattr(gbm_model, '_model_dict_info'): 

326 gbm_text, info = gbm_model._model_dict_info 

327 else: 

328 if verbose >= 2: 

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

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

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

332 if opsetml is None: 

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

334 if verbose >= 2: 

335 print( # pragma: no cover 

336 "[convert_lightgbm] modify_tree_for_rule_in_set") 

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

338 info=info) 

339 

340 attrs = get_default_tree_classifier_attribute_pairs() 

341 attrs['name'] = operator.full_name 

342 

343 # Create different attributes for classifier and 

344 # regressor, respectively 

345 post_transform = None 

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

347 n_classes = 1 

348 attrs['post_transform'] = 'LOGISTIC' 

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

350 n_classes = gbm_text['num_class'] 

351 attrs['post_transform'] = 'SOFTMAX' 

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

353 n_classes = 1 # Regressor has only one output variable 

354 attrs['post_transform'] = 'NONE' 

355 attrs['n_targets'] = n_classes 

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

357 n_classes = 1 # Regressor has only one output variable 

358 attrs['n_targets'] = n_classes 

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

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

361 attrs['post_transform'] = 'NONE' 

362 post_transform = "Exp" 

363 else: 

364 raise RuntimeError( # pragma: no cover 

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

366 gbm_text['objective'])) 

367 

368 # Use the same algorithm to parse the tree 

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

370 from tqdm import tqdm 

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

372 loop.set_description("parse") 

373 else: 

374 loop = gbm_text['tree_info'] 

375 for i, tree in enumerate(loop): 

376 tree_id = i 

377 class_id = tree_id % n_classes 

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

379 learning_rate = 1. 

380 _parse_tree_structure( 

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

382 

383 if verbose >= 2: 

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

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

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

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

388 # before trees with larger indexes in nodes_nodeids. 

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

390 tree_number = len(node_numbers_per_tree.keys()) 

391 accumulated_node_numbers = [0] * tree_number 

392 for i in range(1, tree_number): 

393 accumulated_node_numbers[i] = ( 

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

395 global_node_indexes = [] 

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

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

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

399 global_node_indexes.append( 

400 accumulated_node_numbers[tree_id] + node_id) 

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

402 if k.startswith('nodes_'): 

403 merged_indexes = zip( 

404 copy.deepcopy(global_node_indexes), v) 

405 sorted_list = [pair[1] 

406 for pair in sorted(merged_indexes, 

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

408 attrs[k] = sorted_list 

409 

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

411 if dtype != numpy.float64: 

412 dtype = numpy.float32 

413 

414 if dtype == numpy.float64: 

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

416 'class_weights', 'base_values']: 

417 if key not in attrs: 

418 continue 

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

420 

421 # Create ONNX object 

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

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

424 # Prepare label information for both of TreeEnsembleClassifier 

425 # and ZipMap 

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

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

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

429 for i in gbm_model.classes_): 

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

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

432 attrs['classlabels_int64s'] = class_labels 

433 zipmap_attrs['classlabels_int64s'] = class_labels 

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

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

436 attrs['classlabels_strings'] = class_labels 

437 zipmap_attrs['classlabels_strings'] = class_labels 

438 else: 

439 raise ValueError( # pragma: no cover 

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

441 

442 # Create tree classifier 

443 probability_tensor_name = scope.get_unique_variable_name( 

444 'probability_tensor') 

445 label_tensor_name = scope.get_unique_variable_name('label_tensor') 

446 

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

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

449 [label_tensor_name, probability_tensor_name], 

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

451 else: 

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

453 [label_tensor_name, probability_tensor_name], 

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

455 

456 prob_tensor = probability_tensor_name 

457 

458 if gbm_model.boosting_type == 'rf': 

459 col_index_name = scope.get_unique_variable_name('col_index') 

460 first_col_name = scope.get_unique_variable_name('first_col') 

461 zeroth_col_name = scope.get_unique_variable_name('zeroth_col') 

462 denominator_name = scope.get_unique_variable_name('denominator') 

463 modified_first_col_name = scope.get_unique_variable_name( 

464 'modified_first_col') 

465 unit_float_tensor_name = scope.get_unique_variable_name( 

466 'unit_float_tensor') 

467 merged_prob_name = scope.get_unique_variable_name('merged_prob') 

468 predicted_label_name = scope.get_unique_variable_name( 

469 'predicted_label') 

470 classes_name = scope.get_unique_variable_name('classes') 

471 final_label_name = scope.get_unique_variable_name('final_label') 

472 

473 container.add_initializer( 

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

475 container.add_initializer( 

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

477 container.add_initializer( 

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

479 container.add_initializer(classes_name, class_type, 

480 [len(class_labels)], class_labels) 

481 

482 container.add_node( 

483 'ArrayFeatureExtractor', 

484 [probability_tensor_name, col_index_name], 

485 first_col_name, 

486 name=scope.get_unique_operator_name( 

487 'ArrayFeatureExtractor'), 

488 op_domain='ai.onnx.ml') 

489 apply_div(scope, [first_col_name, denominator_name], 

490 modified_first_col_name, container, broadcast=1) 

491 apply_sub( 

492 scope, [unit_float_tensor_name, modified_first_col_name], 

493 zeroth_col_name, container, broadcast=1) 

494 container.add_node( 

495 'Concat', [zeroth_col_name, modified_first_col_name], 

496 merged_prob_name, 

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

498 container.add_node( 

499 'ArgMax', merged_prob_name, 

500 predicted_label_name, 

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

502 container.add_node( 

503 'ArrayFeatureExtractor', [classes_name, predicted_label_name], 

504 final_label_name, 

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

506 op_domain='ai.onnx.ml') 

507 apply_reshape(scope, final_label_name, 

508 operator.outputs[0].full_name, 

509 container, desired_shape=[-1, ]) 

510 prob_tensor = merged_prob_name 

511 else: 

512 container.add_node('Identity', label_tensor_name, 

513 operator.outputs[0].full_name, 

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

515 

516 # Convert probability tensor to probability map 

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

518 container.add_node('Identity', prob_tensor, 

519 operator.outputs[1].full_name) 

520 else: 

521 # Create tree regressor 

522 output_name = scope.get_unique_variable_name('output') 

523 

524 keys_to_be_renamed = list( 

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

526 

527 for k in keys_to_be_renamed: 

528 # Rename class_* attribute to target_* 

529 # because TreeEnsebmleClassifier 

530 # and TreeEnsembleClassifier have different ONNX attributes 

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

532 del attrs[k] 

533 

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

535 split = options['split'] 

536 if split == -1: 

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

538 container.add_node( 

539 'TreeEnsembleRegressorDouble', operator.input_full_names, 

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

541 else: 

542 container.add_node( 

543 'TreeEnsembleRegressor', operator.input_full_names, 

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

545 else: 

546 tree_attrs = _split_tree_ensemble_atts(attrs, split) 

547 tree_nodes = [] 

548 for i, ats in enumerate(tree_attrs): 

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

550 if dtype == numpy.float64: 

551 container.add_node( 

552 'TreeEnsembleRegressorDouble', operator.input_full_names, 

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

554 tree_nodes.append(tree_name) 

555 else: 

556 container.add_node( 

557 'TreeEnsembleRegressor', operator.input_full_names, 

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

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

560 container.add_node( 

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

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

563 tree_nodes.append(cast_name) 

564 if dtype == numpy.float64: 

565 container.add_node( 

566 'Sum', tree_nodes, output_name, 

567 name=scope.get_unique_operator_name("sumtree%d" % len(tree_nodes))) 

568 else: 

569 cast_name = scope.get_unique_variable_name('ftrees') 

570 container.add_node( 

571 'Sum', tree_nodes, cast_name, 

572 name=scope.get_unique_operator_name("sumtree%d" % len(tree_nodes))) 

573 container.add_node( 

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

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

576 

577 if gbm_model.boosting_type == 'rf': 

578 denominator_name = scope.get_unique_variable_name('denominator') 

579 

580 container.add_initializer( 

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

582 [], [100.0]) 

583 

584 apply_div(scope, [output_name, denominator_name], 

585 operator.output_full_names, container, broadcast=1) 

586 elif post_transform: 

587 container.add_node( 

588 post_transform, output_name, 

589 operator.output_full_names, 

590 name=scope.get_unique_operator_name( 

591 post_transform)) 

592 else: 

593 container.add_node('Identity', output_name, 

594 operator.output_full_names, 

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

596 if opsetml >= 3: 

597 _fix_tree_ensemble(scope, container, opsetml, dtype) 

598 if verbose >= 2: 

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