From ca54f799ee12efc531d618c390db2d68acb9b929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana=20G=C3=B3mez?= Date: Mon, 10 Mar 2025 11:18:04 +0100 Subject: [PATCH] Fix XSpode predict --- bayesnet/classifiers/Classifier.cc | 3 + bayesnet/classifiers/XSPODE.cc | 58 ++++++- bayesnet/classifiers/XSPODE.h | 19 ++- tests/CMakeLists.txt | 1 + tests/TestBayesModels.cc | 18 ++- tests/TestBoostXBAODE.cc | 234 +++++++++++++++++++++++++++++ 6 files changed, 310 insertions(+), 23 deletions(-) create mode 100644 tests/TestBoostXBAODE.cc diff --git a/bayesnet/classifiers/Classifier.cc b/bayesnet/classifiers/Classifier.cc index 5262401..2af73e4 100644 --- a/bayesnet/classifiers/Classifier.cc +++ b/bayesnet/classifiers/Classifier.cc @@ -22,8 +22,11 @@ namespace bayesnet { auto n_classes = states.at(className).size(); metrics = Metrics(dataset, features, className, n_classes); model.initialize(); + std::cout << "Ahora buildmodel"<< std::endl; buildModel(weights); + std::cout << "Ahora trainmodel"<< std::endl; trainModel(weights, smoothing); + std::cout << "Después de trainmodel"<< std::endl; fitted = true; return *this; } diff --git a/bayesnet/classifiers/XSPODE.cc b/bayesnet/classifiers/XSPODE.cc index 91711d2..a139747 100644 --- a/bayesnet/classifiers/XSPODE.cc +++ b/bayesnet/classifiers/XSPODE.cc @@ -3,7 +3,12 @@ // SPDX-FileType: SOURCE // SPDX-License-Identifier: MIT // *************************************************************** - +#include +#include +#include +#include +#include +#include #include "XSPODE.h" @@ -20,6 +25,17 @@ namespace bayesnet { 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(std::vector>& X, std::vector& y, torch::Tensor& weights_, const Smoothing_t smoothing) @@ -28,6 +44,7 @@ namespace bayesnet { n = X.size(); buildModel(weights_); trainModel(weights_, smoothing); + fitted=true; } // -------------------------------------- @@ -89,7 +106,7 @@ namespace bayesnet { for (int f = 0; f < nFeatures_; f++) { instance[f] = dataset[f][i].item(); } - instance[nFeatures_] = dataset[-1].item(); + instance[nFeatures_] = dataset[-1][i].item(); addSample(instance, weights[i].item()); } @@ -205,7 +222,6 @@ namespace bayesnet { } } } - } // -------------------------------------- @@ -218,8 +234,10 @@ namespace bayesnet { // -------------------------------------- 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++) { @@ -295,9 +313,6 @@ namespace bayesnet { } std::vector XSpode::predict(std::vector>& test_data) { - if (!fitted) { - throw std::logic_error(CLASSIFIER_NOT_FITTED); - } auto probabilities = predict_proba(test_data); std::vector predictions(probabilities.size(), 0); @@ -375,5 +390,34 @@ namespace bayesnet { } std::vector& XSpode::getStates() { return states_; } + // ------------------------------------------------------ + // Predict overrides (classifier interface) + // ------------------------------------------------------ + torch::Tensor predict(torch::Tensor& X) + { + auto X_ = TensorUtils::to_matrix(X); + return predict(X_); + } + std::vector predict(std::vector>& X) + { + auto proba = predict_proba(X); + std::vector predictions(proba.size(), 0); + for (size_t i = 0; i < proba.size(); i++) { + predictions[i] = std::distance(proba[i].begin(), std::max_element(proba[i].begin(), proba[i].end())); + } + return predictions; + } + torch::Tensor predict_proba(torch::Tensor& X) + { + auto X_ = TensorUtils::to_matrix(X); + return predict_proba(X_); + } + torch::Tensor Classifier::predict(torch::Tensor& X) + { + auto X_ = TensorUtils::to_matrix(X); + auto predict = predict(X_); + return TensorUtils::to_tensor(predict); + } + } diff --git a/bayesnet/classifiers/XSPODE.h b/bayesnet/classifiers/XSPODE.h index 41301ad..03848de 100644 --- a/bayesnet/classifiers/XSPODE.h +++ b/bayesnet/classifiers/XSPODE.h @@ -8,15 +8,6 @@ #define XSPODE_H #include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include "Classifier.h" #include "bayesnet/utils/CountingSemaphore.h" @@ -32,7 +23,6 @@ namespace bayesnet { std::vector predict(std::vector>& test_data); void normalize(std::vector& v) const; std::string to_string() const; - int statesClass() const; int getNFeatures() const; int getNumberOfNodes() const override; int getNumberOfEdges() const override; @@ -41,6 +31,15 @@ namespace bayesnet { 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 setHyperparameters(const nlohmann::json& hyperparameters_) override; + + // + // Classifier interface + // + 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; protected: void buildModel(const torch::Tensor& weights) override; void trainModel(const torch::Tensor& weights, const bayesnet::Smoothing_t smoothing) override; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 11f11f0..e79c36b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ if(ENABLE_TESTING) add_test(NAME WA2DE COMMAND TestBayesNet "[WA2DE]") add_test(NAME BoostA2DE COMMAND TestBayesNet "[BoostA2DE]") add_test(NAME BoostAODE COMMAND TestBayesNet "[BoostAODE]") + add_test(NAME XBAODE COMMAND TestBayesNet "[XBAODE]") add_test(NAME Classifier COMMAND TestBayesNet "[Classifier]") add_test(NAME Ensemble COMMAND TestBayesNet "[Ensemble]") add_test(NAME FeatureSelection COMMAND TestBayesNet "[FeatureSelection]") diff --git a/tests/TestBayesModels.cc b/tests/TestBayesModels.cc index 50616ca..127568f 100644 --- a/tests/TestBayesModels.cc +++ b/tests/TestBayesModels.cc @@ -12,6 +12,7 @@ #include "bayesnet/classifiers/KDB.h" #include "bayesnet/classifiers/TAN.h" #include "bayesnet/classifiers/SPODE.h" +#include "bayesnet/classifiers/XSPODE.h" #include "bayesnet/classifiers/TANLd.h" #include "bayesnet/classifiers/KDBLd.h" #include "bayesnet/classifiers/SPODELd.h" @@ -26,26 +27,27 @@ TEST_CASE("Test Bayesian Classifiers score & version", "[Models]") { map , float> scores{ // Diabetes - {{"diabetes", "AODE"}, 0.82161}, {{"diabetes", "KDB"}, 0.852865}, {{"diabetes", "SPODE"}, 0.802083}, {{"diabetes", "TAN"}, 0.821615}, + {{"diabetes", "AODE"}, 0.82161}, {{"diabetes", "KDB"}, 0.852865}, {{"diabetes", "XSPODE"}, 0.802083}, {{"diabetes", "SPODE"}, 0.802083}, {{"diabetes", "TAN"}, 0.821615}, {{"diabetes", "AODELd"}, 0.8125f}, {{"diabetes", "KDBLd"}, 0.80208f}, {{"diabetes", "SPODELd"}, 0.7890625f}, {{"diabetes", "TANLd"}, 0.803385437f}, {{"diabetes", "BoostAODE"}, 0.83984f}, // Ecoli - {{"ecoli", "AODE"}, 0.889881}, {{"ecoli", "KDB"}, 0.889881}, {{"ecoli", "SPODE"}, 0.880952}, {{"ecoli", "TAN"}, 0.892857}, + {{"ecoli", "AODE"}, 0.889881}, {{"ecoli", "KDB"}, 0.889881}, {{"ecoli", "XSPODE"}, 0.880952}, {{"ecoli", "SPODE"}, 0.880952}, {{"ecoli", "TAN"}, 0.892857}, {{"ecoli", "AODELd"}, 0.875f}, {{"ecoli", "KDBLd"}, 0.880952358f}, {{"ecoli", "SPODELd"}, 0.839285731f}, {{"ecoli", "TANLd"}, 0.848214269f}, {{"ecoli", "BoostAODE"}, 0.89583f}, // Glass - {{"glass", "AODE"}, 0.79439}, {{"glass", "KDB"}, 0.827103}, {{"glass", "SPODE"}, 0.775701}, {{"glass", "TAN"}, 0.827103}, + {{"glass", "AODE"}, 0.79439}, {{"glass", "KDB"}, 0.827103}, {{"glass", "XSPODE"}, 0.775701}, {{"glass", "SPODE"}, 0.775701}, {{"glass", "TAN"}, 0.827103}, {{"glass", "AODELd"}, 0.799065411f}, {{"glass", "KDBLd"}, 0.82710278f}, {{"glass", "SPODELd"}, 0.780373812f}, {{"glass", "TANLd"}, 0.869158864f}, {{"glass", "BoostAODE"}, 0.84579f}, // Iris - {{"iris", "AODE"}, 0.973333}, {{"iris", "KDB"}, 0.973333}, {{"iris", "SPODE"}, 0.973333}, {{"iris", "TAN"}, 0.973333}, + {{"iris", "AODE"}, 0.973333}, {{"iris", "KDB"}, 0.973333}, {{"iris", "XSPODE"}, 0.973333}, {{"iris", "SPODE"}, 0.973333}, {{"iris", "TAN"}, 0.973333}, {{"iris", "AODELd"}, 0.973333}, {{"iris", "KDBLd"}, 0.973333}, {{"iris", "SPODELd"}, 0.96f}, {{"iris", "TANLd"}, 0.97333f}, {{"iris", "BoostAODE"}, 0.98f} }; std::map models{ {"AODE", new bayesnet::AODE()}, {"AODELd", new bayesnet::AODELd()}, {"BoostAODE", new bayesnet::BoostAODE()}, {"KDB", new bayesnet::KDB(2)}, {"KDBLd", new bayesnet::KDBLd(2)}, - {"SPODE", new bayesnet::SPODE(1)}, {"SPODELd", new bayesnet::SPODELd(1)}, + {"XSPODE", new bayesnet::XSpode(1)}, {"SPODE", new bayesnet::SPODE(1)}, {"SPODELd", new bayesnet::SPODELd(1)}, {"TAN", new bayesnet::TAN()}, {"TANLd", new bayesnet::TANLd()} }; - std::string name = GENERATE("AODE", "AODELd", "KDB", "KDBLd", "SPODE", "SPODELd", "TAN", "TANLd"); + // std::string name = GENERATE("AODE", "AODELd", "KDB", "KDBLd", "SPODE", "XSPODE", "SPODELd", "TAN", "TANLd"); + std::string name = GENERATE("XSPODE"); auto clf = models[name]; SECTION("Test " + name + " classifier") @@ -54,8 +56,12 @@ TEST_CASE("Test Bayesian Classifiers score & version", "[Models]") auto clf = models[name]; auto discretize = name.substr(name.length() - 2) != "Ld"; auto raw = RawDatasets(file_name, discretize); + if (name == "XSPODE") { + std::cout << "Fitting XSPODE" << std::endl; + } clf->fit(raw.Xt, raw.yt, raw.features, raw.className, raw.states, raw.smoothing); auto score = clf->score(raw.Xt, raw.yt); + std::cout << "Classifier: " << name << " File: " << file_name << " Score: " << score << " expected = " << scores[{file_name, name}] << std::endl; INFO("Classifier: " << name << " File: " << file_name); REQUIRE(score == Catch::Approx(scores[{file_name, name}]).epsilon(raw.epsilon)); REQUIRE(clf->getStatus() == bayesnet::NORMAL); diff --git a/tests/TestBoostXBAODE.cc b/tests/TestBoostXBAODE.cc new file mode 100644 index 0000000..f579b81 --- /dev/null +++ b/tests/TestBoostXBAODE.cc @@ -0,0 +1,234 @@ +// *************************************************************** +// 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