diff --git a/onnxruntime/core/providers/cpu/ml/tree_ensemble_classifier.cc b/onnxruntime/core/providers/cpu/ml/tree_ensemble_classifier.cc index 4c7ac2b327..c0f312076c 100644 --- a/onnxruntime/core/providers/cpu/ml/tree_ensemble_classifier.cc +++ b/onnxruntime/core/providers/cpu/ml/tree_ensemble_classifier.cc @@ -293,6 +293,58 @@ void TreeEnsembleClassifier::Initialize() { base_values_.size() == weights_classes_.size()); } +void get_max_weight(const std::map& classes, int64_t& maxclass, float& maxweight) { + maxclass = -1; + maxweight = 0.f; + for (auto& classe : classes) { + if (maxclass == -1 || classe.second > maxweight) { + maxclass = classe.first; + maxweight = classe.second; + } + } +} + +void get_weight_class_positive(std::map& classes, float& pos_weight) { + auto it_classes = classes.find(1); + pos_weight = it_classes == classes.end() + ? (classes.size() > 0 ? classes[0] : 0.f) // only 1 class + : it_classes->second; +} + +template +void _set_score_binary(int64_t i, LabelType* y_data, int& write_additional_scores, + bool weights_are_all_positive_, + std::map& classes, + const std::vector& classes_labels_, + const std::set& weights_classes_, + LabelType positive_label, LabelType negative_label) { + float pos_weight; + get_weight_class_positive(classes, pos_weight); + if (classes_labels_.size() == 2 && weights_classes_.size() == 1) { + if (weights_are_all_positive_) { + if (pos_weight > 0.5) { + y_data[i] = classes_labels_[1]; // positive label + write_additional_scores = 0; + } else { + y_data[i] = classes_labels_[0]; // negative label + write_additional_scores = 1; + } + } else { + if (pos_weight > 0) { + y_data[i] = classes_labels_[1]; // positive label + write_additional_scores = 2; + } else { + y_data[i] = classes_labels_[0]; // negative label + write_additional_scores = 3; + } + } + } else if (pos_weight > 0) { + y_data[i] = positive_label; // positive label + } else { + y_data[i] = negative_label; // negative label + } +} + template common::Status TreeEnsembleClassifier::Compute(OpKernelContext* context) const { const Tensor& X = *context->Input(0); @@ -317,11 +369,6 @@ common::Status TreeEnsembleClassifier::Compute(OpKernelContext* context) cons scores.clear(); int64_t current_weight_0 = i * stride; std::map classes; - // fill in base values, this might be empty but that is ok - for (int64_t k = 0, end = static_cast(base_values_.size()); k < end; ++k) { - auto p1 = std::make_pair(k, base_values_[k]); - classes.insert(p1); - } // walk each tree from its root for (size_t j = 0, end = roots_.size(); j < end; ++j) { ORT_RETURN_IF_ERROR(ProcessTreeNode(classes, roots_[j], x_data, current_weight_0)); @@ -331,12 +378,18 @@ common::Status TreeEnsembleClassifier::Compute(OpKernelContext* context) cons // write top class int write_additional_scores = -1; if (class_count_ > 2) { - for (auto& classe : classes) { - if (maxclass == -1 || classe.second > maxweight) { - maxclass = classe.first; - maxweight = classe.second; + // add base values + std::map::iterator it_classes; + for (int64_t k = 0, end = static_cast(base_values_.size()); k < end; ++k) { + it_classes = classes.find(k); + if (it_classes == classes.end()) { + auto p1 = std::make_pair(k, base_values_[k]); + classes.insert(p1); + } else { + it_classes->second += base_values_[k]; } } + get_max_weight(classes, maxclass, maxweight); if (using_strings_) { Y->template MutableData()[i] = classlabels_strings_[maxclass]; } else { @@ -344,68 +397,32 @@ common::Status TreeEnsembleClassifier::Compute(OpKernelContext* context) cons } } else // binary case { - maxweight = !classes.empty() ? classes[0] : 0.f; // only 1 class + if (base_values_.size() == 2) { + // add base values + std::map::iterator it_classes; + it_classes = classes.find(1); + if (it_classes == classes.end()) { + // base_value_[0] is not used. It assumes base_value[0] == base_value[1] in this case. + // The specification does not forbid it but does not say what the output should be in that case. + std::map::iterator it_classes0 = classes.find(0); + classes[1] = base_values_[1] + it_classes0->second; + it_classes0->second = -classes[1]; + } else { + // binary as multiclass + it_classes->second += base_values_[1]; + classes[0] += base_values_[0]; + } + } if (using_strings_) { - auto* y_data = Y->template MutableData(); - if (classlabels_strings_.size() == 2 && - weights_are_all_positive_ && - maxweight > 0.5 && - weights_classes_.size() == 1) { - y_data[i] = classlabels_strings_[1]; // positive label - write_additional_scores = 0; - } else if (classlabels_strings_.size() == 2 && - weights_are_all_positive_ && - maxweight <= 0.5 && - weights_classes_.size() == 1) { - y_data[i] = classlabels_strings_[0]; // negative label - write_additional_scores = 1; - } else if (classlabels_strings_.size() == 2 && - maxweight > 0 && - !weights_are_all_positive_ && weights_classes_.size() == 1) { - y_data[i] = classlabels_strings_[1]; // pos label - write_additional_scores = 2; - } else if (classlabels_strings_.size() == 2 && - maxweight <= 0 && - !weights_are_all_positive_ && - weights_classes_.size() == 1) { - y_data[i] = classlabels_strings_[0]; // neg label - write_additional_scores = 3; - } else if (maxweight > 0) { - y_data[i] = "1"; // positive label - } else { - y_data[i] = "0"; // negative label - } + _set_score_binary(i, Y->template MutableData(), + write_additional_scores, weights_are_all_positive_, + classes, classlabels_strings_, + weights_classes_, "1", "0"); } else { - auto* y_data = Y->template MutableData(); - if (classlabels_int64s_.size() == 2 && - weights_are_all_positive_ && - maxweight > 0.5 && - weights_classes_.size() == 1) { - y_data[i] = classlabels_int64s_[1]; // positive label - write_additional_scores = 0; - } else if (classlabels_int64s_.size() == 2 && - weights_are_all_positive_ && - maxweight <= 0.5 && - weights_classes_.size() == 1) { - y_data[i] = classlabels_int64s_[0]; // negative label - write_additional_scores = 1; - } else if (classlabels_int64s_.size() == 2 && - maxweight > 0 && - !weights_are_all_positive_ && - weights_classes_.size() == 1) { - y_data[i] = classlabels_int64s_[1]; // pos label - write_additional_scores = 2; - } else if (classlabels_int64s_.size() == 2 && - maxweight <= 0 && - !weights_are_all_positive_ && - weights_classes_.size() == 1) { - y_data[i] = classlabels_int64s_[0]; // neg label - write_additional_scores = 3; - } else if (maxweight > 0) { - y_data[i] = 1; // positive label - } else { - y_data[i] = 0; // negative label - } + _set_score_binary(i, Y->template MutableData(), + write_additional_scores, weights_are_all_positive_, + classes, classlabels_int64s_, + weights_classes_, 1, 0); } } // write float values, might not have all the classes in the output yet @@ -426,9 +443,9 @@ common::Status TreeEnsembleClassifier::Compute(OpKernelContext* context) cons } write_scores(scores, post_transform_, zindex, Z, write_additional_scores); zindex += scores.size(); - } // for every batch + } // namespace ml return Status::OK(); -} +} // namespace ml template common::Status TreeEnsembleClassifier::ProcessTreeNode(std::map& classes, diff --git a/onnxruntime/test/providers/cpu/ml/tree_ensembler_classifier_test.cc b/onnxruntime/test/providers/cpu/ml/tree_ensembler_classifier_test.cc index 36318226ef..f1b7616943 100644 --- a/onnxruntime/test/providers/cpu/ml/tree_ensembler_classifier_test.cc +++ b/onnxruntime/test/providers/cpu/ml/tree_ensembler_classifier_test.cc @@ -151,5 +151,120 @@ TEST(MLOpTest, TreeEnsembleClassifierBinary) { test.Run(); } +TEST(MLOpTest, TreeEnsembleClassifierBinaryBaseValue) { + OpTester test("TreeEnsembleClassifier", 1, onnxruntime::kMLDomain); + + // The example was generated by the following python script: + // model = GradientBoostingClassifier(n_estimators = 1, max_depth = 2) + // X, y = make_classification(10, n_features = 4, random_state = 42) + // X = X[:, :2] + // model.fit(X, y) + // model.init_.class_prior_ = np.array([0.231, 0.231]) + + std::vector base_values = {-1.202673316001892f, -1.202673316001892f}; + std::vector class_ids = {0, 0, 0}; + std::vector class_nodeids = {2, 3, 4}; + std::vector class_treeids = {0, 0, 0}; + std::vector class_weights = {-0.2f, -0.06f, 0.2f}; + std::vector classlabels_int64s = {0, 1}; + std::vector nodes_falsenodeids = {4, 3, 0, 0, 0}; + std::vector nodes_featureids = {0, 0, 0, 0, 0}; + std::vector nodes_hitrates = {1, 1, 1, 1, 1}; + std::vector nodes_missing_value_tracks_true = {0, 0, 0, 0, 0}; + std::vector nodes_modes = {"BRANCH_LEQ", "BRANCH_LEQ", "LEAF", "LEAF", "LEAF"}; + std::vector nodes_nodeids = {0, 1, 2, 3, 4}; + std::vector nodes_treeids = {0, 0, 0, 0, 0}; + std::vector nodes_truenodeids = {1, 2, 0, 0, 0}; + std::vector nodes_values = {0.21111594140529633f, -0.8440752029418945f, 0, 0, 0}; + std::string post_transform = "LOGISTIC"; + + std::vector X = {-0.92533575f, -1.14021544f, -0.46171143f, -0.58723065f, 1.44044386f, 1.77736657f}; + std::vector results = {0, 0, 0}; + std::vector probs = {}; + std::vector log_probs = {}; + std::vector scores{0.802607834f, 0.197392166f, 0.779485941f, 0.220514059f, 0.731583834f, 0.268416166f}; + + //define the context of the operator call + const int N = 3; + test.AddAttribute("base_values", base_values); + test.AddAttribute("class_ids", class_ids); + test.AddAttribute("class_nodeids", class_nodeids); + test.AddAttribute("class_treeids", class_treeids); + test.AddAttribute("class_weights", class_weights); + test.AddAttribute("classlabels_int64s", classlabels_int64s); + test.AddAttribute("nodes_falsenodeids", nodes_falsenodeids); + test.AddAttribute("nodes_featureids", nodes_featureids); + test.AddAttribute("nodes_hitrates", nodes_hitrates); + test.AddAttribute("nodes_modes", nodes_modes); + test.AddAttribute("nodes_nodeids", nodes_nodeids); + test.AddAttribute("nodes_treeids", nodes_treeids); + test.AddAttribute("nodes_truenodeids", nodes_truenodeids); + test.AddAttribute("nodes_values", nodes_values); + test.AddAttribute("post_transform", post_transform); + + test.AddInput("X", {N, 2}, X); + test.AddOutput("Y", {N}, results); + test.AddOutput("Z", {N, 2}, scores); + + test.Run(); +} + +TEST(MLOpTest, TreeEnsembleClassifierBinaryBaseValueNull) { + OpTester test("TreeEnsembleClassifier", 1, onnxruntime::kMLDomain); + + // The example was generated by the following python script: + // model = GradientBoostingClassifier(n_estimators = 1, max_depth = 2) + // X, y = make_classification(10, n_features = 4, random_state = 42) + // X = X[:, :2] + // model.fit(X, y) + + std::vector base_values = {0, 0}; + std::vector class_ids = {0, 0, 0}; + std::vector class_nodeids = {2, 3, 4}; + std::vector class_treeids = {0, 0, 0}; + std::vector class_weights = {-0.2f, -0.0666f, 0.2f}; + std::vector classlabels_int64s = {0, 1}; + std::vector nodes_falsenodeids = {4, 3, 0, 0, 0}; + std::vector nodes_featureids = {0, 0, 0, 0, 0}; + std::vector nodes_hitrates = {1, 1, 1, 1, 1}; + std::vector nodes_missing_value_tracks_true = {0, 0, 0, 0, 0}; + std::vector nodes_modes = {"BRANCH_LEQ", "BRANCH_LEQ", "LEAF", "LEAF", "LEAF"}; + std::vector nodes_nodeids = {0, 1, 2, 3, 4}; + std::vector nodes_treeids = {0, 0, 0, 0, 0}; + std::vector nodes_truenodeids = {1, 2, 0, 0, 0}; + std::vector nodes_values = {0.24055418372154236f, -0.8440752029418945f, 0, 0, 0}; + std::string post_transform = "LOGISTIC"; + + std::vector X = {-0.92533575f, -1.14021544f, -0.46171143f, -0.58723065f, 1.44044386f, 1.77736657f}; + std::vector results = {0, 0, 1}; + std::vector probs = {}; + std::vector log_probs = {}; + std::vector scores{0.549834f, 0.450166f, 0.5166605f, 0.4833395f, 0.450166f, 0.549834f}; + + //define the context of the operator call + const int N = 3; + test.AddAttribute("base_values", base_values); + test.AddAttribute("class_ids", class_ids); + test.AddAttribute("class_nodeids", class_nodeids); + test.AddAttribute("class_treeids", class_treeids); + test.AddAttribute("class_weights", class_weights); + test.AddAttribute("classlabels_int64s", classlabels_int64s); + test.AddAttribute("nodes_falsenodeids", nodes_falsenodeids); + test.AddAttribute("nodes_featureids", nodes_featureids); + test.AddAttribute("nodes_hitrates", nodes_hitrates); + test.AddAttribute("nodes_modes", nodes_modes); + test.AddAttribute("nodes_nodeids", nodes_nodeids); + test.AddAttribute("nodes_treeids", nodes_treeids); + test.AddAttribute("nodes_truenodeids", nodes_truenodeids); + test.AddAttribute("nodes_values", nodes_values); + test.AddAttribute("post_transform", post_transform); + + test.AddInput("X", {N, 2}, X); + test.AddOutput("Y", {N}, results); + test.AddOutput("Z", {N, 2}, scores); + + test.Run(); +} + } // namespace test } // namespace onnxruntime