diff --git a/bayesnet/ensembles/WA2DE.cc b/bayesnet/ensembles/WA2DE.cc new file mode 100644 index 0000000..578f0ab --- /dev/null +++ b/bayesnet/ensembles/WA2DE.cc @@ -0,0 +1,267 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** +#include "WA2DE.h" +namespace bayesnet { + WA2DE::WA2DE(bool predict_voting) + : num_classes_(0), num_attributes_(0), total_count_(0.0), weighted_a2de_(false), smoothing_factor_(1.0) + { + validHyperparameters = { "predict_voting" }; + std::cout << "WA2DE classifier created.\n"; + } + + void bayesnet::WA2DE::setHyperparameters(const nlohmann::json& hyperparameters_) + { + auto hyperparameters = hyperparameters_; + if (hyperparameters.contains("predict_voting")) { + predict_voting = hyperparameters["predict_voting"]; + hyperparameters.erase("predict_voting"); + } + Classifier::setHyperparameters(hyperparameters); + } + + + void WA2DE::buildModel(const torch::Tensor& weights) + { + for (int c = 0; c < num_classes_; ++c) { + class_counts_[c] += 1e-4; // Laplace smoothing + } + for (int a = 0; a < num_attributes_; ++a) { + for (int v = 0; v < attribute_cardinalities_[a]; ++v) { + for (int c = 0; c < num_classes_; ++c) { + freq_attr_class_[a][v][c] = + (freq_attr_class_[a][v][c] + 1.0) / (class_counts_[c] + attribute_cardinalities_[a]); + } + } + } + + for (int sp = 0; sp < num_attributes_; ++sp) { + for (int spv = 0; spv < attribute_cardinalities_[sp]; ++spv) { + for (int ch = 0; ch < num_attributes_; ++ch) { + if (sp != ch) { + for (int chv = 0; chv < attribute_cardinalities_[ch]; ++chv) { + for (int c = 0; c < num_classes_; ++c) { + freq_pair_class_[sp][spv][ch][chv][c] = + (freq_pair_class_[sp][spv][ch][chv][c] + 1.0) / + (class_counts_[c] + attribute_cardinalities_[sp] * attribute_cardinalities_[ch]); + } + } + } + } + } + } + std::cout << "Model probabilities computed.\n"; + } + void WA2DE::trainModel(const torch::Tensor& weights, const Smoothing_t smoothing) + { + auto data = dataset.clone(); + auto labels = data[-1]; + // Remove class row from data + data = data.index({ at::indexing::Slice(0, -1) }); + std::cout << "Training A2DE model...\n"; + std::cout << "Data: " << data.sizes() << std::endl; + std::cout << "Labels: " << labels.sizes() << std::endl; + std::cout << std::string(80, '-') << std::endl; + if (data.dim() != 2 || labels.dim() != 1) { + throw std::invalid_argument("Invalid input dimensions."); + } + num_attributes_ = data.size(0); + num_classes_ = labels.max().item() + 1; + total_count_ = data.size(1); + std::cout << "Number of attributes: " << num_attributes_ << std::endl; + std::cout << "Number of classes: " << num_classes_ << std::endl; + std::cout << "Total count: " << total_count_ << std::endl; + + // Compute cardinalities + attribute_cardinalities_.clear(); + for (int i = 0; i < num_attributes_; ++i) { + attribute_cardinalities_.push_back(data[i].max().item() + 1); + } + std::cout << "Attribute cardinalities: "; + for (int i = 0; i < num_attributes_; ++i) { + std::cout << attribute_cardinalities_[i] << " "; + } + std::cout << std::endl; + // output the map of states + std::cout << "States: "; + for (int i = 0; i < states.size() - 1; i++) { + std::cout << features[i] << " " << states[features[i]].size() << std::endl; + } + + // Resize storage + class_counts_.resize(num_classes_, 0.0); + freq_attr_class_.resize(num_attributes_); + freq_pair_class_.resize(num_attributes_); + + for (int i = 0; i < num_attributes_; ++i) { + freq_attr_class_[i].resize(attribute_cardinalities_[i], std::vector(num_classes_, 0.0)); + freq_pair_class_[i].resize(attribute_cardinalities_[i]); // Ensure first level exists + for (int j = 0; j < attribute_cardinalities_[i]; ++j) { + freq_pair_class_[i][j].resize(num_attributes_); // Ensure second level exists + for (int k = 0; k < num_attributes_; ++k) { + if (i != k) { + freq_pair_class_[i][j][k].resize(attribute_cardinalities_[k]); // Ensure third level exists + for (int l = 0; l < attribute_cardinalities_[k]; ++l) { + freq_pair_class_[i][j][k][l].resize(num_classes_, 0.0); // Finally, initialize with 0.0 + } + } + } + } + } + // Count frequencies + auto data_cpu = data.to(torch::kCPU); + auto labels_cpu = labels.to(torch::kCPU); + int32_t* data_ptr = data_cpu.data_ptr(); + int32_t* labels_ptr = labels_cpu.data_ptr(); + + for (int i = 0; i < total_count_; ++i) { + int class_label = labels_ptr[i]; + class_counts_[class_label] += 1.0; + + std::vector attr_values(num_attributes_); + for (int a = 0; a < num_attributes_; ++a) { + attr_values[a] = toIntValue(a, data_ptr[i * num_attributes_ + a]); + freq_attr_class_[a][attr_values[a]][class_label] += 1.0; + } + + // Pairwise counts + for (int sp = 0; sp < num_attributes_; ++sp) { + for (int ch = 0; ch < num_attributes_; ++ch) { + if (sp != ch) { + freq_pair_class_[sp][attr_values[sp]][ch][attr_values[ch]][class_label] += 1.0; + } + } + } + } + std::cout << "Verifying Frequency Counts:\n"; + for (int c = 0; c < num_classes_; ++c) { + std::cout << "Class " << c << " Count: " << class_counts_[c] << std::endl; + } + + for (int a = 0; a < num_attributes_; ++a) { + for (int v = 0; v < attribute_cardinalities_[a]; ++v) { + std::cout << "P(A[" << a << "]=" << v << "|C): "; + for (int c = 0; c < num_classes_; ++c) { + std::cout << freq_attr_class_[a][v][c] << " "; + } + std::cout << std::endl; + } + } + + } + + torch::Tensor WA2DE::computeProbabilities(const torch::Tensor& data) const + { + int M = data.size(1); + auto output = torch::zeros({ M, num_classes_ }, torch::kF64); + + auto data_cpu = data.to(torch::kCPU); + int32_t* data_ptr = data_cpu.data_ptr(); + + for (int i = 0; i < M; ++i) { + std::vector attr_values(num_attributes_); + for (int a = 0; a < num_attributes_; ++a) { + attr_values[a] = toIntValue(a, data_ptr[i * num_attributes_ + a]); + } + + std::vector log_prob(num_classes_, 0.0); + for (int c = 0; c < num_classes_; ++c) { + log_prob[c] = std::log((class_counts_[c] + smoothing_factor_) / (total_count_ + num_classes_ * smoothing_factor_)); + + double sum_log = 0.0; + for (int sp = 0; sp < num_attributes_; ++sp) { + double sp_log = log_prob[c]; + for (int ch = 0; ch < num_attributes_; ++ch) { + if (sp == ch) continue; + double num = freq_pair_class_[sp][attr_values[sp]][ch][attr_values[ch]][c] + smoothing_factor_; + double denom = class_counts_[c] + attribute_cardinalities_[sp] * attribute_cardinalities_[ch] * smoothing_factor_; + sp_log += std::log(num / denom); + } + sum_log += std::exp(sp_log); + } + log_prob[c] = std::log(sum_log / num_attributes_); + } + + double max_log = *std::max_element(log_prob.begin(), log_prob.end()); + double sum_exp = 0.0; + for (int c = 0; c < num_classes_; ++c) { + sum_exp += std::exp(log_prob[c] - max_log); + } + double log_sum_exp = max_log + std::log(sum_exp); + + for (int c = 0; c < num_classes_; ++c) { + output[i][c] = std::exp(log_prob[c] - log_sum_exp); + } + } + + return output.to(torch::kF32); + } + int WA2DE::toIntValue(int attributeIndex, float value) const + { + int v = static_cast(value); + return std::max(0, std::min(v, attribute_cardinalities_[attributeIndex] - 1)); + } + torch::Tensor WA2DE::AODEConditionalProb(const torch::Tensor& data) + { + int M = data.size(1); // Number of test samples + torch::Tensor output = torch::zeros({ M, num_classes_ }, torch::kF32); + + auto data_cpu = data.to(torch::kCPU); + int32_t* data_ptr = data_cpu.data_ptr(); + + for (int i = 0; i < M; ++i) { + std::vector attr_values(num_attributes_); + for (int a = 0; a < num_attributes_; ++a) { + attr_values[a] = toIntValue(a, data_ptr[i * num_attributes_ + a]); + } + + std::vector log_prob(num_classes_, 0.0); + for (int c = 0; c < num_classes_; ++c) { + log_prob[c] = std::log(class_counts_[c] / total_count_); + + double sum_log = 0.0; + for (int sp = 0; sp < num_attributes_; ++sp) { + double sp_log = log_prob[c]; + for (int ch = 0; ch < num_attributes_; ++ch) { + if (sp == ch) continue; + double prob = freq_pair_class_[sp][attr_values[sp]][ch][attr_values[ch]][c]; + sp_log += std::log(prob); + } + sum_log += std::exp(sp_log); + } + log_prob[c] = std::log(sum_log / num_attributes_); + } + + double max_log = *std::max_element(log_prob.begin(), log_prob.end()); + double sum_exp = 0.0; + for (int c = 0; c < num_classes_; ++c) { + sum_exp += std::exp(log_prob[c] - max_log); + } + double log_sum_exp = max_log + std::log(sum_exp); + + for (int c = 0; c < num_classes_; ++c) { + output[i][c] = std::exp(log_prob[c] - log_sum_exp); + } + } + + return output; + } + + double WA2DE::score(const torch::Tensor& X, const torch::Tensor& y) + { + torch::Tensor preds = AODEConditionalProb(X); + torch::Tensor pred_labels = preds.argmax(1); + + auto correct = pred_labels.eq(y).sum().item(); + auto total = y.size(0); + + return static_cast(correct) / total; + } + + std::vector WA2DE::graph(const std::string& title) const + { + return { title, "Graph visualization not implemented." }; + } +} \ No newline at end of file diff --git a/bayesnet/ensembles/WA2DE.h b/bayesnet/ensembles/WA2DE.h new file mode 100644 index 0000000..7008025 --- /dev/null +++ b/bayesnet/ensembles/WA2DE.h @@ -0,0 +1,53 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** +#ifndef WA2DE_H +#define WA2DE_H +#include "Ensemble.h" +#include +#include +#include +#include +namespace bayesnet { + /** + * Geoffrey I. Webb's A2DE (Averaged 2-Dependence Estimators) classifier + * Implements the A2DE algorithm as an ensemble of SPODE models. + */ + class WA2DE : public Ensemble { + public: + explicit WA2DE(bool predict_voting = false); + virtual ~WA2DE() {}; + + // Override method to set hyperparameters + void setHyperparameters(const nlohmann::json& hyperparameters) override; + + // Graph visualization function + std::vector graph(const std::string& title = "A2DE") const override; + torch::Tensor computeProbabilities(const torch::Tensor& data) const; + double score(const torch::Tensor& X, const torch::Tensor& y); + protected: + // Model-building function + void buildModel(const torch::Tensor& weights) override; + + private: + int num_classes_; // Number of classes + int num_attributes_; // Number of attributes + std::vector attribute_cardinalities_; // Cardinalities of attributes + + // Frequency counts (similar to Java implementation) + std::vector class_counts_; // Class frequency + std::vector>> freq_attr_class_; // P(A | C) + std::vector>>>> freq_pair_class_; // P(A_i, A_j | C) + + double total_count_; // Total instance count + + bool weighted_a2de_; // Whether to use weighted A2DE + double smoothing_factor_; // Smoothing parameter (default: Laplace) + torch::Tensor AODEConditionalProb(const torch::Tensor& data); + void trainModel(const torch::Tensor& data, const Smoothing_t smoothing); + int toIntValue(int attributeIndex, float value) const; + }; +} +#endif \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9006d15..7868f5f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,11 +9,12 @@ 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 + TestBayesModels.cc TestBayesMetrics.cc TestFeatureSelection.cc TestBoostAODE.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) add_test(NAME A2DE COMMAND TestBayesNet "[A2DE]") + add_test(NAME WA2DE COMMAND TestBayesNet "[WA2DE]") add_test(NAME BoostA2DE COMMAND TestBayesNet "[BoostA2DE]") add_test(NAME BoostAODE COMMAND TestBayesNet "[BoostAODE]") add_test(NAME Classifier COMMAND TestBayesNet "[Classifier]") diff --git a/tests/TestWA2DE.cc b/tests/TestWA2DE.cc new file mode 100644 index 0000000..f77a312 --- /dev/null +++ b/tests/TestWA2DE.cc @@ -0,0 +1,31 @@ +// *************************************************************** +// SPDX-FileCopyrightText: Copyright 2024 Ricardo Montañana Gómez +// SPDX-FileType: SOURCE +// SPDX-License-Identifier: MIT +// *************************************************************** + +#include +#include +#include +#include +#include "bayesnet/ensembles/WA2DE.h" +#include "TestUtils.h" + + +TEST_CASE("Fit and Score", "[WA2DE]") +{ + auto raw = RawDatasets("iris", true); + auto clf = bayesnet::WA2DE(); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + REQUIRE(clf.score(raw.Xt, raw.yt) == Catch::Approx(0.831776).epsilon(raw.epsilon)); +} +TEST_CASE("Test graph", "[WA2DE]") +{ + auto raw = RawDatasets("iris", true); + auto clf = bayesnet::WA2DE(); + clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); + auto graph = clf.graph("BayesNet WA2DE"); + REQUIRE(graph.size() == 2); + REQUIRE(graph[0] == "BayesNet WA2DE"); + REQUIRE(graph[1] == "Graph visualization not implemented."); +}