diff --git a/bayesnet/classifiers/SPnDE.cc b/bayesnet/classifiers/SPnDE.cc index 984f78c..73bfa7e 100644 --- a/bayesnet/classifiers/SPnDE.cc +++ b/bayesnet/classifiers/SPnDE.cc @@ -35,4 +35,4 @@ namespace bayesnet { return model.graph(name); } -} \ No newline at end of file +} diff --git a/bayesnet/classifiers/XSPnDE.cc b/bayesnet/classifiers/XSPnDE.cc new file mode 100644 index 0000000..4eedc55 --- /dev/null +++ b/bayesnet/classifiers/XSPnDE.cc @@ -0,0 +1,575 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#include "XSPnDE.h" +#include // for pthread_setname_np on linux +#include +#include +#include +#include +#include +#include "bayesnet/utils/TensorUtils.h" + +namespace bayesnet { + +// -------------------------------------- +// Constructor +// -------------------------------------- +XSpnde::XSpnde(int spIndex1, int spIndex2) + : superParent1_{ spIndex1 } + , superParent2_{ spIndex2 } + , nFeatures_{0} + , statesClass_{0} + , alpha_{1.0} + , initializer_{1.0} + , semaphore_{ CountingSemaphore::getInstance() } + , Classifier(Network()) +{ + validHyperparameters = { "parent1", "parent2" }; +} + +// -------------------------------------- +// setHyperparameters +// -------------------------------------- +void XSpnde::setHyperparameters(const nlohmann::json &hyperparameters_) +{ + auto hyperparameters = hyperparameters_; + if (hyperparameters.contains("parent1")) { + superParent1_ = hyperparameters["parent1"]; + hyperparameters.erase("parent1"); + } + if (hyperparameters.contains("parent2")) { + superParent2_ = hyperparameters["parent2"]; + hyperparameters.erase("parent2"); + } + // Hand off anything else to base Classifier + Classifier::setHyperparameters(hyperparameters); +} + +// -------------------------------------- +// fitx +// -------------------------------------- +void XSpnde::fitx(torch::Tensor & X, torch::Tensor & y, + torch::Tensor & weights_, const Smoothing_t smoothing) +{ + m = X.size(1); // number of samples + n = X.size(0); // number of features + dataset = X; + + // Build the dataset in your environment if needed: + buildDataset(y); + + // Construct the data structures needed for counting + buildModel(weights_); + + // Accumulate counts & convert to probabilities + trainModel(weights_, smoothing); + fitted = true; +} + +// -------------------------------------- +// buildModel +// -------------------------------------- +void XSpnde::buildModel(const torch::Tensor &weights) +{ + nFeatures_ = n; + + // Derive the number of states for each feature from the dataset + // states_[f] = max value in dataset[f] + 1. + states_.resize(nFeatures_); + for (int f = 0; f < nFeatures_; f++) { + // This is naive: we take max in feature f. You might adapt for real data. + states_[f] = dataset[f].max().item() + 1; + } + // Class states: + statesClass_ = dataset[-1].max().item() + 1; + + // Initialize the class counts + classCounts_.resize(statesClass_, 0.0); + + // For sp1 -> p(sp1Val| c) + sp1FeatureCounts_.resize(states_[superParent1_] * statesClass_, 0.0); + + // For sp2 -> p(sp2Val| c) + sp2FeatureCounts_.resize(states_[superParent2_] * statesClass_, 0.0); + + // For child features, we store p(childVal | c, sp1Val, sp2Val). + // childCounts_ will hold raw counts. We’ll gather them in one big vector. + // We need an offset for each feature. + childOffsets_.resize(nFeatures_, -1); + + int totalSize = 0; + for (int f = 0; f < nFeatures_; f++) { + if (f == superParent1_ || f == superParent2_) { + // skip the superparents + childOffsets_[f] = -1; + continue; + } + childOffsets_[f] = totalSize; + // block size for a single child f: states_[f] * statesClass_ + // * states_[superParent1_] + // * states_[superParent2_]. + totalSize += (states_[f] * statesClass_ + * states_[superParent1_] + * states_[superParent2_]); + } + childCounts_.resize(totalSize, 0.0); +} + +// -------------------------------------- +// trainModel +// -------------------------------------- +void XSpnde::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(); // class + double w = weights[i].item(); + addSample(instance, w); + } + + // Choose alpha based on smoothing: + 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 + } + + // Large initializer factor for numerical stability + initializer_ = std::numeric_limits::max() / (nFeatures_ * nFeatures_); + + // Convert raw counts to probabilities + computeProbabilities(); +} + +// -------------------------------------- +// addSample +// -------------------------------------- +void XSpnde::addSample(const std::vector &instance, double weight) +{ + if (weight <= 0.0) + return; + + int c = instance.back(); + // increment classCounts + classCounts_[c] += weight; + + int sp1Val = instance[superParent1_]; + int sp2Val = instance[superParent2_]; + + // p(sp1|c) + sp1FeatureCounts_[sp1Val * statesClass_ + c] += weight; + + // p(sp2|c) + sp2FeatureCounts_[sp2Val * statesClass_ + c] += weight; + + // p(childVal| c, sp1Val, sp2Val) + for (int f = 0; f < nFeatures_; f++) { + if (f == superParent1_ || f == superParent2_) + continue; + + int childVal = instance[f]; + int offset = childOffsets_[f]; + // block layout: + // offset + (sp1Val*(states_[sp2_]* states_[f]* statesClass_)) + // + (sp2Val*(states_[f]* statesClass_)) + // + childVal*(statesClass_) + // + c + int blockSizeSp2 = states_[superParent2_] + * states_[f] + * statesClass_; + int blockSizeChild = states_[f] * statesClass_; + + int idx = offset + + sp1Val*blockSizeSp2 + + sp2Val*blockSizeChild + + childVal*statesClass_ + + c; + childCounts_[idx] += weight; + } +} + +// -------------------------------------- +// computeProbabilities +// -------------------------------------- +void XSpnde::computeProbabilities() +{ + double totalCount = std::accumulate(classCounts_.begin(), + classCounts_.end(), 0.0); + + // 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(sp1Val| c) + sp1FeatureProbs_.resize(sp1FeatureCounts_.size()); + int sp1Card = states_[superParent1_]; + for (int spVal = 0; spVal < sp1Card; spVal++) { + for (int c = 0; c < statesClass_; c++) { + double denom = classCounts_[c] + alpha_ * sp1Card; + double num = sp1FeatureCounts_[spVal * statesClass_ + c] + alpha_; + sp1FeatureProbs_[spVal * statesClass_ + c] = + (denom <= 0.0 ? 0.0 : num / denom); + } + } + + // p(sp2Val| c) + sp2FeatureProbs_.resize(sp2FeatureCounts_.size()); + int sp2Card = states_[superParent2_]; + for (int spVal = 0; spVal < sp2Card; spVal++) { + for (int c = 0; c < statesClass_; c++) { + double denom = classCounts_[c] + alpha_ * sp2Card; + double num = sp2FeatureCounts_[spVal * statesClass_ + c] + alpha_; + sp2FeatureProbs_[spVal * statesClass_ + c] = + (denom <= 0.0 ? 0.0 : num / denom); + } + } + + // p(childVal| c, sp1Val, sp2Val) + childProbs_.resize(childCounts_.size()); + int offset = 0; + for (int f = 0; f < nFeatures_; f++) { + if (f == superParent1_ || f == superParent2_) + continue; + + int fCard = states_[f]; + int sp1Card_ = states_[superParent1_]; + int sp2Card_ = states_[superParent2_]; + int childBlockSizeSp2 = sp2Card_ * fCard * statesClass_; + int childBlockSizeF = fCard * statesClass_; + + int blockSize = fCard * sp1Card_ * sp2Card_ * statesClass_; + for (int sp1Val = 0; sp1Val < sp1Card_; sp1Val++) { + for (int sp2Val = 0; sp2Val < sp2Card_; sp2Val++) { + for (int childVal = 0; childVal < fCard; childVal++) { + for (int c = 0; c < statesClass_; c++) { + // index in childCounts_ + int idx = offset + + sp1Val*childBlockSizeSp2 + + sp2Val*childBlockSizeF + + childVal*statesClass_ + + c; + double num = childCounts_[idx] + alpha_; + // denominator is the count of (sp1Val,sp2Val,c) plus alpha * fCard + // We can find that by summing childVal dimension, but we already + // have it in childCounts_[...] or we can re-check the superparent + // counts if your approach is purely hierarchical. + // Here we'll do it like the XSpode approach: sp1&sp2 are + // conditionally independent given c, so denominators come from + // summing the relevant block or we treat sp1,sp2 as "parents." + // A simpler approach: + double sumSp1Sp2C = 0.0; + // sum over all childVal: + for (int cv = 0; cv < fCard; cv++) { + int idx2 = offset + + sp1Val*childBlockSizeSp2 + + sp2Val*childBlockSizeF + + cv*statesClass_ + c; + sumSp1Sp2C += childCounts_[idx2]; + } + double denom = sumSp1Sp2C + alpha_ * fCard; + childProbs_[idx] = (denom <= 0.0 ? 0.0 : num / denom); + } + } + } + } + offset += blockSize; + } +} + +// -------------------------------------- +// predict_proba (single instance) +// -------------------------------------- +std::vector XSpnde::predict_proba(const std::vector &instance) const +{ + if (!fitted) { + throw std::logic_error(CLASSIFIER_NOT_FITTED); + } + std::vector probs(statesClass_, 0.0); + + int sp1Val = instance[superParent1_]; + int sp2Val = instance[superParent2_]; + + // Start with p(c) * p(sp1Val| c) * p(sp2Val| c) + for (int c = 0; c < statesClass_; c++) { + double pC = classPriors_[c]; + double pSp1C = sp1FeatureProbs_[sp1Val * statesClass_ + c]; + double pSp2C = sp2FeatureProbs_[sp2Val * statesClass_ + c]; + probs[c] = pC * pSp1C * pSp2C * initializer_; + } + + // Multiply by each child feature f + int offset = 0; + for (int f = 0; f < nFeatures_; f++) { + if (f == superParent1_ || f == superParent2_) + continue; + + int valF = instance[f]; + int fCard = states_[f]; + int sp1Card = states_[superParent1_]; + int sp2Card = states_[superParent2_]; + int blockSizeSp2 = sp2Card * fCard * statesClass_; + int blockSizeF = fCard * statesClass_; + + // base index for childProbs_ for this child and sp1Val, sp2Val + int base = offset + + sp1Val*blockSizeSp2 + + sp2Val*blockSizeF + + valF*statesClass_; + for (int c = 0; c < statesClass_; c++) { + probs[c] *= childProbs_[base + c]; + } + offset += (fCard * sp1Card * sp2Card * statesClass_); + } + + // Normalize + normalize(probs); + return probs; +} + +// -------------------------------------- +// predict_proba (batch) +// -------------------------------------- +std::vector> XSpnde::predict_proba(std::vector> &test_data) +{ + int test_size = test_data[0].size(); // each feature is test_data[f], size = #samples + int sample_size = test_data.size(); // = nFeatures_ + std::vector> probabilities( + test_size, std::vector(statesClass_, 0.0)); + + // same concurrency approach + 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 = + "XSpnde-" + 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] = 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 &th : threads) { + th.join(); + } + return probabilities; +} + +// -------------------------------------- +// predict (single instance) +// -------------------------------------- +int XSpnde::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())) + ); +} + +// -------------------------------------- +// predict (batch of data) +// -------------------------------------- +std::vector XSpnde::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] = static_cast( + std::distance(probabilities[i].begin(), + std::max_element(probabilities[i].begin(), + probabilities[i].end())) + ); + } + return predictions; +} + +// -------------------------------------- +// predict (torch::Tensor version) +// -------------------------------------- +torch::Tensor XSpnde::predict(torch::Tensor &X) +{ + auto X_ = TensorUtils::to_matrix(X); + auto result_v = predict(X_); + return torch::tensor(result_v, torch::kInt32); +} + +// -------------------------------------- +// predict_proba (torch::Tensor version) +// -------------------------------------- +torch::Tensor XSpnde::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 < (int)result_v.size(); ++i) { + result.index_put_({ i, "..." }, torch::tensor(result_v[i])); + } + return result; +} + +// -------------------------------------- +// score (torch::Tensor version) +// -------------------------------------- +float XSpnde::score(torch::Tensor &X, torch::Tensor &y) +{ + torch::Tensor y_pred = predict(X); + return (y_pred == y).sum().item() / y.size(0); +} + +// -------------------------------------- +// score (vector version) +// -------------------------------------- +float XSpnde::score(std::vector> &X, std::vector &y) +{ + auto y_pred = predict(X); + int correct = 0; + for (size_t i = 0; i < y_pred.size(); ++i) { + if (y_pred[i] == y[i]) { + correct++; + } + } + return static_cast(correct) / static_cast(y_pred.size()); +} + +// -------------------------------------- +// Utility: normalize +// -------------------------------------- +void XSpnde::normalize(std::vector &v) const +{ + double sum = 0.0; + for (auto &val : v) { + sum += val; + } + if (sum > 0.0) { + for (auto &val : v) { + val /= sum; + } + } +} + +// -------------------------------------- +// to_string +// -------------------------------------- +std::string XSpnde::to_string() const +{ + std::ostringstream oss; + oss << "----- XSpnde Model -----\n" + << "nFeatures_ = " << nFeatures_ << "\n" + << "superParent1_ = " << superParent1_ << "\n" + << "superParent2_ = " << superParent2_ << "\n" + << "statesClass_ = " << statesClass_ << "\n\n"; + + oss << "States: ["; + for (auto s : states_) oss << s << " "; + oss << "]\n"; + + oss << "classCounts_:\n"; + for (auto v : classCounts_) oss << v << " "; + oss << "\nclassPriors_:\n"; + for (auto v : classPriors_) oss << v << " "; + oss << "\nsp1FeatureCounts_ (size=" << sp1FeatureCounts_.size() << ")\n"; + for (auto v : sp1FeatureCounts_) oss << v << " "; + oss << "\nsp2FeatureCounts_ (size=" << sp2FeatureCounts_.size() << ")\n"; + for (auto v : sp2FeatureCounts_) oss << v << " "; + oss << "\nchildCounts_ (size=" << childCounts_.size() << ")\n"; + for (auto v : childCounts_) oss << v << " "; + + oss << "\nchildOffsets_:\n"; + for (auto c : childOffsets_) oss << c << " "; + + oss << "\n----------------------------------------\n"; + return oss.str(); +} + +// -------------------------------------- +// Some introspection about the graph +// -------------------------------------- +int XSpnde::getNumberOfNodes() const +{ + // nFeatures + 1 class node + return nFeatures_ + 1; +} + +int XSpnde::getClassNumStates() const +{ + return statesClass_; +} + +int XSpnde::getNFeatures() const +{ + return nFeatures_; +} + +int XSpnde::getNumberOfStates() const +{ + // purely an example. Possibly you want to sum up actual + // cardinalities or something else. + return std::accumulate(states_.begin(), states_.end(), 0) * nFeatures_; +} + +int XSpnde::getNumberOfEdges() const +{ + // In an SPNDE with n=2, for each feature we have edges from class, sp1, sp2. + // So that’s 3*(nFeatures_) edges, minus the ones for the superparents themselves, + // plus the edges from class->superparent1, class->superparent2. + // For a quick approximation: + // - class->sp1, class->sp2 => 2 edges + // - class->child => (nFeatures -2) edges + // - sp1->child, sp2->child => 2*(nFeatures -2) edges + // total = 2 + (nFeatures-2) + 2*(nFeatures-2) = 2 + 3*(nFeatures-2) + // = 3nFeatures - 4 (just an example). + // You can adapt to your liking: + return 3 * nFeatures_ - 4; +} + +} // namespace bayesnet + diff --git a/bayesnet/classifiers/XSPnDE.h b/bayesnet/classifiers/XSPnDE.h new file mode 100644 index 0000000..0e9c9b9 --- /dev/null +++ b/bayesnet/classifiers/XSPnDE.h @@ -0,0 +1,75 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#ifndef XSPNDE_H +#define XSPNDE_H + +#include "Classifier.h" +#include "bayesnet/utils/CountingSemaphore.h" +#include +#include + +namespace bayesnet { + +class XSpnde : public Classifier { + public: + XSpnde(int spIndex1, int spIndex2); + void setHyperparameters(const nlohmann::json &hyperparameters_) override; + void fitx(torch::Tensor &X, torch::Tensor &y, torch::Tensor &weights_, const Smoothing_t smoothing); + std::vector predict_proba(const std::vector &instance) const; + std::vector> predict_proba(std::vector> &test_data) override; + int predict(const std::vector &instance) const; + std::vector predict(std::vector> &test_data) override; + torch::Tensor predict(torch::Tensor &X) override; + torch::Tensor predict_proba(torch::Tensor &X) override; + + float score(torch::Tensor &X, torch::Tensor &y) override; + float score(std::vector> &X, std::vector &y) override; + std::string to_string() const; + std::vector graph(const std::string &title) const override { + return std::vector({title}); + } + + int getNumberOfNodes() const override; + int getNumberOfEdges() const override; + int getNFeatures() const; + int getClassNumStates() const override; + int getNumberOfStates() const override; + + protected: + void buildModel(const torch::Tensor &weights) override; + void trainModel(const torch::Tensor &weights, const bayesnet::Smoothing_t smoothing) override; + + private: + void addSample(const std::vector &instance, double weight); + void normalize(std::vector &v) const; + void computeProbabilities(); + + int superParent1_; + int superParent2_; + int nFeatures_; + int statesClass_; + double alpha_; + double initializer_; + + std::vector states_; + std::vector classCounts_; + std::vector classPriors_; + std::vector sp1FeatureCounts_, sp1FeatureProbs_; + std::vector sp2FeatureCounts_, sp2FeatureProbs_; + // childOffsets_[f] will be the offset into childCounts_ for feature f. + // If f is either superParent1 or superParent2, childOffsets_[f] = -1 + std::vector childOffsets_; + // For each child f, we store p(x_f | c, sp1Val, sp2Val). We'll store the raw + // counts in childCounts_, and the probabilities in childProbs_, with a + // dimension block of size: states_[f]* statesClass_* states_[sp1]* states_[sp2]. + std::vector childCounts_; + std::vector childProbs_; + CountingSemaphore &semaphore_; +}; + +} // namespace bayesnet +#endif // XSPNDE_H diff --git a/bayesnet/ensembles/BoostA2DE.cc b/bayesnet/ensembles/BoostA2DE.cc index e16a67d..64a0d63 100644 --- a/bayesnet/ensembles/BoostA2DE.cc +++ b/bayesnet/ensembles/BoostA2DE.cc @@ -4,14 +4,9 @@ // SPDX-License-Identifier: MIT // *************************************************************** -#include -#include #include #include #include -#include "bayesnet/feature_selection/CFS.h" -#include "bayesnet/feature_selection/FCBF.h" -#include "bayesnet/feature_selection/IWSS.h" #include "BoostA2DE.h" namespace bayesnet { @@ -167,4 +162,4 @@ namespace bayesnet { { return Ensemble::graph(title); } -} \ No newline at end of file +} diff --git a/bayesnet/ensembles/XBAODE.h b/bayesnet/ensembles/XBAODE.h index 04dd8c1..65eab1c 100644 --- a/bayesnet/ensembles/XBAODE.h +++ b/bayesnet/ensembles/XBAODE.h @@ -8,17 +8,10 @@ #define XBAODE_H #include #include -#include - -#include -#include "bayesnet/classifiers/XSPODE.h" #include "Boost.h" namespace bayesnet { 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 public: XBAODE(); std::string getVersion() override { return version; }; @@ -33,4 +26,4 @@ namespace bayesnet { std::string version = "0.9.7"; }; } -#endif // XBAODE_H \ No newline at end of file +#endif // XBAODE_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ae76a96..1df73f7 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 + add_executable(TestBayesNet TestBayesNetwork.cc TestBayesNode.cc TestBayesClassifier.cc TestXSPnDE.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) @@ -18,6 +18,7 @@ if(ENABLE_TESTING) add_test(NAME BoostA2DE COMMAND TestBayesNet "[BoostA2DE]") add_test(NAME BoostAODE COMMAND TestBayesNet "[BoostAODE]") add_test(NAME XSPODE COMMAND TestBayesNet "[XSPODE]") + add_test(NAME XSPnDE COMMAND TestBayesNet "[XSPnDE]") add_test(NAME XBAODE COMMAND TestBayesNet "[XBAODE]") add_test(NAME Classifier COMMAND TestBayesNet "[Classifier]") add_test(NAME Ensemble COMMAND TestBayesNet "[Ensemble]") diff --git a/tests/TestXSPnDE.cc b/tests/TestXSPnDE.cc new file mode 100644 index 0000000..9b1b7ec --- /dev/null +++ b/tests/TestXSPnDE.cc @@ -0,0 +1,120 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#include +#include +#include +#include "bayesnet/classifiers/XSPnDE.h" // <-- your new 2-superparent classifier +#include "TestUtils.h" // for RawDatasets, etc. + +// Helper function to handle each (sp1, sp2) pair in tests +static void check_spnde_pair( + int sp1, + int sp2, + RawDatasets &raw, + bool fitVector, + bool fitTensor) +{ + // Create our classifier + bayesnet::XSpnde clf(sp1, sp2); + + // Option A: fit with vector-based data + if (fitVector) { + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + } + // Option B: fit with the whole dataset in torch::Tensor form + else if (fitTensor) { + // your “tensor” version of fit + clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing); + } + // Option C: or you might do the “dataset” version: + else { + clf.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing); + } + + // 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 +} + +// ------------------------------------------------------------ +// 1) Fit vector test +// ------------------------------------------------------------ +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); + } +} + +// ------------------------------------------------------------ +// 2) Fit dataset test +// ------------------------------------------------------------ +TEST_CASE("fit dataset test (XSPNDE)", "[XSPNDE]") { + auto raw = RawDatasets("iris", true); + + // Again test multiple pairs: + std::vector> parentPairs = { + {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); + } +} + +// ------------------------------------------------------------ +// 3) Tensors dataset predict & predict_proba +// ------------------------------------------------------------ +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} + }; + + 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); + REQUIRE(clf.getNumberOfEdges() == 8); + REQUIRE(clf.getNotes().size() == 0); + + // Check the score + 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. + } +} +