diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 959c820..7825077 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,7 +27,6 @@ add_executable( reports/ReportExcel.cpp reports/ReportBase.cpp reports/ExcelFile.cpp results/Result.cpp experimental_clfs/XA1DE.cpp - experimental_clfs/XBAODE.cpp experimental_clfs/ExpClf.cpp ) target_link_libraries(b_best Boost::boost "${PyClassifiers}" "${BayesNet}" fimdlp ${Python3_LIBRARIES} "${TORCH_LIBRARIES}" ${LIBTORCH_PYTHON} Boost::python Boost::numpy "${XLSXWRITER_LIB}") @@ -41,7 +40,6 @@ add_executable(b_grid commands/b_grid.cpp ${grid_sources} reports/ReportConsole.cpp reports/ReportBase.cpp results/Result.cpp experimental_clfs/XA1DE.cpp - experimental_clfs/XBAODE.cpp experimental_clfs/ExpClf.cpp ) target_link_libraries(b_grid ${MPI_CXX_LIBRARIES} "${PyClassifiers}" "${BayesNet}" fimdlp ${Python3_LIBRARIES} "${TORCH_LIBRARIES}" ${LIBTORCH_PYTHON} Boost::python Boost::numpy) @@ -53,7 +51,6 @@ add_executable(b_list commands/b_list.cpp reports/ReportExcel.cpp reports/ExcelFile.cpp reports/ReportBase.cpp reports/DatasetsExcel.cpp reports/DatasetsConsole.cpp reports/ReportsPaged.cpp results/Result.cpp results/ResultsDatasetExcel.cpp results/ResultsDataset.cpp results/ResultsDatasetConsole.cpp experimental_clfs/XA1DE.cpp - experimental_clfs/XBAODE.cpp experimental_clfs/ExpClf.cpp ) target_link_libraries(b_list "${PyClassifiers}" "${BayesNet}" fimdlp ${Python3_LIBRARIES} "${TORCH_LIBRARIES}" ${LIBTORCH_PYTHON} Boost::python Boost::numpy "${XLSXWRITER_LIB}") @@ -66,7 +63,6 @@ add_executable(b_main commands/b_main.cpp ${main_sources} reports/ReportConsole.cpp reports/ReportBase.cpp results/Result.cpp experimental_clfs/XA1DE.cpp - experimental_clfs/XBAODE.cpp experimental_clfs/ExpClf.cpp ) target_link_libraries(b_main "${PyClassifiers}" "${BayesNet}" fimdlp ${Python3_LIBRARIES} "${TORCH_LIBRARIES}" ${LIBTORCH_PYTHON} Boost::python Boost::numpy) diff --git a/src/experimental_clfs/ExpEnsemble.cpp b/src/experimental_clfs/ExpEnsemble.cpp new file mode 100644 index 0000000..1c9dd22 --- /dev/null +++ b/src/experimental_clfs/ExpEnsemble.cpp @@ -0,0 +1,158 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2025 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#include "ExpEnsemble.h" +#include "TensorUtils.hpp" + +namespace platform { + ExpEnsemble::ExpEnsemble() : semaphore_{ CountingSemaphore::getInstance() }, Boost(false) + { + validHyperparameters = {}; + } + // + // Parents + // + void ExpEnsemble::add_model(std::unique_ptr model) + { + models.push_back(std::move(model)); + n_models++; + } + void ExpEnsemble::remove_last_model() + { + models.pop_back(); + n_models--; + } + // + // Predict + // + torch::Tensor ExpEnsemble::predict(torch::Tensor& X) + { + auto X_ = TensorUtils::to_matrix(X); + torch::Tensor y = torch::tensor(predict(X_)); + return y; + } + torch::Tensor ExpEnsemble::predict_proba(torch::Tensor& X) + { + auto X_ = TensorUtils::to_matrix(X); + auto probabilities = predict_proba(X_); + auto n_samples = X.size(1); + int n_classes = probabilities[0].size(); + auto y = torch::zeros({ n_samples, n_classes }); + for (int i = 0; i < n_samples; i++) { + for (int j = 0; j < n_classes; j++) { + y[i][j] = probabilities[i][j]; + } + } + return y; + } + float ExpEnsemble::score(torch::Tensor& X, torch::Tensor& y) + { + auto X_ = TensorUtils::to_matrix(X); + auto y_ = TensorUtils::to_vector(y); + return score(X_, y_); + } + std::vector> ExpEnsemble::predict_proba(const std::vector>& test_data) + { + int test_size = test_data[0].size(); + int sample_size = test_data.size(); + auto probabilities = std::vector>(test_size, std::vector(getClassNumStates())); + int chunk_size = std::min(150, int(test_size / semaphore_.getMaxCount()) + 1); + std::vector threads; + auto worker = [&](const std::vector>& samples, int begin, int chunk, int sample_size, std::vector>& predictions) { + std::string threadName = "(V)PWorker-" + std::to_string(begin) + "-" + std::to_string(chunk); +#if defined(__linux__) + pthread_setname_np(pthread_self(), threadName.c_str()); +#else + pthread_setname_np(threadName.c_str()); +#endif + + std::vector instance(sample_size); + for (int sample = begin; sample < begin + chunk; ++sample) { + for (int feature = 0; feature < sample_size; ++feature) { + instance[feature] = samples[feature][sample]; + } + // predictions[sample] = aode_.predict_proba(instance); + } + semaphore_.release(); + }; + for (int begin = 0; begin < test_size; begin += chunk_size) { + int chunk = std::min(chunk_size, test_size - begin); + semaphore_.acquire(); + threads.emplace_back(worker, test_data, begin, chunk, sample_size, std::ref(probabilities)); + } + for (auto& thread : threads) { + thread.join(); + } + return probabilities; + } + std::vector ExpEnsemble::predict(std::vector>& test_data) + { + if (!fitted) { + throw std::logic_error(CLASSIFIER_NOT_FITTED); + } + auto probabilities = predict_proba(test_data); + std::vector predictions(probabilities.size(), 0); + + for (size_t i = 0; i < probabilities.size(); i++) { + predictions[i] = std::distance(probabilities[i].begin(), std::max_element(probabilities[i].begin(), probabilities[i].end())); + } + + return predictions; + } + float ExpEnsemble::score(std::vector>& test_data, std::vector& labels) + { + Timer timer; + timer.start(); + std::vector predictions = predict(test_data); + int correct = 0; + + for (size_t i = 0; i < predictions.size(); i++) { + if (predictions[i] == labels[i]) { + correct++; + } + } + if (debug) { + std::cout << "* Time to predict: " << timer.getDurationString() << std::endl; + } + return static_cast(correct) / predictions.size(); + } + + // + // statistics + // + int ExpEnsemble::getNumberOfNodes() const + { + if (models_.empty()) { + return 0; + } + return n_models * (models_.at(0)->getNFeatures() + 1); + } + int ExpEnsemble::getNumberOfEdges() const + { + if (models_.empty()) { + return 0; + } + return n_models * (2 * models_.at(0)->getNFeatures() - 1); + } + int ExpEnsemble::getNumberOfStates() const + { + if (models_.empty()) { + return 0; + } + auto states = models_.at(0)->getStates(); + int nFeatures = models_.at(0)->getNFeatures(); + return std::accumulate(states.begin(), states.end(), 0) * nFeatures * n_models; + } + int ExpEnsemble::getClassNumStates() const + { + if (models_.empty()) { + return 0; + } + return models_.at(0)->statesClass(); + } + + +} \ No newline at end of file diff --git a/src/experimental_clfs/ExpEnsemble.h b/src/experimental_clfs/ExpEnsemble.h new file mode 100644 index 0000000..2da97b9 --- /dev/null +++ b/src/experimental_clfs/ExpEnsemble.h @@ -0,0 +1,66 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2025 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#ifndef EXPENSEMBLE_H +#define EXPENSEMBLE_H +#include +#include +#include +#include +#include +#include +#include +#include "common/Timer.hpp" +#include "CountingSemaphore.hpp" +#include "XSpode.hpp" + +namespace platform { + class ExpEnsemble : public bayesnet::Boost { + public: + ExpEnsemble(); + virtual ~ExpEnsemble() = default; + std::vector predict(std::vector>& X) override; + torch::Tensor predict(torch::Tensor& X) override; + torch::Tensor predict_proba(torch::Tensor& X) override; + std::vector predict_spode(std::vector>& test_data, int parent); + std::vector> predict_proba(const std::vector>& X); + float score(std::vector>& X, std::vector& y) override; + float score(torch::Tensor& X, torch::Tensor& y) override; + int getNumberOfNodes() const override; + int getNumberOfEdges() const override; + int getNumberOfStates() const override; + int getClassNumStates() const override; + std::vector show() const override { return {}; } + std::vector topological_order() override { return {}; } + std::string dump_cpt() const override { return ""; } + void setDebug(bool debug) { this->debug = debug; } + bayesnet::status_t getStatus() const override { return status; } + std::vector getNotes() const override { return notes; } + std::vector graph(const std::string& title = "") const override { return {}; } + protected: + void add_model(std::unique_ptr model); + void remove_last_model(); + bool debug = false; + std::vector > models_; + torch::Tensor weights_; + std::vector significanceModels_; + const std::string CLASSIFIER_NOT_FITTED = "Classifier has not been fitted"; + inline void normalize_weights(int num_instances) + { + double sum = weights_.sum().item(); + if (sum == 0) { + weights_ = torch::full({ num_instances }, 1.0); + } else { + for (int i = 0; i < weights_.size(0); ++i) { + weights_[i] = weights_[i].item() * num_instances / sum; + } + } + } + private: + CountingSemaphore& semaphore_; + }; +} +#endif // EXPENSEMBLE_H \ No newline at end of file diff --git a/src/experimental_clfs/XBAODE.cpp b/src/experimental_clfs/XBAODE.cpp index 184f730..9fd29d5 100644 --- a/src/experimental_clfs/XBAODE.cpp +++ b/src/experimental_clfs/XBAODE.cpp @@ -19,6 +19,16 @@ namespace platform { validHyperparameters = { "alpha_block", "order", "convergence", "convergence_best", "bisection", "threshold", "maxTolerance", "predict_voting", "select_features" }; } + void XBAODE::add_model(std::unique_ptr model) + { + models.push_back(std::move(model)); + n_models++; + } + void XBAODE::remove_last_model() + { + models.pop_back(); + n_models--; + } void XBAODE::trainModel(const torch::Tensor& weights, const bayesnet::Smoothing_t smoothing) { fitted = true; @@ -30,30 +40,36 @@ namespace platform { // // Logging setup // - loguru::set_thread_name("XBAODE"); - loguru::g_stderr_verbosity = loguru::Verbosity_OFF; - loguru::add_file("XBAODE.log", loguru::Truncate, loguru::Verbosity_MAX); + // loguru::set_thread_name("XBAODE"); + // loguru::g_stderr_verbosity = loguru::Verbosity_OFF; + // loguru::add_file("XBAODE.log", loguru::Truncate, loguru::Verbosity_MAX); // Algorithm based on the adaboost algorithm for classification // as explained in Ensemble methods (Zhi-Hua Zhou, 2012) double alpha_t = 0; - weights_ = torch::full({ m }, 1.0 / static_cast(m), torch::kFloat64); + weights_ = torch::full({ m }, 1.0 / static_cast(m), torch::kFloat64); // m initialized in Classifier.cc + significanceModels.resize(n, 0.0); // n initialized in Classifier.cc bool finished = false; std::vector featuresUsed; - aode_.fit(X_train_, y_train_, features, className, states, weights_, false); n_models = 0; + std::unique_ptr model; if (selectFeatures) { featuresUsed = featureSelection(weights_); - add_active_parents(featuresUsed); + for (const auto& parent : featuresUsed) { + model = std::unique_ptr(new XSpode(parent)); + model->fit(X_train_, y_train_, weights_, smoothing); + std::cout << model->getNFeatures() << std::endl; + add_model(std::move(model)); + } notes.push_back("Used features in initialization: " + std::to_string(featuresUsed.size()) + " of " + std::to_string(features.size()) + " with " + select_features_algorithm); - auto ypred = ExpClf::predict(X_train); + auto ypred = ExpEnsemble::predict(X_train); std::tie(weights_, alpha_t, finished) = update_weights(y_train, ypred, weights_); // Update significance of the models for (const auto& parent : featuresUsed) { - aode_.significance_models_[parent] = alpha_t; + significanceModels_[parent] = alpha_t; } n_models = featuresUsed.size(); - VLOG_SCOPE_F(1, "SelectFeatures. alpha_t: %f n_models: %d", alpha_t, n_models); + // VLOG_SCOPE_F(1, "SelectFeatures. alpha_t: %f n_models: %d", alpha_t, n_models); if (finished) { return; } @@ -83,29 +99,28 @@ namespace platform { ); 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()); + // VLOG_SCOPE_F(1, "counter=%d k=%d featureSelection.size: %zu", counter, k, featureSelection.size()); while (counter++ < k && featureSelection.size() > 0) { auto feature = featureSelection[0]; featureSelection.erase(featureSelection.begin()); - auto model = XSpode(feature); - model.fit(X_train_, y_train_, weights_, smoothing); + model = std::unique_ptr(new XSpode(feature)); + model->fit(X_train_, y_train_, weights_, smoothing); std::vector ypred; if (alpha_block) { // // Compute the prediction with the current ensemble + model // // Add the model to the ensemble - n_models++; - aode_.significance_models_[feature] = 1.0; - aode_.add_active_parent(feature); + significanceModels[feature] = 1.0; + add_model(std::move(model)); // Compute the prediction - ypred = ExpClf::predict(X_train_); + ypred = ExpEnsemble::predict(X_train_); // Remove the model from the ensemble - aode_.significance_models_[feature] = 0.0; - aode_.remove_last_parent(); - n_models--; + significanceModels[feature] = 0.0; + model = std::move(models_.back()); + remove_last_model(); } else { - ypred = model.predict(X_train_); + ypred = model->predict(X_train_); } // Step 3.1: Compute the classifier amout of say auto ypred_t = torch::tensor(ypred); @@ -113,13 +128,12 @@ namespace platform { // Step 3.4: Store classifier and its accuracy to weigh its future vote numItemsPack++; featuresUsed.push_back(feature); - aode_.add_active_parent(feature); - aode_.significance_models_[feature] = alpha_t; - n_models++; - VLOG_SCOPE_F(2, "finished: %d numItemsPack: %d n_models: %d featuresUsed: %zu", finished, numItemsPack, n_models, featuresUsed.size()); + add_model(std::move(model)); + significanceModels[feature] = alpha_t; + // VLOG_SCOPE_F(2, "finished: %d numItemsPack: %d n_models: %d featuresUsed: %zu", finished, numItemsPack, n_models, featuresUsed.size()); } // End of the pack if (convergence && !finished) { - auto y_val_predict = ExpClf::predict(X_test); + auto y_val_predict = ExpEnsemble::predict(X_test); double accuracy = (y_val_predict == y_test).sum().item() / (double)y_test.size(0); if (priorAccuracy == 0) { priorAccuracy = accuracy; @@ -127,10 +141,10 @@ namespace platform { 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); + // 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; } @@ -142,22 +156,21 @@ namespace platform { priorAccuracy = accuracy; } } - VLOG_SCOPE_F(1, "tolerance: %d featuresUsed.size: %zu features.size: %zu", tolerance, featuresUsed.size(), features.size()); + // VLOG_SCOPE_F(1, "tolerance: %d featuresUsed.size: %zu features.size: %zu", tolerance, featuresUsed.size(), features.size()); finished = finished || tolerance > maxTolerance || featuresUsed.size() == features.size(); } 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); + // VLOG_SCOPE_F(4, "Convergence threshold reached & %d models eliminated of %d", numItemsPack, n_models); for (int i = featuresUsed.size() - 1; i >= featuresUsed.size() - numItemsPack; --i) { - aode_.remove_last_parent(); - aode_.significance_models_[featuresUsed[i]] = 0.0; - n_models--; + remove_last_model(); + significanceModels[featuresUsed[i]] = 0.0; } - VLOG_SCOPE_F(4, "*Convergence threshold %d models left & %d features used.", n_models, featuresUsed.size()); + // VLOG_SCOPE_F(4, "*Convergence threshold %d models left & %d features used.", n_models, featuresUsed.size()); } 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); + // VLOG_SCOPE_F(4, "Convergence threshold reached & 0 models eliminated n_models=%d numItemsPack=%d", n_models, numItemsPack); } } if (featuresUsed.size() != features.size()) { diff --git a/src/experimental_clfs/XBAODE.h b/src/experimental_clfs/XBAODE.h index ca757e5..e0fb33e 100644 --- a/src/experimental_clfs/XBAODE.h +++ b/src/experimental_clfs/XBAODE.h @@ -12,10 +12,10 @@ #include #include #include "common/Timer.hpp" -#include "ExpClf.h" +#include "ExpEnsemble.h" namespace platform { - class XBAODE { + class XBAODE : public Boost { // Hay que hacer un vector de modelos entrenados y hacer un predict ensemble con todos ellos // Probar XA1DE con smooth original y laplace y comprobar diferencias si se pasan pesos a 1 o a 1/m @@ -25,10 +25,10 @@ namespace platform { protected: void trainModel(const torch::Tensor& weights, const bayesnet::Smoothing_t smoothing) override; private: + void add_model(std::unique_ptr model); + void remove_last_model(); std::vector> X_train_, X_test_; std::vector y_train_, y_test_; - torch::Tensor dataset; - int n_models; std::string version = "0.9.7"; }; } diff --git a/src/experimental_clfs/XSpode.hpp b/src/experimental_clfs/XSpode.hpp index 030e1d7..42c7b49 100644 --- a/src/experimental_clfs/XSpode.hpp +++ b/src/experimental_clfs/XSpode.hpp @@ -11,24 +11,29 @@ #include #include #include +#include +#include +#include #include "CountingSemaphore.hpp" namespace platform { - class XSpode { + class XSpode : public bayesnet::Classifier { public: // -------------------------------------- // Constructor // // Supply which feature index is the single super-parent (“spIndex”). // -------------------------------------- - XSpode(int spIndex) + explicit XSpode(int spIndex) : superParent_{ spIndex }, nFeatures_{ 0 }, statesClass_{ 0 }, + fitted_{ false }, alpha_{ 1.0 }, - semaphore_{ CountingSemaphore::getInstance() } + initializer_{ 1.0 }, + semaphore_{ CountingSemaphore::getInstance() } : bayesnet::Classifier(bayesnet::Network()) { } @@ -380,6 +385,17 @@ namespace platform { oss << "---------------------\n"; return oss.str(); } + int statesClass() const { return statesClass_; } + int getNFeatures() const { return nFeatures_; } + int getNumberOfStates() const + { + return std::accumulate(states_.begin(), states_.end(), 0) * nFeatures_; + } + int getNumberOfEdges() const + { + return nFeatures_ * (2 * nFeatures_ - 1); + } + std::vector& getStates() { return states_; } private: // -------------------------------------- diff --git a/src/main/Models.h b/src/main/Models.h index 4da2f57..488e8d3 100644 --- a/src/main/Models.h +++ b/src/main/Models.h @@ -5,11 +5,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -21,7 +23,7 @@ #include #include #include "../experimental_clfs/XA1DE.h" -#include "../experimental_clfs/XBAODE.h" + namespace platform { class Models { public: diff --git a/src/main/modelRegister.h b/src/main/modelRegister.h index cd86995..e739c22 100644 --- a/src/main/modelRegister.h +++ b/src/main/modelRegister.h @@ -35,9 +35,11 @@ namespace platform { [](void) -> bayesnet::BaseClassifier* { return new pywrap::RandomForest();}); static Registrar registrarXGB("XGBoost", [](void) -> bayesnet::BaseClassifier* { return new pywrap::XGBoost();}); - static Registrar registrarXA1DE("XA1DE", - [](void) -> bayesnet::BaseClassifier* { return new XA1DE();}); + static Registrar registrarXSPODE("XSPODE", + [](void) -> bayesnet::BaseClassifier* { return new bayesnet::XSpode(0);}); static Registrar registrarXBAODE("XBAODE", - [](void) -> bayesnet::BaseClassifier* { return new XBAODE();}); + [](void) -> bayesnet::BaseClassifier* { return new bayesnet::XBAODE();}); + static Registrar registrarXA1DE("XA1DE", + [](void) -> bayesnet::BaseClassifier* { return new XA1DE();}); } #endif \ No newline at end of file