From 70c7d3dd3da657752b9f274bc37ef0dfae1be52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana=20G=C3=B3mez?= Date: Fri, 14 Mar 2025 18:55:29 +0100 Subject: [PATCH] Add test to 99.1% --- README.md | 2 +- bayesnet/classifiers/KDB.cc | 2 +- bayesnet/classifiers/KDB.h | 5 +- tests/TestBayesModels.cc | 152 +++++++++++++++++++++++------------- 4 files changed, 101 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index c27ff4a..3e6c967 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=rmontanana_BayesNet&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=rmontanana_BayesNet) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=rmontanana_BayesNet&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=rmontanana_BayesNet) ![Gitea Last Commit](https://img.shields.io/gitea/last-commit/rmontanana/bayesnet?gitea_url=https://gitea.rmontanana.es:3000&logo=gitea) -[![Coverage Badge](https://img.shields.io/badge/Coverage-99,0%25-green)](html/index.html) +[![Coverage Badge](https://img.shields.io/badge/Coverage-99,1%25-green)](html/index.html) [![DOI](https://zenodo.org/badge/667782806.svg)](https://doi.org/10.5281/zenodo.14210344) Bayesian Network Classifiers library diff --git a/bayesnet/classifiers/KDB.cc b/bayesnet/classifiers/KDB.cc index e9582c8..b89bd75 100644 --- a/bayesnet/classifiers/KDB.cc +++ b/bayesnet/classifiers/KDB.cc @@ -3,7 +3,7 @@ // SPDX-FileType: SOURCE // SPDX-License-Identifier: MIT // *************************************************************** - +#include "bayesnet/utils/bayesnetUtils.h" #include "KDB.h" namespace bayesnet { diff --git a/bayesnet/classifiers/KDB.h b/bayesnet/classifiers/KDB.h index a8d6cab..85e9353 100644 --- a/bayesnet/classifiers/KDB.h +++ b/bayesnet/classifiers/KDB.h @@ -7,15 +7,14 @@ #ifndef KDB_H #define KDB_H #include -#include "bayesnet/utils/bayesnetUtils.h" #include "Classifier.h" namespace bayesnet { class KDB : public Classifier { private: int k; float theta; - void add_m_edges(int idx, std::vector& S, torch::Tensor& weights); protected: + void add_m_edges(int idx, std::vector& S, torch::Tensor& weights); void buildModel(const torch::Tensor& weights) override; public: explicit KDB(int k, float theta = 0.03); @@ -24,4 +23,4 @@ namespace bayesnet { std::vector graph(const std::string& name = "KDB") const override; }; } -#endif \ No newline at end of file +#endif diff --git a/tests/TestBayesModels.cc b/tests/TestBayesModels.cc index c6ad27f..34ee728 100644 --- a/tests/TestBayesModels.cc +++ b/tests/TestBayesModels.cc @@ -22,7 +22,8 @@ const std::string ACTUAL_VERSION = "1.0.6"; -TEST_CASE("Test Bayesian Classifiers score & version", "[Models]") { +TEST_CASE("Test Bayesian Classifiers score & version", "[Models]") +{ map, float> scores{// Diabetes {{"diabetes", "AODE"}, 0.82161}, {{"diabetes", "KDB"}, 0.852865}, @@ -66,8 +67,8 @@ TEST_CASE("Test Bayesian Classifiers score & version", "[Models]") { {{"iris", "KDBLd"}, 0.973333}, {{"iris", "SPODELd"}, 0.96f}, {{"iris", "TANLd"}, 0.97333f}, - {{"iris", "BoostAODE"}, 0.98f}}; - std::map models{{"AODE", new bayesnet::AODE()}, + {{"iris", "BoostAODE"}, 0.98f} }; + std::map models{ {"AODE", new bayesnet::AODE()}, {"AODELd", new bayesnet::AODELd()}, {"BoostAODE", new bayesnet::BoostAODE()}, {"KDB", new bayesnet::KDB(2)}, @@ -76,12 +77,13 @@ TEST_CASE("Test Bayesian Classifiers score & version", "[Models]") { {"SPODE", new bayesnet::SPODE(1)}, {"SPODELd", new bayesnet::SPODELd(1)}, {"TAN", new bayesnet::TAN()}, - {"TANLd", new bayesnet::TANLd()}}; + {"TANLd", new bayesnet::TANLd()} }; std::string name = GENERATE("AODE", "AODELd", "KDB", "KDBLd", "SPODE", "XSPODE", "SPODELd", "TAN", "TANLd"); auto clf = models[name]; - SECTION("Test " + name + " classifier") { - for (const std::string &file_name : {"glass", "iris", "ecoli", "diabetes"}) { + SECTION("Test " + name + " classifier") + { + for (const std::string& file_name : { "glass", "iris", "ecoli", "diabetes" }) { auto clf = models[name]; auto discretize = name.substr(name.length() - 2) != "Ld"; auto raw = RawDatasets(file_name, discretize); @@ -94,21 +96,24 @@ TEST_CASE("Test Bayesian Classifiers score & version", "[Models]") { REQUIRE(clf->getStatus() == bayesnet::NORMAL); } } - SECTION("Library check version") { + SECTION("Library check version") + { INFO("Checking version of " << name << " classifier"); REQUIRE(clf->getVersion() == ACTUAL_VERSION); } delete clf; } -TEST_CASE("Models features & Graph", "[Models]") { +TEST_CASE("Models features & Graph", "[Models]") +{ auto graph = std::vector( - {"digraph BayesNet {\nlabel=\nfontsize=30\nfontcolor=blue\nlabelloc=t\nlayout=circo\n", + { "digraph BayesNet {\nlabel=\nfontsize=30\nfontcolor=blue\nlabelloc=t\nlayout=circo\n", "\"class\" [shape=circle, fontcolor=red, fillcolor=lightblue, style=filled ] \n", "\"class\" -> \"sepallength\"", "\"class\" -> \"sepalwidth\"", "\"class\" -> \"petallength\"", "\"class\" -> \"petalwidth\"", "\"petallength\" [shape=circle] \n", "\"petallength\" -> \"sepallength\"", "\"petalwidth\" [shape=circle] \n", "\"sepallength\" [shape=circle] \n", "\"sepallength\" -> \"sepalwidth\"", - "\"sepalwidth\" [shape=circle] \n", "\"sepalwidth\" -> \"petalwidth\"", "}\n"}); - SECTION("Test TAN") { + "\"sepalwidth\" [shape=circle] \n", "\"sepalwidth\" -> \"petalwidth\"", "}\n" }); + SECTION("Test TAN") + { auto raw = RawDatasets("iris", true); auto clf = bayesnet::TAN(); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); @@ -117,11 +122,12 @@ TEST_CASE("Models features & Graph", "[Models]") { REQUIRE(clf.getNumberOfStates() == 19); REQUIRE(clf.getClassNumStates() == 3); REQUIRE(clf.show() == std::vector{"class -> sepallength, sepalwidth, petallength, petalwidth, ", - "petallength -> sepallength, ", "petalwidth -> ", - "sepallength -> sepalwidth, ", "sepalwidth -> petalwidth, "}); + "petallength -> sepallength, ", "petalwidth -> ", + "sepallength -> sepalwidth, ", "sepalwidth -> petalwidth, "}); REQUIRE(clf.graph("Test") == graph); } - SECTION("Test TANLd") { + SECTION("Test TANLd") + { auto clf = bayesnet::TANLd(); auto raw = RawDatasets("iris", false); clf.fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing); @@ -130,12 +136,13 @@ TEST_CASE("Models features & Graph", "[Models]") { REQUIRE(clf.getNumberOfStates() == 27); REQUIRE(clf.getClassNumStates() == 3); REQUIRE(clf.show() == std::vector{"class -> sepallength, sepalwidth, petallength, petalwidth, ", - "petallength -> sepallength, ", "petalwidth -> ", - "sepallength -> sepalwidth, ", "sepalwidth -> petalwidth, "}); + "petallength -> sepallength, ", "petalwidth -> ", + "sepallength -> sepalwidth, ", "sepalwidth -> petalwidth, "}); REQUIRE(clf.graph("Test") == graph); } } -TEST_CASE("Get num features & num edges", "[Models]") { +TEST_CASE("Get num features & num edges", "[Models]") +{ auto raw = RawDatasets("iris", true); auto clf = bayesnet::KDB(2); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); @@ -143,9 +150,10 @@ TEST_CASE("Get num features & num edges", "[Models]") { REQUIRE(clf.getNumberOfEdges() == 8); } -TEST_CASE("Model predict_proba", "[Models]") { +TEST_CASE("Model predict_proba", "[Models]") +{ std::string model = GENERATE("TAN", "SPODE", "BoostAODEproba", "BoostAODEvoting"); - auto res_prob_tan = std::vector>({{0.00375671, 0.994457, 0.00178621}, + auto res_prob_tan = std::vector>({ {0.00375671, 0.994457, 0.00178621}, {0.00137462, 0.992734, 0.00589123}, {0.00137462, 0.992734, 0.00589123}, {0.00137462, 0.992734, 0.00589123}, @@ -153,8 +161,8 @@ TEST_CASE("Model predict_proba", "[Models]") { {0.00494209, 0.0978534, 0.897205}, {0.0054192, 0.974275, 0.0203054}, {0.00433012, 0.985054, 0.0106159}, - {0.000860806, 0.996922, 0.00221698}}); - auto res_prob_spode = std::vector>({{0.00419032, 0.994247, 0.00156265}, + {0.000860806, 0.996922, 0.00221698} }); + auto res_prob_spode = std::vector>({ {0.00419032, 0.994247, 0.00156265}, {0.00172808, 0.993433, 0.00483862}, {0.00172808, 0.993433, 0.00483862}, {0.00172808, 0.993433, 0.00483862}, @@ -162,8 +170,8 @@ TEST_CASE("Model predict_proba", "[Models]") { {0.0120674, 0.357909, 0.630024}, {0.00386239, 0.913919, 0.0822185}, {0.0244389, 0.966447, 0.00911374}, - {0.003135, 0.991799, 0.0050661}}); - auto res_prob_baode = std::vector>({{0.0112349, 0.962274, 0.0264907}, + {0.003135, 0.991799, 0.0050661} }); + auto res_prob_baode = std::vector>({ {0.0112349, 0.962274, 0.0264907}, {0.00371025, 0.950592, 0.0456973}, {0.00371025, 0.950592, 0.0456973}, {0.00371025, 0.950592, 0.0456973}, @@ -171,21 +179,22 @@ TEST_CASE("Model predict_proba", "[Models]") { {0.0252205, 0.113564, 0.861215}, {0.0284828, 0.770524, 0.200993}, {0.0213182, 0.857189, 0.121493}, - {0.00868436, 0.949494, 0.0418215}}); + {0.00868436, 0.949494, 0.0418215} }); auto res_prob_voting = std::vector>( - {{0, 1, 0}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0}, {0, 0, 1}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0}}); - std::map>> res_prob{{"TAN", res_prob_tan}, + { {0, 1, 0}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0}, {0, 0, 1}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0} }); + std::map>> res_prob{ {"TAN", res_prob_tan}, {"SPODE", res_prob_spode}, {"BoostAODEproba", res_prob_baode}, - {"BoostAODEvoting", res_prob_voting}}; - std::map models{{"TAN", new bayesnet::TAN()}, + {"BoostAODEvoting", res_prob_voting} }; + std::map models{ {"TAN", new bayesnet::TAN()}, {"SPODE", new bayesnet::SPODE(0)}, {"BoostAODEproba", new bayesnet::BoostAODE(false)}, - {"BoostAODEvoting", new bayesnet::BoostAODE(true)}}; + {"BoostAODEvoting", new bayesnet::BoostAODE(true)} }; int init_index = 78; auto raw = RawDatasets("iris", true); - SECTION("Test " + model + " predict_proba") { + SECTION("Test " + model + " predict_proba") + { auto clf = models[model]; clf->fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); auto y_pred_proba = clf->predict_proba(raw.Xv); @@ -214,14 +223,15 @@ TEST_CASE("Model predict_proba", "[Models]") { for (int j = 0; j < 3; j++) { REQUIRE(res_prob[model][i][j] == Catch::Approx(y_pred_proba[i + init_index][j]).epsilon(raw.epsilon)); REQUIRE(res_prob[model][i][j] == - Catch::Approx(yt_pred_proba[i + init_index][j].item()).epsilon(raw.epsilon)); + Catch::Approx(yt_pred_proba[i + init_index][j].item()).epsilon(raw.epsilon)); } } delete clf; } } -TEST_CASE("AODE voting-proba", "[Models]") { +TEST_CASE("AODE voting-proba", "[Models]") +{ auto raw = RawDatasets("glass", true); auto clf = bayesnet::AODE(false); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); @@ -229,7 +239,7 @@ TEST_CASE("AODE voting-proba", "[Models]") { 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.79439f).epsilon(raw.epsilon)); @@ -238,7 +248,8 @@ TEST_CASE("AODE voting-proba", "[Models]") { REQUIRE(pred_proba[67][0] == Catch::Approx(0.702184).epsilon(raw.epsilon)); REQUIRE(clf.topological_order() == std::vector()); } -TEST_CASE("SPODELd dataset", "[Models]") { +TEST_CASE("SPODELd dataset", "[Models]") +{ auto raw = RawDatasets("iris", false); auto clf = bayesnet::SPODELd(0); // raw.dataset.to(torch::kFloat32); @@ -249,7 +260,8 @@ TEST_CASE("SPODELd dataset", "[Models]") { REQUIRE(score == Catch::Approx(0.97333f).epsilon(raw.epsilon)); REQUIRE(scoret == Catch::Approx(0.97333f).epsilon(raw.epsilon)); } -TEST_CASE("KDB with hyperparameters", "[Models]") { +TEST_CASE("KDB with hyperparameters", "[Models]") +{ auto raw = RawDatasets("glass", true); auto clf = bayesnet::KDB(2); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); @@ -257,18 +269,20 @@ TEST_CASE("KDB with hyperparameters", "[Models]") { clf.setHyperparameters({ {"k", 3}, {"theta", 0.7}, - }); + }); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); auto scoret = clf.score(raw.Xv, raw.yv); REQUIRE(score == Catch::Approx(0.827103).epsilon(raw.epsilon)); REQUIRE(scoret == Catch::Approx(0.761682).epsilon(raw.epsilon)); } -TEST_CASE("Incorrect type of data for SPODELd", "[Models]") { +TEST_CASE("Incorrect type of data for SPODELd", "[Models]") +{ auto raw = RawDatasets("iris", true); auto clf = bayesnet::SPODELd(0); REQUIRE_THROWS_AS(clf.fit(raw.dataset, raw.features, raw.className, raw.states, raw.smoothing), std::runtime_error); } -TEST_CASE("Predict, predict_proba & score without fitting", "[Models]") { +TEST_CASE("Predict, predict_proba & score without fitting", "[Models]") +{ auto clf = bayesnet::AODE(); auto raw = RawDatasets("iris", true); std::string message = "Ensemble has not been fitted"; @@ -285,55 +299,83 @@ TEST_CASE("Predict, predict_proba & score without fitting", "[Models]") { REQUIRE_THROWS_WITH(clf.score(raw.Xv, raw.yv), message); REQUIRE_THROWS_WITH(clf.score(raw.Xt, raw.yt), message); } -TEST_CASE("TAN & SPODE with hyperparameters", "[Models]") { +TEST_CASE("TAN & SPODE with hyperparameters", "[Models]") +{ auto raw = RawDatasets("iris", true); auto clf = bayesnet::TAN(); clf.setHyperparameters({ {"parent", 1}, - }); + }); clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); auto score = clf.score(raw.Xv, raw.yv); REQUIRE(score == Catch::Approx(0.973333).epsilon(raw.epsilon)); auto clf2 = bayesnet::SPODE(0); clf2.setHyperparameters({ {"parent", 1}, - }); + }); clf2.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing); auto score2 = clf2.score(raw.Xv, raw.yv); REQUIRE(score2 == Catch::Approx(0.973333).epsilon(raw.epsilon)); } -TEST_CASE("TAN & SPODE with invalid hyperparameters", "[Models]") { +TEST_CASE("TAN & SPODE with invalid hyperparameters", "[Models]") +{ auto raw = RawDatasets("iris", true); auto clf = bayesnet::TAN(); clf.setHyperparameters({ {"parent", 5}, - }); + }); REQUIRE_THROWS_AS(clf.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing), - std::invalid_argument); + std::invalid_argument); auto clf2 = bayesnet::SPODE(0); clf2.setHyperparameters({ {"parent", 5}, - }); + }); REQUIRE_THROWS_AS(clf2.fit(raw.Xv, raw.yv, raw.features, raw.className, raw.states, raw.smoothing), - std::invalid_argument); + std::invalid_argument); } -TEST_CASE("Check proposal checkInput", "[Models]") { +TEST_CASE("Check proposal checkInput", "[Models]") +{ class testProposal : public bayesnet::Proposal { - public: - testProposal(torch::Tensor &dataset_, std::vector &features_, std::string &className_) - : Proposal(dataset_, features_, className_) {} - void test_X_y(const torch::Tensor &X, const torch::Tensor &y) { checkInput(X, y); } + public: + testProposal(torch::Tensor& dataset_, std::vector& features_, std::string& className_) + : Proposal(dataset_, features_, className_) + { + } + void test_X_y(const torch::Tensor& X, const torch::Tensor& y) { checkInput(X, y); } }; auto raw = RawDatasets("iris", true); auto clf = testProposal(raw.dataset, raw.features, raw.className); - torch::Tensor X = torch::randint(0, 3, {10, 4}); - torch::Tensor y = torch::rand({10}); + torch::Tensor X = torch::randint(0, 3, { 10, 4 }); + torch::Tensor y = torch::rand({ 10 }); INFO("Check X is not float"); REQUIRE_THROWS_AS(clf.test_X_y(X, y), std::invalid_argument); - X = torch::rand({10, 4}); + X = torch::rand({ 10, 4 }); INFO("Check y is not integer"); REQUIRE_THROWS_AS(clf.test_X_y(X, y), std::invalid_argument); - y = torch::randint(0, 3, {10}); + y = torch::randint(0, 3, { 10 }); INFO("X and y are correct"); REQUIRE_NOTHROW(clf.test_X_y(X, y)); } +TEST_CASE("Check KDB loop detection", "[Models]") +{ + class testKDB : public bayesnet::KDB { + public: + testKDB() : KDB(2, 0) {} + void test_add_m_edges(std::vector features_, int idx, std::vector& S, torch::Tensor& weights) + { + features = features_; + add_m_edges(idx, S, weights); + } + }; + auto clf = testKDB(); + auto features = std::vector{ "A", "B", "C" }; + int idx = 0; + std::vector S = { 0 }; + torch::Tensor weights = torch::tensor({ + { 1.0, 10.0, 0.0 }, // row0 -> picks col1 + { 0.0, 1.0, 10.0 }, // row1 -> picks col2 + { 10.0, 0.0, 1.0 }, // row2 -> picks col0 + }); + REQUIRE_NOTHROW(clf.test_add_m_edges(features, 0, S, weights)); + REQUIRE_NOTHROW(clf.test_add_m_edges(features, 1, S, weights)); +}