diff --git a/bayesnet/ensembles/BoostA2DE.cc b/bayesnet/ensembles/BoostA2DE.cc new file mode 100644 index 0000000..aadb4c6 --- /dev/null +++ b/bayesnet/ensembles/BoostA2DE.cc @@ -0,0 +1,89 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#include +#include +#include +#include +#include +#include "bayesnet/feature_selection/CFS.h" +#include "bayesnet/feature_selection/FCBF.h" +#include "bayesnet/feature_selection/IWSS.h" +#include "BoostA2DE.h" + +namespace bayesnet { + + BoostA2DE::BoostA2DE(bool predict_voting) : Ensemble(predict_voting) + { + validHyperparameters = { + "maxModels", "bisection", "order", "convergence", "convergence_best", "threshold", + "select_features", "maxTolerance", "predict_voting", "block_update" + }; + + } + void BoostA2DE::buildModel(const torch::Tensor& weights) + { + models.clear(); + + } + void BoostA2DE::setHyperparameters(const nlohmann::json& hyperparameters_) + { + auto hyperparameters = hyperparameters_; + if (hyperparameters.contains("order")) { + std::vector algos = { Orders.ASC, Orders.DESC, Orders.RAND }; + order_algorithm = hyperparameters["order"]; + if (std::find(algos.begin(), algos.end(), order_algorithm) == algos.end()) { + throw std::invalid_argument("Invalid order algorithm, valid values [" + Orders.ASC + ", " + Orders.DESC + ", " + Orders.RAND + "]"); + } + hyperparameters.erase("order"); + } + if (hyperparameters.contains("convergence")) { + convergence = hyperparameters["convergence"]; + hyperparameters.erase("convergence"); + } + if (hyperparameters.contains("convergence_best")) { + convergence_best = hyperparameters["convergence_best"]; + hyperparameters.erase("convergence_best"); + } + if (hyperparameters.contains("bisection")) { + bisection = hyperparameters["bisection"]; + hyperparameters.erase("bisection"); + } + if (hyperparameters.contains("threshold")) { + threshold = hyperparameters["threshold"]; + hyperparameters.erase("threshold"); + } + if (hyperparameters.contains("maxTolerance")) { + maxTolerance = hyperparameters["maxTolerance"]; + if (maxTolerance < 1 || maxTolerance > 4) + throw std::invalid_argument("Invalid maxTolerance value, must be greater in [1, 4]"); + hyperparameters.erase("maxTolerance"); + } + if (hyperparameters.contains("predict_voting")) { + predict_voting = hyperparameters["predict_voting"]; + hyperparameters.erase("predict_voting"); + } + if (hyperparameters.contains("select_features")) { + auto selectedAlgorithm = hyperparameters["select_features"]; + std::vector algos = { SelectFeatures.IWSS, SelectFeatures.CFS, SelectFeatures.FCBF }; + selectFeatures = true; + select_features_algorithm = selectedAlgorithm; + if (std::find(algos.begin(), algos.end(), selectedAlgorithm) == algos.end()) { + throw std::invalid_argument("Invalid selectFeatures value, valid values [" + SelectFeatures.IWSS + ", " + SelectFeatures.CFS + ", " + SelectFeatures.FCBF + "]"); + } + hyperparameters.erase("select_features"); + } + if (hyperparameters.contains("block_update")) { + block_update = hyperparameters["block_update"]; + hyperparameters.erase("block_update"); + } + Classifier::setHyperparameters(hyperparameters); + } + std::vector BoostA2DE::graph(const std::string& title) const + { + return Ensemble::graph(title); + } +} \ No newline at end of file diff --git a/bayesnet/ensembles/BoostA2DE.h b/bayesnet/ensembles/BoostA2DE.h new file mode 100644 index 0000000..b9e9ad7 --- /dev/null +++ b/bayesnet/ensembles/BoostA2DE.h @@ -0,0 +1,38 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#ifndef BOOSTA2DE_H +#define BOOSTA2DE_H +#include +#include "boost.h" +#include "bayesnet/classifiers/SPnDE.h" +#include "bayesnet/feature_selection/FeatureSelect.h" +#include "Ensemble.h" +namespace bayesnet { + class BoostA2DE : public Ensemble { + public: + explicit BoostA2DE(bool predict_voting = false); + virtual ~BoostA2DE() = default; + std::vector graph(const std::string& title = "BoostA2DE") const override; + void setHyperparameters(const nlohmann::json& hyperparameters_) override; + protected: + void buildModel(const torch::Tensor& weights) override; + private: + torch::Tensor X_train, y_train, X_test, y_test; + // Hyperparameters + bool bisection = true; // if true, use bisection stratety to add k models at once to the ensemble + int maxTolerance = 3; + std::string order_algorithm; // order to process the KBest features asc, desc, rand + bool convergence = true; //if true, stop when the model does not improve + bool convergence_best = false; // wether to keep the best accuracy to the moment or the last accuracy as prior accuracy + bool selectFeatures = false; // if true, use feature selection + std::string select_features_algorithm = Orders.DESC; // Selected feature selection algorithm + FeatureSelect* featureSelector = nullptr; + double threshold = -1; + bool block_update = false; + }; +} +#endif \ No newline at end of file diff --git a/bayesnet/ensembles/BoostAODE.h b/bayesnet/ensembles/BoostAODE.h index dac0004..c4f8844 100644 --- a/bayesnet/ensembles/BoostAODE.h +++ b/bayesnet/ensembles/BoostAODE.h @@ -9,18 +9,9 @@ #include #include "bayesnet/classifiers/SPODE.h" #include "bayesnet/feature_selection/FeatureSelect.h" +#include "boost.h" #include "Ensemble.h" namespace bayesnet { - const struct { - std::string CFS = "CFS"; - std::string FCBF = "FCBF"; - std::string IWSS = "IWSS"; - }SelectFeatures; - const struct { - std::string ASC = "asc"; - std::string DESC = "desc"; - std::string RAND = "rand"; - }Orders; class BoostAODE : public Ensemble { public: explicit BoostAODE(bool predict_voting = false); diff --git a/bayesnet/ensembles/boost.h b/bayesnet/ensembles/boost.h new file mode 100644 index 0000000..54d6ce7 --- /dev/null +++ b/bayesnet/ensembles/boost.h @@ -0,0 +1,13 @@ +#ifndef BOOST_H +#define BOOST_H +const struct { + std::string CFS = "CFS"; + std::string FCBF = "FCBF"; + std::string IWSS = "IWSS"; +}SelectFeatures; +const struct { + std::string ASC = "asc"; + std::string DESC = "desc"; + std::string RAND = "rand"; +}Orders; +#endif \ No newline at end of file diff --git a/docs/BoostAODE.md b/docs/BoostAODE.md index def453d..308d00c 100644 --- a/docs/BoostAODE.md +++ b/docs/BoostAODE.md @@ -27,4 +27,4 @@ The hyperparameters defined in the algorithm are: ## Operation -### [Algorithm](./algorithm.md) +### [Base Algorithm](./algorithm.md) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a878442..4befe44 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,7 +10,7 @@ if(ENABLE_TESTING) file(GLOB_RECURSE BayesNet_SOURCES "${BayesNet_SOURCE_DIR}/bayesnet/*.cc") add_executable(TestBayesNet TestBayesNetwork.cc TestBayesNode.cc TestBayesClassifier.cc TestBayesModels.cc TestBayesMetrics.cc TestFeatureSelection.cc TestBoostAODE.cc TestA2DE.cc - TestUtils.cc TestBayesEnsemble.cc TestModulesVersions.cc ${BayesNet_SOURCES}) + TestUtils.cc TestBayesEnsemble.cc TestModulesVersions.cc TestBoostA2DE.cc ${BayesNet_SOURCES}) target_link_libraries(TestBayesNet PUBLIC "${TORCH_LIBRARIES}" ArffFiles mdlp PRIVATE Catch2::Catch2WithMain) add_test(NAME BayesNetworkTest COMMAND TestBayesNet) add_test(NAME Network COMMAND TestBayesNet "[Network]") @@ -22,5 +22,6 @@ if(ENABLE_TESTING) add_test(NAME Models COMMAND TestBayesNet "[Models]") add_test(NAME BoostAODE COMMAND TestBayesNet "[BoostAODE]") add_test(NAME A2DE COMMAND TestBayesNet "[A2DE]") + add_test(NAME BoostA2DE COMMAND TestBayesNet "[BoostA2DE]") add_test(NAME Modules COMMAND TestBayesNet "[Modules]") endif(ENABLE_TESTING) diff --git a/tests/TestBoostA2DE.cc b/tests/TestBoostA2DE.cc new file mode 100644 index 0000000..1be717d --- /dev/null +++ b/tests/TestBoostA2DE.cc @@ -0,0 +1,212 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#include +#include +#include +#include +#include "bayesnet/ensembles/BoostA2DE.h" +#include "TestUtils.h" + + +TEST_CASE("Feature_select CFS", "[BoostA2DE]") +{ + auto raw = RawDatasets("iris", true); + auto clf = bayesnet::BoostA2DE(); + clf.setHyperparameters({ {"select_features", "CFS"} }); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states); + REQUIRE(clf.getNumberOfNodes() == 0); + REQUIRE(clf.getNumberOfEdges() == 0); + // REQUIRE(clf.getNotes().size() == 2); + // REQUIRE(clf.getNotes()[0] == "Used features in initialization: 6 of 9 with CFS"); + // REQUIRE(clf.getNotes()[1] == "Number of models: 9"); +} +// TEST_CASE("Feature_select IWSS", "[BoostAODE]") +// { +// auto raw = RawDatasets("glass", true); +// auto clf = bayesnet::BoostAODE(); +// clf.setHyperparameters({ {"select_features", "IWSS"}, {"threshold", 0.5 } }); +// clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states); +// REQUIRE(clf.getNumberOfNodes() == 90); +// REQUIRE(clf.getNumberOfEdges() == 153); +// REQUIRE(clf.getNotes().size() == 2); +// REQUIRE(clf.getNotes()[0] == "Used features in initialization: 4 of 9 with IWSS"); +// REQUIRE(clf.getNotes()[1] == "Number of models: 9"); +// } +// TEST_CASE("Feature_select FCBF", "[BoostAODE]") +// { +// auto raw = RawDatasets("glass", true); +// auto clf = bayesnet::BoostAODE(); +// clf.setHyperparameters({ {"select_features", "FCBF"}, {"threshold", 1e-7 } }); +// clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states); +// REQUIRE(clf.getNumberOfNodes() == 90); +// REQUIRE(clf.getNumberOfEdges() == 153); +// REQUIRE(clf.getNotes().size() == 2); +// REQUIRE(clf.getNotes()[0] == "Used features in initialization: 4 of 9 with FCBF"); +// REQUIRE(clf.getNotes()[1] == "Number of models: 9"); +// } +// TEST_CASE("Test used features in train note and score", "[BoostAODE]") +// { +// auto raw = RawDatasets("diabetes", true); +// auto clf = bayesnet::BoostAODE(true); +// clf.setHyperparameters({ +// {"order", "asc"}, +// {"convergence", true}, +// {"select_features","CFS"}, +// }); +// clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states); +// REQUIRE(clf.getNumberOfNodes() == 72); +// REQUIRE(clf.getNumberOfEdges() == 120); +// REQUIRE(clf.getNotes().size() == 2); +// REQUIRE(clf.getNotes()[0] == "Used features in initialization: 6 of 8 with CFS"); +// REQUIRE(clf.getNotes()[1] == "Number of models: 8"); +// auto score = clf.score(raw.Xv, raw.yv); +// auto scoret = clf.score(raw.Xt, raw.yt); +// REQUIRE(score == Catch::Approx(0.809895813).epsilon(raw.epsilon)); +// REQUIRE(scoret == Catch::Approx(0.809895813).epsilon(raw.epsilon)); +// } +// TEST_CASE("Voting vs proba", "[BoostAODE]") +// { +// auto raw = RawDatasets("iris", true); +// auto clf = bayesnet::BoostAODE(false); +// clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states); +// auto score_proba = clf.score(raw.Xv, raw.yv); +// auto pred_proba = clf.predict_proba(raw.Xv); +// clf.setHyperparameters({ +// {"predict_voting",true}, +// }); +// auto score_voting = clf.score(raw.Xv, raw.yv); +// auto pred_voting = clf.predict_proba(raw.Xv); +// REQUIRE(score_proba == Catch::Approx(0.97333).epsilon(raw.epsilon)); +// REQUIRE(score_voting == Catch::Approx(0.98).epsilon(raw.epsilon)); +// REQUIRE(pred_voting[83][2] == Catch::Approx(1.0).epsilon(raw.epsilon)); +// REQUIRE(pred_proba[83][2] == Catch::Approx(0.86121525).epsilon(raw.epsilon)); +// REQUIRE(clf.dump_cpt() == ""); +// REQUIRE(clf.topological_order() == std::vector()); +// } +// TEST_CASE("Order asc, desc & random", "[BoostAODE]") +// { +// auto raw = RawDatasets("glass", true); +// std::map scores{ +// {"asc", 0.83645f }, { "desc", 0.84579f }, { "rand", 0.84112 } +// }; +// for (const std::string& order : { "asc", "desc", "rand" }) { +// auto clf = bayesnet::BoostAODE(); +// clf.setHyperparameters({ +// {"order", order}, +// {"bisection", false}, +// {"maxTolerance", 1}, +// {"convergence", false}, +// }); +// clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states); +// auto score = clf.score(raw.Xv, raw.yv); +// auto scoret = clf.score(raw.Xt, raw.yt); +// INFO("BoostAODE order: " + order); +// REQUIRE(score == Catch::Approx(scores[order]).epsilon(raw.epsilon)); +// REQUIRE(scoret == Catch::Approx(scores[order]).epsilon(raw.epsilon)); +// } +// } +// TEST_CASE("Oddities", "[BoostAODE]") +// { +// auto clf = bayesnet::BoostAODE(); +// auto raw = RawDatasets("iris", true); +// auto bad_hyper = nlohmann::json{ +// { { "order", "duck" } }, +// { { "select_features", "duck" } }, +// { { "maxTolerance", 0 } }, +// { { "maxTolerance", 5 } }, +// }; +// for (const auto& hyper : bad_hyper.items()) { +// INFO("BoostAODE hyper: " + hyper.value().dump()); +// REQUIRE_THROWS_AS(clf.setHyperparameters(hyper.value()), std::invalid_argument); +// } +// REQUIRE_THROWS_AS(clf.setHyperparameters({ {"maxTolerance", 0 } }), std::invalid_argument); +// auto bad_hyper_fit = nlohmann::json{ +// { { "select_features","IWSS" }, { "threshold", -0.01 } }, +// { { "select_features","IWSS" }, { "threshold", 0.51 } }, +// { { "select_features","FCBF" }, { "threshold", 1e-8 } }, +// { { "select_features","FCBF" }, { "threshold", 1.01 } }, +// }; +// for (const auto& hyper : bad_hyper_fit.items()) { +// INFO("BoostAODE hyper: " + hyper.value().dump()); +// clf.setHyperparameters(hyper.value()); +// REQUIRE_THROWS_AS(clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states), std::invalid_argument); +// } +// } + +// TEST_CASE("Bisection Best", "[BoostAODE]") +// { +// auto clf = bayesnet::BoostAODE(); +// auto raw = RawDatasets("kdd_JapaneseVowels", true, 1200, true, false); +// clf.setHyperparameters({ +// {"bisection", true}, +// {"maxTolerance", 3}, +// {"convergence", true}, +// {"block_update", false}, +// {"convergence_best", false}, +// }); +// clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states); +// REQUIRE(clf.getNumberOfNodes() == 210); +// REQUIRE(clf.getNumberOfEdges() == 378); +// REQUIRE(clf.getNotes().size() == 1); +// REQUIRE(clf.getNotes().at(0) == "Number of models: 14"); +// auto score = clf.score(raw.X_test, raw.y_test); +// auto scoret = clf.score(raw.X_test, raw.y_test); +// REQUIRE(score == Catch::Approx(0.991666675f).epsilon(raw.epsilon)); +// REQUIRE(scoret == Catch::Approx(0.991666675f).epsilon(raw.epsilon)); +// } +// TEST_CASE("Bisection Best vs Last", "[BoostAODE]") +// { +// auto raw = RawDatasets("kdd_JapaneseVowels", true, 1500, true, false); +// auto clf = bayesnet::BoostAODE(true); +// auto hyperparameters = nlohmann::json{ +// {"bisection", true}, +// {"maxTolerance", 3}, +// {"convergence", true}, +// {"convergence_best", true}, +// }; +// clf.setHyperparameters(hyperparameters); +// clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states); +// auto score_best = clf.score(raw.X_test, raw.y_test); +// REQUIRE(score_best == Catch::Approx(0.980000019f).epsilon(raw.epsilon)); +// // Now we will set the hyperparameter to use the last accuracy +// hyperparameters["convergence_best"] = false; +// clf.setHyperparameters(hyperparameters); +// clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states); +// auto score_last = clf.score(raw.X_test, raw.y_test); +// REQUIRE(score_last == Catch::Approx(0.976666689f).epsilon(raw.epsilon)); +// } + +// TEST_CASE("Block Update", "[BoostAODE]") +// { +// auto clf = bayesnet::BoostAODE(); +// auto raw = RawDatasets("mfeat-factors", true, 500); +// clf.setHyperparameters({ +// {"bisection", true}, +// {"block_update", true}, +// {"maxTolerance", 3}, +// {"convergence", true}, +// }); +// clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states); +// REQUIRE(clf.getNumberOfNodes() == 868); +// REQUIRE(clf.getNumberOfEdges() == 1724); +// REQUIRE(clf.getNotes().size() == 3); +// REQUIRE(clf.getNotes()[0] == "Convergence threshold reached & 15 models eliminated"); +// REQUIRE(clf.getNotes()[1] == "Used features in train: 19 of 216"); +// REQUIRE(clf.getNotes()[2] == "Number of models: 4"); +// auto score = clf.score(raw.X_test, raw.y_test); +// auto scoret = clf.score(raw.X_test, raw.y_test); +// REQUIRE(score == Catch::Approx(0.99f).epsilon(raw.epsilon)); +// REQUIRE(scoret == Catch::Approx(0.99f).epsilon(raw.epsilon)); +// // +// // std::cout << "Number of nodes " << clf.getNumberOfNodes() << std::endl; +// // std::cout << "Number of edges " << clf.getNumberOfEdges() << std::endl; +// // std::cout << "Notes size " << clf.getNotes().size() << std::endl; +// // for (auto note : clf.getNotes()) { +// // std::cout << note << std::endl; +// // } +// // std::cout << "Score " << score << std::endl; +// } \ No newline at end of file