diff --git a/CMakeLists.txt b/CMakeLists.txt index 4100c39f..23d55fcb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,6 +108,7 @@ include(cmake/Version.cmake) add_subdirectory(src) if(BUILD_CPP_TEST) + enable_testing() add_subdirectory(tests/cpp) endif() diff --git a/src/model_loader/sklearn.cc b/src/model_loader/sklearn.cc index e91acedb..c159068c 100644 --- a/src/model_loader/sklearn.cc +++ b/src/model_loader/sklearn.cc @@ -26,35 +26,6 @@ namespace treelite::model_loader::sklearn { namespace detail { namespace stdex = std::experimental; -// Multidimensional array views. Use row-major (C) layout -template -using Array2DView = stdex::mdspan, stdex::layout_right>; - -class IsolationForestMixIn { - public: - explicit IsolationForestMixIn(double ratio_c) : ratio_c_{ratio_c} {} - - void HandleMetadata(model_builder::ModelBuilder& builder, int n_trees, int n_features, - [[maybe_unused]] int n_targets, [[maybe_unused]] std::int32_t const* n_classes) { - model_builder::Metadata metadata{n_features, TaskType::kIsolationForest, true, 1, {1}, {1, 1}}; - model_builder::TreeAnnotation tree_annotation{ - n_trees, std::vector(n_trees, 0), std::vector(n_trees, 0)}; - - std::ostringstream oss; - model_builder::PostProcessorFunc postprocessor{ - "exponential_standard_ratio", {{"ratio_c", ratio_c_}}}; - - builder.InitializeMetadata(metadata, tree_annotation, postprocessor, {0.0}, std::nullopt); - } - - void HandleLeafNode(model_builder::ModelBuilder& builder, int tree_id, int node_id, - double const** value, [[maybe_unused]] std::int32_t const* n_classes) const { - builder.LeafScalar(value[tree_id][node_id]); - } - - private: - double ratio_c_; -}; class GradientBoostingRegressorMixIn { public: @@ -370,18 +341,6 @@ std::unique_ptr LoadHistGradientBoosting(MixIn& mixin, int n_tr } // namespace detail -std::unique_ptr LoadIsolationForest(int n_estimators, int n_features, - std::int64_t const* node_count, std::int64_t const** children_left, - std::int64_t const** children_right, std::int64_t const** feature, double const** threshold, - double const** value, std::int64_t const** n_node_samples, - double const** weighted_n_node_samples, double const** impurity, double ratio_c) { - detail::IsolationForestMixIn mixin{ratio_c}; - std::vector n_classes{1}; - return detail::LoadSKLearnModel(mixin, n_estimators, n_features, 1, n_classes.data(), node_count, - children_left, children_right, feature, threshold, value, n_node_samples, - weighted_n_node_samples, impurity); -} - std::unique_ptr LoadGradientBoostingRegressor(int n_iter, int n_features, std::int64_t const* node_count, std::int64_t const** children_left, std::int64_t const** children_right, std::int64_t const** feature, double const** threshold, diff --git a/src/model_loader/sklearn_bulk.cc b/src/model_loader/sklearn_bulk.cc index d486f67e..30e77384 100644 --- a/src/model_loader/sklearn_bulk.cc +++ b/src/model_loader/sklearn_bulk.cc @@ -349,4 +349,65 @@ std::unique_ptr LoadRandomForestRegressor(int n_estimators, int return model; } +/** + * Load an IsolationForest using bulk construction + * + * This is an optimized version that constructs trees in bulk rather than + * going through the ModelBuilder node-by-node. + */ +std::unique_ptr LoadIsolationForest(int n_estimators, int n_features, + std::int64_t const* node_count, std::int64_t const** children_left, + std::int64_t const** children_right, std::int64_t const** feature, double const** threshold, + double const** value, std::int64_t const** n_node_samples, + double const** weighted_n_node_samples, double const** impurity, double ratio_c) { + TREELITE_CHECK_GT(n_estimators, 0) << "n_estimators must be at least 1"; + TREELITE_CHECK_GT(n_features, 0) << "n_features must be at least 1"; + + // Create model with double precision + auto model = Model::Create(); + + // Set up model metadata + std::int32_t const n_targets = 1; + model->num_feature = n_features; + model->task_type = TaskType::kIsolationForest; + model->average_tree_output = true; + model->num_target = n_targets; + + // For isolation forests, num_class is always 1 + model->num_class = std::vector(n_targets, 1); + + // Set leaf vector shape + model->leaf_vector_shape = std::vector{n_targets, 1}; + + // Set up tree annotation arrays + model->target_id = std::vector(n_estimators, 0); + model->class_id = std::vector(n_estimators, 0); + + // Set postprocessor + model->postprocessor = "exponential_standard_ratio"; + model->ratio_c = static_cast(ratio_c); + + // Set base scores + model->base_scores = std::vector{0.0}; + + // Get the typed model preset + auto& preset = std::get>(model->variant_); + preset.trees.resize(n_estimators); + + // Construct each tree using bulk operations + for (int tree_id = 0; tree_id < n_estimators; ++tree_id) { + int const n_nodes = static_cast(node_count[tree_id]); + std::int64_t const total_sample_cnt = n_node_samples[tree_id][0]; + + BulkConstructTree(preset.trees[tree_id], n_nodes, children_left[tree_id], + children_right[tree_id], feature[tree_id], threshold[tree_id], value[tree_id], + n_node_samples[tree_id], weighted_n_node_samples[tree_id], impurity[tree_id], + total_sample_cnt, n_targets, + 1, // max_num_class = 1 for isolation forests + false); // is_classifier + } + + return model; +} + } // namespace treelite::model_loader::sklearn diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index 9bcadb1b..497e9356 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -7,6 +7,11 @@ target_link_libraries(treelite_cpp_test PRIVATE objtreelite rapidjson GTest::gtest GTest::gmock fmt::fmt-header-only std::mdspan) set_output_directory(treelite_cpp_test ${PROJECT_BINARY_DIR}) +add_test( + NAME TestTreeliteLib + COMMAND treelite_cpp_test + WORKING_DIRECTORY ${treelite_BINARY_DIR} +) if(MSVC) target_compile_options(treelite_cpp_test PRIVATE