From 8c41df4bf6a69ec5c7260b647c5f9e12a828ddc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xavier=20dupr=C3=A9?= Date: Fri, 20 Dec 2019 14:06:59 +0100 Subject: [PATCH] update C++ code --- .../test_onnxrt_python_runtime_ml_tree.py | 35 +++- .../ops_cpu/op_tree_ensemble_regressor_p_.cpp | 195 +++++++++++------- 2 files changed, 155 insertions(+), 75 deletions(-) diff --git a/_unittests/ut_onnxrt/test_onnxrt_python_runtime_ml_tree.py b/_unittests/ut_onnxrt/test_onnxrt_python_runtime_ml_tree.py index 02ebcd299..b4a9c35ba 100644 --- a/_unittests/ut_onnxrt/test_onnxrt_python_runtime_ml_tree.py +++ b/_unittests/ut_onnxrt/test_onnxrt_python_runtime_ml_tree.py @@ -145,6 +145,39 @@ def test_onnxrt_python_DecisionTreeRegressor(self): clr = DecisionTreeRegressor() clr.fit(X_train, y_train) + model_def = to_onnx(clr, X_train.astype(numpy.float32)) + oinf = OnnxInference(model_def) + text = "\n".join(map(lambda x: str(x.ops_), oinf.sequence_)) + self.assertIn("TreeEnsembleRegressor", text) + + for i in range(0, 20): + y = oinf.run({'X': X_test.astype(numpy.float32)[i: i + 1]}) + self.assertEqual(list(sorted(y)), ['variable']) + lexp = clr.predict(X_test[i: i + 1]) + self.assertEqual(lexp.shape, y['variable'].shape) + self.assertEqualArray(lexp, y['variable']) + + for i in range(0, 20): + y = oinf.run({'X': X_test.astype(numpy.float32)[i: i + 2]}) + self.assertEqual(list(sorted(y)), ['variable']) + lexp = clr.predict(X_test[i: i + 2]) + self.assertEqual(lexp.shape, y['variable'].shape) + self.assertEqualArray(lexp, y['variable']) + + y = oinf.run({'X': X_test.astype(numpy.float32)}) + self.assertEqual(list(sorted(y)), ['variable']) + lexp = clr.predict(X_test) + self.assertEqual(lexp.shape, y['variable'].shape) + self.assertEqualArray(lexp, y['variable']) + + def test_onnxrt_python_DecisionTreeRegressor2(self): + iris = load_iris() + X, y = iris.data, iris.target + y = numpy.vstack([y, y]).T + X_train, X_test, y_train, _ = train_test_split(X, y, random_state=11) + clr = DecisionTreeRegressor() + clr.fit(X_train, y_train) + model_def = to_onnx(clr, X_train.astype(numpy.float32)) oinf = OnnxInference(model_def) text = "\n".join(map(lambda x: str(x.ops_), oinf.sequence_)) @@ -313,5 +346,5 @@ def test_openmp_compilation(self): if __name__ == "__main__": - TestOnnxrtPythonRuntimeMlTree().test_onnxrt_python_DecisionTreeClassifier_mlabel() + # TestOnnxrtPythonRuntimeMlTree().test_onnxrt_python_DecisionTreeRegressor() unittest.main() diff --git a/mlprodict/onnxrt/ops_cpu/op_tree_ensemble_regressor_p_.cpp b/mlprodict/onnxrt/ops_cpu/op_tree_ensemble_regressor_p_.cpp index fbcb6363e..ed7debbb6 100644 --- a/mlprodict/onnxrt/ops_cpu/op_tree_ensemble_regressor_p_.cpp +++ b/mlprodict/onnxrt/ops_cpu/op_tree_ensemble_regressor_p_.cpp @@ -26,58 +26,60 @@ namespace py = pybind11; #include "op_common_.hpp" +struct TreeNodeElementId { + int tree_id; + int node_id; + inline bool operator == (const TreeNodeElementId& xyz) const { + return (tree_id == xyz.tree_id) && (node_id == xyz.node_id); + } + inline bool operator < (const TreeNodeElementId& xyz) const { + return ((tree_id < xyz.tree_id) || ( + tree_id == xyz.tree_id && node_id < xyz.node_id)); + } +}; + +template +struct SparseValue { + int64_t i; + NTYPE value; +}; + +enum MissingTrack { + NONE, + TRUE, + FALSE +}; + +template +struct TreeNodeElement { + TreeNodeElementId id; + int feature_id; + NTYPE value; + NTYPE hitrates; + NODE_MODE mode; + TreeNodeElement *truenode; + TreeNodeElement *falsenode; + MissingTrack missing_tracks; + std::vector> weights; + + bool is_not_leave; + bool is_missing_track_true; +}; + template class RuntimeTreeEnsembleRegressorP { public: - - struct TreeNodeElementId { - int tree_id; - int node_id; - inline bool operator == (const TreeNodeElementId& xyz) const { - return (tree_id == xyz.tree_id) && (node_id == xyz.node_id); - } - inline bool operator < (const TreeNodeElementId& xyz) const { - return ((tree_id < xyz.tree_id) || ( - tree_id == xyz.tree_id && node_id < xyz.node_id)); - } - }; - - struct SparseValue { - int64_t i; - NTYPE value; - }; - - enum MissingTrack { - NONE, - TRUE, - FALSE - }; - - struct TreeNodeElement { - TreeNodeElementId id; - int feature_id; - NTYPE value; - NTYPE hitrates; - NODE_MODE mode; - TreeNodeElement *truenode; - TreeNodeElement *falsenode; - MissingTrack missing_tracks; - std::vector weights; - - bool is_not_leave; - bool is_missing_track_true; - }; - + // tree_ensemble_regressor.h std::vector base_values_; int64_t n_targets_; POST_EVAL_TRANSFORM post_transform_; AGGREGATE_FUNCTION aggregate_function_; int64_t nbnodes_; - TreeNodeElement* nodes_; - std::vector roots_; + TreeNodeElement* nodes_; + std::vector*> roots_; int64_t max_tree_depth_; int64_t nbtrees_; @@ -112,9 +114,14 @@ class RuntimeTreeEnsembleRegressorP py::array_t compute(py::array_t X) const; - void ProcessTreeNode(NTYPE* predictions, TreeNodeElement * root, - const NTYPE* x_data, - unsigned char* has_predictions) const; + TreeNodeElement * ProcessTreeNodeLeave( + TreeNodeElement * root, const NTYPE* x_data) const; + void ProcessTreeNodePrediction1( + NTYPE* predictions, TreeNodeElement * leave, + unsigned char* has_predictions) const; + void ProcessTreeNodePrediction( + NTYPE* predictions, TreeNodeElement * leave, + unsigned char* has_predictions) const; std::string runtime_options(); std::vector get_nodes_modes() const; @@ -246,13 +253,13 @@ void RuntimeTreeEnsembleRegressorP::init( std::vector*> roots_; */ nbnodes_ = nodes_treeids_.size(); - nodes_ = new TreeNodeElement[(int)nbnodes_]; + nodes_ = new TreeNodeElement[(int)nbnodes_]; roots_.clear(); - std::map idi; + std::map*> idi; size_t i; for (i = 0; i < nodes_treeids_.size(); ++i) { - TreeNodeElement * node = nodes_ + i; + TreeNodeElement * node = nodes_ + i; node->id.tree_id = (int)nodes_treeids_[i]; node->id.node_id = (int)nodes_nodeids_[i]; node->feature_id = (int)nodes_featureids_[i]; @@ -272,11 +279,11 @@ void RuntimeTreeEnsembleRegressorP::init( sprintf(buffer, "Node %d in tree %d is already there.", (int)node->id.node_id, (int)node->id.tree_id); throw std::runtime_error(buffer); } - idi.insert(std::pair(node->id, node)); + idi.insert(std::pair*>(node->id, node)); } TreeNodeElementId coor; - TreeNodeElement * it; + TreeNodeElement * it; for(i = 0; i < (size_t)nbnodes_; ++i) { it = nodes_ + i; if (!it->is_not_leave) @@ -333,7 +340,7 @@ void RuntimeTreeEnsembleRegressorP::init( } TreeNodeElementId ind; - SparseValue w; + SparseValue w; for (i = 0; i < target_nodeids_.size(); i++) { ind.tree_id = (int)target_treeids_[i]; ind.node_id = (int)target_nodeids_[i]; @@ -407,7 +414,10 @@ py::detail::unchecked_mutable_reference _mutable_unchecked1(py::array scores = 0; \ has_scores = 0; \ for (j = 0; j < (size_t)nbtrees_; ++j) \ - ProcessTreeNode(&scores, roots_[j], x_data + i * stride, &has_scores); \ + ProcessTreeNodePrediction1( \ + &scores, \ + ProcessTreeNodeLeave(roots_[j], x_data + i * stride), \ + &has_scores); \ val = has_scores \ ? (aggregate_function_ == AGGREGATE_FUNCTION::AVERAGE \ ? scores / roots_.size() \ @@ -422,8 +432,10 @@ py::detail::unchecked_mutable_reference _mutable_unchecked1(py::array std::fill(outputs.begin(), outputs.end(), (NTYPE)0); \ std::fill(has_scores.begin(), has_scores.end(), 0); \ for (j = 0; j < roots_.size(); ++j) \ - ProcessTreeNode(scores.data(), roots_[j], x_data + current_weight_0, \ - has_scores.data()); \ + ProcessTreeNodePrediction( \ + scores.data(), \ + ProcessTreeNodeLeave(roots_[j], x_data + current_weight_0), \ + has_scores.data()); \ for (jt = 0; jt < n_targets_; ++jt) { \ val = base_values_.size() == (size_t)n_targets_ ? base_values_[jt] : 0.f; \ val = (has_scores[jt]) \ @@ -454,14 +466,20 @@ void RuntimeTreeEnsembleRegressorP::compute_gil_free( if (nbtrees_ <= omp_tree_) { for (int64_t j = 0; j < nbtrees_; ++j) - ProcessTreeNode(&scores, roots_[j], x_data, &has_scores); + ProcessTreeNodePrediction1( + &scores, + ProcessTreeNodeLeave(roots_[j], x_data), + &has_scores); } else { #ifdef USE_OPENMP #pragma omp parallel for reduction(|: has_scores) reduction(+: scores) #endif for (int64_t j = 0; j < nbtrees_; ++j) - ProcessTreeNode(&scores, roots_[j], x_data, &has_scores); + ProcessTreeNodePrediction1( + &scores, + ProcessTreeNodeLeave(roots_[j], x_data), + &has_scores); } NTYPE val = has_scores @@ -504,7 +522,10 @@ void RuntimeTreeEnsembleRegressorP::compute_gil_free( // #pragma omp parallel for reduction(vecdplus: scores) reduction(maxdplus: has_scores) // #endif for (j = 0; j < nbtrees_; ++j) - ProcessTreeNode(scores.data(), roots_[j], x_data, has_scores.data()); + ProcessTreeNodePrediction( + scores.data(), + ProcessTreeNodeLeave(roots_[j], x_data), + has_scores.data()); std::vector outputs(n_targets_); NTYPE val; @@ -549,47 +570,41 @@ void RuntimeTreeEnsembleRegressorP::compute_gil_free( #define TREE_FIND_VALUE(CMP) \ if (has_missing_tracks_) { \ - while (root->is_not_leave && loopcount >= 0) { \ + while (root->is_not_leave) { \ val = x_data[root->feature_id]; \ root = (val CMP root->value || \ (root->is_missing_track_true && _isnan_(val) )) \ ? root->truenode : root->falsenode; \ - --loopcount; \ } \ } \ else { \ - while (root->is_not_leave && loopcount >= 0) { \ + while (root->is_not_leave) { \ val = x_data[root->feature_id]; \ root = val CMP root->value ? root->truenode : root->falsenode; \ - --loopcount; \ } \ } template -void RuntimeTreeEnsembleRegressorP::ProcessTreeNode( - NTYPE* predictions, TreeNodeElement * root, - const NTYPE* x_data, - unsigned char* has_predictions) const { +TreeNodeElement * + RuntimeTreeEnsembleRegressorP::ProcessTreeNodeLeave( + TreeNodeElement * root, const NTYPE* x_data) const { NTYPE val; if (same_mode_) { - int64_t loopcount = max_tree_depth_; switch(root->mode) { case NODE_MODE::BRANCH_LEQ: if (has_missing_tracks_) { - while (root->is_not_leave && loopcount >= 0) { + while (root->is_not_leave) { val = x_data[root->feature_id]; root = (val <= root->value || (root->is_missing_track_true && _isnan_(val) )) ? root->truenode : root->falsenode; - --loopcount; } } else { - while (root->is_not_leave && loopcount >= 0) { + while (root->is_not_leave) { val = x_data[root->feature_id]; root = val <= root->value ? root->truenode : root->falsenode; - --loopcount; } } break; @@ -618,9 +633,8 @@ void RuntimeTreeEnsembleRegressorP::ProcessTreeNode( } } else { // Different rules to compare to node thresholds. - int64_t loopcount = 0; NTYPE threshold; - while ((root->is_not_leave) && (loopcount <= max_tree_depth_)) { + while (root->is_not_leave) { val = x_data[root->feature_id]; threshold = root->value; switch (root->mode) { @@ -660,10 +674,15 @@ void RuntimeTreeEnsembleRegressorP::ProcessTreeNode( throw std::runtime_error(err_msg.str()); } } - ++loopcount; } } + return root; +} +template +void RuntimeTreeEnsembleRegressorP::ProcessTreeNodePrediction( + NTYPE* predictions, TreeNodeElement * root, + unsigned char* has_predictions) const { //should be at leaf switch(aggregate_function_) { case AGGREGATE_FUNCTION::AVERAGE: @@ -691,6 +710,32 @@ void RuntimeTreeEnsembleRegressorP::ProcessTreeNode( } +template +inline void RuntimeTreeEnsembleRegressorP::ProcessTreeNodePrediction1( + NTYPE* predictions, TreeNodeElement * root, + unsigned char* has_predictions) const { + switch(aggregate_function_) { + case AGGREGATE_FUNCTION::AVERAGE: + case AGGREGATE_FUNCTION::SUM: + *predictions = *has_predictions + ? *predictions + root->weights[0].value + : root->weights[0].value; + *has_predictions = 1; + break; + case AGGREGATE_FUNCTION::MIN: + *predictions = (!(*has_predictions) || root->weights[0].value < *predictions) + ? root->weights[0].value : *predictions; + *has_predictions = 1; + break; + case AGGREGATE_FUNCTION::MAX: + *predictions = (!(*has_predictions) || root->weights[0].value > *predictions) + ? root->weights[0].value : *predictions; + *has_predictions = 1; + break; + } +} + + template py::array_t RuntimeTreeEnsembleRegressorP::debug_threshold( py::array_t values) const { @@ -739,8 +784,10 @@ py::array_t RuntimeTreeEnsembleRegressorP::compute_tree_outputs(py for (size_t j = 0; j < roots_.size(); ++j, ++itb) { std::vector scores(n_targets_, (NTYPE)0); std::vector has_scores(n_targets_, 0); - ProcessTreeNode(scores.data(), roots_[j], x_data + current_weight_0, - has_scores.data()); + ProcessTreeNodePrediction( + scores.data(), + ProcessTreeNodeLeave(roots_[j], x_data + current_weight_0), + has_scores.data()); *itb = scores[0]; } }