From 443e5cc88204452f4c40e195e0c0572904f676c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana=20G=C3=B3mez?= Date: Thu, 22 Feb 2024 11:45:40 +0100 Subject: [PATCH] Implement classifier.predict_proba & test --- CHANGELOG.md | 8 ++++- src/BayesNet/BaseClassifier.h | 3 ++ src/BayesNet/BoostAODE.cc | 2 +- src/BayesNet/Classifier.cc | 34 ++++++++++++++++--- src/BayesNet/Classifier.h | 51 +++++++++++++++-------------- src/BayesNet/Ensemble.cc | 61 ++++++++++++++++++++++------------- src/BayesNet/Ensemble.h | 4 +-- src/BayesNet/Network.h | 34 +++++++++---------- tests/TestBayesModels.cc | 40 ++++++++++++++++++++++- 9 files changed, 165 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fa6c5a..24c41a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## =[Unreleased] + +### Added + +- predict_proba method in Classifier +- predict_proba method in BoostAODE +- predict_voting parameter in BoostAODE constructor to use voting or probability to predict (default is voting) ## [1.0.2] - 2024-02-20 diff --git a/src/BayesNet/BaseClassifier.h b/src/BayesNet/BaseClassifier.h index e84f494..8e5631c 100644 --- a/src/BayesNet/BaseClassifier.h +++ b/src/BayesNet/BaseClassifier.h @@ -16,12 +16,15 @@ namespace bayesnet { virtual ~BaseClassifier() = default; torch::Tensor virtual predict(torch::Tensor& X) = 0; std::vector virtual predict(std::vector>& X) = 0; + torch::Tensor virtual predict_proba(torch::Tensor& X) = 0; + std::vector> virtual predict_proba(std::vector>& X) = 0; status_t virtual getStatus() const = 0; float virtual score(std::vector>& X, std::vector& y) = 0; float virtual score(torch::Tensor& X, torch::Tensor& y) = 0; int virtual getNumberOfNodes()const = 0; int virtual getNumberOfEdges()const = 0; int virtual getNumberOfStates() const = 0; + int virtual getClassNumStates() const = 0; std::vector virtual show() const = 0; std::vector virtual graph(const std::string& title = "") const = 0; virtual std::string getVersion() = 0; diff --git a/src/BayesNet/BoostAODE.cc b/src/BayesNet/BoostAODE.cc index 959cada..fe6ab72 100644 --- a/src/BayesNet/BoostAODE.cc +++ b/src/BayesNet/BoostAODE.cc @@ -8,7 +8,7 @@ #include "folding.hpp" namespace bayesnet { - BoostAODE::BoostAODE() : Ensemble() + BoostAODE::BoostAODE() : Ensemble(false) { validHyperparameters = { "repeatSparent", "maxModels", "ascending", "convergence", "threshold", "select_features", "tolerance" }; diff --git a/src/BayesNet/Classifier.cc b/src/BayesNet/Classifier.cc index c8ee3ef..176f369 100644 --- a/src/BayesNet/Classifier.cc +++ b/src/BayesNet/Classifier.cc @@ -3,6 +3,7 @@ namespace bayesnet { Classifier::Classifier(Network model) : model(model), m(0), n(0), metrics(Metrics()), fitted(false) {} + const std::string CLASSIFIER_NOT_FITTED = "Classifier has not been fitted"; Classifier& Classifier::build(const std::vector& features, const std::string& className, std::map>& states, const torch::Tensor& weights) { this->features = features; @@ -87,14 +88,14 @@ namespace bayesnet { torch::Tensor Classifier::predict(torch::Tensor& X) { if (!fitted) { - throw std::logic_error("Classifier has not been fitted"); + throw std::logic_error(CLASSIFIER_NOT_FITTED); } return model.predict(X); } std::vector Classifier::predict(std::vector>& X) { if (!fitted) { - throw std::logic_error("Classifier has not been fitted"); + throw std::logic_error(CLASSIFIER_NOT_FITTED); } auto m_ = X[0].size(); auto n_ = X.size(); @@ -105,10 +106,31 @@ namespace bayesnet { auto yp = model.predict(Xd); return yp; } + torch::Tensor Classifier::predict_proba(torch::Tensor& X) + { + if (!fitted) { + throw std::logic_error(CLASSIFIER_NOT_FITTED); + } + return model.predict_proba(X); + } + std::vector> Classifier::predict_proba(std::vector>& X) + { + if (!fitted) { + throw std::logic_error(CLASSIFIER_NOT_FITTED); + } + auto m_ = X[0].size(); + auto n_ = X.size(); + std::vector> Xd(n_, std::vector(m_, 0)); + for (auto i = 0; i < n_; i++) { + Xd[i] = std::vector(X[i].begin(), X[i].end()); + } + auto yp = model.predict_proba(Xd); + return yp; + } float Classifier::score(torch::Tensor& X, torch::Tensor& y) { if (!fitted) { - throw std::logic_error("Classifier has not been fitted"); + throw std::logic_error(CLASSIFIER_NOT_FITTED); } torch::Tensor y_pred = predict(X); return (y_pred == y).sum().item() / y.size(0); @@ -116,7 +138,7 @@ namespace bayesnet { float Classifier::score(std::vector>& X, std::vector& y) { if (!fitted) { - throw std::logic_error("Classifier has not been fitted"); + throw std::logic_error(CLASSIFIER_NOT_FITTED); } return model.score(X, y); } @@ -145,6 +167,10 @@ namespace bayesnet { { return fitted ? model.getStates() : 0; } + int Classifier::getClassNumStates() const + { + return fitted ? model.getClassNumStates() : 0; + } std::vector Classifier::topological_order() { return model.topological_sort(); diff --git a/src/BayesNet/Classifier.h b/src/BayesNet/Classifier.h index 24657ca..639ab8f 100644 --- a/src/BayesNet/Classifier.h +++ b/src/BayesNet/Classifier.h @@ -7,8 +7,31 @@ namespace bayesnet { class Classifier : public BaseClassifier { - private: - Classifier& build(const std::vector& features, const std::string& className, std::map>& states, const torch::Tensor& weights); + public: + Classifier(Network model); + virtual ~Classifier() = default; + Classifier& fit(std::vector>& X, std::vector& y, const std::vector& features, const std::string& className, std::map>& states) override; + Classifier& fit(torch::Tensor& X, torch::Tensor& y, const std::vector& features, const std::string& className, std::map>& states) override; + Classifier& fit(torch::Tensor& dataset, const std::vector& features, const std::string& className, std::map>& states) override; + Classifier& fit(torch::Tensor& dataset, const std::vector& features, const std::string& className, std::map>& states, const torch::Tensor& weights) override; + void addNodes(); + int getNumberOfNodes() const override; + int getNumberOfEdges() const override; + int getNumberOfStates() const override; + int getClassNumStates() const override; + torch::Tensor predict(torch::Tensor& X) override; + std::vector predict(std::vector>& X) override; + torch::Tensor predict_proba(torch::Tensor& X) override; + std::vector> predict_proba(std::vector>& X) override; + status_t getStatus() const override { return status; } + std::string getVersion() override { return { project_version.begin(), project_version.end() }; }; + float score(torch::Tensor& X, torch::Tensor& y) override; + float score(std::vector>& X, std::vector& y) override; + std::vector show() const override; + std::vector topological_order() override; + std::vector getNotes() const override { return notes; } + void dump_cpt() const override; + void setHyperparameters(const nlohmann::json& hyperparameters) override; //For classifiers that don't have hyperparameters protected: bool fitted; int m, n; // m: number of samples, n: number of features @@ -24,28 +47,8 @@ namespace bayesnet { virtual void buildModel(const torch::Tensor& weights) = 0; void trainModel(const torch::Tensor& weights) override; void buildDataset(torch::Tensor& y); - public: - Classifier(Network model); - virtual ~Classifier() = default; - Classifier& fit(std::vector>& X, std::vector& y, const std::vector& features, const std::string& className, std::map>& states) override; - Classifier& fit(torch::Tensor& X, torch::Tensor& y, const std::vector& features, const std::string& className, std::map>& states) override; - Classifier& fit(torch::Tensor& dataset, const std::vector& features, const std::string& className, std::map>& states) override; - Classifier& fit(torch::Tensor& dataset, const std::vector& features, const std::string& className, std::map>& states, const torch::Tensor& weights) override; - void addNodes(); - int getNumberOfNodes() const override; - int getNumberOfEdges() const override; - int getNumberOfStates() const override; - torch::Tensor predict(torch::Tensor& X) override; - status_t getStatus() const override { return status; } - std::string getVersion() override { return { project_version.begin(), project_version.end() }; }; - std::vector predict(std::vector>& X) override; - float score(torch::Tensor& X, torch::Tensor& y) override; - float score(std::vector>& X, std::vector& y) override; - std::vector show() const override; - std::vector topological_order() override; - std::vector getNotes() const override { return notes; } - void dump_cpt() const override; - void setHyperparameters(const nlohmann::json& hyperparameters) override; //For classifiers that don't have hyperparameters + private: + Classifier& build(const std::vector& features, const std::string& className, std::map>& states, const torch::Tensor& weights); }; } #endif diff --git a/src/BayesNet/Ensemble.cc b/src/BayesNet/Ensemble.cc index 2eedc8d..966275d 100644 --- a/src/BayesNet/Ensemble.cc +++ b/src/BayesNet/Ensemble.cc @@ -2,7 +2,10 @@ namespace bayesnet { - Ensemble::Ensemble(bool predict_voting) : Classifier(Network()), n_models(0), predict_voting(predict_voting) {}; + Ensemble::Ensemble(bool predict_voting) : Classifier(Network()), n_models(0), predict_voting(predict_voting) + { + + }; const std::string ENSEMBLE_NOT_FITTED = "Ensemble has not been fitted"; void Ensemble::trainModel(const torch::Tensor& weights) { @@ -37,7 +40,7 @@ namespace bayesnet { if (!fitted) { throw std::logic_error(ENSEMBLE_NOT_FITTED); } - return predict_voting ? do_predict_voting(X) : do_predict_prob(X); + return do_predict_voting(X); } torch::Tensor Ensemble::predict(torch::Tensor& X) @@ -45,27 +48,32 @@ namespace bayesnet { if (!fitted) { throw std::logic_error(ENSEMBLE_NOT_FITTED); } - return predict_voting ? do_predict_voting(X) : do_predict_prob(X); + return do_predict_voting(X); } - torch::Tensor Ensemble::do_predict_prob(torch::Tensor& X) + torch::Tensor Ensemble::predict_proba(torch::Tensor& X) { - torch::Tensor y_pred = torch::zeros({ X.size(1), n_models }, torch::kFloat32); - // auto threads{ std::vector() }; - // std::mutex mtx; - // for (auto i = 0; i < n_models; ++i) { - // threads.push_back(std::thread([&, i]() { - // auto ypredict = models[i]->predict(X); - // std::lock_guard lock(mtx); - // y_pred.index_put_({ "...", i }, ypredict); - // })); - // } - // for (auto& thread : threads) { - // thread.join(); - // } + auto n_states = getClassNumStates(); + torch::Tensor y_pred = torch::zeros({ X.size(1), n_states }, torch::kFloat32); + auto threads{ std::vector() }; + std::mutex mtx; + for (auto i = 0; i < n_models; ++i) { + threads.push_back(std::thread([&, i]() { + auto ypredict = models[i]->predict_proba(X); + ypredict *= significanceModels[i]; + std::lock_guard lock(mtx); + y_pred.index_put_({ "...", i }, ypredict); + })); + } + for (auto& thread : threads) { + thread.join(); + } + auto sum = std::reduce(significanceModels.begin(), significanceModels.end()); + y_pred /= sum; return y_pred; } - std::vector Ensemble::do_predict_prob(std::vector>& X) + std::vector> Ensemble::predict_proba(std::vector>& X) { + // long m_ = X[0].size(); // long n_ = X.size(); // vector> Xd(n_, vector(m_, 0)); @@ -77,7 +85,7 @@ namespace bayesnet { // y_pred.index_put_({ "...", i }, torch::tensor(models[i]->predict(Xd), torch::kInt32)); // } // return voting(y_pred); - return std::vector(); + return std::vector>(); } torch::Tensor Ensemble::do_predict_voting(torch::Tensor& X) { @@ -105,14 +113,23 @@ namespace bayesnet { Xd[i] = std::vector(X[i].begin(), X[i].end()); } torch::Tensor y_pred = torch::zeros({ m_, n_models }, torch::kInt32); + auto threads{ std::vector() }; + std::mutex mtx; for (auto i = 0; i < n_models; ++i) { - y_pred.index_put_({ "...", i }, torch::tensor(models[i]->predict(Xd), torch::kInt32)); + threads.push_back(std::thread([&, i]() { + auto ypredict = models[i]->predict(Xd); + std::lock_guard lock(mtx); + y_pred.index_put_({ "...", i }, torch::tensor(ypredict, torch::kInt32)); + })); + } + for (auto& thread : threads) { + thread.join(); } return voting(y_pred); } float Ensemble::score(torch::Tensor& X, torch::Tensor& y) { - auto y_pred = predict_voting ? do_predict_voting(X) : do_predict_prob(X); + auto y_pred = do_predict_voting(X); int correct = 0; for (int i = 0; i < y_pred.size(0); ++i) { if (y_pred[i].item() == y[i].item()) { @@ -123,7 +140,7 @@ namespace bayesnet { } float Ensemble::score(std::vector>& X, std::vector& y) { - auto y_pred = predict_voting ? do_predict_voting(X) : do_predict_prob(X); + auto y_pred = do_predict_voting(X); int correct = 0; for (int i = 0; i < y_pred.size(); ++i) { if (y_pred[i] == y[i]) { diff --git a/src/BayesNet/Ensemble.h b/src/BayesNet/Ensemble.h index b748235..da18fd6 100644 --- a/src/BayesNet/Ensemble.h +++ b/src/BayesNet/Ensemble.h @@ -12,10 +12,10 @@ namespace bayesnet { virtual ~Ensemble() = default; torch::Tensor predict(torch::Tensor& X) override; std::vector predict(std::vector>& X) override; + torch::Tensor predict_proba(torch::Tensor& X) override; + std::vector> predict_proba(std::vector>& X) override; torch::Tensor do_predict_voting(torch::Tensor& X); std::vector do_predict_voting(std::vector>& X); - torch::Tensor do_predict_prob(torch::Tensor& X); - std::vector do_predict_prob(std::vector>& X); float score(torch::Tensor& X, torch::Tensor& y) override; float score(std::vector>& X, std::vector& y) override; int getNumberOfNodes() const override; diff --git a/src/BayesNet/Network.h b/src/BayesNet/Network.h index 2a3795e..ad45055 100644 --- a/src/BayesNet/Network.h +++ b/src/BayesNet/Network.h @@ -7,23 +7,6 @@ namespace bayesnet { class Network { - private: - std::map> nodes; - bool fitted; - float maxThreads = 0.95; - int classNumStates; - std::vector features; // Including classname - std::string className; - double laplaceSmoothing; - torch::Tensor samples; // nxm tensor used to fit the model - bool isCyclic(const std::string&, std::unordered_set&, std::unordered_set&); - std::vector predict_sample(const std::vector&); - std::vector predict_sample(const torch::Tensor&); - std::vector exactInference(std::map&); - double computeFactor(std::map&); - void completeFit(const std::map>& states, const torch::Tensor& weights); - void checkFitData(int n_features, int n_samples, int n_samples_y, const std::vector& featureNames, const std::string& className, const std::map>& states, const torch::Tensor& weights); - void setStates(const std::map>&); public: Network(); explicit Network(float); @@ -58,6 +41,23 @@ namespace bayesnet { void initialize(); void dump_cpt() const; inline std::string version() { return { project_version.begin(), project_version.end() }; } + private: + std::map> nodes; + bool fitted; + float maxThreads = 0.95; + int classNumStates; + std::vector features; // Including classname + std::string className; + double laplaceSmoothing; + torch::Tensor samples; // nxm tensor used to fit the model + bool isCyclic(const std::string&, std::unordered_set&, std::unordered_set&); + std::vector predict_sample(const std::vector&); + std::vector predict_sample(const torch::Tensor&); + std::vector exactInference(std::map&); + double computeFactor(std::map&); + void completeFit(const std::map>& states, const torch::Tensor& weights); + void checkFitData(int n_features, int n_samples, int n_samples_y, const std::vector& featureNames, const std::string& className, const std::map>& states, const torch::Tensor& weights); + void setStates(const std::map>&); }; } #endif \ No newline at end of file diff --git a/tests/TestBayesModels.cc b/tests/TestBayesModels.cc index bed1783..26912c0 100644 --- a/tests/TestBayesModels.cc +++ b/tests/TestBayesModels.cc @@ -165,7 +165,6 @@ TEST_CASE("BoostAODE test used features in train note", "[BayesNet]") {"convergence", true}, {"repeatSparent",true}, {"select_features","CFS"}, - {"tolerance", 3} }); clf.fit(raw.Xv, raw.yv, raw.featuresv, raw.classNamev, raw.statesv); REQUIRE(clf.getNumberOfNodes() == 72); @@ -175,3 +174,42 @@ TEST_CASE("BoostAODE test used features in train note", "[BayesNet]") REQUIRE(clf.getNotes()[1] == "Used features in train: 7 of 8"); REQUIRE(clf.getNotes()[2] == "Number of models: 8"); } +TEST_CASE("TAN predict_proba", "[BayesNet]") +{ + auto raw = RawDatasets("iris", true); + auto clf = bayesnet::TAN(); + clf.fit(raw.Xv, raw.yv, raw.featuresv, raw.classNamev, raw.statesv); + auto y_pred_proba = clf.predict_proba(raw.Xv); + auto y_pred = clf.predict(raw.Xv); + auto yt_pred_proba = clf.predict_proba(raw.Xt); + REQUIRE(y_pred.size() == y_pred_proba.size()); + REQUIRE(y_pred.size() == yt_pred_proba.size(0)); + REQUIRE(y_pred.size() == raw.yv.size()); + REQUIRE(y_pred_proba[0].size() == 3); + REQUIRE(yt_pred_proba.size(1) == y_pred_proba[0].size()); + for (int i = 0; i < y_pred_proba.size(); ++i) { + auto maxElem = max_element(y_pred_proba[i].begin(), y_pred_proba[i].end()); + int predictedClass = distance(y_pred_proba[i].begin(), maxElem); + REQUIRE(predictedClass == y_pred[i]); + REQUIRE(yt_pred_proba[i].argmax().item() == y_pred[i]); + } +} + +// TEST_CASE("BoostAODE predict_proba", "[BayesNet]") +// { +// auto raw = RawDatasets("iris", true); +// auto clf = bayesnet::BoostAODE(); +// clf.fit(raw.Xv, raw.yv, raw.featuresv, raw.classNamev, raw.statesv); +// auto y_pred = clf.predict_proba(raw.Xv); +// REQUIRE(y_pred.size(0) == raw.yv.size(0)); +// REQUIRE(y_pred.size(1) == 3); +// auto y_pred2 = clf.predict_proba(raw.Xv); +// REQUIRE(y_pred2.size(0) == raw.yv.size(0)); +// REQUIRE(y_pred2.size(1) == 3); +// REQUIRE(y_pred.equal(y_pred2)); +// for (int i = 0; i < y_pred.size(0); ++i) { +// for (int j = 0; j < y_pred.size(1); ++j) { +// REQUIRE(y_pred[i][j].item() == y_pred2[i][j].item()); +// } +// } +// }