Add docs support
Some checks failed
CI/CD Pipeline / Code Linting (push) Failing after 24s
CI/CD Pipeline / Build and Test (Debug, clang, ubuntu-latest) (push) Failing after 5m17s
CI/CD Pipeline / Build and Test (Debug, gcc, ubuntu-latest) (push) Failing after 5m32s
CI/CD Pipeline / Build and Test (Release, clang, ubuntu-20.04) (push) Failing after 5m45s
CI/CD Pipeline / Build and Test (Release, clang, ubuntu-latest) (push) Failing after 5m12s
CI/CD Pipeline / Build and Test (Release, gcc, ubuntu-20.04) (push) Failing after 5m22s
CI/CD Pipeline / Build and Test (Release, gcc, ubuntu-latest) (push) Failing after 5m26s
CI/CD Pipeline / Docker Build Test (push) Failing after 1m7s
CI/CD Pipeline / Performance Benchmarks (push) Has been skipped
CI/CD Pipeline / Build Documentation (push) Failing after 18s
CI/CD Pipeline / Create Release Package (push) Has been skipped

This commit is contained in:
2025-06-23 10:02:36 +02:00
parent d6dc083a5a
commit 5302dd9a8a
14 changed files with 4311 additions and 439 deletions

526
src/svm_classifier.cpp Normal file
View File

@@ -0,0 +1,526 @@
#include "svm_classifier/svm_classifier.hpp"
#include <algorithm>
#include <random>
#include <chrono>
#include <fstream>
#include <sstream>
#include <cmath>
namespace svm_classifier {
SVMClassifier::SVMClassifier()
: is_fitted_(false)
, n_features_(0)
{
data_converter_ = std::make_unique<DataConverter>();
initialize_multiclass_strategy();
}
SVMClassifier::SVMClassifier(const nlohmann::json& config) : SVMClassifier()
{
set_parameters(config);
initialize_multiclass_strategy();
}
SVMClassifier::SVMClassifier(KernelType kernel, double C, MulticlassStrategy multiclass_strategy)
: is_fitted_(false)
, n_features_(0)
{
params_.set_kernel_type(kernel);
params_.set_C(C);
params_.set_multiclass_strategy(multiclass_strategy);
data_converter_ = std::make_unique<DataConverter>();
initialize_multiclass_strategy();
}
SVMClassifier::~SVMClassifier() = default;
SVMClassifier::SVMClassifier(SVMClassifier&& other) noexcept
: params_(std::move(other.params_))
, multiclass_strategy_(std::move(other.multiclass_strategy_))
, data_converter_(std::move(other.data_converter_))
, is_fitted_(other.is_fitted_)
, n_features_(other.n_features_)
, training_metrics_(other.training_metrics_)
{
other.is_fitted_ = false;
other.n_features_ = 0;
}
SVMClassifier& SVMClassifier::operator=(SVMClassifier&& other) noexcept
{
if (this != &other) {
params_ = std::move(other.params_);
multiclass_strategy_ = std::move(other.multiclass_strategy_);
data_converter_ = std::move(other.data_converter_);
is_fitted_ = other.is_fitted_;
n_features_ = other.n_features_;
training_metrics_ = other.training_metrics_;
other.is_fitted_ = false;
other.n_features_ = 0;
}
return *this;
}
TrainingMetrics SVMClassifier::fit(const torch::Tensor& X, const torch::Tensor& y)
{
validate_input(X, y, false);
// Store number of features
n_features_ = X.size(1);
// Set gamma to auto if needed
if (params_.get_gamma() == -1.0) {
params_.set_gamma(1.0 / n_features_);
}
// Train the multiclass strategy
training_metrics_ = multiclass_strategy_->fit(X, y, params_, *data_converter_);
is_fitted_ = true;
return training_metrics_;
}
torch::Tensor SVMClassifier::predict(const torch::Tensor& X)
{
validate_input(X, torch::Tensor(), true);
auto predictions = multiclass_strategy_->predict(X, *data_converter_);
return data_converter_->from_predictions(std::vector<double>(predictions.begin(), predictions.end()));
}
torch::Tensor SVMClassifier::predict_proba(const torch::Tensor& X)
{
if (!supports_probability()) {
throw std::runtime_error("Probability prediction not supported. Set probability=true during training.");
}
validate_input(X, torch::Tensor(), true);
auto probabilities = multiclass_strategy_->predict_proba(X, *data_converter_);
return data_converter_->from_probabilities(probabilities);
}
torch::Tensor SVMClassifier::decision_function(const torch::Tensor& X)
{
validate_input(X, torch::Tensor(), true);
auto decision_values = multiclass_strategy_->decision_function(X, *data_converter_);
return data_converter_->from_decision_values(decision_values);
}
double SVMClassifier::score(const torch::Tensor& X, const torch::Tensor& y_true)
{
validate_input(X, y_true, true);
auto predictions = predict(X);
auto y_true_cpu = y_true.to(torch::kCPU);
auto predictions_cpu = predictions.to(torch::kCPU);
// Calculate accuracy
auto correct = (predictions_cpu == y_true_cpu);
return correct.to(torch::kFloat32).mean().item<double>();
}
EvaluationMetrics SVMClassifier::evaluate(const torch::Tensor& X, const torch::Tensor& y_true)
{
validate_input(X, y_true, true);
auto predictions = predict(X);
auto y_true_cpu = y_true.to(torch::kCPU);
auto predictions_cpu = predictions.to(torch::kCPU);
// Convert to std::vector for easier processing
std::vector<int> y_true_vec, y_pred_vec;
for (int i = 0; i < y_true_cpu.size(0); ++i) {
y_true_vec.push_back(y_true_cpu[i].item<int>());
y_pred_vec.push_back(predictions_cpu[i].item<int>());
}
EvaluationMetrics metrics;
// Calculate accuracy
metrics.accuracy = score(X, y_true);
// Calculate confusion matrix
metrics.confusion_matrix = calculate_confusion_matrix(y_true_vec, y_pred_vec);
// Calculate precision, recall, and F1-score
auto [precision, recall, f1] = calculate_metrics_from_confusion_matrix(metrics.confusion_matrix);
metrics.precision = precision;
metrics.recall = recall;
metrics.f1_score = f1;
return metrics;
}
void SVMClassifier::set_parameters(const nlohmann::json& config)
{
params_.set_parameters(config);
// Re-initialize multiclass strategy if strategy changed
initialize_multiclass_strategy();
// Reset fitted state if already fitted
if (is_fitted_) {
is_fitted_ = false;
n_features_ = 0;
}
}
nlohmann::json SVMClassifier::get_parameters() const
{
auto params = params_.get_parameters();
// Add classifier-specific information
params["is_fitted"] = is_fitted_;
params["n_features"] = n_features_;
params["n_classes"] = get_n_classes();
params["svm_library"] = (get_svm_library() == SVMLibrary::LIBLINEAR) ? "liblinear" : "libsvm";
return params;
}
int SVMClassifier::get_n_classes() const
{
if (!is_fitted_) {
return 0;
}
return multiclass_strategy_->get_n_classes();
}
std::vector<int> SVMClassifier::get_classes() const
{
if (!is_fitted_) {
return {};
}
return multiclass_strategy_->get_classes();
}
bool SVMClassifier::supports_probability() const
{
if (!is_fitted_) {
return params_.get_probability();
}
return multiclass_strategy_->supports_probability();
}
void SVMClassifier::save_model(const std::string& filename) const
{
if (!is_fitted_) {
throw std::runtime_error("Cannot save unfitted model");
}
// For now, save parameters as JSON
// Full model serialization would require more complex implementation
std::ofstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Cannot open file for writing: " + filename);
}
nlohmann::json model_data = {
{"parameters", get_parameters()},
{"training_metrics", {
{"training_time", training_metrics_.training_time},
{"support_vectors", training_metrics_.support_vectors},
{"iterations", training_metrics_.iterations},
{"objective_value", training_metrics_.objective_value}
}},
{"classes", get_classes()},
{"version", "1.0"}
};
file << model_data.dump(2);
file.close();
}
void SVMClassifier::load_model(const std::string& filename)
{
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Cannot open file for reading: " + filename);
}
nlohmann::json model_data;
file >> model_data;
file.close();
// Load parameters
if (model_data.contains("parameters")) {
set_parameters(model_data["parameters"]);
}
// Load training metrics
if (model_data.contains("training_metrics")) {
auto tm = model_data["training_metrics"];
training_metrics_.training_time = tm.value("training_time", 0.0);
training_metrics_.support_vectors = tm.value("support_vectors", 0);
training_metrics_.iterations = tm.value("iterations", 0);
training_metrics_.objective_value = tm.value("objective_value", 0.0);
training_metrics_.status = TrainingStatus::SUCCESS;
}
// Note: Full model loading would require serializing the actual SVM models
// For now, this provides parameter persistence
throw std::runtime_error("Full model loading not yet implemented. Only parameter loading is supported.");
}
std::vector<double> SVMClassifier::cross_validate(const torch::Tensor& X,
const torch::Tensor& y,
int cv)
{
validate_input(X, y, false);
if (cv < 2) {
throw std::invalid_argument("Number of folds must be >= 2");
}
std::vector<double> scores;
scores.reserve(cv);
// Store original fitted state
bool was_fitted = is_fitted_;
auto original_metrics = training_metrics_;
for (int fold = 0; fold < cv; ++fold) {
auto [X_train, y_train, X_val, y_val] = split_for_cv(X, y, fold, cv);
// Create temporary classifier with same parameters
SVMClassifier temp_clf(params_.get_parameters());
// Train on training fold
temp_clf.fit(X_train, y_train);
// Evaluate on validation fold
double fold_score = temp_clf.score(X_val, y_val);
scores.push_back(fold_score);
}
// Restore original state
is_fitted_ = was_fitted;
training_metrics_ = original_metrics;
return scores;
}
nlohmann::json SVMClassifier::grid_search(const torch::Tensor& X,
const torch::Tensor& y,
const nlohmann::json& param_grid,
int cv)
{
validate_input(X, y, false);
auto param_combinations = generate_param_combinations(param_grid);
double best_score = -1.0;
nlohmann::json best_params;
std::vector<double> all_scores;
for (const auto& params : param_combinations) {
SVMClassifier temp_clf(params);
auto scores = temp_clf.cross_validate(X, y, cv);
double mean_score = std::accumulate(scores.begin(), scores.end(), 0.0) / scores.size();
all_scores.push_back(mean_score);
if (mean_score > best_score) {
best_score = mean_score;
best_params = params;
}
}
return {
{"best_params", best_params},
{"best_score", best_score},
{"cv_results", all_scores}
};
}
torch::Tensor SVMClassifier::get_feature_importance() const
{
if (!is_fitted_) {
throw std::runtime_error("Model is not fitted");
}
if (params_.get_kernel_type() != KernelType::LINEAR) {
throw std::runtime_error("Feature importance only available for linear kernels");
}
// This would require access to the linear model weights
// Implementation depends on the multiclass strategy and would need
// to extract weights from the underlying liblinear models
throw std::runtime_error("Feature importance extraction not yet implemented");
}
void SVMClassifier::reset()
{
is_fitted_ = false;
n_features_ = 0;
training_metrics_ = TrainingMetrics();
data_converter_->cleanup();
}
void SVMClassifier::validate_input(const torch::Tensor& X,
const torch::Tensor& y,
bool check_fitted)
{
if (check_fitted && !is_fitted_) {
throw std::runtime_error("This SVMClassifier instance is not fitted yet. "
"Call 'fit' with appropriate arguments before using this estimator.");
}
data_converter_->validate_tensors(X, y);
if (check_fitted && X.size(1) != n_features_) {
throw std::invalid_argument(
"Number of features in X (" + std::to_string(X.size(1)) +
") does not match number of features during training (" + std::to_string(n_features_) + ")"
);
}
}
void SVMClassifier::initialize_multiclass_strategy()
{
multiclass_strategy_ = create_multiclass_strategy(params_.get_multiclass_strategy());
}
std::vector<std::vector<int>> SVMClassifier::calculate_confusion_matrix(const std::vector<int>& y_true,
const std::vector<int>& y_pred)
{
// Get unique classes
std::set<int> unique_classes;
for (int label : y_true) unique_classes.insert(label);
for (int label : y_pred) unique_classes.insert(label);
std::vector<int> classes(unique_classes.begin(), unique_classes.end());
std::sort(classes.begin(), classes.end());
int n_classes = classes.size();
std::vector<std::vector<int>> confusion_matrix(n_classes, std::vector<int>(n_classes, 0));
// Create class to index mapping
std::unordered_map<int, int> class_to_idx;
for (size_t i = 0; i < classes.size(); ++i) {
class_to_idx[classes[i]] = i;
}
// Fill confusion matrix
for (size_t i = 0; i < y_true.size(); ++i) {
int true_idx = class_to_idx[y_true[i]];
int pred_idx = class_to_idx[y_pred[i]];
confusion_matrix[true_idx][pred_idx]++;
}
return confusion_matrix;
}
std::tuple<double, double, double> SVMClassifier::calculate_metrics_from_confusion_matrix(
const std::vector<std::vector<int>>& confusion_matrix)
{
int n_classes = confusion_matrix.size();
if (n_classes == 0) {
return { 0.0, 0.0, 0.0 };
}
std::vector<double> precision(n_classes), recall(n_classes), f1(n_classes);
for (int i = 0; i < n_classes; ++i) {
int tp = confusion_matrix[i][i];
int fp = 0, fn = 0;
// Calculate false positives and false negatives
for (int j = 0; j < n_classes; ++j) {
if (i != j) {
fp += confusion_matrix[j][i]; // False positives
fn += confusion_matrix[i][j]; // False negatives
}
}
// Calculate precision, recall, and F1-score for this class
precision[i] = (tp + fp > 0) ? static_cast<double>(tp) / (tp + fp) : 0.0;
recall[i] = (tp + fn > 0) ? static_cast<double>(tp) / (tp + fn) : 0.0;
f1[i] = (precision[i] + recall[i] > 0) ?
2.0 * precision[i] * recall[i] / (precision[i] + recall[i]) : 0.0;
}
// Calculate macro averages
double macro_precision = std::accumulate(precision.begin(), precision.end(), 0.0) / n_classes;
double macro_recall = std::accumulate(recall.begin(), recall.end(), 0.0) / n_classes;
double macro_f1 = std::accumulate(f1.begin(), f1.end(), 0.0) / n_classes;
return { macro_precision, macro_recall, macro_f1 };
}
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
SVMClassifier::split_for_cv(const torch::Tensor& X, const torch::Tensor& y, int fold, int n_folds)
{
int n_samples = X.size(0);
int fold_size = n_samples / n_folds;
int remainder = n_samples % n_folds;
// Calculate start and end indices for validation fold
int val_start = fold * fold_size + std::min(fold, remainder);
int val_end = val_start + fold_size + (fold < remainder ? 1 : 0);
// Create indices
auto all_indices = torch::arange(n_samples, torch::kLong);
auto val_indices = all_indices.slice(0, val_start, val_end);
// Training indices (everything except validation)
auto train_indices = torch::cat({
all_indices.slice(0, 0, val_start),
all_indices.slice(0, val_end, n_samples)
});
// Split data
auto X_train = X.index_select(0, train_indices);
auto y_train = y.index_select(0, train_indices);
auto X_val = X.index_select(0, val_indices);
auto y_val = y.index_select(0, val_indices);
return { X_train, y_train, X_val, y_val };
}
std::vector<nlohmann::json> SVMClassifier::generate_param_combinations(const nlohmann::json& param_grid)
{
std::vector<nlohmann::json> combinations;
// Extract parameter names and values
std::vector<std::string> param_names;
std::vector<std::vector<nlohmann::json>> param_values;
for (auto& [key, value] : param_grid.items()) {
param_names.push_back(key);
if (value.is_array()) {
param_values.push_back(value);
} else {
param_values.push_back({ value });
}
}
// Generate all combinations using recursive approach
std::function<void(int, nlohmann::json&)> generate_combinations =
[&](int param_idx, nlohmann::json& current_params) {
if (param_idx == param_names.size()) {
combinations.push_back(current_params);
return;
}
for (const auto& value : param_values[param_idx]) {
current_params[param_names[param_idx]] = value;
generate_combinations(param_idx + 1, current_params);
}
};
nlohmann::json current_params;
generate_combinations(0, current_params);
return combinations;
}
} // namespace svm_classifier