From dda9740e837562289efdbe94d4cf7407fad8446c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Monta=C3=B1ana=20G=C3=B3mez?= Date: Wed, 18 Jun 2025 18:03:19 +0200 Subject: [PATCH] Test AdaBoost fine but unoptimized --- src/experimental_clfs/AdaBoost.cpp | 238 ++++++++++++-- tests/TestAdaBoost.cpp | 499 +++++++---------------------- 2 files changed, 322 insertions(+), 415 deletions(-) diff --git a/src/experimental_clfs/AdaBoost.cpp b/src/experimental_clfs/AdaBoost.cpp index 3e276a2..4f21a77 100644 --- a/src/experimental_clfs/AdaBoost.cpp +++ b/src/experimental_clfs/AdaBoost.cpp @@ -300,6 +300,101 @@ namespace bayesnet { return predictions; } + // torch::Tensor AdaBoost::predict_proba(torch::Tensor& X) + // { + // if (!fitted) { + // throw std::runtime_error(CLASSIFIER_NOT_FITTED); + // } + + // if (models.empty()) { + // throw std::runtime_error("No models have been trained"); + // } + + // // X should be (n_features, n_samples) + // if (X.size(0) != n) { + // throw std::runtime_error("Input has wrong number of features. Expected " + + // std::to_string(n) + " but got " + std::to_string(X.size(0))); + // } + + // int n_samples = X.size(1); + // torch::Tensor probabilities = torch::zeros({ n_samples, n_classes }); + + // for (int i = 0; i < n_samples; i++) { + // auto sample = X.index({ torch::indexing::Slice(), i }); + // probabilities[i] = predictProbaSample(sample); + // } + + // return probabilities; + // } + + std::vector AdaBoost::predict(std::vector>& X) + { + // Convert to tensor - X is samples x features, need to transpose + torch::Tensor X_tensor = platform::TensorUtils::to_matrix(X); + auto predictions = predict(X_tensor); + std::vector result = platform::TensorUtils::to_vector(predictions); + return result; + } + + std::vector> AdaBoost::predict_proba(std::vector>& X) + { + auto n_samples = X[0].size(); + + if (debug) { + std::cout << "=== predict_proba vector method debug ===" << std::endl; + std::cout << "Input X dimensions: " << X.size() << " features x " << n_samples << " samples" << std::endl; + std::cout << "Input data:" << std::endl; + for (size_t i = 0; i < X.size(); i++) { + std::cout << " Feature " << i << ": ["; + for (size_t j = 0; j < X[i].size(); j++) { + std::cout << X[i][j]; + if (j < X[i].size() - 1) std::cout << ", "; + } + std::cout << "]" << std::endl; + } + } + + // Convert to tensor - X is features x samples, need to transpose for tensor format + torch::Tensor X_tensor = platform::TensorUtils::to_matrix(X); + + if (debug) { + std::cout << "Converted tensor shape: " << X_tensor.sizes() << std::endl; + std::cout << "Tensor data: " << X_tensor << std::endl; + } + + auto proba_tensor = predict_proba(X_tensor); // Call tensor method + + if (debug) { + std::cout << "Proba tensor shape: " << proba_tensor.sizes() << std::endl; + std::cout << "Proba tensor data: " << proba_tensor << std::endl; + } + + std::vector> result(n_samples, std::vector(n_classes, 0.0)); + + for (size_t i = 0; i < n_samples; i++) { + for (int j = 0; j < n_classes; j++) { + result[i][j] = proba_tensor[i][j].item(); + } + + if (debug) { + std::cout << "Sample " << i << " converted: ["; + for (int j = 0; j < n_classes; j++) { + std::cout << result[i][j]; + if (j < n_classes - 1) std::cout << ", "; + } + std::cout << "]" << std::endl; + } + } + + if (debug) { + std::cout << "=== End predict_proba vector method debug ===" << std::endl; + } + + return result; + } + + // También agregar debug al método tensor predict_proba: + torch::Tensor AdaBoost::predict_proba(torch::Tensor& X) { if (!fitted) { @@ -317,43 +412,85 @@ namespace bayesnet { } int n_samples = X.size(1); + + if (debug) { + std::cout << "=== predict_proba tensor method debug ===" << std::endl; + std::cout << "Input tensor shape: " << X.sizes() << std::endl; + std::cout << "Number of samples: " << n_samples << std::endl; + std::cout << "Number of classes: " << n_classes << std::endl; + } + torch::Tensor probabilities = torch::zeros({ n_samples, n_classes }); for (int i = 0; i < n_samples; i++) { auto sample = X.index({ torch::indexing::Slice(), i }); - probabilities[i] = predictProbaSample(sample); + + if (debug) { + std::cout << "Processing sample " << i << ": " << sample << std::endl; + } + + auto sample_probs = predictProbaSample(sample); + + if (debug) { + std::cout << "Sample " << i << " probabilities from predictProbaSample: " << sample_probs << std::endl; + } + + probabilities[i] = sample_probs; + + if (debug) { + std::cout << "Assigned to probabilities[" << i << "]: " << probabilities[i] << std::endl; + } + } + + if (debug) { + std::cout << "Final probabilities tensor: " << probabilities << std::endl; + std::cout << "=== End predict_proba tensor method debug ===" << std::endl; } return probabilities; } - std::vector AdaBoost::predict(std::vector>& X) - { - // Convert to tensor - X is samples x features, need to transpose - torch::Tensor X_tensor = platform::TensorUtils::to_matrix(X); - auto predictions = predict(X_tensor); - std::vector result = platform::TensorUtils::to_vector(predictions); - return result; - } + // int AdaBoost::predictSample(const torch::Tensor& x) const + // { + // if (!fitted) { + // throw std::runtime_error(CLASSIFIER_NOT_FITTED); + // } - std::vector> AdaBoost::predict_proba(std::vector>& X) - { - auto n_samples = X[0].size(); - // Convert to tensor - X is samples x features, need to transpose - torch::Tensor X_tensor = platform::TensorUtils::to_matrix(X); - auto proba_tensor = predict_proba(X_tensor); + // if (models.empty()) { + // throw std::runtime_error("No models have been trained"); + // } - std::vector> result(n_samples, std::vector(n_classes, 0.0)); + // // x should be a 1D tensor with n features + // if (x.size(0) != n) { + // throw std::runtime_error("Input sample has wrong number of features. Expected " + + // std::to_string(n) + " but got " + std::to_string(x.size(0))); + // } - for (size_t i = 0; i < n_samples; i++) { - for (int j = 0; j < n_classes; j++) { - result[i][j] = proba_tensor[i][j].item(); - } - } + // // Initialize class votes + // std::vector class_votes(n_classes, 0.0); - return result; - } + // // Accumulate weighted votes from all estimators + // for (size_t i = 0; i < models.size(); i++) { + // if (alphas[i] <= 0) continue; // Skip estimators with zero or negative weight + // try { + // // Get prediction from this estimator + // int predicted_class = static_cast(models[i].get())->predictSample(x); + // // Add weighted vote for this class + // if (predicted_class >= 0 && predicted_class < n_classes) { + // class_votes[predicted_class] += alphas[i]; + // } + // } + // catch (const std::exception& e) { + // std::cerr << "Error in estimator " << i << ": " << e.what() << std::endl; + // continue; + // } + // } + + // // Return class with highest weighted vote + // return std::distance(class_votes.begin(), + // std::max_element(class_votes.begin(), class_votes.end())); + // } int AdaBoost::predictSample(const torch::Tensor& x) const { if (!fitted) { @@ -370,30 +507,67 @@ namespace bayesnet { std::to_string(n) + " but got " + std::to_string(x.size(0))); } - // Initialize class votes + // Initialize class votes with zeros std::vector class_votes(n_classes, 0.0); - // Accumulate weighted votes from all estimators + if (debug) { + std::cout << "=== predictSample Debug ===" << std::endl; + std::cout << "Number of models: " << models.size() << std::endl; + } + + // Accumulate votes from all estimators (same logic as predictProbaSample) for (size_t i = 0; i < models.size(); i++) { - if (alphas[i] <= 0) continue; // Skip estimators with zero or negative weight + double alpha = alphas[i]; + + // Skip invalid estimators + if (alpha <= 0 || !std::isfinite(alpha)) { + if (debug) std::cout << "Skipping model " << i << " (alpha=" << alpha << ")" << std::endl; + continue; + } + try { - // Get prediction from this estimator + // Get class prediction from this estimator int predicted_class = static_cast(models[i].get())->predictSample(x); + if (debug) { + std::cout << "Model " << i << ": predicts class " << predicted_class + << " with alpha " << alpha << std::endl; + } + // Add weighted vote for this class if (predicted_class >= 0 && predicted_class < n_classes) { - class_votes[predicted_class] += alphas[i]; + class_votes[predicted_class] += alpha; } } catch (const std::exception& e) { - std::cerr << "Error in estimator " << i << ": " << e.what() << std::endl; + if (debug) std::cout << "Error in model " << i << ": " << e.what() << std::endl; continue; } } - // Return class with highest weighted vote - return std::distance(class_votes.begin(), - std::max_element(class_votes.begin(), class_votes.end())); + // Find class with maximum votes + int best_class = 0; + double max_votes = class_votes[0]; + + for (int j = 1; j < n_classes; j++) { + if (class_votes[j] > max_votes) { + max_votes = class_votes[j]; + best_class = j; + } + } + + if (debug) { + std::cout << "Class votes: ["; + for (int j = 0; j < n_classes; j++) { + std::cout << class_votes[j]; + if (j < n_classes - 1) std::cout << ", "; + } + std::cout << "]" << std::endl; + std::cout << "Best class: " << best_class << " with " << max_votes << " votes" << std::endl; + std::cout << "=== End predictSample Debug ===" << std::endl; + } + + return best_class; } torch::Tensor AdaBoost::predictProbaSample(const torch::Tensor& x) const diff --git a/tests/TestAdaBoost.cpp b/tests/TestAdaBoost.cpp index 7fb887b..81d5673 100644 --- a/tests/TestAdaBoost.cpp +++ b/tests/TestAdaBoost.cpp @@ -19,6 +19,7 @@ using namespace bayesnet; using namespace Catch::Matchers; +static const bool DEBUG = false; TEST_CASE("AdaBoost Construction", "[AdaBoost]") { @@ -141,6 +142,7 @@ TEST_CASE("AdaBoost Basic Functionality", "[AdaBoost]") SECTION("Prediction with vector interface") { AdaBoost ada(10, 3); + ada.setDebug(DEBUG); // Enable debug to investigate ada.fit(X, y, features, className, states, Smoothing_t::NONE); auto predictions = ada.predict(X); @@ -159,6 +161,7 @@ TEST_CASE("AdaBoost Basic Functionality", "[AdaBoost]") SECTION("Probability predictions with vector interface") { AdaBoost ada(10, 3); + ada.setDebug(DEBUG); // ENABLE DEBUG HERE TOO ada.fit(X, y, features, className, states, Smoothing_t::NONE); auto proba = ada.predict_proba(X); @@ -183,8 +186,16 @@ TEST_CASE("AdaBoost Basic Functionality", "[AdaBoost]") correct++; } - // Check that predict_proba matches the expected predict value - REQUIRE(pred == (p[0] > p[1] ? 0 : 1)); + INFO("Probability test - Sample " << i << ": pred=" << pred << ", probs=[" << p[0] << "," << p[1] << "], expected_from_probs=" << predicted_class); + + // Handle ties + if (std::abs(p[0] - p[1]) < 1e-10) { + INFO("Tie detected in probabilities"); + // Either prediction is valid in case of tie + } else { + // Check that predict_proba matches the expected predict value + REQUIRE(pred == predicted_class); + } } double accuracy = static_cast(correct) / n_samples; REQUIRE(accuracy > 0.99); // Should achieve good accuracy on this simple dataset @@ -230,103 +241,50 @@ TEST_CASE("AdaBoost Tensor Interface", "[AdaBoost]") } } -TEST_CASE("AdaBoost on Iris Dataset", "[AdaBoost][iris]") +TEST_CASE("AdaBoost SAMME Algorithm Validation", "[AdaBoost]") { auto raw = RawDatasets("iris", true); - SECTION("Training with vector interface") + SECTION("Prediction consistency with probabilities") { - AdaBoost ada(30, 3); - - REQUIRE_NOTHROW(ada.fit(raw.Xv, raw.yv, raw.featuresv, raw.classNamev, raw.statesv, Smoothing_t::NONE)); - - auto predictions = ada.predict(raw.Xv); - REQUIRE(predictions.size() == raw.yv.size()); - - // Calculate accuracy - int correct = 0; - for (size_t i = 0; i < predictions.size(); i++) { - if (predictions[i] == raw.yv[i]) correct++; - } - double accuracy = static_cast(correct) / raw.yv.size(); - REQUIRE(accuracy > 0.85); // Should achieve good accuracy - - // Test probability predictions - auto proba = ada.predict_proba(raw.Xv); - REQUIRE(proba.size() == raw.yv.size()); - REQUIRE(proba[0].size() == 3); // Three classes - - // Verify estimator weights and errors - auto weights = ada.getEstimatorWeights(); - auto errors = ada.getTrainingErrors(); - - REQUIRE(weights.size() == errors.size()); - REQUIRE(weights.size() > 0); - - // All weights should be positive (for non-zero error estimators) - for (double w : weights) { - REQUIRE(w >= 0.0); - } - - // All errors should be less than 0.5 (better than random) - for (double e : errors) { - REQUIRE(e < 0.5); - REQUIRE(e >= 0.0); - } - } - - SECTION("Different number of estimators") - { - std::vector n_estimators = { 5, 15, 25 }; - - for (int n_est : n_estimators) { - AdaBoost ada(n_est, 2); - ada.fit(raw.dataset, raw.featurest, raw.classNamet, raw.statest, Smoothing_t::NONE); - - auto predictions = ada.predict(raw.Xt); - REQUIRE(predictions.size(0) == raw.yt.size(0)); - - // Check that we don't exceed the specified number of estimators - auto weights = ada.getEstimatorWeights(); - REQUIRE(static_cast(weights.size()) <= n_est); - } - } - - SECTION("Different base estimator depths") - { - std::vector depths = { 1, 2, 4 }; - - for (int depth : depths) { - AdaBoost ada(15, depth); - ada.fit(raw.dataset, raw.featurest, raw.classNamet, raw.statest, Smoothing_t::NONE); - - auto predictions = ada.predict(raw.Xt); - REQUIRE(predictions.size(0) == raw.yt.size(0)); - } - } -} - -TEST_CASE("AdaBoost Edge Cases", "[AdaBoost]") -{ - auto raw = RawDatasets("iris", true); - - SECTION("Single estimator (depth 1 stump)") - { - AdaBoost ada(1, 1); // Single decision stump + AdaBoost ada(15, 3); + ada.setDebug(DEBUG); // Enable debug for ALL instances ada.fit(raw.dataset, raw.featurest, raw.classNamet, raw.statest, Smoothing_t::NONE); auto predictions = ada.predict(raw.Xt); - REQUIRE(predictions.size(0) == raw.yt.size(0)); + auto probabilities = ada.predict_proba(raw.Xt); - auto weights = ada.getEstimatorWeights(); - REQUIRE(weights.size() == 1); + REQUIRE(predictions.size(0) == probabilities.size(0)); + REQUIRE(probabilities.size(1) == 3); // Three classes in Iris + + // For each sample, predicted class should correspond to highest probability + for (int i = 0; i < predictions.size(0); i++) { + int predicted_class = predictions[i].item(); + auto probs = probabilities[i]; + + // Find class with highest probability + auto max_prob_idx = torch::argmax(probs).item(); + + // Predicted class should match class with highest probability + REQUIRE(predicted_class == max_prob_idx); + + // Probabilities should sum to 1 + double sum_probs = torch::sum(probs).item(); + REQUIRE(sum_probs == Catch::Approx(1.0).epsilon(1e-6)); + + // All probabilities should be non-negative + for (int j = 0; j < 3; j++) { + REQUIRE(probs[j].item() >= 0.0); + REQUIRE(probs[j].item() <= 1.0); + } + } } - SECTION("Perfect classifier scenario") + SECTION("Weighted voting verification") { - // Create a perfectly separable dataset + // Simple dataset where we can verify the weighted voting std::vector> X = { {0,0,1,1}, {0,1,0,1} }; - std::vector y = { 0, 0, 1, 1 }; + std::vector y = { 0, 1, 1, 0 }; std::vector features = { "f1", "f2" }; std::string className = "class"; std::map> states; @@ -334,191 +292,61 @@ TEST_CASE("AdaBoost Edge Cases", "[AdaBoost]") states["f2"] = { 0, 1 }; states["class"] = { 0, 1 }; - AdaBoost ada(10, 3); - ada.fit(X, y, features, className, states, Smoothing_t::NONE); - - auto predictions = ada.predict(X); - REQUIRE(predictions.size() == 4); - - // Should achieve perfect accuracy - int correct = 0; - for (size_t i = 0; i < predictions.size(); i++) { - if (predictions[i] == y[i]) correct++; - } - REQUIRE(correct == 4); - - // Should stop early due to perfect classification - auto errors = ada.getTrainingErrors(); - if (errors.size() > 0) { - REQUIRE(errors.back() < 1e-10); // Very low error - } - } - - SECTION("Small dataset") - { - // Very small dataset - std::vector> X = { {0,1}, {1,0} }; - std::vector y = { 0, 1 }; - std::vector features = { "f1", "f2" }; - std::string className = "class"; - std::map> states; - states["f1"] = { 0, 1 }; - states["f2"] = { 0, 1 }; - states["class"] = { 0, 1 }; - - AdaBoost ada(5, 1); - REQUIRE_NOTHROW(ada.fit(X, y, features, className, states, Smoothing_t::NONE)); - - auto predictions = ada.predict(X); - REQUIRE(predictions.size() == 2); - } -} - -TEST_CASE("AdaBoost Graph Visualization", "[AdaBoost]") -{ - // Simple dataset for visualization - std::vector> X = { {0,0,1,1}, {0,1,0,1} }; - std::vector y = { 0, 1, 1, 0 }; // XOR pattern - std::vector features = { "x1", "x2" }; - std::string className = "xor"; - std::map> states; - states["x1"] = { 0, 1 }; - states["x2"] = { 0, 1 }; - states["xor"] = { 0, 1 }; - - SECTION("Graph generation") - { AdaBoost ada(5, 2); + ada.setDebug(DEBUG); // Enable debug for detailed logging ada.fit(X, y, features, className, states, Smoothing_t::NONE); - auto graph_lines = ada.graph(); + INFO("=== Final test verification ==="); + auto predictions = ada.predict(X); + auto probabilities = ada.predict_proba(X); + auto alphas = ada.getEstimatorWeights(); - REQUIRE(graph_lines.size() > 2); - REQUIRE(graph_lines.front() == "digraph AdaBoost {"); - REQUIRE(graph_lines.back() == "}"); - - // Should contain base estimator references - bool has_estimators = false; - for (const auto& line : graph_lines) { - if (line.find("Estimator") != std::string::npos) { - has_estimators = true; - break; - } + INFO("Training info:"); + for (size_t i = 0; i < alphas.size(); i++) { + INFO(" Model " << i << ": alpha=" << alphas[i]); } - REQUIRE(has_estimators); - // Should contain alpha values - bool has_alpha = false; - for (const auto& line : graph_lines) { - if (line.find("α") != std::string::npos || line.find("alpha") != std::string::npos) { - has_alpha = true; - break; - } + REQUIRE(predictions.size() == 4); + REQUIRE(probabilities.size() == 4); + REQUIRE(probabilities[0].size() == 2); // Two classes + REQUIRE(alphas.size() > 0); + + // Verify that estimator weights are reasonable + for (double alpha : alphas) { + REQUIRE(alpha >= 0.0); // Alphas should be non-negative } - REQUIRE(has_alpha); - } - SECTION("Graph with title") - { - AdaBoost ada(3, 1); - ada.fit(X, y, features, className, states, Smoothing_t::NONE); + // Verify prediction-probability consistency with detailed logging + for (size_t i = 0; i < predictions.size(); i++) { + int pred = predictions[i]; + auto probs = probabilities[i]; - auto graph_lines = ada.graph("XOR AdaBoost"); + INFO("Final check - Sample " << i << ": predicted=" << pred << ", probabilities=[" << probs[0] << "," << probs[1] << "]"); - bool has_title = false; - for (const auto& line : graph_lines) { - if (line.find("label=\"XOR AdaBoost\"") != std::string::npos) { - has_title = true; - break; + // Handle the case where probabilities are exactly equal (tie) + if (std::abs(probs[0] - probs[1]) < 1e-10) { + INFO("Tie detected in probabilities - either prediction is valid"); + REQUIRE((pred == 0 || pred == 1)); + } else { + // Normal case - prediction should match max probability + int expected_pred = (probs[0] > probs[1]) ? 0 : 1; + INFO("Expected prediction based on probs: " << expected_pred); + REQUIRE(pred == expected_pred); } + + REQUIRE(probs[0] + probs[1] == Catch::Approx(1.0).epsilon(1e-6)); } - REQUIRE(has_title); - } -} - -TEST_CASE("AdaBoost with Weights", "[AdaBoost]") -{ - auto raw = RawDatasets("iris", true); - - SECTION("Uniform weights") - { - AdaBoost ada(20, 3); - ada.fit(raw.dataset, raw.featurest, raw.classNamet, raw.statest, raw.weights, Smoothing_t::NONE); - - auto predictions = ada.predict(raw.Xt); - REQUIRE(predictions.size(0) == raw.yt.size(0)); - - auto weights = ada.getEstimatorWeights(); - REQUIRE(weights.size() > 0); } - SECTION("Non-uniform weights") + SECTION("Empty models edge case") { - auto weights = torch::ones({ raw.nSamples }); - weights.index({ torch::indexing::Slice(0, 50) }) *= 3.0; // Emphasize first class - weights = weights / weights.sum(); + AdaBoost ada(1, 1); + ada.setDebug(DEBUG); // Enable debug for ALL instances - AdaBoost ada(15, 2); - ada.fit(raw.dataset, raw.featurest, raw.classNamet, raw.statest, weights, Smoothing_t::NONE); - - auto predictions = ada.predict(raw.Xt); - REQUIRE(predictions.size(0) == raw.yt.size(0)); - - // Check that training completed successfully - auto estimator_weights = ada.getEstimatorWeights(); - auto errors = ada.getTrainingErrors(); - - REQUIRE(estimator_weights.size() == errors.size()); - REQUIRE(estimator_weights.size() > 0); - } -} - -TEST_CASE("AdaBoost Input Dimension Validation", "[AdaBoost]") -{ - auto raw = RawDatasets("iris", true); - - SECTION("Correct input dimensions") - { - AdaBoost ada(10, 2); - ada.fit(raw.dataset, raw.featurest, raw.classNamet, raw.statest, Smoothing_t::NONE); - - // Test with correct tensor dimensions (features x samples) - REQUIRE_NOTHROW(ada.predict(raw.Xt)); - REQUIRE_NOTHROW(ada.predict_proba(raw.Xt)); - - // Test with correct vector dimensions (features x samples) - REQUIRE_NOTHROW(ada.predict(raw.Xv)); - REQUIRE_NOTHROW(ada.predict_proba(raw.Xv)); - } - - SECTION("Dimension consistency between interfaces") - { - AdaBoost ada(10, 2); - ada.fit(raw.dataset, raw.featurest, raw.classNamet, raw.statest, Smoothing_t::NONE); - - // Get predictions from both interfaces - auto tensor_predictions = ada.predict(raw.Xt); - auto vector_predictions = ada.predict(raw.Xv); - - // Should have same number of predictions - REQUIRE(tensor_predictions.size(0) == static_cast(vector_predictions.size())); - - // Test probability predictions - auto tensor_proba = ada.predict_proba(raw.Xt); - auto vector_proba = ada.predict_proba(raw.Xv); - - REQUIRE(tensor_proba.size(0) == static_cast(vector_proba.size())); - REQUIRE(tensor_proba.size(1) == static_cast(vector_proba[0].size())); - - // Verify predictions match between interfaces - for (int i = 0; i < tensor_predictions.size(0); i++) { - REQUIRE(tensor_predictions[i].item() == vector_predictions[i]); - - // Verify probabilities match between interfaces - for (int j = 0; j < tensor_proba.size(1); j++) { - REQUIRE(tensor_proba[i][j].item() == Catch::Approx(vector_proba[i][j]).epsilon(1e-10)); - } - } + // Try to predict before fitting + std::vector> X = { {0}, {1} }; + REQUIRE_THROWS_WITH(ada.predict(X), ContainsSubstring("not been fitted")); + REQUIRE_THROWS_WITH(ada.predict_proba(X), ContainsSubstring("not been fitted")); } } @@ -548,6 +376,7 @@ TEST_CASE("AdaBoost Debug - Simple Dataset Analysis", "[AdaBoost][debug]") SECTION("Debug training process") { AdaBoost ada(5, 3); // Few estimators for debugging + ada.setDebug(DEBUG); // This should work perfectly on this simple dataset REQUIRE_NOTHROW(ada.fit(X, y, features, className, states, Smoothing_t::NONE)); @@ -603,7 +432,14 @@ TEST_CASE("AdaBoost Debug - Simple Dataset Analysis", "[AdaBoost][debug]") // Predicted class should match highest probability int pred_class = predictions[i]; - REQUIRE(pred_class == (p[0] > p[1] ? 0 : 1)); + + // Handle ties + if (std::abs(p[0] - p[1]) < 1e-10) { + INFO("Tie detected - probabilities are equal"); + REQUIRE((pred_class == 0 || pred_class == 1)); + } else { + REQUIRE(pred_class == (p[0] > p[1] ? 0 : 1)); + } } } @@ -621,6 +457,7 @@ TEST_CASE("AdaBoost Debug - Simple Dataset Analysis", "[AdaBoost][debug]") double tree_accuracy = static_cast(tree_correct) / n_samples; AdaBoost ada(5, 3); + ada.setDebug(DEBUG); ada.fit(X, y, features, className, states, Smoothing_t::NONE); auto ada_predictions = ada.predict(X); @@ -639,95 +476,6 @@ TEST_CASE("AdaBoost Debug - Simple Dataset Analysis", "[AdaBoost][debug]") } } -TEST_CASE("AdaBoost SAMME Algorithm Validation", "[AdaBoost]") -{ - auto raw = RawDatasets("iris", true); - - SECTION("Prediction consistency with probabilities") - { - AdaBoost ada(15, 3); - ada.fit(raw.dataset, raw.featurest, raw.classNamet, raw.statest, Smoothing_t::NONE); - - auto predictions = ada.predict(raw.Xt); - auto probabilities = ada.predict_proba(raw.Xt); - - REQUIRE(predictions.size(0) == probabilities.size(0)); - REQUIRE(probabilities.size(1) == 3); // Three classes in Iris - - // For each sample, predicted class should correspond to highest probability - for (int i = 0; i < predictions.size(0); i++) { - int predicted_class = predictions[i].item(); - auto probs = probabilities[i]; - - // Find class with highest probability - auto max_prob_idx = torch::argmax(probs).item(); - - // Predicted class should match class with highest probability - REQUIRE(predicted_class == max_prob_idx); - - // Probabilities should sum to 1 - double sum_probs = torch::sum(probs).item(); - REQUIRE(sum_probs == Catch::Approx(1.0).epsilon(1e-6)); - - // All probabilities should be non-negative - for (int j = 0; j < 3; j++) { - REQUIRE(probs[j].item() >= 0.0); - REQUIRE(probs[j].item() <= 1.0); - } - } - } - - SECTION("Weighted voting verification") - { - // Simple dataset where we can verify the weighted voting - std::vector> X = { {0,0,1,1}, {0,1,0,1} }; - std::vector y = { 0, 1, 1, 0 }; - std::vector features = { "f1", "f2" }; - std::string className = "class"; - std::map> states; - states["f1"] = { 0, 1 }; - states["f2"] = { 0, 1 }; - states["class"] = { 0, 1 }; - - AdaBoost ada(5, 2); - ada.fit(X, y, features, className, states, Smoothing_t::NONE); - - auto predictions = ada.predict(X); - auto probabilities = ada.predict_proba(X); - auto alphas = ada.getEstimatorWeights(); - - REQUIRE(predictions.size() == 4); - REQUIRE(probabilities.size() == 4); - REQUIRE(probabilities[0].size() == 2); // Two classes - REQUIRE(alphas.size() > 0); - - // Verify that estimator weights are reasonable - for (double alpha : alphas) { - REQUIRE(alpha >= 0.0); // Alphas should be non-negative - } - - // Verify prediction-probability consistency - for (size_t i = 0; i < predictions.size(); i++) { - int pred = predictions[i]; - auto probs = probabilities[i]; - INFO("Sample " << i << ": predicted=" << pred - << ", probabilities=[" << probs[0] << ", " << probs[1] << "]"); - - REQUIRE(pred == (probs[0] > probs[1] ? 0 : 1)); - REQUIRE(probs[0] + probs[1] == Catch::Approx(1.0).epsilon(1e-6)); - } - } - - SECTION("Empty models edge case") - { - AdaBoost ada(1, 1); - - // Try to predict before fitting - std::vector> X = { {0}, {1} }; - REQUIRE_THROWS_WITH(ada.predict(X), ContainsSubstring("not been fitted")); - REQUIRE_THROWS_WITH(ada.predict_proba(X), ContainsSubstring("not been fitted")); - } -} TEST_CASE("AdaBoost Predict-Proba Consistency Fix", "[AdaBoost][consistency]") { // Simple binary classification dataset @@ -743,20 +491,31 @@ TEST_CASE("AdaBoost Predict-Proba Consistency Fix", "[AdaBoost][consistency]") SECTION("Binary classification consistency") { AdaBoost ada(3, 2); - ada.setDebug(true); // Enable debug output + ada.setDebug(DEBUG); // Enable debug output ada.fit(X, y, features, className, states, Smoothing_t::NONE); + INFO("=== Debugging predict vs predict_proba consistency ==="); + + // Get training info + auto alphas = ada.getEstimatorWeights(); + auto errors = ada.getTrainingErrors(); + + INFO("Training completed:"); + INFO(" Number of models: " << alphas.size()); + for (size_t i = 0; i < alphas.size(); i++) { + INFO(" Model " << i << ": alpha=" << alphas[i] << ", error=" << errors[i]); + } + auto predictions = ada.predict(X); auto probabilities = ada.predict_proba(X); - INFO("=== Debugging predict vs predict_proba consistency ==="); - // Verify consistency for each sample for (size_t i = 0; i < predictions.size(); i++) { int predicted_class = predictions[i]; auto probs = probabilities[i]; INFO("Sample " << i << ":"); + INFO(" Features: [" << X[0][i] << ", " << X[1][i] << "]"); INFO(" True class: " << y[i]); INFO(" Predicted class: " << predicted_class); INFO(" Probabilities: [" << probs[0] << ", " << probs[1] << "]"); @@ -765,7 +524,14 @@ TEST_CASE("AdaBoost Predict-Proba Consistency Fix", "[AdaBoost][consistency]") int max_prob_class = (probs[0] > probs[1]) ? 0 : 1; INFO(" Max prob class: " << max_prob_class); - REQUIRE(predicted_class == max_prob_class); + // Handle tie case (when probabilities are equal) + if (std::abs(probs[0] - probs[1]) < 1e-10) { + INFO(" Tie detected - probabilities are equal"); + // In case of tie, either prediction is valid + REQUIRE((predicted_class == 0 || predicted_class == 1)); + } else { + REQUIRE(predicted_class == max_prob_class); + } // Probabilities should sum to 1 double sum_probs = probs[0] + probs[1]; @@ -778,37 +544,4 @@ TEST_CASE("AdaBoost Predict-Proba Consistency Fix", "[AdaBoost][consistency]") REQUIRE(probs[1] <= 1.0); } } - - SECTION("Multi-class consistency") - { - auto raw = RawDatasets("iris", true); - - AdaBoost ada(5, 2); - ada.fit(raw.dataset, raw.featurest, raw.classNamet, raw.statest, Smoothing_t::NONE); - - auto predictions = ada.predict(raw.Xt); - auto probabilities = ada.predict_proba(raw.Xt); - - // Check consistency for first 10 samples - for (int i = 0; i < std::min(static_cast(10), predictions.size(0)); i++) { - int predicted_class = predictions[i].item(); - auto probs = probabilities[i]; - - // Find class with maximum probability - auto max_prob_idx = torch::argmax(probs).item(); - - INFO("Sample " << i << ":"); - INFO(" Predicted class: " << predicted_class); - INFO(" Max prob class: " << max_prob_idx); - INFO(" Probabilities: [" << probs[0].item() << ", " - << probs[1].item() << ", " << probs[2].item() << "]"); - - // They must match - REQUIRE(predicted_class == max_prob_idx); - - // Probabilities should sum to 1 - double sum_probs = torch::sum(probs).item(); - REQUIRE(sum_probs == Catch::Approx(1.0).epsilon(1e-6)); - } - } } \ No newline at end of file