From 71b05cc1a7e22e40b5c420c95c64ebfd4275e493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana=20G=C3=B3mez?= Date: Tue, 11 Mar 2025 18:16:50 +0100 Subject: [PATCH] Begin XBAODE tests --- Makefile | 4 +- bayesnet/classifiers/Classifier.cc | 2 +- bayesnet/classifiers/XSPODE.cc | 803 +++++++++++++++-------------- bayesnet/classifiers/XSPODE.h | 4 +- bayesnet/ensembles/Ensemble.h | 3 +- bayesnet/ensembles/XBAODE.cc | 12 +- sample/sample.cc | 5 +- tests/CMakeLists.txt | 2 +- tests/TestBoostXBAODE.cc | 234 --------- tests/TestXBAODE.cc | 243 +++++++++ 10 files changed, 678 insertions(+), 634 deletions(-) delete mode 100644 tests/TestBoostXBAODE.cc create mode 100644 tests/TestXBAODE.cc diff --git a/Makefile b/Makefile index d48d477..a77c862 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ fname = "tests/data/iris.arff" sample: ## Build sample @echo ">>> Building Sample..."; @if [ -d ./sample/build ]; then rm -rf ./sample/build; fi - @cd sample && cmake -B build -S . && cmake --build build -t bayesnet_sample + @cd sample && cmake -B build -S . -D CMAKE_BUILD_TYPE=Debug && cmake --build build -t bayesnet_sample sample/build/bayesnet_sample $(fname) @echo ">>> Done"; @@ -105,7 +105,7 @@ fname = "tests/data/iris.arff" sample2: ## Build sample2 @echo ">>> Building Sample..."; @if [ -d ./sample/build ]; then rm -rf ./sample/build; fi - @cd sample && cmake -B build -S . && cmake --build build -t bayesnet_sample_xspode + @cd sample && cmake -B build -S . -D CMAKE_BUILD_TYPE=Debug && cmake --build build -t bayesnet_sample_xspode sample/build/bayesnet_sample_xspode $(fname) @echo ">>> Done"; diff --git a/bayesnet/classifiers/Classifier.cc b/bayesnet/classifiers/Classifier.cc index 4194049..483ddbd 100644 --- a/bayesnet/classifiers/Classifier.cc +++ b/bayesnet/classifiers/Classifier.cc @@ -190,4 +190,4 @@ namespace bayesnet { throw std::invalid_argument("Invalid hyperparameters" + hyperparameters.dump()); } } -} \ No newline at end of file +} diff --git a/bayesnet/classifiers/XSPODE.cc b/bayesnet/classifiers/XSPODE.cc index fdb1048..f2ea2a9 100644 --- a/bayesnet/classifiers/XSPODE.cc +++ b/bayesnet/classifiers/XSPODE.cc @@ -3,420 +3,449 @@ // SPDX-FileType: SOURCE // SPDX-License-Identifier: MIT // *************************************************************** -#include -#include -#include -#include -#include -#include #include "XSPODE.h" #include "bayesnet/utils/TensorUtils.h" - +#include +#include +#include +#include +#include +#include namespace bayesnet { - // -------------------------------------- - // Constructor - // -------------------------------------- - XSpode::XSpode(int spIndex) - : superParent_{ spIndex }, - nFeatures_{ 0 }, - statesClass_{ 0 }, - alpha_{ 1.0 }, - initializer_{ 1.0 }, - semaphore_{ CountingSemaphore::getInstance() }, Classifier(Network()) - { - validHyperparameters = { "parent" }; + // -------------------------------------- + // Constructor + // -------------------------------------- + XSpode::XSpode(int spIndex) + : superParent_{ spIndex }, nFeatures_{ 0 }, statesClass_{ 0 }, alpha_{ 1.0 }, + initializer_{ 1.0 }, semaphore_{ CountingSemaphore::getInstance() }, + Classifier(Network()) + { + validHyperparameters = { "parent" }; + } + + void XSpode::setHyperparameters(const nlohmann::json& hyperparameters_) + { + auto hyperparameters = hyperparameters_; + if (hyperparameters.contains("parent")) { + superParent_ = hyperparameters["parent"]; + hyperparameters.erase("parent"); + } + Classifier::setHyperparameters(hyperparameters); + } + + void XSpode::fit(torch::Tensor & X, torch::Tensor& y, torch::Tensor& weights_, const Smoothing_t smoothing) + { + m = X.size(1); + n = X.size(0); + dataset = X; + buildDataset(y); + buildModel(weights_); + trainModel(weights_, smoothing); + fitted = true; + } + + // -------------------------------------- + // trainModel + // -------------------------------------- + // Initialize storage needed for the super-parent and child features counts and + // probs. + // -------------------------------------- + void XSpode::buildModel(const torch::Tensor& weights) + { + int numInstances = m; + nFeatures_ = n; + + // Derive the number of states for each feature and for the class. + // (This is just one approach; adapt to match your environment.) + // Here, we assume the user also gave us the total #states per feature in e.g. + // statesMap. We'll simply reconstruct the integer states_ array. The last + // entry is statesClass_. + states_.resize(nFeatures_); + for (int f = 0; f < nFeatures_; f++) { + // Suppose you look up in “statesMap” by the feature name, or read directly + // from X. We'll assume states_[f] = max value in X[f] + 1. + states_[f] = dataset[f].max().item() + 1; + } + // For the class: states_.back() = max(y)+1 + statesClass_ = dataset[-1].max().item() + 1; + + // Initialize counts + classCounts_.resize(statesClass_, 0.0); + // p(x_sp = spVal | c) + // We'll store these counts in spFeatureCounts_[spVal * statesClass_ + c]. + spFeatureCounts_.resize(states_[superParent_] * statesClass_, 0.0); + + // For each child ≠ sp, we store p(childVal| c, spVal) in a separate block of + // childCounts_. childCounts_ will be sized as sum_{child≠sp} (states_[child] + // * statesClass_ * states_[sp]). We also need an offset for each child to + // index into childCounts_. + childOffsets_.resize(nFeatures_, -1); + int totalSize = 0; + for (int f = 0; f < nFeatures_; f++) { + if (f == superParent_) + continue; // skip sp + childOffsets_[f] = totalSize; + // block size for this child's counts: states_[f] * statesClass_ * + // states_[superParent_] + totalSize += (states_[f] * statesClass_ * states_[superParent_]); + } + childCounts_.resize(totalSize, 0.0); + } + // -------------------------------------- + // buildModel + // -------------------------------------- + // + // We only store conditional probabilities for: + // p(x_sp| c) (the super-parent feature) + // p(x_child| c, x_sp) for all child ≠ sp + // + // -------------------------------------- + void XSpode::trainModel(const torch::Tensor& weights, + const bayesnet::Smoothing_t smoothing) + { + // Accumulate raw counts + for (int i = 0; i < m; i++) { + std::vector instance(nFeatures_ + 1); + for (int f = 0; f < nFeatures_; f++) { + instance[f] = dataset[f][i].item(); + } + instance[nFeatures_] = dataset[-1][i].item(); + addSample(instance, weights[i].item()); + } + switch (smoothing) { + case bayesnet::Smoothing_t::ORIGINAL: + alpha_ = 1.0 / m; + break; + case bayesnet::Smoothing_t::LAPLACE: + alpha_ = 1.0; + break; + default: + alpha_ = 0.0; // No smoothing + } + initializer_ = std::numeric_limits::max() / + (nFeatures_ * nFeatures_); // for numerical stability + // Convert raw counts to probabilities + computeProbabilities(); + } + + // -------------------------------------- + // addSample + // -------------------------------------- + // + // instance has size nFeatures_ + 1, with the class at the end. + // We add 1 to the appropriate counters for each (c, superParentVal, childVal). + // + void XSpode::addSample(const std::vector& instance, double weight) + { + if (weight <= 0.0) + return; + + int c = instance.back(); + // (A) increment classCounts + classCounts_[c] += weight; + + // (B) increment super-parent counts => p(x_sp | c) + int spVal = instance[superParent_]; + spFeatureCounts_[spVal * statesClass_ + c] += weight; + + // (C) increment child counts => p(childVal | c, x_sp) + for (int f = 0; f < nFeatures_; f++) { + if (f == superParent_) + continue; + int childVal = instance[f]; + int offset = childOffsets_[f]; + // Compute index in childCounts_. + // Layout: [ offset + (spVal * states_[f] + childVal) * statesClass_ + c ] + int blockSize = states_[f] * statesClass_; + int idx = offset + spVal * blockSize + childVal * statesClass_ + c; + childCounts_[idx] += weight; + } + } + + // -------------------------------------- + // computeProbabilities + // -------------------------------------- + // + // Once all samples are added in COUNTS mode, call this to: + // p(c) + // p(x_sp = spVal | c) + // p(x_child = v | c, x_sp = s_sp) + // + // -------------------------------------- + void XSpode::computeProbabilities() + { + double totalCount = + std::accumulate(classCounts_.begin(), classCounts_.end(), 0.0); + + // p(c) => classPriors_ + classPriors_.resize(statesClass_, 0.0); + if (totalCount <= 0.0) { + // fallback => uniform + double unif = 1.0 / static_cast(statesClass_); + for (int c = 0; c < statesClass_; c++) { + classPriors_[c] = unif; + } + } else { + for (int c = 0; c < statesClass_; c++) { + classPriors_[c] = + (classCounts_[c] + alpha_) / (totalCount + alpha_ * statesClass_); + } } - void XSpode::setHyperparameters(const nlohmann::json& hyperparameters_) - { - auto hyperparameters = hyperparameters_; - if (hyperparameters.contains("parent")) { - superParent_ = hyperparameters["parent"]; - hyperparameters.erase("parent"); - } - Classifier::setHyperparameters(hyperparameters); + // p(x_sp | c) + spFeatureProbs_.resize(spFeatureCounts_.size()); + // denominator for spVal * statesClass_ + c is just classCounts_[c] + alpha_ * + // (#states of sp) + int spCard = states_[superParent_]; + for (int spVal = 0; spVal < spCard; spVal++) { + for (int c = 0; c < statesClass_; c++) { + double denom = classCounts_[c] + alpha_ * spCard; + double num = spFeatureCounts_[spVal * statesClass_ + c] + alpha_; + spFeatureProbs_[spVal * statesClass_ + c] = (denom <= 0.0 ? 0.0 : num / denom); + } } - void XSpode::fit(std::vector>& X, std::vector& y, torch::Tensor& weights_, const Smoothing_t smoothing) - { - m = X[0].size(); - n = X.size(); - buildModel(weights_); - trainModel(weights_, smoothing); - fitted = true; + // p(x_child | c, x_sp) + childProbs_.resize(childCounts_.size()); + for (int f = 0; f < nFeatures_; f++) { + if (f == superParent_) + continue; + int offset = childOffsets_[f]; + int childCard = states_[f]; + + // For each spVal, c, childVal in childCounts_: + for (int spVal = 0; spVal < spCard; spVal++) { + for (int childVal = 0; childVal < childCard; childVal++) { + for (int c = 0; c < statesClass_; c++) { + int idx = offset + spVal * (childCard * statesClass_) + + childVal * statesClass_ + c; + + double num = childCounts_[idx] + alpha_; + // denominator = spFeatureCounts_[spVal * statesClass_ + c] + alpha_ * + // (#states of child) + double denom = + spFeatureCounts_[spVal * statesClass_ + c] + alpha_ * childCard; + childProbs_[idx] = (denom <= 0.0 ? 0.0 : num / denom); + } + } + } + } + } + + // -------------------------------------- + // predict_proba + // -------------------------------------- + // + // For a single instance x of dimension nFeatures_: + // P(c | x) ∝ p(c) × p(x_sp | c) × ∏(child ≠ sp) p(x_child | c, x_sp). + // + // -------------------------------------- + std::vector XSpode::predict_proba(const std::vector& instance) const + { + if (!fitted) { + throw std::logic_error(CLASSIFIER_NOT_FITTED); + } + std::vector probs(statesClass_, 0.0); + // Multiply p(c) × p(x_sp | c) + int spVal = instance[superParent_]; + for (int c = 0; c < statesClass_; c++) { + double pc = classPriors_[c]; + double pSpC = spFeatureProbs_[spVal * statesClass_ + c]; + probs[c] = pc * pSpC * initializer_; } - // -------------------------------------- - // trainModel - // -------------------------------------- - // Initialize storage needed for the super-parent and child features counts and probs. - // -------------------------------------- - void XSpode::buildModel(const torch::Tensor& weights) - { - int numInstances = m; - nFeatures_ = n; - - // Derive the number of states for each feature and for the class. - // (This is just one approach; adapt to match your environment.) - // Here, we assume the user also gave us the total #states per feature in e.g. statesMap. - // We'll simply reconstruct the integer states_ array. The last entry is statesClass_. - states_.resize(nFeatures_); - for (int f = 0; f < nFeatures_; f++) { - // Suppose you look up in “statesMap” by the feature name, or read directly from X. - // We'll assume states_[f] = max value in X[f] + 1. - states_[f] = dataset[f].max().item() + 1; - } - // For the class: states_.back() = max(y)+1 - statesClass_ = dataset[-1].max().item() + 1; - - // Initialize counts - classCounts_.resize(statesClass_, 0.0); - // p(x_sp = spVal | c) - // We'll store these counts in spFeatureCounts_[spVal * statesClass_ + c]. - spFeatureCounts_.resize(states_[superParent_] * statesClass_, 0.0); - - // For each child ≠ sp, we store p(childVal| c, spVal) in a separate block of childCounts_. - // childCounts_ will be sized as sum_{child≠sp} (states_[child] * statesClass_ * states_[sp]). - // We also need an offset for each child to index into childCounts_. - childOffsets_.resize(nFeatures_, -1); - int totalSize = 0; - for (int f = 0; f < nFeatures_; f++) { - if (f == superParent_) continue; // skip sp - childOffsets_[f] = totalSize; - // block size for this child's counts: states_[f] * statesClass_ * states_[superParent_] - totalSize += (states_[f] * statesClass_ * states_[superParent_]); - } - childCounts_.resize(totalSize, 0.0); - } - // -------------------------------------- - // buildModel - // -------------------------------------- - // - // We only store conditional probabilities for: - // p(x_sp| c) (the super-parent feature) - // p(x_child| c, x_sp) for all child ≠ sp - // - // -------------------------------------- - void XSpode::trainModel(const torch::Tensor& weights, const bayesnet::Smoothing_t smoothing) - { - // Accumulate raw counts - for (int i = 0; i < m; i++) { - std::vector instance(nFeatures_ + 1); - for (int f = 0; f < nFeatures_; f++) { - instance[f] = dataset[f][i].item(); - } - instance[nFeatures_] = dataset[-1][i].item(); - addSample(instance, weights[i].item()); - } - switch (smoothing) { - case bayesnet::Smoothing_t::ORIGINAL: - alpha_ = 1.0 / m; - break; - case bayesnet::Smoothing_t::LAPLACE: - alpha_ = 1.0; - break; - default: - alpha_ = 0.0; // No smoothing - } - initializer_ = std::numeric_limits::max() / (nFeatures_ * nFeatures_); // for numerical stability - // Convert raw counts to probabilities - computeProbabilities(); + // Multiply by each child’s probability p(x_child | c, x_sp) + for (int feature = 0; feature < nFeatures_; feature++) { + if (feature == superParent_) + continue; // skip sp + int sf = instance[feature]; + int offset = childOffsets_[feature]; + int childCard = states_[feature]; // not used directly, but for clarity + // Index into childProbs_ = offset + spVal*(childCard*statesClass_) + + // childVal*statesClass_ + c + int base = offset + spVal * (childCard * statesClass_) + sf * statesClass_; + for (int c = 0; c < statesClass_; c++) { + probs[c] *= childProbs_[base + c]; + } } - // -------------------------------------- - // addSample - // -------------------------------------- - // - // instance has size nFeatures_ + 1, with the class at the end. - // We add 1 to the appropriate counters for each (c, superParentVal, childVal). - // - void XSpode::addSample(const std::vector& instance, double weight) - { - if (weight <= 0.0) return; + // Normalize + normalize(probs); + return probs; + } + std::vector> XSpode::predict_proba(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(statesClass_)); - int c = instance.back(); - // (A) increment classCounts - classCounts_[c] += weight; - - // (B) increment super-parent counts => p(x_sp | c) - int spVal = instance[superParent_]; - spFeatureCounts_[spVal * statesClass_ + c] += weight; - - // (C) increment child counts => p(childVal | c, x_sp) - for (int f = 0; f < nFeatures_; f++) { - if (f == superParent_) continue; - int childVal = instance[f]; - int offset = childOffsets_[f]; - // Compute index in childCounts_. - // Layout: [ offset + (spVal * states_[f] + childVal) * statesClass_ + c ] - int blockSize = states_[f] * statesClass_; - int idx = offset + spVal * blockSize + childVal * statesClass_ + c; - childCounts_[idx] += weight; - } - } - - // -------------------------------------- - // computeProbabilities - // -------------------------------------- - // - // Once all samples are added in COUNTS mode, call this to: - // p(c) - // p(x_sp = spVal | c) - // p(x_child = v | c, x_sp = s_sp) - // - // -------------------------------------- - void XSpode::computeProbabilities() - { - double totalCount = std::accumulate(classCounts_.begin(), classCounts_.end(), 0.0); - - // p(c) => classPriors_ - classPriors_.resize(statesClass_, 0.0); - if (totalCount <= 0.0) { - // fallback => uniform - double unif = 1.0 / static_cast(statesClass_); - for (int c = 0; c < statesClass_; c++) { - classPriors_[c] = unif; - } - } else { - for (int c = 0; c < statesClass_; c++) { - classPriors_[c] = (classCounts_[c] + alpha_) - / (totalCount + alpha_ * statesClass_); - } - } - - // p(x_sp | c) - spFeatureProbs_.resize(spFeatureCounts_.size()); - // denominator for spVal * statesClass_ + c is just classCounts_[c] + alpha_ * (#states of sp) - int spCard = states_[superParent_]; - for (int spVal = 0; spVal < spCard; spVal++) { - for (int c = 0; c < statesClass_; c++) { - double denom = classCounts_[c] + alpha_ * spCard; - double num = spFeatureCounts_[spVal * statesClass_ + c] + alpha_; - spFeatureProbs_[spVal * statesClass_ + c] = (denom <= 0.0 ? 0.0 : num / denom); - } - } - - // p(x_child | c, x_sp) - childProbs_.resize(childCounts_.size()); - for (int f = 0; f < nFeatures_; f++) { - if (f == superParent_) continue; - int offset = childOffsets_[f]; - int childCard = states_[f]; - - // For each spVal, c, childVal in childCounts_: - for (int spVal = 0; spVal < spCard; spVal++) { - for (int childVal = 0; childVal < childCard; childVal++) { - for (int c = 0; c < statesClass_; c++) { - int idx = offset + spVal * (childCard * statesClass_) - + childVal * statesClass_ - + c; - - double num = childCounts_[idx] + alpha_; - // denominator = spFeatureCounts_[spVal * statesClass_ + c] + alpha_ * (#states of child) - double denom = spFeatureCounts_[spVal * statesClass_ + c] - + alpha_ * childCard; - childProbs_[idx] = (denom <= 0.0 ? 0.0 : num / denom); - } - } - } - } - } - - // -------------------------------------- - // predict_proba - // -------------------------------------- - // - // For a single instance x of dimension nFeatures_: - // P(c | x) ∝ p(c) × p(x_sp | c) × ∏(child ≠ sp) p(x_child | c, x_sp). - // - // -------------------------------------- - std::vector XSpode::predict_proba(const std::vector& instance) const - { - if (!fitted) { - throw std::logic_error(CLASSIFIER_NOT_FITTED); - } - std::vector probs(statesClass_, 0.0); - // Multiply p(c) × p(x_sp | c) - int spVal = instance[superParent_]; - for (int c = 0; c < statesClass_; c++) { - double pc = classPriors_[c]; - double pSpC = spFeatureProbs_[spVal * statesClass_ + c]; - probs[c] = pc * pSpC * initializer_; - } - - // Multiply by each child’s probability p(x_child | c, x_sp) - for (int feature = 0; feature < nFeatures_; feature++) { - if (feature == superParent_) continue; // skip sp - int sf = instance[feature]; - int offset = childOffsets_[feature]; - int childCard = states_[feature]; // not used directly, but for clarity - // Index into childProbs_ = offset + spVal*(childCard*statesClass_) + childVal*statesClass_ + c - int base = offset + spVal * (childCard * statesClass_) + sf * statesClass_; - for (int c = 0; c < statesClass_; c++) { - probs[c] *= childProbs_[base + c]; - } - } - - // Normalize - normalize(probs); - return probs; - } - std::vector> XSpode::predict_proba(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(statesClass_)); - - 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); + 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()); + pthread_setname_np(pthread_self(), threadName.c_str()); #else - pthread_setname_np(threadName.c_str()); + 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] = 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)); + 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] = predict_proba(instance); } - for (auto& thread : threads) { - thread.join(); - } - return probabilities; + 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; + } - // -------------------------------------- - // Utility: normalize - // -------------------------------------- - void XSpode::normalize(std::vector& v) const - { - double sum = 0.0; - for (auto val : v) { sum += val; } - if (sum <= 0.0) { - return; - } - for (auto& val : v) { - val /= sum; - } + // -------------------------------------- + // Utility: normalize + // -------------------------------------- + void XSpode::normalize(std::vector& v) const + { + double sum = 0.0; + for (auto val : v) { + sum += val; } + if (sum <= 0.0) { + return; + } + for (auto& val : v) { + val /= sum; + } + } - // -------------------------------------- - // representation of the model - // -------------------------------------- - std::string XSpode::to_string() const - { - std::ostringstream oss; - oss << "---- SPODE Model ----" << std::endl - << "nFeatures_ = " << nFeatures_ << std::endl - << "superParent_ = " << superParent_ << std::endl - << "statesClass_ = " << statesClass_ << std::endl - << std::endl; + // -------------------------------------- + // representation of the model + // -------------------------------------- + std::string XSpode::to_string() const + { + std::ostringstream oss; + oss << "----- XSpode Model -----" << std::endl + << "nFeatures_ = " << nFeatures_ << std::endl + << "superParent_ = " << superParent_ << std::endl + << "statesClass_ = " << statesClass_ << std::endl + << std::endl; - oss << "States: ["; - for (int s : states_) oss << s << " "; - oss << "]" << std::endl; - oss << "classCounts_: ["; - for (double c : classCounts_) oss << c << " "; - oss << "]" << std::endl; - oss << "classPriors_: ["; - for (double c : classPriors_) oss << c << " "; - oss << "]" << std::endl; - oss << "spFeatureCounts_: size = " << spFeatureCounts_.size() << std::endl << "["; - for (double c : spFeatureCounts_) oss << c << " "; - oss << "]" << std::endl; - oss << "spFeatureProbs_: size = " << spFeatureProbs_.size() << std::endl << "["; - for (double c : spFeatureProbs_) oss << c << " "; - oss << "]" << std::endl; - oss << "childCounts_: size = " << childCounts_.size() << std::endl << "["; - for (double cc : childCounts_) oss << cc << " "; - oss << "]" << std::endl; + oss << "States: ["; + for (int s : states_) + oss << s << " "; + oss << "]" << std::endl; + oss << "classCounts_: ["; + for (double c : classCounts_) + oss << c << " "; + oss << "]" << std::endl; + oss << "classPriors_: ["; + for (double c : classPriors_) + oss << c << " "; + oss << "]" << std::endl; + oss << "spFeatureCounts_: size = " << spFeatureCounts_.size() << std::endl + << "["; + for (double c : spFeatureCounts_) + oss << c << " "; + oss << "]" << std::endl; + oss << "spFeatureProbs_: size = " << spFeatureProbs_.size() << std::endl + << "["; + for (double c : spFeatureProbs_) + oss << c << " "; + oss << "]" << std::endl; + oss << "childCounts_: size = " << childCounts_.size() << std::endl << "["; + for (double cc : childCounts_) + oss << cc << " "; + oss << "]" << std::endl; - for (double cp : childProbs_) oss << cp << " "; - oss << "]" << std::endl; - oss << "childOffsets_: ["; - for (int co : childOffsets_) oss << co << " "; - oss << "]" << std::endl; - oss << "---------------------" << std::endl; - return oss.str(); - } - int XSpode::getNumberOfNodes() const { return nFeatures_ + 1; } - int XSpode::getClassNumStates() const { return statesClass_; } - int XSpode::getNFeatures() const { return nFeatures_; } - int XSpode::getNumberOfStates() const - { - return std::accumulate(states_.begin(), states_.end(), 0) * nFeatures_; - } - int XSpode::getNumberOfEdges() const - { - return nFeatures_ * (2 * nFeatures_ - 1); - } - std::vector& XSpode::getStates() { return states_; } + for (double cp : childProbs_) + oss << cp << " "; + oss << "]" << std::endl; + oss << "childOffsets_: ["; + for (int co : childOffsets_) + oss << co << " "; + oss << "]" << std::endl; + oss << std::string(40,'-') << std::endl; + return oss.str(); + } + int XSpode::getNumberOfNodes() const { return nFeatures_ + 1; } + int XSpode::getClassNumStates() const { return statesClass_; } + int XSpode::getNFeatures() const { return nFeatures_; } + int XSpode::getNumberOfStates() const + { + return std::accumulate(states_.begin(), states_.end(), 0) * nFeatures_; + } + int XSpode::getNumberOfEdges() const + { + return nFeatures_ * (2 * nFeatures_ - 1); + } + std::vector& XSpode::getStates() { return states_; } - // ------------------------------------------------------ - // Predict overrides (classifier interface) - // ------------------------------------------------------ - int XSpode::predict(const std::vector& instance) const - { - auto p = predict_proba(instance); - return static_cast(std::distance(p.begin(), - std::max_element(p.begin(), p.end()))); - } - std::vector XSpode::predict(std::vector>& test_data) - { - auto probabilities = predict_proba(test_data); - std::vector predictions(probabilities.size(), 0); + // ------------------------------------------------------ + // Predict overrides (classifier interface) + // ------------------------------------------------------ + int XSpode::predict(const std::vector& instance) const + { + auto p = predict_proba(instance); + return static_cast(std::distance(p.begin(), std::max_element(p.begin(), p.end()))); + } + std::vector XSpode::predict(std::vector>& test_data) + { + 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; + 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())); } - torch::Tensor XSpode::predict(torch::Tensor& X) - { - auto X_ = TensorUtils::to_matrix(X); - auto result_v = predict(X_); - return torch::tensor(result_v, torch::kInt32); + return predictions; + } + torch::Tensor XSpode::predict(torch::Tensor& X) + { + auto X_ = TensorUtils::to_matrix(X); + auto result_v = predict(X_); + return torch::tensor(result_v, torch::kInt32); + } + torch::Tensor XSpode::predict_proba(torch::Tensor& X) + { + auto X_ = TensorUtils::to_matrix(X); + auto result_v = predict_proba(X_); + int n_samples = X.size(1); + torch::Tensor result = + torch::zeros({ n_samples, statesClass_ }, torch::kDouble); + for (int i = 0; i < result_v.size(); ++i) { + result.index_put_({ i, "..." }, torch::tensor(result_v[i])); } - torch::Tensor XSpode::predict_proba(torch::Tensor& X) - { - auto X_ = TensorUtils::to_matrix(X); - auto result_v = predict_proba(X_); - torch::Tensor result; - for (int i = 0; i < result_v.size(); ++i) { - result.index_put_({ i, "..." }, torch::tensor(result_v[i], torch::kDouble)); - } - return result; + return result; + } + float XSpode::score(torch::Tensor& X, torch::Tensor& y) + { + torch::Tensor y_pred = predict(X); + return (y_pred == y).sum().item() / y.size(0); + } + float XSpode::score(std::vector>& X, std::vector& y) + { + auto y_pred = this->predict(X); + int correct = 0; + for (int i = 0; i < y_pred.size(); ++i) { + if (y_pred[i] == y[i]) { + correct++; + } } - float XSpode::score(torch::Tensor& X, torch::Tensor& y) - { - torch::Tensor y_pred = predict(X); - return (y_pred == y).sum().item() / y.size(0); - } - float XSpode::score(std::vector>& X, std::vector& y) - { - auto y_pred = this->predict(X); - int correct = 0; - for (int i = 0; i < y_pred.size(); ++i) { - if (y_pred[i] == y[i]) { - correct++; - } - } - return (double)correct / y_pred.size(); - } -} - + return (double)correct / y_pred.size(); + } +} // namespace bayesnet diff --git a/bayesnet/classifiers/XSPODE.h b/bayesnet/classifiers/XSPODE.h index 1691952..632548b 100644 --- a/bayesnet/classifiers/XSPODE.h +++ b/bayesnet/classifiers/XSPODE.h @@ -9,7 +9,7 @@ #include #include -#include "Classifier.h" +#include "Classifier.h" #include "bayesnet/utils/CountingSemaphore.h" namespace bayesnet { @@ -29,7 +29,7 @@ namespace bayesnet { int getClassNumStates() const override; std::vector& getStates(); std::vector graph(const std::string& title) const override { return std::vector({ title }); } - void fit(std::vector>& X, std::vector& y, torch::Tensor& weights_, const Smoothing_t smoothing); + void fit(torch::Tensor& X, torch::Tensor& y, torch::Tensor& weights_, const Smoothing_t smoothing); void setHyperparameters(const nlohmann::json& hyperparameters_) override; // diff --git a/bayesnet/ensembles/Ensemble.h b/bayesnet/ensembles/Ensemble.h index c046f54..e914653 100644 --- a/bayesnet/ensembles/Ensemble.h +++ b/bayesnet/ensembles/Ensemble.h @@ -41,6 +41,7 @@ namespace bayesnet { return output; } protected: + void trainModel(const torch::Tensor& weights, const Smoothing_t smoothing) override; torch::Tensor predict_average_voting(torch::Tensor& X); std::vector> predict_average_voting(std::vector>& X); torch::Tensor predict_average_proba(torch::Tensor& X); @@ -48,10 +49,10 @@ namespace bayesnet { torch::Tensor compute_arg_max(torch::Tensor& X); std::vector compute_arg_max(std::vector>& X); torch::Tensor voting(torch::Tensor& votes); + // Attributes unsigned n_models; std::vector> models; std::vector significanceModels; - void trainModel(const torch::Tensor& weights, const Smoothing_t smoothing) override; bool predict_voting; }; } diff --git a/bayesnet/ensembles/XBAODE.cc b/bayesnet/ensembles/XBAODE.cc index 982dbff..3274b59 100644 --- a/bayesnet/ensembles/XBAODE.cc +++ b/bayesnet/ensembles/XBAODE.cc @@ -36,7 +36,8 @@ namespace bayesnet { std::vector featuresSelected = featureSelection(weights_); for (const int& feature : featuresSelected) { std::unique_ptr model = std::make_unique(feature); - model->fit(dataset, features, className, states, weights_, smoothing); + // model->fit(dataset, features, className, states, weights_, smoothing); + dynamic_cast(model.get())->fit(X_train, y_train, 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); @@ -57,6 +58,7 @@ namespace bayesnet { n_models = 0; if (selectFeatures) { featuresUsed = initializeModels(smoothing); + std::cout << "features used: " << featuresUsed.size() << std::endl; auto ypred = predict(X_train_); auto ypred_t = torch::tensor(ypred); std::tie(weights_, alpha_t, finished) = update_weights(y_train, ypred_t, weights_); @@ -103,7 +105,11 @@ namespace bayesnet { featureSelection.erase(featureSelection.begin()); std::unique_ptr model; model = std::make_unique(feature); - dynamic_cast(model.get())->fit(X_train_, y_train_, weights_, smoothing); // using exclusive XSpode fit method + dynamic_cast(model.get())->fit(X_train, y_train, weights_, smoothing); // using exclusive XSpode fit method + // DEBUG + std::cout << "Model fitted." << std::endl; + std::cout << dynamic_cast(model.get())->to_string() << std::endl; + // DEBUG std::vector ypred; if (alpha_block) { // @@ -176,4 +182,4 @@ namespace bayesnet { notes.push_back("Number of models: " + std::to_string(n_models)); return; } -} \ No newline at end of file +} diff --git a/sample/sample.cc b/sample/sample.cc index 421381d..e13abfa 100644 --- a/sample/sample.cc +++ b/sample/sample.cc @@ -6,7 +6,7 @@ #include #include -#include +#include std::vector discretizeDataset(std::vector& X, mdlp::labels_t& y) { @@ -57,7 +57,7 @@ int main(int argc, char* argv[]) std::vector features; std::string className; map> states; - auto clf = bayesnet::BoostAODE(false); // false for not using voting in predict + auto clf = bayesnet::XBAODE(); // false for not using voting in predict std::cout << "Library version: " << clf.getVersion() << std::endl; tie(X, y, features, className, states) = loadDataset(file_name, true); torch::Tensor weights = torch::full({ X.size(1) }, 15, torch::kDouble); @@ -73,7 +73,6 @@ int main(int argc, char* argv[]) oss << "y dimensions: " << y.sizes(); throw std::runtime_error(oss.str()); } - //Classifier& fit(torch::Tensor& dataset, const std::vector& features, const std::string& className, std::map>& states, const torch::Tensor& weights, const Smoothing_t smoothing) override; clf.fit(dataset, features, className, states, weights, bayesnet::Smoothing_t::LAPLACE); auto score = clf.score(X, y); std::cout << "File: " << file_name << " Model: BoostAODE score: " << score << std::endl; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e79c36b..56778d8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,7 +10,7 @@ if(ENABLE_TESTING) ) file(GLOB_RECURSE BayesNet_SOURCES "${BayesNet_SOURCE_DIR}/bayesnet/*.cc") add_executable(TestBayesNet TestBayesNetwork.cc TestBayesNode.cc TestBayesClassifier.cc - TestBayesModels.cc TestBayesMetrics.cc TestFeatureSelection.cc TestBoostAODE.cc TestA2DE.cc TestWA2DE.cc + TestBayesModels.cc TestBayesMetrics.cc TestFeatureSelection.cc TestBoostAODE.cc TestXBAODE.cc TestA2DE.cc TestWA2DE.cc TestUtils.cc TestBayesEnsemble.cc TestModulesVersions.cc TestBoostA2DE.cc TestMST.cc ${BayesNet_SOURCES}) target_link_libraries(TestBayesNet PUBLIC "${TORCH_LIBRARIES}" fimdlp PRIVATE Catch2::Catch2WithMain) add_test(NAME BayesNetworkTest COMMAND TestBayesNet) diff --git a/tests/TestBoostXBAODE.cc b/tests/TestBoostXBAODE.cc deleted file mode 100644 index f579b81..0000000 --- a/tests/TestBoostXBAODE.cc +++ /dev/null @@ -1,234 +0,0 @@ -// *************************************************************** -// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez -// SPDX-FileType: SOURCE -// SPDX-License-Identifier: MIT -// *************************************************************** - -#include -#include -#include -#include -#include -#include "bayesnet/ensembles/XBAODE.h" -#include "TestUtils.h" - - -TEST_CASE("Feature_select CFS", "[XBAODE]") -{ - auto raw = RawDatasets("glass", true); - auto clf = bayesnet::XBAODE(); - 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); - REQUIRE(clf.getNotes().size() == 2); - REQUIRE(clf.getNotes()[0] == "Used features in initialization: 6 of 9 with CFS"); - REQUIRE(clf.getNotes()[1] == "Number of models: 9"); -} -TEST_CASE("Feature_select IWSS", "[XBAODE]") -{ - auto raw = RawDatasets("glass", true); - auto clf = bayesnet::XBAODE(); - 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); - REQUIRE(clf.getNotes().size() == 2); - REQUIRE(clf.getNotes()[0] == "Used features in initialization: 4 of 9 with IWSS"); - REQUIRE(clf.getNotes()[1] == "Number of models: 9"); -} -TEST_CASE("Feature_select FCBF", "[XBAODE]") -{ - auto raw = RawDatasets("glass", true); - auto clf = bayesnet::XBAODE(); - 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); - REQUIRE(clf.getNotes().size() == 2); - REQUIRE(clf.getNotes()[0] == "Used features in initialization: 4 of 9 with FCBF"); - REQUIRE(clf.getNotes()[1] == "Number of models: 9"); -} -TEST_CASE("Test used features in train note and score", "[XBAODE]") -{ - auto raw = RawDatasets("diabetes", true); - auto clf = bayesnet::XBAODE(true); - 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() == 72); - REQUIRE(clf.getNumberOfEdges() == 120); - REQUIRE(clf.getNotes().size() == 2); - REQUIRE(clf.getNotes()[0] == "Used features in initialization: 6 of 8 with CFS"); - REQUIRE(clf.getNotes()[1] == "Number of models: 8"); - auto score = clf.score(raw.Xv, raw.yv); - auto scoret = clf.score(raw.Xt, raw.yt); - REQUIRE(score == Catch::Approx(0.809895813).epsilon(raw.epsilon)); - REQUIRE(scoret == Catch::Approx(0.809895813).epsilon(raw.epsilon)); -} -TEST_CASE("Voting vs proba", "[XBAODE]") -{ - auto raw = RawDatasets("iris", true); - auto clf = bayesnet::XBAODE(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}, - }); - auto score_voting = clf.score(raw.Xv, raw.yv); - auto pred_voting = clf.predict_proba(raw.Xv); - REQUIRE(score_proba == Catch::Approx(0.97333).epsilon(raw.epsilon)); - REQUIRE(score_voting == Catch::Approx(0.98).epsilon(raw.epsilon)); - REQUIRE(pred_voting[83][2] == Catch::Approx(1.0).epsilon(raw.epsilon)); - REQUIRE(pred_proba[83][2] == Catch::Approx(0.86121525).epsilon(raw.epsilon)); - REQUIRE(clf.dump_cpt() == ""); - REQUIRE(clf.topological_order() == std::vector()); -} -TEST_CASE("Order asc, desc & random", "[XBAODE]") -{ - auto raw = RawDatasets("glass", true); - std::map scores{ - {"asc", 0.83645f }, { "desc", 0.84579f }, { "rand", 0.84112 } - }; - for (const std::string& order : { "asc", "desc", "rand" }) { - auto clf = bayesnet::XBAODE(); - 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); - INFO("XBAODE order: " << order); - REQUIRE(score == Catch::Approx(scores[order]).epsilon(raw.epsilon)); - REQUIRE(scoret == Catch::Approx(scores[order]).epsilon(raw.epsilon)); - } -} -TEST_CASE("Oddities", "[XBAODE]") -{ - auto clf = bayesnet::XBAODE(); - 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("XBAODE 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("XBAODE 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("XBAODE hyper: " << hyper.value().dump()); - REQUIRE_THROWS_AS(clf.setHyperparameters(hyper.value()), std::invalid_argument); - } -} -TEST_CASE("Bisection Best", "[XBAODE]") -{ - auto clf = bayesnet::XBAODE(); - 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() == 210); - REQUIRE(clf.getNumberOfEdges() == 378); - REQUIRE(clf.getNotes().size() == 1); - REQUIRE(clf.getNotes().at(0) == "Number of models: 14"); - auto score = clf.score(raw.X_test, raw.y_test); - auto scoret = clf.score(raw.X_test, raw.y_test); - REQUIRE(score == Catch::Approx(0.991666675f).epsilon(raw.epsilon)); - REQUIRE(scoret == Catch::Approx(0.991666675f).epsilon(raw.epsilon)); -} -TEST_CASE("Bisection Best vs Last", "[XBAODE]") -{ - auto raw = RawDatasets("kdd_JapaneseVowels", true, 1500, true, false); - auto clf = bayesnet::XBAODE(true); - auto hyperparameters = nlohmann::json{ - {"bisection", true}, - {"maxTolerance", 3}, - {"convergence", true}, - {"convergence_best", true}, - }; - clf.setHyperparameters(hyperparameters); - clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); - auto score_best = clf.score(raw.X_test, raw.y_test); - REQUIRE(score_best == Catch::Approx(0.980000019f).epsilon(raw.epsilon)); - // Now we will set the hyperparameter to use the last accuracy - hyperparameters["convergence_best"] = false; - clf.setHyperparameters(hyperparameters); - clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); - 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", "[XBAODE]") -{ - auto clf = bayesnet::XBAODE(); - auto raw = RawDatasets("mfeat-factors", true, 500); - clf.setHyperparameters({ - {"bisection", true}, - {"block_update", true}, - {"maxTolerance", 3}, - {"convergence", true}, - }); - clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); - REQUIRE(clf.getNumberOfNodes() == 868); - REQUIRE(clf.getNumberOfEdges() == 1724); - REQUIRE(clf.getNotes().size() == 3); - REQUIRE(clf.getNotes()[0] == "Convergence threshold reached & 15 models eliminated"); - REQUIRE(clf.getNotes()[1] == "Used features in train: 19 of 216"); - REQUIRE(clf.getNotes()[2] == "Number of models: 4"); - auto score = clf.score(raw.X_test, raw.y_test); - auto scoret = clf.score(raw.X_test, raw.y_test); - REQUIRE(score == Catch::Approx(0.99f).epsilon(raw.epsilon)); - REQUIRE(scoret == Catch::Approx(0.99f).epsilon(raw.epsilon)); - // - // std::cout << "Number of nodes " << clf.getNumberOfNodes() << std::endl; - // std::cout << "Number of edges " << clf.getNumberOfEdges() << std::endl; - // std::cout << "Notes size " << clf.getNotes().size() << std::endl; - // for (auto note : clf.getNotes()) { - // std::cout << note << std::endl; - // } - // std::cout << "Score " << score << std::endl; -} -TEST_CASE("Alphablock", "[XBAODE]") -{ - auto clf_alpha = bayesnet::XBAODE(); - auto clf_no_alpha = bayesnet::XBAODE(); - 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/TestXBAODE.cc b/tests/TestXBAODE.cc new file mode 100644 index 0000000..3144d1b --- /dev/null +++ b/tests/TestXBAODE.cc @@ -0,0 +1,243 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#include +#include +#include +#include +#include +#include "bayesnet/ensembles/XBAODE.h" +#include "TestUtils.h" + + +TEST_CASE("Normal test", "[XBAODE]") +{ + auto raw = RawDatasets("iris", true); + auto clf = bayesnet::XBAODE(); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + REQUIRE(clf.getNumberOfNodes() == 20); + REQUIRE(clf.getNumberOfEdges() == 112); + REQUIRE(clf.getNotes().size() == 1); +} +//TEST_CASE("Feature_select CFS", "[XBAODE]") +//{ +// auto raw = RawDatasets("glass", true); +// auto clf = bayesnet::XBAODE(); +// clf.setHyperparameters({ {"select_features", "CFS"} }); +// clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); +// REQUIRE(clf.getNumberOfNodes() == 97); +// REQUIRE(clf.getNumberOfEdges() == 153); +// REQUIRE(clf.getNotes().size() == 2); +// REQUIRE(clf.getNotes()[0] == "Used features in initialization: 6 of 9 with CFS"); +// REQUIRE(clf.getNotes()[1] == "Number of models: 9"); +//} +// TEST_CASE("Feature_select IWSS", "[XBAODE]") +// { +// auto raw = RawDatasets("glass", true); +// auto clf = bayesnet::XBAODE(); +// 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); +// REQUIRE(clf.getNotes().size() == 2); +// REQUIRE(clf.getNotes()[0] == "Used features in initialization: 4 of 9 with IWSS"); +// REQUIRE(clf.getNotes()[1] == "Number of models: 9"); +// } +// TEST_CASE("Feature_select FCBF", "[XBAODE]") +// { +// auto raw = RawDatasets("glass", true); +// auto clf = bayesnet::XBAODE(); +// 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); +// REQUIRE(clf.getNotes().size() == 2); +// REQUIRE(clf.getNotes()[0] == "Used features in initialization: 4 of 9 with FCBF"); +// REQUIRE(clf.getNotes()[1] == "Number of models: 9"); +// } +// TEST_CASE("Test used features in train note and score", "[XBAODE]") +// { +// auto raw = RawDatasets("diabetes", true); +// auto clf = bayesnet::XBAODE(true); +// 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() == 72); +// REQUIRE(clf.getNumberOfEdges() == 120); +// REQUIRE(clf.getNotes().size() == 2); +// REQUIRE(clf.getNotes()[0] == "Used features in initialization: 6 of 8 with CFS"); +// REQUIRE(clf.getNotes()[1] == "Number of models: 8"); +// auto score = clf.score(raw.Xv, raw.yv); +// auto scoret = clf.score(raw.Xt, raw.yt); +// REQUIRE(score == Catch::Approx(0.809895813).epsilon(raw.epsilon)); +// REQUIRE(scoret == Catch::Approx(0.809895813).epsilon(raw.epsilon)); +// } +// TEST_CASE("Voting vs proba", "[XBAODE]") +// { +// auto raw = RawDatasets("iris", true); +// auto clf = bayesnet::XBAODE(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}, +// }); +// auto score_voting = clf.score(raw.Xv, raw.yv); +// auto pred_voting = clf.predict_proba(raw.Xv); +// REQUIRE(score_proba == Catch::Approx(0.97333).epsilon(raw.epsilon)); +// REQUIRE(score_voting == Catch::Approx(0.98).epsilon(raw.epsilon)); +// REQUIRE(pred_voting[83][2] == Catch::Approx(1.0).epsilon(raw.epsilon)); +// REQUIRE(pred_proba[83][2] == Catch::Approx(0.86121525).epsilon(raw.epsilon)); +// REQUIRE(clf.dump_cpt() == ""); +// REQUIRE(clf.topological_order() == std::vector()); +// } +// TEST_CASE("Order asc, desc & random", "[XBAODE]") +// { +// auto raw = RawDatasets("glass", true); +// std::map scores{ +// {"asc", 0.83645f }, { "desc", 0.84579f }, { "rand", 0.84112 } +// }; +// for (const std::string& order : { "asc", "desc", "rand" }) { +// auto clf = bayesnet::XBAODE(); +// 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); +// INFO("XBAODE order: " << order); +// REQUIRE(score == Catch::Approx(scores[order]).epsilon(raw.epsilon)); +// REQUIRE(scoret == Catch::Approx(scores[order]).epsilon(raw.epsilon)); +// } +// } +// TEST_CASE("Oddities", "[XBAODE]") +// { +// auto clf = bayesnet::XBAODE(); +// 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("XBAODE 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("XBAODE 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("XBAODE hyper: " << hyper.value().dump()); +// REQUIRE_THROWS_AS(clf.setHyperparameters(hyper.value()), std::invalid_argument); +// } +// } +// TEST_CASE("Bisection Best", "[XBAODE]") +// { +// auto clf = bayesnet::XBAODE(); +// 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() == 210); +// REQUIRE(clf.getNumberOfEdges() == 378); +// REQUIRE(clf.getNotes().size() == 1); +// REQUIRE(clf.getNotes().at(0) == "Number of models: 14"); +// auto score = clf.score(raw.X_test, raw.y_test); +// auto scoret = clf.score(raw.X_test, raw.y_test); +// REQUIRE(score == Catch::Approx(0.991666675f).epsilon(raw.epsilon)); +// REQUIRE(scoret == Catch::Approx(0.991666675f).epsilon(raw.epsilon)); +// } +// TEST_CASE("Bisection Best vs Last", "[XBAODE]") +// { +// auto raw = RawDatasets("kdd_JapaneseVowels", true, 1500, true, false); +// auto clf = bayesnet::XBAODE(true); +// auto hyperparameters = nlohmann::json{ +// {"bisection", true}, +// {"maxTolerance", 3}, +// {"convergence", true}, +// {"convergence_best", true}, +// }; +// clf.setHyperparameters(hyperparameters); +// clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); +// auto score_best = clf.score(raw.X_test, raw.y_test); +// REQUIRE(score_best == Catch::Approx(0.980000019f).epsilon(raw.epsilon)); +// // Now we will set the hyperparameter to use the last accuracy +// hyperparameters["convergence_best"] = false; +// clf.setHyperparameters(hyperparameters); +// clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); +// 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", "[XBAODE]") +// { +// auto clf = bayesnet::XBAODE(); +// auto raw = RawDatasets("mfeat-factors", true, 500); +// clf.setHyperparameters({ +// {"bisection", true}, +// {"block_update", true}, +// {"maxTolerance", 3}, +// {"convergence", true}, +// }); +// clf.fit(raw.X_train, raw.y_train, raw.features, raw.className, raw.states, raw.smoothing); +// REQUIRE(clf.getNumberOfNodes() == 868); +// REQUIRE(clf.getNumberOfEdges() == 1724); +// REQUIRE(clf.getNotes().size() == 3); +// REQUIRE(clf.getNotes()[0] == "Convergence threshold reached & 15 models eliminated"); +// REQUIRE(clf.getNotes()[1] == "Used features in train: 19 of 216"); +// REQUIRE(clf.getNotes()[2] == "Number of models: 4"); +// auto score = clf.score(raw.X_test, raw.y_test); +// auto scoret = clf.score(raw.X_test, raw.y_test); +// REQUIRE(score == Catch::Approx(0.99f).epsilon(raw.epsilon)); +// REQUIRE(scoret == Catch::Approx(0.99f).epsilon(raw.epsilon)); +// // +// // std::cout << "Number of nodes " << clf.getNumberOfNodes() << std::endl; +// // std::cout << "Number of edges " << clf.getNumberOfEdges() << std::endl; +// // std::cout << "Notes size " << clf.getNotes().size() << std::endl; +// // for (auto note : clf.getNotes()) { +// // std::cout << note << std::endl; +// // } +// // std::cout << "Score " << score << std::endl; +// } +// TEST_CASE("Alphablock", "[XBAODE]") +// { +// auto clf_alpha = bayesnet::XBAODE(); +// auto clf_no_alpha = bayesnet::XBAODE(); +// 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)); +// }