From 400967b4e3ef9c33196a488ec1efe1aca0da1ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana=20G=C3=B3mez?= Date: Fri, 14 Mar 2025 14:53:22 +0100 Subject: [PATCH] Add tests to 90% coverage --- bayesnet/classifiers/Proposal.cc | 4 +- bayesnet/ensembles/Boost.cc | 487 ++++++++++++++++--------------- bayesnet/ensembles/Boost.h | 7 +- bayesnet/ensembles/BoostAODE.cc | 4 +- bayesnet/ensembles/BoostAODE.h | 3 +- bayesnet/ensembles/XBA2DE.cc | 168 +++++++++++ bayesnet/ensembles/XBA2DE.h | 28 ++ bayesnet/ensembles/XBAODE.cc | 10 - bayesnet/ensembles/XBAODE.h | 2 - tests/CMakeLists.txt | 3 +- tests/TestBayesModels.cc | 291 ++++++++++-------- tests/TestBoostAODE.cc | 103 +++---- tests/TestXBA2DE.cc | 237 +++++++++++++++ tests/TestXSPnDE.cc | 59 ++-- 14 files changed, 943 insertions(+), 463 deletions(-) create mode 100644 bayesnet/ensembles/XBA2DE.cc create mode 100644 bayesnet/ensembles/XBA2DE.h create mode 100644 tests/TestXBA2DE.cc diff --git a/bayesnet/classifiers/Proposal.cc b/bayesnet/classifiers/Proposal.cc index 6fce507..846cb92 100644 --- a/bayesnet/classifiers/Proposal.cc +++ b/bayesnet/classifiers/Proposal.cc @@ -11,7 +11,7 @@ namespace bayesnet { Proposal::~Proposal() { for (auto& [key, value] : discretizers) { - delete value; + delete value; } } void Proposal::checkInput(const torch::Tensor& X, const torch::Tensor& y) @@ -126,4 +126,4 @@ namespace bayesnet { } return yy; } -} \ No newline at end of file +} diff --git a/bayesnet/ensembles/Boost.cc b/bayesnet/ensembles/Boost.cc index 114db0c..e6c055c 100644 --- a/bayesnet/ensembles/Boost.cc +++ b/bayesnet/ensembles/Boost.cc @@ -3,255 +3,266 @@ // SPDX-FileType: SOURCE // SPDX-License-Identifier: MIT // *************************************************************** -#include +#include "Boost.h" #include "bayesnet/feature_selection/CFS.h" #include "bayesnet/feature_selection/FCBF.h" #include "bayesnet/feature_selection/IWSS.h" -#include "Boost.h" +#include namespace bayesnet { - Boost::Boost(bool predict_voting) : Ensemble(predict_voting) - { - validHyperparameters = { "alpha_block", "order", "convergence", "convergence_best", "bisection", "threshold", "maxTolerance", - "predict_voting", "select_features", "block_update" }; +Boost::Boost(bool predict_voting) : Ensemble(predict_voting) { + validHyperparameters = {"alpha_block", "order", "convergence", "convergence_best", "bisection", + "threshold", "maxTolerance", "predict_voting", "select_features", "block_update"}; +} +void Boost::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"); } - void Boost::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("alpha_block")) { - alpha_block = hyperparameters["alpha_block"]; - hyperparameters.erase("alpha_block"); - } - 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 > 6) - throw std::invalid_argument("Invalid maxTolerance value, must be greater in [1, 6]"); - 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"); - } - if (block_update && alpha_block) { - throw std::invalid_argument("alpha_block and block_update cannot be true at the same time"); - } - if (block_update && !bisection) { - throw std::invalid_argument("block_update needs bisection to be true"); - } - Classifier::setHyperparameters(hyperparameters); + if (hyperparameters.contains("alpha_block")) { + alpha_block = hyperparameters["alpha_block"]; + hyperparameters.erase("alpha_block"); } - void Boost::buildModel(const torch::Tensor& weights) - { - // Models shall be built in trainModel - models.clear(); - significanceModels.clear(); - n_models = 0; - // Prepare the validation dataset - auto y_ = dataset.index({ -1, "..." }); - if (convergence) { - // Prepare train & validation sets from train data - auto fold = folding::StratifiedKFold(5, y_, 271); - auto [train, test] = fold.getFold(0); - auto train_t = torch::tensor(train); - auto test_t = torch::tensor(test); - // Get train and validation sets - X_train = dataset.index({ torch::indexing::Slice(0, dataset.size(0) - 1), train_t }); - y_train = dataset.index({ -1, train_t }); - X_test = dataset.index({ torch::indexing::Slice(0, dataset.size(0) - 1), test_t }); - y_test = dataset.index({ -1, test_t }); - dataset = X_train; - m = X_train.size(1); - auto n_classes = states.at(className).size(); - // Build dataset with train data - buildDataset(y_train); - metrics = Metrics(dataset, features, className, n_classes); - } else { - // Use all data to train - X_train = dataset.index({ torch::indexing::Slice(0, dataset.size(0) - 1), "..." }); - y_train = y_; - } + if (hyperparameters.contains("convergence")) { + convergence = hyperparameters["convergence"]; + hyperparameters.erase("convergence"); } - std::vector Boost::featureSelection(torch::Tensor& weights_) - { - int maxFeatures = 0; - if (select_features_algorithm == SelectFeatures.CFS) { - featureSelector = new CFS(dataset, features, className, maxFeatures, states.at(className).size(), weights_); - } else if (select_features_algorithm == SelectFeatures.IWSS) { - if (threshold < 0 || threshold >0.5) { - throw std::invalid_argument("Invalid threshold value for " + SelectFeatures.IWSS + " [0, 0.5]"); - } - featureSelector = new IWSS(dataset, features, className, maxFeatures, states.at(className).size(), weights_, threshold); - } else if (select_features_algorithm == SelectFeatures.FCBF) { - if (threshold < 1e-7 || threshold > 1) { - throw std::invalid_argument("Invalid threshold value for " + SelectFeatures.FCBF + " [1e-7, 1]"); - } - featureSelector = new FCBF(dataset, features, className, maxFeatures, states.at(className).size(), weights_, threshold); - } - featureSelector->fit(); - auto featuresUsed = featureSelector->getFeatures(); - delete featureSelector; - return featuresUsed; + if (hyperparameters.contains("convergence_best")) { + convergence_best = hyperparameters["convergence_best"]; + hyperparameters.erase("convergence_best"); } - std::tuple Boost::update_weights(torch::Tensor& ytrain, torch::Tensor& ypred, torch::Tensor& weights) - { - bool terminate = false; - double alpha_t = 0; - auto mask_wrong = ypred != ytrain; - auto mask_right = ypred == ytrain; - auto masked_weights = weights * mask_wrong.to(weights.dtype()); - double epsilon_t = masked_weights.sum().item(); - // std::cout << "epsilon_t: " << epsilon_t << " count wrong: " << mask_wrong.sum().item() << " count right: " << mask_right.sum().item() << std::endl; - if (epsilon_t > 0.5) { - // Inverse the weights policy (plot ln(wt)) - // "In each round of AdaBoost, there is a sanity check to ensure that the current base - // learner is better than random guess" (Zhi-Hua Zhou, 2012) - terminate = true; - } else { - double wt = (1 - epsilon_t) / epsilon_t; - alpha_t = epsilon_t == 0 ? 1 : 0.5 * log(wt); - // Step 3.2: Update weights for next classifier - // Step 3.2.1: Update weights of wrong samples - weights += mask_wrong.to(weights.dtype()) * exp(alpha_t) * weights; - // Step 3.2.2: Update weights of right samples - weights += mask_right.to(weights.dtype()) * exp(-alpha_t) * weights; - // Step 3.3: Normalise the weights - double totalWeights = torch::sum(weights).item(); - weights = weights / totalWeights; - } - return { weights, alpha_t, terminate }; + if (hyperparameters.contains("bisection")) { + bisection = hyperparameters["bisection"]; + hyperparameters.erase("bisection"); } - std::tuple Boost::update_weights_block(int k, torch::Tensor& ytrain, torch::Tensor& weights) - { - /* Update Block algorithm - k = # of models in block - n_models = # of models in ensemble to make predictions - n_models_bak = # models saved - models = vector of models to make predictions - models_bak = models not used to make predictions - significances_bak = backup of significances vector + if (hyperparameters.contains("threshold")) { + threshold = hyperparameters["threshold"]; + hyperparameters.erase("threshold"); + } + if (hyperparameters.contains("maxTolerance")) { + maxTolerance = hyperparameters["maxTolerance"]; + if (maxTolerance < 1 || maxTolerance > 6) + throw std::invalid_argument("Invalid maxTolerance value, must be greater in [1, 6]"); + 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"); + } + if (block_update && alpha_block) { + throw std::invalid_argument("alpha_block and block_update cannot be true at the same time"); + } + if (block_update && !bisection) { + throw std::invalid_argument("block_update needs bisection to be true"); + } + Classifier::setHyperparameters(hyperparameters); +} +void Boost::add_model(std::unique_ptr model, double significance) { + models.push_back(std::move(model)); + n_models++; + significanceModels.push_back(significance); +} +void Boost::remove_last_model() { + models.pop_back(); + significanceModels.pop_back(); + n_models--; +} +void Boost::buildModel(const torch::Tensor &weights) { + // Models shall be built in trainModel + models.clear(); + significanceModels.clear(); + n_models = 0; + // Prepare the validation dataset + auto y_ = dataset.index({-1, "..."}); + if (convergence) { + // Prepare train & validation sets from train data + auto fold = folding::StratifiedKFold(5, y_, 271); + auto [train, test] = fold.getFold(0); + auto train_t = torch::tensor(train); + auto test_t = torch::tensor(test); + // Get train and validation sets + X_train = dataset.index({torch::indexing::Slice(0, dataset.size(0) - 1), train_t}); + y_train = dataset.index({-1, train_t}); + X_test = dataset.index({torch::indexing::Slice(0, dataset.size(0) - 1), test_t}); + y_test = dataset.index({-1, test_t}); + dataset = X_train; + m = X_train.size(1); + auto n_classes = states.at(className).size(); + // Build dataset with train data + buildDataset(y_train); + metrics = Metrics(dataset, features, className, n_classes); + } else { + // Use all data to train + X_train = dataset.index({torch::indexing::Slice(0, dataset.size(0) - 1), "..."}); + y_train = y_; + } +} +std::vector Boost::featureSelection(torch::Tensor &weights_) { + int maxFeatures = 0; + if (select_features_algorithm == SelectFeatures.CFS) { + featureSelector = new CFS(dataset, features, className, maxFeatures, states.at(className).size(), weights_); + } else if (select_features_algorithm == SelectFeatures.IWSS) { + if (threshold < 0 || threshold > 0.5) { + throw std::invalid_argument("Invalid threshold value for " + SelectFeatures.IWSS + " [0, 0.5]"); + } + featureSelector = + new IWSS(dataset, features, className, maxFeatures, states.at(className).size(), weights_, threshold); + } else if (select_features_algorithm == SelectFeatures.FCBF) { + if (threshold < 1e-7 || threshold > 1) { + throw std::invalid_argument("Invalid threshold value for " + SelectFeatures.FCBF + " [1e-7, 1]"); + } + featureSelector = + new FCBF(dataset, features, className, maxFeatures, states.at(className).size(), weights_, threshold); + } + featureSelector->fit(); + auto featuresUsed = featureSelector->getFeatures(); + delete featureSelector; + return featuresUsed; +} +std::tuple Boost::update_weights(torch::Tensor &ytrain, torch::Tensor &ypred, + torch::Tensor &weights) { + bool terminate = false; + double alpha_t = 0; + auto mask_wrong = ypred != ytrain; + auto mask_right = ypred == ytrain; + auto masked_weights = weights * mask_wrong.to(weights.dtype()); + double epsilon_t = masked_weights.sum().item(); + // std::cout << "epsilon_t: " << epsilon_t << " count wrong: " << mask_wrong.sum().item() << " count right: " + // << mask_right.sum().item() << std::endl; + if (epsilon_t > 0.5) { + // Inverse the weights policy (plot ln(wt)) + // "In each round of AdaBoost, there is a sanity check to ensure that the current base + // learner is better than random guess" (Zhi-Hua Zhou, 2012) + terminate = true; + } else { + double wt = (1 - epsilon_t) / epsilon_t; + alpha_t = epsilon_t == 0 ? 1 : 0.5 * log(wt); + // Step 3.2: Update weights for next classifier + // Step 3.2.1: Update weights of wrong samples + weights += mask_wrong.to(weights.dtype()) * exp(alpha_t) * weights; + // Step 3.2.2: Update weights of right samples + weights += mask_right.to(weights.dtype()) * exp(-alpha_t) * weights; + // Step 3.3: Normalise the weights + double totalWeights = torch::sum(weights).item(); + weights = weights / totalWeights; + } + return {weights, alpha_t, terminate}; +} +std::tuple Boost::update_weights_block(int k, torch::Tensor &ytrain, + torch::Tensor &weights) { + /* Update Block algorithm + k = # of models in block + n_models = # of models in ensemble to make predictions + n_models_bak = # models saved + models = vector of models to make predictions + models_bak = models not used to make predictions + significances_bak = backup of significances vector - Case list - A) k = 1, n_models = 1 => n = 0 , n_models = n + k - B) k = 1, n_models = n + 1 => n_models = n + k - C) k > 1, n_models = k + 1 => n= 1, n_models = n + k - D) k > 1, n_models = k => n = 0, n_models = n + k - E) k > 1, n_models = k + n => n_models = n + k + Case list + A) k = 1, n_models = 1 => n = 0 , n_models = n + k + B) k = 1, n_models = n + 1 => n_models = n + k + C) k > 1, n_models = k + 1 => n= 1, n_models = n + k + D) k > 1, n_models = k => n = 0, n_models = n + k + E) k > 1, n_models = k + n => n_models = n + k - A, D) n=0, k > 0, n_models == k - 1. n_models_bak <- n_models - 2. significances_bak <- significances - 3. significances = vector(k, 1) - 4. Don’t move any classifiers out of models - 5. n_models <- k - 6. Make prediction, compute alpha, update weights - 7. Don’t restore any classifiers to models - 8. significances <- significances_bak - 9. Update last k significances - 10. n_models <- n_models_bak + A, D) n=0, k > 0, n_models == k + 1. n_models_bak <- n_models + 2. significances_bak <- significances + 3. significances = vector(k, 1) + 4. Don’t move any classifiers out of models + 5. n_models <- k + 6. Make prediction, compute alpha, update weights + 7. Don’t restore any classifiers to models + 8. significances <- significances_bak + 9. Update last k significances + 10. n_models <- n_models_bak - B, C, E) n > 0, k > 0, n_models == n + k - 1. n_models_bak <- n_models - 2. significances_bak <- significances - 3. significances = vector(k, 1) - 4. Move first n classifiers to models_bak - 5. n_models <- k - 6. Make prediction, compute alpha, update weights - 7. Insert classifiers in models_bak to be the first n models - 8. significances <- significances_bak - 9. Update last k significances - 10. n_models <- n_models_bak - */ - // - // Make predict with only the last k models - // - std::unique_ptr model; - std::vector> models_bak; - // 1. n_models_bak <- n_models 2. significances_bak <- significances - auto significance_bak = significanceModels; - auto n_models_bak = n_models; - // 3. significances = vector(k, 1) - significanceModels = std::vector(k, 1.0); - // 4. Move first n classifiers to models_bak - // backup the first n_models - k models (if n_models == k, don't backup any) - for (int i = 0; i < n_models - k; ++i) { - model = std::move(models[0]); - models.erase(models.begin()); - models_bak.push_back(std::move(model)); - } - assert(models.size() == k); - // 5. n_models <- k - n_models = k; - // 6. Make prediction, compute alpha, update weights - auto ypred = predict(X_train); - // - // Update weights - // - double alpha_t; - bool terminate; - std::tie(weights, alpha_t, terminate) = update_weights(y_train, ypred, weights); - // - // Restore the models if needed - // - // 7. Insert classifiers in models_bak to be the first n models - // if n_models_bak == k, don't restore any, because none of them were moved - if (k != n_models_bak) { - // Insert in the same order as they were extracted - int bak_size = models_bak.size(); - for (int i = 0; i < bak_size; ++i) { - model = std::move(models_bak[bak_size - 1 - i]); - models_bak.erase(models_bak.end() - 1); - models.insert(models.begin(), std::move(model)); - } - } - // 8. significances <- significances_bak - significanceModels = significance_bak; - // - // Update the significance of the last k models - // - // 9. Update last k significances - for (int i = 0; i < k; ++i) { - significanceModels[n_models_bak - k + i] = alpha_t; - } - // 10. n_models <- n_models_bak - n_models = n_models_bak; - return { weights, alpha_t, terminate }; + B, C, E) n > 0, k > 0, n_models == n + k + 1. n_models_bak <- n_models + 2. significances_bak <- significances + 3. significances = vector(k, 1) + 4. Move first n classifiers to models_bak + 5. n_models <- k + 6. Make prediction, compute alpha, update weights + 7. Insert classifiers in models_bak to be the first n models + 8. significances <- significances_bak + 9. Update last k significances + 10. n_models <- n_models_bak + */ + // + // Make predict with only the last k models + // + std::unique_ptr model; + std::vector> models_bak; + // 1. n_models_bak <- n_models 2. significances_bak <- significances + auto significance_bak = significanceModels; + auto n_models_bak = n_models; + // 3. significances = vector(k, 1) + significanceModels = std::vector(k, 1.0); + // 4. Move first n classifiers to models_bak + // backup the first n_models - k models (if n_models == k, don't backup any) + for (int i = 0; i < n_models - k; ++i) { + model = std::move(models[0]); + models.erase(models.begin()); + models_bak.push_back(std::move(model)); } -} \ No newline at end of file + assert(models.size() == k); + // 5. n_models <- k + n_models = k; + // 6. Make prediction, compute alpha, update weights + auto ypred = predict(X_train); + // + // Update weights + // + double alpha_t; + bool terminate; + std::tie(weights, alpha_t, terminate) = update_weights(y_train, ypred, weights); + // + // Restore the models if needed + // + // 7. Insert classifiers in models_bak to be the first n models + // if n_models_bak == k, don't restore any, because none of them were moved + if (k != n_models_bak) { + // Insert in the same order as they were extracted + int bak_size = models_bak.size(); + for (int i = 0; i < bak_size; ++i) { + model = std::move(models_bak[bak_size - 1 - i]); + models_bak.erase(models_bak.end() - 1); + models.insert(models.begin(), std::move(model)); + } + } + // 8. significances <- significances_bak + significanceModels = significance_bak; + // + // Update the significance of the last k models + // + // 9. Update last k significances + for (int i = 0; i < k; ++i) { + significanceModels[n_models_bak - k + i] = alpha_t; + } + // 10. n_models <- n_models_bak + n_models = n_models_bak; + return {weights, alpha_t, terminate}; +} +} // namespace bayesnet diff --git a/bayesnet/ensembles/Boost.h b/bayesnet/ensembles/Boost.h index abb5ac2..84c8f8e 100644 --- a/bayesnet/ensembles/Boost.h +++ b/bayesnet/ensembles/Boost.h @@ -34,6 +34,11 @@ namespace bayesnet { void buildModel(const torch::Tensor& weights) override; std::tuple update_weights(torch::Tensor& ytrain, torch::Tensor& ypred, torch::Tensor& weights); std::tuple update_weights_block(int k, torch::Tensor& ytrain, torch::Tensor& weights); + void add_model(std::unique_ptr model, double significance); + void remove_last_model(); + // + // Attributes + // 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 @@ -49,4 +54,4 @@ namespace bayesnet { bool alpha_block = false; // if true, the alpha is computed with the ensemble built so far and the new model }; } -#endif \ No newline at end of file +#endif diff --git a/bayesnet/ensembles/BoostAODE.cc b/bayesnet/ensembles/BoostAODE.cc index a1bc4b6..c90a571 100644 --- a/bayesnet/ensembles/BoostAODE.cc +++ b/bayesnet/ensembles/BoostAODE.cc @@ -6,10 +6,10 @@ #include #include -#include #include #include #include "BoostAODE.h" +#include "bayesnet/classifiers/SPODE.h" #include #include @@ -180,4 +180,4 @@ namespace bayesnet { { return Ensemble::graph(title); } -} \ No newline at end of file +} diff --git a/bayesnet/ensembles/BoostAODE.h b/bayesnet/ensembles/BoostAODE.h index bc66ec1..8b399e2 100644 --- a/bayesnet/ensembles/BoostAODE.h +++ b/bayesnet/ensembles/BoostAODE.h @@ -8,7 +8,6 @@ #define BOOSTAODE_H #include #include -#include "bayesnet/classifiers/SPODE.h" #include "Boost.h" namespace bayesnet { @@ -23,4 +22,4 @@ namespace bayesnet { std::vector initializeModels(const Smoothing_t smoothing); }; } -#endif \ No newline at end of file +#endif diff --git a/bayesnet/ensembles/XBA2DE.cc b/bayesnet/ensembles/XBA2DE.cc new file mode 100644 index 0000000..856af4b --- /dev/null +++ b/bayesnet/ensembles/XBA2DE.cc @@ -0,0 +1,168 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2025 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#include +#include +#include "XBA2DE.h" +#include "bayesnet/classifiers/XSPnDE.h" +#include "bayesnet/utils/TensorUtils.h" + +namespace bayesnet { + +XBA2DE::XBA2DE(bool predict_voting) : Boost(predict_voting) {} +std::vector XBA2DE::initializeModels(const Smoothing_t smoothing) { + torch::Tensor weights_ = torch::full({m}, 1.0 / m, torch::kFloat64); + std::vector featuresSelected = featureSelection(weights_); + if (featuresSelected.size() < 2) { + notes.push_back("No features selected in initialization"); + status = ERROR; + return std::vector(); + } + for (int i = 0; i < featuresSelected.size() - 1; i++) { + for (int j = i + 1; j < featuresSelected.size(); j++) { + std::unique_ptr model = std::make_unique(featuresSelected[i], featuresSelected[j]); + model->fit(dataset, features, className, states, weights_, smoothing); + add_model(std::move(model), 1.0); + } + } + notes.push_back("Used features in initialization: " + std::to_string(featuresSelected.size()) + " of " + + std::to_string(features.size()) + " with " + select_features_algorithm); + return featuresSelected; +} +void XBA2DE::trainModel(const torch::Tensor &weights, const Smoothing_t smoothing) { + // + // Logging setup + // + // loguru::set_thread_name("XBA2DE"); + // loguru::g_stderr_verbosity = loguru::Verbosity_OFF; + // loguru::add_file("boostA2DE.log", loguru::Truncate, loguru::Verbosity_MAX); + + // Algorithm based on the adaboost algorithm for classification + // as explained in Ensemble methods (Zhi-Hua Zhou, 2012) + X_train_ = TensorUtils::to_matrix(X_train); + y_train_ = TensorUtils::to_vector(y_train); + if (convergence) { + X_test_ = TensorUtils::to_matrix(X_test); + y_test_ = TensorUtils::to_vector(y_test); + } + fitted = true; + double alpha_t = 0; + torch::Tensor weights_ = torch::full({m}, 1.0 / m, torch::kFloat64); + bool finished = false; + std::vector featuresUsed; + if (selectFeatures) { + featuresUsed = initializeModels(smoothing); + if (featuresUsed.size() == 0) { + return; + } + auto ypred = predict(X_train); + std::tie(weights_, alpha_t, finished) = update_weights(y_train, ypred, weights_); + // Update significance of the models + for (int i = 0; i < n_models; ++i) { + significanceModels[i] = alpha_t; + } + if (finished) { + return; + } + } + int numItemsPack = 0; // The counter of the models inserted in the current pack + // Variables to control the accuracy finish condition + double priorAccuracy = 0.0; + double improvement = 1.0; + double convergence_threshold = 1e-4; + int tolerance = 0; // number of times the accuracy is lower than the convergence_threshold + // Step 0: Set the finish condition + // epsilon sub t > 0.5 => inverse the weights policy + // validation error is not decreasing + // run out of features + bool ascending = order_algorithm == Orders.ASC; + std::mt19937 g{173}; + std::vector> pairSelection; + while (!finished) { + // Step 1: Build ranking with mutual information + pairSelection = metrics.SelectKPairs(weights_, featuresUsed, ascending, 0); // Get all the pairs sorted + if (order_algorithm == Orders.RAND) { + std::shuffle(pairSelection.begin(), pairSelection.end(), g); + } + int k = bisection ? pow(2, tolerance) : 1; + int counter = 0; // The model counter of the current pack + // VLOG_SCOPE_F(1, "counter=%d k=%d featureSelection.size: %zu", counter, k, featureSelection.size()); + while (counter++ < k && pairSelection.size() > 0) { + auto feature_pair = pairSelection[0]; + pairSelection.erase(pairSelection.begin()); + std::unique_ptr model; + model = std::make_unique(feature_pair.first, feature_pair.second); + model->fit(dataset, features, className, states, weights_, smoothing); + alpha_t = 0.0; + if (!block_update) { + auto ypred = model->predict(X_train); + // Step 3.1: Compute the classifier amout of say + std::tie(weights_, alpha_t, finished) = update_weights(y_train, ypred, weights_); + } + // Step 3.4: Store classifier and its accuracy to weigh its future vote + numItemsPack++; + models.push_back(std::move(model)); + significanceModels.push_back(alpha_t); + n_models++; + // VLOG_SCOPE_F(2, "numItemsPack: %d n_models: %d featuresUsed: %zu", numItemsPack, n_models, + // featuresUsed.size()); + } + if (block_update) { + std::tie(weights_, alpha_t, finished) = update_weights_block(k, y_train, weights_); + } + if (convergence && !finished) { + auto y_val_predict = predict(X_test); + double accuracy = (y_val_predict == y_test).sum().item() / (double)y_test.size(0); + if (priorAccuracy == 0) { + priorAccuracy = accuracy; + } else { + improvement = accuracy - priorAccuracy; + } + if (improvement < convergence_threshold) { + // VLOG_SCOPE_F(3, " (improvement=threshold) Reset. tolerance: %d numItemsPack: %d improvement: %f + // prior: %f current: %f", tolerance, numItemsPack, improvement, priorAccuracy, accuracy); + tolerance = 0; // Reset the counter if the model performs better + numItemsPack = 0; + } + if (convergence_best) { + // Keep the best accuracy until now as the prior accuracy + priorAccuracy = std::max(accuracy, priorAccuracy); + } else { + // Keep the last accuray obtained as the prior accuracy + priorAccuracy = accuracy; + } + } + // VLOG_SCOPE_F(1, "tolerance: %d featuresUsed.size: %zu features.size: %zu", tolerance, featuresUsed.size(), + // features.size()); + finished = finished || tolerance > maxTolerance || pairSelection.size() == 0; + } + if (tolerance > maxTolerance) { + if (numItemsPack < n_models) { + notes.push_back("Convergence threshold reached & " + std::to_string(numItemsPack) + " models eliminated"); + // VLOG_SCOPE_F(4, "Convergence threshold reached & %d models eliminated of %d", numItemsPack, n_models); + for (int i = 0; i < numItemsPack; ++i) { + significanceModels.pop_back(); + models.pop_back(); + n_models--; + } + } else { + notes.push_back("Convergence threshold reached & 0 models eliminated"); + // VLOG_SCOPE_F(4, "Convergence threshold reached & 0 models eliminated n_models=%d numItemsPack=%d", + // n_models, numItemsPack); + } + } + if (pairSelection.size() > 0) { + notes.push_back("Pairs not used in train: " + std::to_string(pairSelection.size())); + status = WARNING; + } + notes.push_back("Number of models: " + std::to_string(n_models)); +} +std::vector XBA2DE::graph(const std::string &title) const { return Ensemble::graph(title); } +} // namespace bayesnet diff --git a/bayesnet/ensembles/XBA2DE.h b/bayesnet/ensembles/XBA2DE.h new file mode 100644 index 0000000..bb6fd48 --- /dev/null +++ b/bayesnet/ensembles/XBA2DE.h @@ -0,0 +1,28 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2025 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#ifndef XBA2DE_H +#define XBA2DE_H +#include +#include +#include "Boost.h" +namespace bayesnet { + class XBA2DE : public Boost { + public: + explicit XBA2DE(bool predict_voting = false); + virtual ~XBA2DE() = default; + std::vector graph(const std::string& title = "XBA2DE") const override; + std::string getVersion() override { return version; }; + protected: + void trainModel(const torch::Tensor& weights, const Smoothing_t smoothing) override; + private: + std::vector initializeModels(const Smoothing_t smoothing); + std::vector> X_train_, X_test_; + std::vector y_train_, y_test_; + std::string version = "0.9.7"; + }; +} +#endif diff --git a/bayesnet/ensembles/XBAODE.cc b/bayesnet/ensembles/XBAODE.cc index ddf86da..ddf8115 100644 --- a/bayesnet/ensembles/XBAODE.cc +++ b/bayesnet/ensembles/XBAODE.cc @@ -15,16 +15,6 @@ XBAODE::XBAODE() : Boost(false) { validHyperparameters = {"alpha_block", "order", "convergence", "convergence_best", "bisection", "threshold", "maxTolerance", "predict_voting", "select_features"}; } -void XBAODE::add_model(std::unique_ptr model, double significance) { - models.push_back(std::move(model)); - n_models++; - significanceModels.push_back(significance); -} -void XBAODE::remove_last_model() { - models.pop_back(); - significanceModels.pop_back(); - n_models--; -} std::vector XBAODE::initializeModels(const Smoothing_t smoothing) { torch::Tensor weights_ = torch::full({m}, 1.0 / m, torch::kFloat64); std::vector featuresSelected = featureSelection(weights_); diff --git a/bayesnet/ensembles/XBAODE.h b/bayesnet/ensembles/XBAODE.h index 65eab1c..4e96e64 100644 --- a/bayesnet/ensembles/XBAODE.h +++ b/bayesnet/ensembles/XBAODE.h @@ -18,8 +18,6 @@ namespace bayesnet { protected: void trainModel(const torch::Tensor& weights, const bayesnet::Smoothing_t smoothing) override; private: - void add_model(std::unique_ptr model, double significance); - void remove_last_model(); std::vector initializeModels(const Smoothing_t smoothing); std::vector> X_train_, X_test_; std::vector y_train_, y_test_; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1df73f7..11f2b2c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,7 +9,7 @@ if(ENABLE_TESTING) ${CMAKE_BINARY_DIR}/configured_files/include ) file(GLOB_RECURSE BayesNet_SOURCES "${BayesNet_SOURCE_DIR}/bayesnet/*.cc") - add_executable(TestBayesNet TestBayesNetwork.cc TestBayesNode.cc TestBayesClassifier.cc TestXSPnDE.cc + add_executable(TestBayesNet TestBayesNetwork.cc TestBayesNode.cc TestBayesClassifier.cc TestXSPnDE.cc TestXBA2DE.cc TestBayesModels.cc TestBayesMetrics.cc TestFeatureSelection.cc TestBoostAODE.cc TestXBAODE.cc TestA2DE.cc TestUtils.cc TestBayesEnsemble.cc TestModulesVersions.cc TestBoostA2DE.cc TestMST.cc TestXSPODE.cc ${BayesNet_SOURCES}) target_link_libraries(TestBayesNet PUBLIC "${TORCH_LIBRARIES}" fimdlp PRIVATE Catch2::Catch2WithMain) @@ -20,6 +20,7 @@ if(ENABLE_TESTING) add_test(NAME XSPODE COMMAND TestBayesNet "[XSPODE]") add_test(NAME XSPnDE COMMAND TestBayesNet "[XSPnDE]") add_test(NAME XBAODE COMMAND TestBayesNet "[XBAODE]") + add_test(NAME XBA2DE COMMAND TestBayesNet "[XBA2DE]") add_test(NAME Classifier COMMAND TestBayesNet "[Classifier]") add_test(NAME Ensemble COMMAND TestBayesNet "[Ensemble]") add_test(NAME FeatureSelection COMMAND TestBayesNet "[FeatureSelection]") diff --git a/tests/TestBayesModels.cc b/tests/TestBayesModels.cc index a84df23..c6ad27f 100644 --- a/tests/TestBayesModels.cc +++ b/tests/TestBayesModels.cc @@ -4,83 +4,111 @@ // SPDX-License-Identifier: MIT // *************************************************************** -#include -#include #include +#include #include #include +#include "TestUtils.h" #include "bayesnet/classifiers/KDB.h" -#include "bayesnet/classifiers/TAN.h" -#include "bayesnet/classifiers/SPODE.h" -#include "bayesnet/classifiers/XSPODE.h" -#include "bayesnet/classifiers/TANLd.h" #include "bayesnet/classifiers/KDBLd.h" +#include "bayesnet/classifiers/SPODE.h" #include "bayesnet/classifiers/SPODELd.h" +#include "bayesnet/classifiers/TAN.h" +#include "bayesnet/classifiers/TANLd.h" +#include "bayesnet/classifiers/XSPODE.h" #include "bayesnet/ensembles/AODE.h" #include "bayesnet/ensembles/AODELd.h" #include "bayesnet/ensembles/BoostAODE.h" -#include "TestUtils.h" const std::string ACTUAL_VERSION = "1.0.6"; -TEST_CASE("Test Bayesian Classifiers score & version", "[Models]") -{ - map , float> scores{ - // Diabetes - {{"diabetes", "AODE"}, 0.82161}, {{"diabetes", "KDB"}, 0.852865}, {{"diabetes", "XSPODE"}, 0.631510437f}, {{"diabetes", "SPODE"}, 0.802083}, {{"diabetes", "TAN"}, 0.821615}, - {{"diabetes", "AODELd"}, 0.8125f}, {{"diabetes", "KDBLd"}, 0.80208f}, {{"diabetes", "SPODELd"}, 0.7890625f}, {{"diabetes", "TANLd"}, 0.803385437f}, {{"diabetes", "BoostAODE"}, 0.83984f}, - // Ecoli - {{"ecoli", "AODE"}, 0.889881}, {{"ecoli", "KDB"}, 0.889881}, {{"ecoli", "XSPODE"}, 0.696428597f}, {{"ecoli", "SPODE"}, 0.880952}, {{"ecoli", "TAN"}, 0.892857}, - {{"ecoli", "AODELd"}, 0.875f}, {{"ecoli", "KDBLd"}, 0.880952358f}, {{"ecoli", "SPODELd"}, 0.839285731f}, {{"ecoli", "TANLd"}, 0.848214269f}, {{"ecoli", "BoostAODE"}, 0.89583f}, - // Glass - {{"glass", "AODE"}, 0.79439}, {{"glass", "KDB"}, 0.827103}, {{"glass", "XSPODE"}, 0.775701}, {{"glass", "SPODE"}, 0.775701}, {{"glass", "TAN"}, 0.827103}, - {{"glass", "AODELd"}, 0.799065411f}, {{"glass", "KDBLd"}, 0.82710278f}, {{"glass", "SPODELd"}, 0.780373812f}, {{"glass", "TANLd"}, 0.869158864f}, {{"glass", "BoostAODE"}, 0.84579f}, - // Iris - {{"iris", "AODE"}, 0.973333}, {{"iris", "KDB"}, 0.973333}, {{"iris", "XSPODE"}, 0.853333354f}, {{"iris", "SPODE"}, 0.973333}, {{"iris", "TAN"}, 0.973333}, - {{"iris", "AODELd"}, 0.973333}, {{"iris", "KDBLd"}, 0.973333}, {{"iris", "SPODELd"}, 0.96f}, {{"iris", "TANLd"}, 0.97333f}, {{"iris", "BoostAODE"}, 0.98f} - }; - std::map models{ - {"AODE", new bayesnet::AODE()}, {"AODELd", new bayesnet::AODELd()}, - {"BoostAODE", new bayesnet::BoostAODE()}, - {"KDB", new bayesnet::KDB(2)}, {"KDBLd", new bayesnet::KDBLd(2)}, - {"XSPODE", new bayesnet::XSpode(1)}, {"SPODE", new bayesnet::SPODE(1)}, {"SPODELd", new bayesnet::SPODELd(1)}, - {"TAN", new bayesnet::TAN()}, {"TANLd", new bayesnet::TANLd()} - }; +TEST_CASE("Test Bayesian Classifiers score & version", "[Models]") { + map, float> scores{// Diabetes + {{"diabetes", "AODE"}, 0.82161}, + {{"diabetes", "KDB"}, 0.852865}, + {{"diabetes", "XSPODE"}, 0.631510437f}, + {{"diabetes", "SPODE"}, 0.802083}, + {{"diabetes", "TAN"}, 0.821615}, + {{"diabetes", "AODELd"}, 0.8125f}, + {{"diabetes", "KDBLd"}, 0.80208f}, + {{"diabetes", "SPODELd"}, 0.7890625f}, + {{"diabetes", "TANLd"}, 0.803385437f}, + {{"diabetes", "BoostAODE"}, 0.83984f}, + // Ecoli + {{"ecoli", "AODE"}, 0.889881}, + {{"ecoli", "KDB"}, 0.889881}, + {{"ecoli", "XSPODE"}, 0.696428597f}, + {{"ecoli", "SPODE"}, 0.880952}, + {{"ecoli", "TAN"}, 0.892857}, + {{"ecoli", "AODELd"}, 0.875f}, + {{"ecoli", "KDBLd"}, 0.880952358f}, + {{"ecoli", "SPODELd"}, 0.839285731f}, + {{"ecoli", "TANLd"}, 0.848214269f}, + {{"ecoli", "BoostAODE"}, 0.89583f}, + // Glass + {{"glass", "AODE"}, 0.79439}, + {{"glass", "KDB"}, 0.827103}, + {{"glass", "XSPODE"}, 0.775701}, + {{"glass", "SPODE"}, 0.775701}, + {{"glass", "TAN"}, 0.827103}, + {{"glass", "AODELd"}, 0.799065411f}, + {{"glass", "KDBLd"}, 0.82710278f}, + {{"glass", "SPODELd"}, 0.780373812f}, + {{"glass", "TANLd"}, 0.869158864f}, + {{"glass", "BoostAODE"}, 0.84579f}, + // Iris + {{"iris", "AODE"}, 0.973333}, + {{"iris", "KDB"}, 0.973333}, + {{"iris", "XSPODE"}, 0.853333354f}, + {{"iris", "SPODE"}, 0.973333}, + {{"iris", "TAN"}, 0.973333}, + {{"iris", "AODELd"}, 0.973333}, + {{"iris", "KDBLd"}, 0.973333}, + {{"iris", "SPODELd"}, 0.96f}, + {{"iris", "TANLd"}, 0.97333f}, + {{"iris", "BoostAODE"}, 0.98f}}; + std::map models{{"AODE", new bayesnet::AODE()}, + {"AODELd", new bayesnet::AODELd()}, + {"BoostAODE", new bayesnet::BoostAODE()}, + {"KDB", new bayesnet::KDB(2)}, + {"KDBLd", new bayesnet::KDBLd(2)}, + {"XSPODE", new bayesnet::XSpode(1)}, + {"SPODE", new bayesnet::SPODE(1)}, + {"SPODELd", new bayesnet::SPODELd(1)}, + {"TAN", new bayesnet::TAN()}, + {"TANLd", new bayesnet::TANLd()}}; std::string name = GENERATE("AODE", "AODELd", "KDB", "KDBLd", "SPODE", "XSPODE", "SPODELd", "TAN", "TANLd"); auto clf = models[name]; - SECTION("Test " + name + " classifier") - { - for (const std::string& file_name : { "glass", "iris", "ecoli", "diabetes" }) { + SECTION("Test " + name + " classifier") { + for (const std::string &file_name : {"glass", "iris", "ecoli", "diabetes"}) { auto clf = models[name]; auto discretize = name.substr(name.length() - 2) != "Ld"; auto raw = RawDatasets(file_name, discretize); clf->fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing); auto score = clf->score(raw.Xt, raw.yt); - // std::cout << "Classifier: " << name << " File: " << file_name << " Score: " << score << " expected = " << scores[{file_name, name}] << std::endl; + // std::cout << "Classifier: " << name << " File: " << file_name << " Score: " << score << " expected = " << + // scores[{file_name, name}] << std::endl; INFO("Classifier: " << name << " File: " << file_name); REQUIRE(score == Catch::Approx(scores[{file_name, name}]).epsilon(raw.epsilon)); REQUIRE(clf->getStatus() == bayesnet::NORMAL); } } - SECTION("Library check version") - { + SECTION("Library check version") { INFO("Checking version of " << name << " classifier"); REQUIRE(clf->getVersion() == ACTUAL_VERSION); } delete clf; } -TEST_CASE("Models features & Graph", "[Models]") -{ - auto graph = std::vector({ "digraph BayesNet {\nlabel=\nfontsize=30\nfontcolor=blue\nlabelloc=t\nlayout=circo\n", - "\"class\" [shape=circle, fontcolor=red, fillcolor=lightblue, style=filled ] \n", - "\"class\" -> \"sepallength\"", "\"class\" -> \"sepalwidth\"", "\"class\" -> \"petallength\"", "\"class\" -> \"petalwidth\"", "\"petallength\" [shape=circle] \n", - "\"petallength\" -> \"sepallength\"", "\"petalwidth\" [shape=circle] \n", "\"sepallength\" [shape=circle] \n", - "\"sepallength\" -> \"sepalwidth\"", "\"sepalwidth\" [shape=circle] \n", "\"sepalwidth\" -> \"petalwidth\"", "}\n" - } - ); - SECTION("Test TAN") - { +TEST_CASE("Models features & Graph", "[Models]") { + auto graph = std::vector( + {"digraph BayesNet {\nlabel=\nfontsize=30\nfontcolor=blue\nlabelloc=t\nlayout=circo\n", + "\"class\" [shape=circle, fontcolor=red, fillcolor=lightblue, style=filled ] \n", + "\"class\" -> \"sepallength\"", "\"class\" -> \"sepalwidth\"", "\"class\" -> \"petallength\"", + "\"class\" -> \"petalwidth\"", "\"petallength\" [shape=circle] \n", "\"petallength\" -> \"sepallength\"", + "\"petalwidth\" [shape=circle] \n", "\"sepallength\" [shape=circle] \n", "\"sepallength\" -> \"sepalwidth\"", + "\"sepalwidth\" [shape=circle] \n", "\"sepalwidth\" -> \"petalwidth\"", "}\n"}); + SECTION("Test TAN") { auto raw = RawDatasets("iris", true); auto clf = bayesnet::TAN(); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); @@ -88,11 +116,12 @@ TEST_CASE("Models features & Graph", "[Models]") REQUIRE(clf.getNumberOfEdges() == 7); REQUIRE(clf.getNumberOfStates() == 19); REQUIRE(clf.getClassNumStates() == 3); - REQUIRE(clf.show() == std::vector{"class -> sepallength, sepalwidth, petallength, petalwidth, ", "petallength -> sepallength, ", "petalwidth -> ", "sepallength -> sepalwidth, ", "sepalwidth -> petalwidth, "}); + REQUIRE(clf.show() == std::vector{"class -> sepallength, sepalwidth, petallength, petalwidth, ", + "petallength -> sepallength, ", "petalwidth -> ", + "sepallength -> sepalwidth, ", "sepalwidth -> petalwidth, "}); REQUIRE(clf.graph("Test") == graph); } - SECTION("Test TANLd") - { + SECTION("Test TANLd") { auto clf = bayesnet::TANLd(); auto raw = RawDatasets("iris", false); clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing); @@ -100,12 +129,13 @@ TEST_CASE("Models features & Graph", "[Models]") REQUIRE(clf.getNumberOfEdges() == 7); REQUIRE(clf.getNumberOfStates() == 27); REQUIRE(clf.getClassNumStates() == 3); - REQUIRE(clf.show() == std::vector{"class -> sepallength, sepalwidth, petallength, petalwidth, ", "petallength -> sepallength, ", "petalwidth -> ", "sepallength -> sepalwidth, ", "sepalwidth -> petalwidth, "}); + REQUIRE(clf.show() == std::vector{"class -> sepallength, sepalwidth, petallength, petalwidth, ", + "petallength -> sepallength, ", "petalwidth -> ", + "sepallength -> sepalwidth, ", "sepalwidth -> petalwidth, "}); REQUIRE(clf.graph("Test") == graph); } } -TEST_CASE("Get num features & num edges", "[Models]") -{ +TEST_CASE("Get num features & num edges", "[Models]") { auto raw = RawDatasets("iris", true); auto clf = bayesnet::KDB(2); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); @@ -113,60 +143,49 @@ TEST_CASE("Get num features & num edges", "[Models]") REQUIRE(clf.getNumberOfEdges() == 8); } -TEST_CASE("Model predict_proba", "[Models]") -{ +TEST_CASE("Model predict_proba", "[Models]") { std::string model = GENERATE("TAN", "SPODE", "BoostAODEproba", "BoostAODEvoting"); - auto res_prob_tan = std::vector>({ - { 0.00375671, 0.994457, 0.00178621 }, - { 0.00137462, 0.992734, 0.00589123 }, - { 0.00137462, 0.992734, 0.00589123 }, - { 0.00137462, 0.992734, 0.00589123 }, - { 0.00218225, 0.992877, 0.00494094 }, - { 0.00494209, 0.0978534, 0.897205 }, - { 0.0054192, 0.974275, 0.0203054 }, - { 0.00433012, 0.985054, 0.0106159 }, - { 0.000860806, 0.996922, 0.00221698 } - }); - auto res_prob_spode = std::vector>({ - {0.00419032, 0.994247, 0.00156265}, - {0.00172808, 0.993433, 0.00483862}, - {0.00172808, 0.993433, 0.00483862}, - {0.00172808, 0.993433, 0.00483862}, - {0.00279211, 0.993737, 0.00347077}, - {0.0120674, 0.357909, 0.630024}, - {0.00386239, 0.913919, 0.0822185}, - {0.0244389, 0.966447, 0.00911374}, - {0.003135, 0.991799, 0.0050661} - }); - auto res_prob_baode = std::vector>({ - {0.0112349, 0.962274, 0.0264907}, - {0.00371025, 0.950592, 0.0456973}, - {0.00371025, 0.950592, 0.0456973}, - {0.00371025, 0.950592, 0.0456973}, - {0.00369275, 0.84967, 0.146637}, - {0.0252205, 0.113564, 0.861215}, - {0.0284828, 0.770524, 0.200993}, - {0.0213182, 0.857189, 0.121493}, - {0.00868436, 0.949494, 0.0418215} - }); - auto res_prob_voting = std::vector>({ - {0, 1, 0}, - {0, 1, 0}, - {0, 1, 0}, - {0, 1, 0}, - {0, 1, 0}, - {0, 0, 1}, - {0, 1, 0}, - {0, 1, 0}, - {0, 1, 0} - }); - std::map>> res_prob{ {"TAN", res_prob_tan}, {"SPODE", res_prob_spode} , {"BoostAODEproba", res_prob_baode }, {"BoostAODEvoting", res_prob_voting } }; - std::map models{ {"TAN", new bayesnet::TAN()}, {"SPODE", new bayesnet::SPODE(0)}, {"BoostAODEproba", new bayesnet::BoostAODE(false)}, {"BoostAODEvoting", new bayesnet::BoostAODE(true)} }; + auto res_prob_tan = std::vector>({{0.00375671, 0.994457, 0.00178621}, + {0.00137462, 0.992734, 0.00589123}, + {0.00137462, 0.992734, 0.00589123}, + {0.00137462, 0.992734, 0.00589123}, + {0.00218225, 0.992877, 0.00494094}, + {0.00494209, 0.0978534, 0.897205}, + {0.0054192, 0.974275, 0.0203054}, + {0.00433012, 0.985054, 0.0106159}, + {0.000860806, 0.996922, 0.00221698}}); + auto res_prob_spode = std::vector>({{0.00419032, 0.994247, 0.00156265}, + {0.00172808, 0.993433, 0.00483862}, + {0.00172808, 0.993433, 0.00483862}, + {0.00172808, 0.993433, 0.00483862}, + {0.00279211, 0.993737, 0.00347077}, + {0.0120674, 0.357909, 0.630024}, + {0.00386239, 0.913919, 0.0822185}, + {0.0244389, 0.966447, 0.00911374}, + {0.003135, 0.991799, 0.0050661}}); + auto res_prob_baode = std::vector>({{0.0112349, 0.962274, 0.0264907}, + {0.00371025, 0.950592, 0.0456973}, + {0.00371025, 0.950592, 0.0456973}, + {0.00371025, 0.950592, 0.0456973}, + {0.00369275, 0.84967, 0.146637}, + {0.0252205, 0.113564, 0.861215}, + {0.0284828, 0.770524, 0.200993}, + {0.0213182, 0.857189, 0.121493}, + {0.00868436, 0.949494, 0.0418215}}); + auto res_prob_voting = std::vector>( + {{0, 1, 0}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0}, {0, 0, 1}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0}}); + std::map>> res_prob{{"TAN", res_prob_tan}, + {"SPODE", res_prob_spode}, + {"BoostAODEproba", res_prob_baode}, + {"BoostAODEvoting", res_prob_voting}}; + std::map models{{"TAN", new bayesnet::TAN()}, + {"SPODE", new bayesnet::SPODE(0)}, + {"BoostAODEproba", new bayesnet::BoostAODE(false)}, + {"BoostAODEvoting", new bayesnet::BoostAODE(true)}}; int init_index = 78; auto raw = RawDatasets("iris", true); - SECTION("Test " + model + " predict_proba") - { + SECTION("Test " + model + " predict_proba") { auto clf = models[model]; clf->fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); auto y_pred_proba = clf->predict_proba(raw.Xv); @@ -194,23 +213,23 @@ TEST_CASE("Model predict_proba", "[Models]") REQUIRE(y_pred[i] == yt_pred[i].item()); for (int j = 0; j < 3; j++) { REQUIRE(res_prob[model][i][j] == Catch::Approx(y_pred_proba[i + init_index][j]).epsilon(raw.epsilon)); - REQUIRE(res_prob[model][i][j] == Catch::Approx(yt_pred_proba[i + init_index][j].item()).epsilon(raw.epsilon)); + REQUIRE(res_prob[model][i][j] == + Catch::Approx(yt_pred_proba[i + init_index][j].item()).epsilon(raw.epsilon)); } } delete clf; } } -TEST_CASE("AODE voting-proba", "[Models]") -{ +TEST_CASE("AODE voting-proba", "[Models]") { auto raw = RawDatasets("glass", true); auto clf = bayesnet::AODE(false); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); auto score_proba = clf.score(raw.Xv, raw.yv); auto pred_proba = clf.predict_proba(raw.Xv); clf.setHyperparameters({ - {"predict_voting",true}, - }); + {"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.79439f).epsilon(raw.epsilon)); @@ -219,8 +238,7 @@ TEST_CASE("AODE voting-proba", "[Models]") REQUIRE(pred_proba[67][0] == Catch::Approx(0.702184).epsilon(raw.epsilon)); REQUIRE(clf.topological_order() == std::vector()); } -TEST_CASE("SPODELd dataset", "[Models]") -{ +TEST_CASE("SPODELd dataset", "[Models]") { auto raw = RawDatasets("iris", false); auto clf = bayesnet::SPODELd(0); // raw.dataset.to(torch::kFloat32); @@ -231,8 +249,7 @@ TEST_CASE("SPODELd dataset", "[Models]") REQUIRE(score == Catch::Approx(0.97333f).epsilon(raw.epsilon)); REQUIRE(scoret == Catch::Approx(0.97333f).epsilon(raw.epsilon)); } -TEST_CASE("KDB with hyperparameters", "[Models]") -{ +TEST_CASE("KDB with hyperparameters", "[Models]") { auto raw = RawDatasets("glass", true); auto clf = bayesnet::KDB(2); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); @@ -240,20 +257,18 @@ TEST_CASE("KDB with hyperparameters", "[Models]") clf.setHyperparameters({ {"k", 3}, {"theta", 0.7}, - }); + }); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); auto scoret = clf.score(raw.Xv, raw.yv); REQUIRE(score == Catch::Approx(0.827103).epsilon(raw.epsilon)); REQUIRE(scoret == Catch::Approx(0.761682).epsilon(raw.epsilon)); } -TEST_CASE("Incorrect type of data for SPODELd", "[Models]") -{ +TEST_CASE("Incorrect type of data for SPODELd", "[Models]") { auto raw = RawDatasets("iris", true); auto clf = bayesnet::SPODELd(0); REQUIRE_THROWS_AS(clf.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing), std::runtime_error); } -TEST_CASE("Predict, predict_proba & score without fitting", "[Models]") -{ +TEST_CASE("Predict, predict_proba & score without fitting", "[Models]") { auto clf = bayesnet::AODE(); auto raw = RawDatasets("iris", true); std::string message = "Ensemble has not been fitted"; @@ -270,35 +285,55 @@ TEST_CASE("Predict, predict_proba & score without fitting", "[Models]") REQUIRE_THROWS_WITH(clf.score(raw.Xv, raw.yv), message); REQUIRE_THROWS_WITH(clf.score(raw.Xt, raw.yt), message); } -TEST_CASE("TAN & SPODE with hyperparameters", "[Models]") -{ +TEST_CASE("TAN & SPODE with hyperparameters", "[Models]") { auto raw = RawDatasets("iris", true); auto clf = bayesnet::TAN(); clf.setHyperparameters({ {"parent", 1}, - }); + }); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); auto score = clf.score(raw.Xv, raw.yv); REQUIRE(score == Catch::Approx(0.973333).epsilon(raw.epsilon)); auto clf2 = bayesnet::SPODE(0); clf2.setHyperparameters({ {"parent", 1}, - }); + }); clf2.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); auto score2 = clf2.score(raw.Xv, raw.yv); REQUIRE(score2 == Catch::Approx(0.973333).epsilon(raw.epsilon)); } -TEST_CASE("TAN & SPODE with invalid hyperparameters", "[Models]") -{ +TEST_CASE("TAN & SPODE with invalid hyperparameters", "[Models]") { auto raw = RawDatasets("iris", true); auto clf = bayesnet::TAN(); clf.setHyperparameters({ {"parent", 5}, - }); - REQUIRE_THROWS_AS(clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing), std::invalid_argument); + }); + REQUIRE_THROWS_AS(clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing), + std::invalid_argument); auto clf2 = bayesnet::SPODE(0); clf2.setHyperparameters({ {"parent", 5}, - }); - REQUIRE_THROWS_AS(clf2.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing), std::invalid_argument); -} \ No newline at end of file + }); + REQUIRE_THROWS_AS(clf2.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing), + std::invalid_argument); +} +TEST_CASE("Check proposal checkInput", "[Models]") { + class testProposal : public bayesnet::Proposal { + public: + testProposal(torch::Tensor &dataset_, std::vector &features_, std::string &className_) + : Proposal(dataset_, features_, className_) {} + void test_X_y(const torch::Tensor &X, const torch::Tensor &y) { checkInput(X, y); } + }; + auto raw = RawDatasets("iris", true); + auto clf = testProposal(raw.dataset, raw.features, raw.className); + torch::Tensor X = torch::randint(0, 3, {10, 4}); + torch::Tensor y = torch::rand({10}); + INFO("Check X is not float"); + REQUIRE_THROWS_AS(clf.test_X_y(X, y), std::invalid_argument); + X = torch::rand({10, 4}); + INFO("Check y is not integer"); + REQUIRE_THROWS_AS(clf.test_X_y(X, y), std::invalid_argument); + y = torch::randint(0, 3, {10}); + INFO("X and y are correct"); + REQUIRE_NOTHROW(clf.test_X_y(X, y)); +} diff --git a/tests/TestBoostAODE.cc b/tests/TestBoostAODE.cc index 7394db2..44b3ed5 100644 --- a/tests/TestBoostAODE.cc +++ b/tests/TestBoostAODE.cc @@ -4,20 +4,17 @@ // SPDX-License-Identifier: MIT // *************************************************************** -#include -#include #include -#include +#include +#include #include -#include "bayesnet/ensembles/BoostAODE.h" #include "TestUtils.h" +#include "bayesnet/ensembles/BoostAODE.h" - -TEST_CASE("Feature_select CFS", "[BoostAODE]") -{ +TEST_CASE("Feature_select CFS", "[BoostAODE]") { auto raw = RawDatasets("glass", true); auto clf = bayesnet::BoostAODE(); - clf.setHyperparameters({ {"select_features", "CFS"} }); + clf.setHyperparameters({{"select_features", "CFS"}}); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); REQUIRE(clf.getNumberOfNodes() == 90); REQUIRE(clf.getNumberOfEdges() == 153); @@ -25,11 +22,10 @@ TEST_CASE("Feature_select CFS", "[BoostAODE]") 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]") -{ +TEST_CASE("Feature_select IWSS", "[BoostAODE]") { auto raw = RawDatasets("glass", true); auto clf = bayesnet::BoostAODE(); - clf.setHyperparameters({ {"select_features", "IWSS"}, {"threshold", 0.5 } }); + clf.setHyperparameters({{"select_features", "IWSS"}, {"threshold", 0.5}}); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); REQUIRE(clf.getNumberOfNodes() == 90); REQUIRE(clf.getNumberOfEdges() == 153); @@ -37,11 +33,10 @@ TEST_CASE("Feature_select IWSS", "[BoostAODE]") 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]") -{ +TEST_CASE("Feature_select FCBF", "[BoostAODE]") { auto raw = RawDatasets("glass", true); auto clf = bayesnet::BoostAODE(); - clf.setHyperparameters({ {"select_features", "FCBF"}, {"threshold", 1e-7 } }); + clf.setHyperparameters({{"select_features", "FCBF"}, {"threshold", 1e-7}}); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); REQUIRE(clf.getNumberOfNodes() == 90); REQUIRE(clf.getNumberOfEdges() == 153); @@ -49,15 +44,14 @@ TEST_CASE("Feature_select FCBF", "[BoostAODE]") 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]") -{ +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"}, - }); + {"select_features", "CFS"}, + }); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); REQUIRE(clf.getNumberOfNodes() == 72); REQUIRE(clf.getNumberOfEdges() == 120); @@ -69,16 +63,15 @@ TEST_CASE("Test used features in train note and score", "[BoostAODE]") REQUIRE(score == Catch::Approx(0.809895813).epsilon(raw.epsilon)); REQUIRE(scoret == Catch::Approx(0.809895813).epsilon(raw.epsilon)); } -TEST_CASE("Voting vs proba", "[BoostAODE]") -{ +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, raw.smoothing); auto score_proba = clf.score(raw.Xv, raw.yv); auto pred_proba = clf.predict_proba(raw.Xv); clf.setHyperparameters({ - {"predict_voting",true}, - }); + {"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)); @@ -88,20 +81,17 @@ TEST_CASE("Voting vs proba", "[BoostAODE]") REQUIRE(clf.dump_cpt().size() == 7004); REQUIRE(clf.topological_order() == std::vector()); } -TEST_CASE("Order asc, desc & random", "[BoostAODE]") -{ +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" }) { + 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, raw.smoothing); auto score = clf.score(raw.Xv, raw.yv); auto scoret = clf.score(raw.Xt, raw.yt); @@ -110,44 +100,43 @@ TEST_CASE("Order asc, desc & random", "[BoostAODE]") REQUIRE(scoret == Catch::Approx(scores[order]).epsilon(raw.epsilon)); } } -TEST_CASE("Oddities", "[BoostAODE]") -{ +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", 7 } }, + {{"order", "duck"}}, + {{"select_features", "duck"}}, + {{"maxTolerance", 0}}, + {{"maxTolerance", 7}}, }; - for (const auto& hyper : bad_hyper.items()) { + 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); + 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 } }, + {{"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()) { + 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, raw.smoothing), std::invalid_argument); + REQUIRE_THROWS_AS(clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing), + std::invalid_argument); } auto bad_hyper_fit2 = nlohmann::json{ - { { "alpha_block", true }, { "block_update", true } }, - { { "bisection", false }, { "block_update", true } }, + {{"alpha_block", true}, {"block_update", true}}, + {{"bisection", false}, {"block_update", true}}, }; - for (const auto& hyper : bad_hyper_fit2.items()) { + for (const auto &hyper : bad_hyper_fit2.items()) { INFO("BoostAODE hyper: " << hyper.value().dump()); REQUIRE_THROWS_AS(clf.setHyperparameters(hyper.value()), std::invalid_argument); } } -TEST_CASE("Bisection Best", "[BoostAODE]") -{ +TEST_CASE("Bisection Best", "[BoostAODE]") { auto clf = bayesnet::BoostAODE(); auto raw = RawDatasets("kdd_JapaneseVowels", true, 1200, true, false); clf.setHyperparameters({ @@ -156,7 +145,7 @@ TEST_CASE("Bisection Best", "[BoostAODE]") {"convergence", true}, {"block_update", false}, {"convergence_best", false}, - }); + }); clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); REQUIRE(clf.getNumberOfNodes() == 210); REQUIRE(clf.getNumberOfEdges() == 378); @@ -167,8 +156,7 @@ TEST_CASE("Bisection Best", "[BoostAODE]") 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]") -{ +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{ @@ -188,8 +176,7 @@ TEST_CASE("Bisection Best vs Last", "[BoostAODE]") 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]") -{ +TEST_CASE("Block Update", "[BoostAODE]") { auto clf = bayesnet::BoostAODE(); auto raw = RawDatasets("mfeat-factors", true, 500); clf.setHyperparameters({ @@ -197,7 +184,7 @@ TEST_CASE("Block Update", "[BoostAODE]") {"block_update", true}, {"maxTolerance", 3}, {"convergence", true}, - }); + }); clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); REQUIRE(clf.getNumberOfNodes() == 868); REQUIRE(clf.getNumberOfEdges() == 1724); @@ -218,18 +205,18 @@ TEST_CASE("Block Update", "[BoostAODE]") // } // std::cout << "Score " << score << std::endl; } -TEST_CASE("Alphablock", "[BoostAODE]") -{ +TEST_CASE("Alphablock", "[BoostAODE]") { auto clf_alpha = bayesnet::BoostAODE(); auto clf_no_alpha = bayesnet::BoostAODE(); auto raw = RawDatasets("diabetes", true); clf_alpha.setHyperparameters({ {"alpha_block", true}, - }); + }); clf_alpha.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); clf_no_alpha.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); auto score_alpha = clf_alpha.score(raw.X_test, raw.y_test); auto score_no_alpha = clf_no_alpha.score(raw.X_test, raw.y_test); REQUIRE(score_alpha == Catch::Approx(0.720779f).epsilon(raw.epsilon)); REQUIRE(score_no_alpha == Catch::Approx(0.733766f).epsilon(raw.epsilon)); -} \ No newline at end of file +} + diff --git a/tests/TestXBA2DE.cc b/tests/TestXBA2DE.cc new file mode 100644 index 0000000..9e1b26f --- /dev/null +++ b/tests/TestXBA2DE.cc @@ -0,0 +1,237 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2025 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#include +#include +#include +#include +#include "TestUtils.h" +#include "bayesnet/ensembles/XBA2DE.h" + +TEST_CASE("Normal test", "[XBA2DE]") { + auto raw = RawDatasets("iris", true); + auto clf = bayesnet::XBA2DE(); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + REQUIRE(clf.getNumberOfNodes() == 5); + REQUIRE(clf.getNumberOfEdges() == 8); + REQUIRE(clf.getNotes().size() == 2); + REQUIRE(clf.getVersion() == "0.9.7"); + REQUIRE(clf.getNotes()[0] == "Convergence threshold reached & 13 models eliminated"); + REQUIRE(clf.getNotes()[1] == "Number of models: 1"); + REQUIRE(clf.getNumberOfStates() == 64); + REQUIRE(clf.score(raw.X_test, raw.y_test) == Catch::Approx(1.0f)); + REQUIRE(clf.graph().size() == 1); +} +TEST_CASE("Feature_select CFS", "[XBA2DE]") { + auto raw = RawDatasets("glass", true); + auto clf = bayesnet::XBA2DE(); + clf.setHyperparameters({{"select_features", "CFS"}}); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + REQUIRE(clf.getNumberOfNodes() == 220); + REQUIRE(clf.getNumberOfEdges() == 506); + 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: 22"); + REQUIRE(clf.score(raw.X_test, raw.y_test) == Catch::Approx(0.720930219)); +} +TEST_CASE("Feature_select IWSS", "[XBA2DE]") { + auto raw = RawDatasets("glass", true); + auto clf = bayesnet::XBA2DE(); + clf.setHyperparameters({{"select_features", "IWSS"}, {"threshold", 0.5}}); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + REQUIRE(clf.getNumberOfNodes() == 220); + REQUIRE(clf.getNumberOfEdges() == 506); + REQUIRE(clf.getNotes().size() == 4); + REQUIRE(clf.getNotes()[0] == "Used features in initialization: 4 of 9 with IWSS"); + REQUIRE(clf.getNotes()[1] == "Convergence threshold reached & 15 models eliminated"); + REQUIRE(clf.getNotes()[2] == "Pairs not used in train: 2"); + REQUIRE(clf.getNotes()[3] == "Number of models: 22"); + REQUIRE(clf.getNumberOfStates() == 5346); + REQUIRE(clf.score(raw.X_test, raw.y_test) == Catch::Approx(0.72093)); +} +TEST_CASE("Feature_select FCBF", "[XBA2DE]") { + auto raw = RawDatasets("glass", true); + auto clf = bayesnet::XBA2DE(); + clf.setHyperparameters({{"select_features", "FCBF"}, {"threshold", 1e-7}}); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + REQUIRE(clf.getNumberOfNodes() == 290); + REQUIRE(clf.getNumberOfEdges() == 667); + REQUIRE(clf.getNumberOfStates() == 7047); + REQUIRE(clf.getNotes().size() == 3); + REQUIRE(clf.getNotes()[0] == "Used features in initialization: 4 of 9 with FCBF"); + REQUIRE(clf.getNotes()[1] == "Pairs not used in train: 2"); + REQUIRE(clf.getNotes()[2] == "Number of models: 29"); + REQUIRE(clf.score(raw.X_test, raw.y_test) == Catch::Approx(0.744186)); +} +TEST_CASE("Test used features in train note and score", "[XBA2DE]") { + auto raw = RawDatasets("diabetes", true); + auto clf = bayesnet::XBA2DE(); + clf.setHyperparameters({ + {"order", "asc"}, + {"convergence", true}, + {"select_features", "CFS"}, + }); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + REQUIRE(clf.getNumberOfNodes() == 144); + REQUIRE(clf.getNumberOfEdges() == 320); + REQUIRE(clf.getNumberOfStates() == 5504); + 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: 16"); + auto score = clf.score(raw.Xv, raw.yv); + auto scoret = clf.score(raw.Xt, raw.yt); + REQUIRE(score == Catch::Approx(0.850260437f).epsilon(raw.epsilon)); + REQUIRE(scoret == Catch::Approx(0.850260437f).epsilon(raw.epsilon)); +} +TEST_CASE("Order asc, desc & random", "[XBA2DE]") { + auto raw = RawDatasets("glass", true); + std::map scores{{"asc", 0.827103}, {"desc", 0.808411}, {"rand", 0.827103}}; + for (const std::string &order : {"asc", "desc", "rand"}) { + auto clf = bayesnet::XBA2DE(); + clf.setHyperparameters({ + {"order", order}, + {"bisection", false}, + {"maxTolerance", 1}, + {"convergence", true}, + }); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + auto score = clf.score(raw.Xv, raw.yv); + auto scoret = clf.score(raw.Xt, raw.yt); + INFO("XBA2DE order: " << order); + REQUIRE(score == Catch::Approx(scores[order]).epsilon(raw.epsilon)); + REQUIRE(scoret == Catch::Approx(scores[order]).epsilon(raw.epsilon)); + } +} +TEST_CASE("Oddities", "[XBA2DE]") { + auto clf = bayesnet::XBA2DE(); + auto raw = RawDatasets("iris", true); + auto bad_hyper = nlohmann::json{ + {{"order", "duck"}}, + {{"select_features", "duck"}}, + {{"maxTolerance", 0}}, + {{"maxTolerance", 7}}, + }; + for (const auto &hyper : bad_hyper.items()) { + INFO("XBA2DE 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("XBA2DE hyper: " << hyper.value().dump()); + clf.setHyperparameters(hyper.value()); + REQUIRE_THROWS_AS(clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing), + std::invalid_argument); + } + auto bad_hyper_fit2 = nlohmann::json{ + {{"alpha_block", true}, {"block_update", true}}, + {{"bisection", false}, {"block_update", true}}, + }; + for (const auto &hyper : bad_hyper_fit2.items()) { + INFO("XBA2DE hyper: " << hyper.value().dump()); + REQUIRE_THROWS_AS(clf.setHyperparameters(hyper.value()), std::invalid_argument); + } + // Check not enough selected features + raw.Xv.pop_back(); + raw.Xv.pop_back(); + raw.Xv.pop_back(); + raw.features.pop_back(); + raw.features.pop_back(); + raw.features.pop_back(); + clf.setHyperparameters({{"select_features", "CFS"}, {"alpha_block", false}, {"block_update", false}}); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + REQUIRE(clf.getNotes().size() == 1); + REQUIRE(clf.getNotes()[0] == "No features selected in initialization"); +} +TEST_CASE("Bisection Best", "[XBA2DE]") { + auto clf = bayesnet::XBA2DE(); + auto raw = RawDatasets("kdd_JapaneseVowels", true, 1200, true, false); + clf.setHyperparameters({ + {"bisection", true}, + {"maxTolerance", 3}, + {"convergence", true}, + {"convergence_best", false}, + }); + clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); + REQUIRE(clf.getNumberOfNodes() == 330); + REQUIRE(clf.getNumberOfEdges() == 836); + REQUIRE(clf.getNumberOfStates() == 31108); + REQUIRE(clf.getNotes().size() == 3); + REQUIRE(clf.getNotes().at(0) == "Convergence threshold reached & 15 models eliminated"); + REQUIRE(clf.getNotes().at(1) == "Pairs not used in train: 83"); + REQUIRE(clf.getNotes().at(2) == "Number of models: 22"); + 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.975).epsilon(raw.epsilon)); + REQUIRE(scoret == Catch::Approx(0.975).epsilon(raw.epsilon)); +} +TEST_CASE("Bisection Best vs Last", "[XBA2DE]") { + auto raw = RawDatasets("kdd_JapaneseVowels", true, 1500, true, false); + auto clf = bayesnet::XBA2DE(); + 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, raw.smoothing); + auto score_best = clf.score(raw.X_test, raw.y_test); + REQUIRE(score_best == Catch::Approx(0.983333).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, raw.smoothing); + auto score_last = clf.score(raw.X_test, raw.y_test); + REQUIRE(score_last == Catch::Approx(0.99).epsilon(raw.epsilon)); +} +TEST_CASE("Block Update", "[XBA2DE]") { + auto clf = bayesnet::XBA2DE(); + auto raw = RawDatasets("kdd_JapaneseVowels", true, 1500, true, false); + 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, raw.smoothing); + REQUIRE(clf.getNumberOfNodes() == 120); + REQUIRE(clf.getNumberOfEdges() == 304); + REQUIRE(clf.getNotes().size() == 3); + REQUIRE(clf.getNotes()[0] == "Convergence threshold reached & 15 models eliminated"); + REQUIRE(clf.getNotes()[1] == "Pairs not used in train: 83"); + REQUIRE(clf.getNotes()[2] == "Number of models: 8"); + 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.963333).epsilon(raw.epsilon)); + REQUIRE(scoret == Catch::Approx(0.963333).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;*/ +} +TEST_CASE("Alphablock", "[XBA2DE]") { + auto clf_alpha = bayesnet::XBA2DE(); + auto clf_no_alpha = bayesnet::XBA2DE(); + auto raw = RawDatasets("diabetes", true); + clf_alpha.setHyperparameters({ + {"alpha_block", true}, + }); + clf_alpha.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); + clf_no_alpha.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); + auto score_alpha = clf_alpha.score(raw.X_test, raw.y_test); + auto score_no_alpha = clf_no_alpha.score(raw.X_test, raw.y_test); + REQUIRE(score_alpha == Catch::Approx(0.714286).epsilon(raw.epsilon)); + REQUIRE(score_no_alpha == Catch::Approx(0.714286).epsilon(raw.epsilon)); +} diff --git a/tests/TestXSPnDE.cc b/tests/TestXSPnDE.cc index 9b1b7ec..4b6afd5 100644 --- a/tests/TestXSPnDE.cc +++ b/tests/TestXSPnDE.cc @@ -37,16 +37,12 @@ static void check_spnde_pair( // Basic checks REQUIRE(clf.getNumberOfNodes() == 5); // for iris: 4 features + 1 class - // For XSpnde, edges are often computed as 3*nFeatures - 4. For iris nFeatures=4 => 3*4 -4 = 8 REQUIRE(clf.getNumberOfEdges() == 8); REQUIRE(clf.getNotes().size() == 0); // Evaluate on test set float sc = clf.score(raw.X_test, raw.y_test); - // If you know the exact expected accuracy for each pair, use: - // REQUIRE(sc == Catch::Approx(someValue)); - // Otherwise, just check it's > some threshold: - REQUIRE(sc >= 0.90f); // placeholder; you can pick your own threshold + REQUIRE(sc >= 0.93f); } // ------------------------------------------------------------ @@ -55,13 +51,10 @@ static void check_spnde_pair( TEST_CASE("fit vector test (XSPNDE)", "[XSPNDE]") { auto raw = RawDatasets("iris", true); - // We’ll test a couple of two-superparent pairs, e.g. (0,1) and (2,3). - // You can add more if you like, e.g. (0,2), (1,3), etc. std::vector> parentPairs = { {0,1}, {2,3} }; for (auto &p : parentPairs) { - // We’re doing the “vector” version check_spnde_pair(p.first, p.second, raw, /*fitVector=*/true, /*fitTensor=*/false); } } @@ -77,7 +70,6 @@ TEST_CASE("fit dataset test (XSPNDE)", "[XSPNDE]") { {0,2}, {1,3} }; for (auto &p : parentPairs) { - // Now do the “dataset” version check_spnde_pair(p.first, p.second, raw, /*fitVector=*/false, /*fitTensor=*/false); } } @@ -88,14 +80,12 @@ TEST_CASE("fit dataset test (XSPNDE)", "[XSPNDE]") { TEST_CASE("tensors dataset predict & predict_proba (XSPNDE)", "[XSPNDE]") { auto raw = RawDatasets("iris", true); - // Let’s test a single pair or multiple pairs. For brevity: std::vector> parentPairs = { - {0,3} + {0,3}, {1,2} }; for (auto &p : parentPairs) { bayesnet::XSpnde clf(p.first, p.second); - // Fit using the “tensor” approach clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing); REQUIRE(clf.getNumberOfNodes() == 5); @@ -106,15 +96,46 @@ TEST_CASE("tensors dataset predict & predict_proba (XSPNDE)", "[XSPNDE]") { float sc = clf.score(raw.X_test, raw.y_test); REQUIRE(sc >= 0.90f); - // You can also test predict_proba on a small slice: - // e.g. the first 3 samples in X_test auto X_reduced = raw.X_test.slice(1, 0, 3); auto proba = clf.predict_proba(X_reduced); - - // If you know exact probabilities, compare them with Catch::Approx. - // For example: - // REQUIRE(proba[0][0].item() == Catch::Approx(0.98)); - // etc. } } +TEST_CASE("Check hyperparameters", "[XSPNDE]") +{ + auto raw = RawDatasets("iris", true); + auto clf = bayesnet::XSpnde(0, 1); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + auto clf2 = bayesnet::XSpnde(2, 3); + clf2.setHyperparameters({{"parent1", 0}, {"parent2", 1}}); + clf2.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + REQUIRE(clf.to_string() == clf2.to_string()); +} +TEST_CASE("Check different smoothing", "[XSPNDE]") +{ + auto raw = RawDatasets("iris", true); + + auto clf = bayesnet::XSpnde(0, 1); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, bayesnet::Smoothing_t::ORIGINAL); + auto clf2 = bayesnet::XSpnde(0, 1); + clf2.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, bayesnet::Smoothing_t::LAPLACE); + auto clf3 = bayesnet::XSpnde(0, 1); + clf3.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, bayesnet::Smoothing_t::NONE); + auto score = clf.score(raw.X_test, raw.y_test); + auto score2 = clf2.score(raw.X_test, raw.y_test); + auto score3 = clf3.score(raw.X_test, raw.y_test); + REQUIRE(score == Catch::Approx(1.0).epsilon(raw.epsilon)); + REQUIRE(score2 == Catch::Approx(0.7333333).epsilon(raw.epsilon)); + REQUIRE(score3 == Catch::Approx(0.966667).epsilon(raw.epsilon)); +} +TEST_CASE("Check rest", "[XSPNDE]") +{ + auto raw = RawDatasets("iris", true); + auto clf = bayesnet::XSpnde(0, 1); + REQUIRE_THROWS_AS(clf.predict_proba(std::vector({1,2,3,4})), std::logic_error); + clf.fitx(raw.Xt, raw.yt, raw.weights, bayesnet::Smoothing_t::ORIGINAL); + REQUIRE(clf.getNFeatures() == 4); + REQUIRE(clf.score(raw.Xv, raw.yv) == Catch::Approx(0.973333359f).epsilon(raw.epsilon)); + REQUIRE(clf.predict({1,2,3,4}) == 1); + +}