1#include "svm_classifier/data_converter.hpp"
2#include "svm.h" // libsvm
3#include "linear.h" // liblinear
8namespace svm_classifier {
13 , sparse_threshold_(1e-8)
23 const torch::Tensor& y)
27 auto X_cpu = ensure_cpu_tensor(X);
29 n_samples_ = X_cpu.size(0);
30 n_features_ = X_cpu.size(1);
32 // Convert tensor data to svm_node structures
33 svm_nodes_storage_ = tensor_to_svm_nodes(X_cpu);
35 // Prepare pointers for svm_problem
37 svm_x_space_.reserve(n_samples_);
39 for (
auto& nodes : svm_nodes_storage_) {
40 svm_x_space_.push_back(nodes.data());
43 // Extract labels if provided
44 if (y.defined() && y.numel() > 0) {
45 svm_y_space_ = extract_labels(y);
48 svm_y_space_.resize(n_samples_, 0.0);
// Dummy labels for prediction
52 auto problem = std::make_unique<svm_problem>();
53 problem->l = n_samples_;
54 problem->x = svm_x_space_.data();
55 problem->y = svm_y_space_.data();
61 const torch::Tensor& y)
65 auto X_cpu = ensure_cpu_tensor(X);
67 n_samples_ = X_cpu.size(0);
68 n_features_ = X_cpu.size(1);
70 // Convert tensor data to feature_node structures
71 linear_nodes_storage_ = tensor_to_linear_nodes(X_cpu);
73 // Prepare pointers for problem
74 linear_x_space_.clear();
75 linear_x_space_.reserve(n_samples_);
77 for (
auto& nodes : linear_nodes_storage_) {
78 linear_x_space_.push_back(nodes.data());
81 // Extract labels if provided
82 if (y.defined() && y.numel() > 0) {
83 linear_y_space_ = extract_labels(y);
85 linear_y_space_.clear();
86 linear_y_space_.resize(n_samples_, 0.0);
// Dummy labels for prediction
90 auto linear_problem = std::make_unique<problem>();
91 linear_problem->l = n_samples_;
92 linear_problem->n = n_features_;
93 linear_problem->x = linear_x_space_.data();
94 linear_problem->y = linear_y_space_.data();
95 linear_problem->bias = -1;
// No bias term by default
97 return linear_problem;
102 validate_tensor_properties(sample, 1,
"sample");
104 auto sample_cpu = ensure_cpu_tensor(sample);
105 single_svm_nodes_ = sample_to_svm_nodes(sample_cpu);
107 return single_svm_nodes_.data();
112 validate_tensor_properties(sample, 1,
"sample");
114 auto sample_cpu = ensure_cpu_tensor(sample);
115 single_linear_nodes_ = sample_to_linear_nodes(sample_cpu);
117 return single_linear_nodes_.data();
122 auto options = torch::TensorOptions().dtype(torch::kInt32);
123 auto tensor = torch::zeros({
static_cast<int64_t
>(predictions.size()) }, options);
125 for (
size_t i = 0; i < predictions.size(); ++i) {
126 tensor[i] =
static_cast<int>(predictions[i]);
134 if (probabilities.empty()) {
135 return torch::empty({ 0, 0 });
138 int n_samples =
static_cast<int>(probabilities.size());
139 int n_classes =
static_cast<int>(probabilities[0].size());
141 auto tensor = torch::zeros({ n_samples, n_classes }, torch::kFloat64);
143 for (
int i = 0; i < n_samples; ++i) {
144 for (
int j = 0; j < n_classes; ++j) {
145 tensor[i][j] = probabilities[i][j];
154 if (decision_values.empty()) {
155 return torch::empty({ 0, 0 });
158 int n_samples =
static_cast<int>(decision_values.size());
159 int n_values =
static_cast<int>(decision_values[0].size());
161 auto tensor = torch::zeros({ n_samples, n_values }, torch::kFloat64);
163 for (
int i = 0; i < n_samples; ++i) {
164 for (
int j = 0; j < n_values; ++j) {
165 tensor[i][j] = decision_values[i][j];
174 validate_tensor_properties(X, 2,
"X");
176 if (y.defined() && y.numel() > 0) {
177 validate_tensor_properties(y, 1,
"y");
179 // Check that number of samples match
180 if (X.size(0) != y.size(0)) {
181 throw std::invalid_argument(
182 "Number of samples in X (" + std::to_string(X.size(0)) +
183 ") does not match number of labels in y (" + std::to_string(y.size(0)) +
")"
188 // Check for reasonable dimensions
189 if (X.size(0) == 0) {
190 throw std::invalid_argument(
"X cannot have 0 samples");
193 if (X.size(1) == 0) {
194 throw std::invalid_argument(
"X cannot have 0 features");
200 svm_nodes_storage_.clear();
201 svm_x_space_.clear();
202 svm_y_space_.clear();
204 linear_nodes_storage_.clear();
205 linear_x_space_.clear();
206 linear_y_space_.clear();
208 single_svm_nodes_.clear();
209 single_linear_nodes_.clear();
215 std::vector<std::vector<svm_node>> DataConverter::tensor_to_svm_nodes(
const torch::Tensor& X)
217 std::vector<std::vector<svm_node>> nodes_storage;
218 nodes_storage.reserve(X.size(0));
220 auto X_acc = X.accessor<float, 2>();
222 for (
int i = 0; i < X.size(0); ++i) {
223 nodes_storage.push_back(sample_to_svm_nodes(X[i]));
226 return nodes_storage;
229 std::vector<std::vector<feature_node>> DataConverter::tensor_to_linear_nodes(
const torch::Tensor& X)
231 std::vector<std::vector<feature_node>> nodes_storage;
232 nodes_storage.reserve(X.size(0));
234 for (
int i = 0; i < X.size(0); ++i) {
235 nodes_storage.push_back(sample_to_linear_nodes(X[i]));
238 return nodes_storage;
241 std::vector<svm_node> DataConverter::sample_to_svm_nodes(
const torch::Tensor& sample)
243 std::vector<svm_node> nodes;
245 auto sample_acc = sample.accessor<float, 1>();
247 // Reserve space (worst case: all features are non-sparse)
248 nodes.reserve(sample.size(0) + 1);
// +1 for terminator
250 for (
int j = 0; j < sample.size(0); ++j) {
251 double value =
static_cast<double>(sample_acc[j]);
253 // Skip sparse features
254 if (std::abs(value) > sparse_threshold_) {
256 node.index = j + 1;
// libsvm uses 1-based indexing
258 nodes.push_back(node);
264 terminator.index = -1;
265 terminator.value = 0;
266 nodes.push_back(terminator);
271 std::vector<feature_node> DataConverter::sample_to_linear_nodes(
const torch::Tensor& sample)
273 std::vector<feature_node> nodes;
275 auto sample_acc = sample.accessor<float, 1>();
277 // Reserve space (worst case: all features are non-sparse)
278 nodes.reserve(sample.size(0) + 1);
// +1 for terminator
280 for (
int j = 0; j < sample.size(0); ++j) {
281 double value =
static_cast<double>(sample_acc[j]);
283 // Skip sparse features
284 if (std::abs(value) > sparse_threshold_) {
286 node.index = j + 1;
// liblinear uses 1-based indexing
288 nodes.push_back(node);
293 feature_node terminator;
294 terminator.index = -1;
295 terminator.value = 0;
296 nodes.push_back(terminator);
301 std::vector<double> DataConverter::extract_labels(
const torch::Tensor& y)
303 auto y_cpu = ensure_cpu_tensor(y);
304 std::vector<double> labels;
305 labels.reserve(y_cpu.size(0));
307 // Handle different tensor types
308 if (y_cpu.dtype() == torch::kInt32) {
309 auto y_acc = y_cpu.accessor<int32_t, 1>();
310 for (
int i = 0; i < y_cpu.size(0); ++i) {
311 labels.push_back(
static_cast<double>(y_acc[i]));
313 }
else if (y_cpu.dtype() == torch::kInt64) {
314 auto y_acc = y_cpu.accessor<int64_t, 1>();
315 for (
int i = 0; i < y_cpu.size(0); ++i) {
316 labels.push_back(
static_cast<double>(y_acc[i]));
318 }
else if (y_cpu.dtype() == torch::kFloat32) {
319 auto y_acc = y_cpu.accessor<float, 1>();
320 for (
int i = 0; i < y_cpu.size(0); ++i) {
321 labels.push_back(
static_cast<double>(y_acc[i]));
323 }
else if (y_cpu.dtype() == torch::kFloat64) {
324 auto y_acc = y_cpu.accessor<double, 1>();
325 for (
int i = 0; i < y_cpu.size(0); ++i) {
326 labels.push_back(y_acc[i]);
329 throw std::invalid_argument(
"Unsupported label tensor dtype");
335 torch::Tensor DataConverter::ensure_cpu_tensor(
const torch::Tensor& tensor)
337 if (tensor.device().type() != torch::kCPU) {
338 return tensor.to(torch::kCPU);
341 // Convert to float32 if not already
342 if (tensor.dtype() != torch::kFloat32) {
343 return tensor.to(torch::kFloat32);
349 void DataConverter::validate_tensor_properties(
const torch::Tensor& tensor,
351 const std::string& name)
353 if (!tensor.defined()) {
354 throw std::invalid_argument(name +
" tensor is not defined");
357 if (tensor.dim() != expected_dims) {
358 throw std::invalid_argument(
359 name +
" must have " + std::to_string(expected_dims) +
360 " dimensions, got " + std::to_string(tensor.dim())
364 if (tensor.numel() == 0) {
365 throw std::invalid_argument(name +
" tensor cannot be empty");
368 // Check for NaN or Inf values
369 if (torch::any(torch::isnan(tensor)).item<bool>()) {
370 throw std::invalid_argument(name +
" contains NaN values");
373 if (torch::any(torch::isinf(tensor)).item<bool>()) {
374 throw std::invalid_argument(name +
" contains infinite values");
378}
// namespace svm_classifier
svm_node * to_svm_node(const torch::Tensor &sample)
Convert single sample to libsvm format.
void cleanup()
Clean up all allocated memory.
torch::Tensor from_decision_values(const std::vector< std::vector< double > > &decision_values)
Convert decision values back to PyTorch tensor.
torch::Tensor from_probabilities(const std::vector< std::vector< double > > &probabilities)
Convert probabilities back to PyTorch tensor.
DataConverter()
Default constructor.
std::unique_ptr< svm_problem > to_svm_problem(const torch::Tensor &X, const torch::Tensor &y=torch::Tensor())
Convert PyTorch tensors to libsvm format.
feature_node * to_feature_node(const torch::Tensor &sample)
Convert single sample to liblinear format.
std::unique_ptr< problem > to_linear_problem(const torch::Tensor &X, const torch::Tensor &y=torch::Tensor())
Convert PyTorch tensors to liblinear format.
void validate_tensors(const torch::Tensor &X, const torch::Tensor &y=torch::Tensor())
Validate input tensors.
torch::Tensor from_predictions(const std::vector< double > &predictions)
Convert predictions back to PyTorch tensor.
~DataConverter()
Destructor - cleans up allocated memory.