Skip to main content

sklears_svm/
multi_label_svm.rs

1//! Multi-Label Support Vector Machines
2//!
3//! This module provides various strategies for multi-label classification using SVMs:
4//! - Binary Relevance: Independent binary classifiers for each label
5//! - Classifier Chains: Sequential classifiers using label predictions as features
6//! - Label Powerset: Multi-class approach treating label combinations as classes
7//! - Multi-Label SVM: Direct multi-label optimization
8
9use scirs2_core::ndarray::{Array1, Array2};
10use std::cmp::Ordering;
11use std::collections::HashMap;
12use std::fmt;
13
14/// Errors that can occur during multi-label SVM training and prediction
15#[derive(Debug, Clone)]
16pub enum MultiLabelError {
17    InvalidInput(String),
18    TrainingError(String),
19    PredictionError(String),
20    DimensionMismatch(String),
21}
22
23impl fmt::Display for MultiLabelError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            MultiLabelError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
27            MultiLabelError::TrainingError(msg) => write!(f, "Training error: {msg}"),
28            MultiLabelError::PredictionError(msg) => write!(f, "Prediction error: {msg}"),
29            MultiLabelError::DimensionMismatch(msg) => write!(f, "Dimension mismatch: {msg}"),
30        }
31    }
32}
33
34impl std::error::Error for MultiLabelError {}
35
36/// Multi-label prediction result
37#[derive(Debug, Clone)]
38pub struct MultiLabelPrediction {
39    /// Binary predictions for each label
40    pub labels: Array2<f64>,
41    /// Confidence scores for each label
42    pub scores: Array2<f64>,
43    /// Label names
44    pub label_names: Vec<String>,
45}
46
47/// Strategy for handling multi-label classification
48#[derive(Debug, Clone)]
49pub enum MultiLabelStrategy {
50    /// Train independent binary classifiers for each label
51    BinaryRelevance,
52    /// Train classifiers in sequence, using previous predictions as features
53    ClassifierChains,
54    /// Transform to multi-class by treating label combinations as classes
55    LabelPowerset,
56    /// Direct multi-label SVM optimization
57    DirectOptimization,
58}
59
60/// Binary Relevance Multi-Label SVM
61///
62/// Trains one binary SVM classifier for each label independently.
63/// This is the simplest and most commonly used approach.
64#[derive(Debug, Clone)]
65pub struct BinaryRelevanceSVM {
66    /// Individual binary classifiers for each label
67    classifiers: Vec<crate::svc::SVC<sklears_core::traits::Trained>>,
68    /// Label names
69    label_names: Vec<String>,
70    /// Number of features
71    n_features: usize,
72    /// SVM hyperparameters
73    pub c: f64,
74    pub kernel: crate::svc::SvcKernel,
75    pub tolerance: f64,
76    pub max_iter: usize,
77}
78
79impl Default for BinaryRelevanceSVM {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl BinaryRelevanceSVM {
86    /// Create a new Binary Relevance Multi-Label SVM
87    pub fn new() -> Self {
88        Self {
89            classifiers: Vec::new(),
90            label_names: Vec::new(),
91            n_features: 0,
92            c: 1.0,
93            kernel: crate::svc::SvcKernel::Rbf { gamma: Some(1.0) },
94            tolerance: 1e-3,
95            max_iter: 1000,
96        }
97    }
98
99    /// Set the regularization parameter C
100    pub fn with_c(mut self, c: f64) -> Self {
101        self.c = c;
102        self
103    }
104
105    /// Set the kernel type
106    pub fn with_kernel(mut self, kernel: crate::svc::SvcKernel) -> Self {
107        self.kernel = kernel;
108        self
109    }
110
111    /// Set convergence tolerance
112    pub fn with_tolerance(mut self, tolerance: f64) -> Self {
113        self.tolerance = tolerance;
114        self
115    }
116
117    /// Set maximum iterations
118    pub fn with_max_iter(mut self, max_iter: usize) -> Self {
119        self.max_iter = max_iter;
120        self
121    }
122
123    /// Fit the multi-label SVM to training data
124    pub fn fit(&mut self, x: &Array2<f64>, y: &Array2<f64>) -> Result<(), MultiLabelError> {
125        let (n_samples, n_features) = x.dim();
126        let (y_samples, n_labels) = y.dim();
127
128        if n_samples != y_samples {
129            return Err(MultiLabelError::DimensionMismatch(format!(
130                "X has {} samples but y has {} samples",
131                n_samples, y_samples
132            )));
133        }
134
135        self.n_features = n_features;
136        self.label_names = (0..n_labels).map(|i| format!("label_{i}")).collect();
137        self.classifiers.clear();
138
139        // Train one binary classifier per label
140        for label_idx in 0..n_labels {
141            let y_binary = y.column(label_idx);
142
143            // Create and configure binary SVM
144            let svm = match &self.kernel {
145                crate::svc::SvcKernel::Linear => crate::svc::SVC::new()
146                    .c(self.c)
147                    .linear()
148                    .tol(self.tolerance)
149                    .max_iter(self.max_iter),
150                crate::svc::SvcKernel::Rbf { gamma } => crate::svc::SVC::new()
151                    .c(self.c)
152                    .rbf(*gamma)
153                    .tol(self.tolerance)
154                    .max_iter(self.max_iter),
155                crate::svc::SvcKernel::Poly {
156                    degree,
157                    gamma,
158                    coef0,
159                } => crate::svc::SVC::new()
160                    .c(self.c)
161                    .poly(*degree, *gamma, *coef0)
162                    .tol(self.tolerance)
163                    .max_iter(self.max_iter),
164                crate::svc::SvcKernel::Sigmoid { gamma, coef0: _ } => {
165                    crate::svc::SVC::new()
166                        .c(self.c)
167                        .rbf(*gamma) // Using RBF as placeholder since sigmoid not directly available
168                        .tol(self.tolerance)
169                        .max_iter(self.max_iter)
170                }
171                crate::svc::SvcKernel::Custom(kernel_type) => crate::svc::SVC::new()
172                    .c(self.c)
173                    .kernel(kernel_type.clone())
174                    .tol(self.tolerance)
175                    .max_iter(self.max_iter),
176            };
177
178            // Convert to binary labels (ensure we have 0/1)
179            let y_binary_vec: Vec<f64> = y_binary
180                .iter()
181                .map(|&val| if val > 0.5 { 1.0 } else { 0.0 })
182                .collect();
183            let y_binary_array = Array1::from_vec(y_binary_vec);
184
185            // Train the binary classifier using the Fit trait
186            use sklears_core::traits::Fit;
187            let fitted_svm = svm.fit(x, &y_binary_array).map_err(|e| {
188                MultiLabelError::TrainingError(format!(
189                    "Failed to train classifier for label {}: {:?}",
190                    label_idx, e
191                ))
192            })?;
193
194            self.classifiers.push(fitted_svm);
195        }
196
197        Ok(())
198    }
199
200    /// Predict labels for new data
201    pub fn predict(&self, x: &Array2<f64>) -> Result<MultiLabelPrediction, MultiLabelError> {
202        if self.classifiers.is_empty() {
203            return Err(MultiLabelError::PredictionError(
204                "Model not trained".to_string(),
205            ));
206        }
207
208        let (n_samples, n_features) = x.dim();
209        if n_features != self.n_features {
210            return Err(MultiLabelError::DimensionMismatch(format!(
211                "Expected {} features, got {}",
212                self.n_features, n_features
213            )));
214        }
215
216        let n_labels = self.classifiers.len();
217        let mut predictions = Array2::zeros((n_samples, n_labels));
218        let mut scores = Array2::zeros((n_samples, n_labels));
219
220        // Get predictions from each binary classifier
221        for (label_idx, classifier) in self.classifiers.iter().enumerate() {
222            use sklears_core::traits::Predict;
223            let binary_pred = classifier.predict(x).map_err(|e| {
224                MultiLabelError::PredictionError(format!(
225                    "Prediction failed for label {}: {:?}",
226                    label_idx, e
227                ))
228            })?;
229
230            let decision_scores = classifier.decision_function(x).map_err(|e| {
231                MultiLabelError::PredictionError(format!(
232                    "Decision function failed for label {}: {:?}",
233                    label_idx, e
234                ))
235            })?;
236
237            // Convert predictions to 0/1
238            for (sample_idx, &pred) in binary_pred.iter().enumerate() {
239                predictions[[sample_idx, label_idx]] = if pred > 0.0 { 1.0 } else { 0.0 };
240                scores[[sample_idx, label_idx]] = decision_scores[sample_idx];
241            }
242        }
243
244        Ok(MultiLabelPrediction {
245            labels: predictions,
246            scores,
247            label_names: self.label_names.clone(),
248        })
249    }
250
251    /// Predict probabilities for each label
252    pub fn predict_proba(&self, x: &Array2<f64>) -> Result<Array2<f64>, MultiLabelError> {
253        if self.classifiers.is_empty() {
254            return Err(MultiLabelError::PredictionError(
255                "Model not trained".to_string(),
256            ));
257        }
258
259        let (n_samples, n_features) = x.dim();
260        if n_features != self.n_features {
261            return Err(MultiLabelError::DimensionMismatch(format!(
262                "Expected {} features, got {}",
263                self.n_features, n_features
264            )));
265        }
266
267        let n_labels = self.classifiers.len();
268        let mut probabilities = Array2::zeros((n_samples, n_labels));
269
270        // Get probabilities from each binary classifier
271        for (label_idx, classifier) in self.classifiers.iter().enumerate() {
272            let decision_scores = classifier.decision_function(x).map_err(|e| {
273                MultiLabelError::PredictionError(format!(
274                    "Decision function failed for label {}: {:?}",
275                    label_idx, e
276                ))
277            })?;
278
279            // Convert decision scores to probabilities using sigmoid
280            for (sample_idx, &score) in decision_scores.iter().enumerate() {
281                probabilities[[sample_idx, label_idx]] = 1.0 / (1.0 + (-score).exp());
282            }
283        }
284
285        Ok(probabilities)
286    }
287}
288
289/// Classifier Chains Multi-Label SVM
290///
291/// Trains binary classifiers in a chain where each classifier uses
292/// the predictions of previous classifiers as additional features.
293#[derive(Debug, Clone)]
294pub struct ClassifierChainsSVM {
295    /// Chain of binary classifiers
296    classifiers: Vec<crate::svc::SVC<sklears_core::traits::Trained>>,
297    /// Label names
298    label_names: Vec<String>,
299    /// Chain order (which label to predict at each step)
300    chain_order: Vec<usize>,
301    /// Number of original features
302    n_features: usize,
303    /// SVM hyperparameters
304    pub c: f64,
305    pub kernel: crate::svc::SvcKernel,
306    pub tolerance: f64,
307    pub max_iter: usize,
308}
309
310impl Default for ClassifierChainsSVM {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316impl ClassifierChainsSVM {
317    /// Create a new Classifier Chains Multi-Label SVM
318    pub fn new() -> Self {
319        Self {
320            classifiers: Vec::new(),
321            label_names: Vec::new(),
322            chain_order: Vec::new(),
323            n_features: 0,
324            c: 1.0,
325            kernel: crate::svc::SvcKernel::Rbf { gamma: Some(1.0) },
326            tolerance: 1e-3,
327            max_iter: 1000,
328        }
329    }
330
331    /// Set the regularization parameter C
332    pub fn with_c(mut self, c: f64) -> Self {
333        self.c = c;
334        self
335    }
336
337    /// Set the kernel type
338    pub fn with_kernel(mut self, kernel: crate::svc::SvcKernel) -> Self {
339        self.kernel = kernel;
340        self
341    }
342
343    /// Set the chain order (default is 0, 1, 2, ...)
344    pub fn with_chain_order(mut self, order: Vec<usize>) -> Self {
345        self.chain_order = order;
346        self
347    }
348
349    /// Fit the classifier chain to training data
350    pub fn fit(&mut self, x: &Array2<f64>, y: &Array2<f64>) -> Result<(), MultiLabelError> {
351        let (n_samples, n_features) = x.dim();
352        let (y_samples, n_labels) = y.dim();
353
354        if n_samples != y_samples {
355            return Err(MultiLabelError::DimensionMismatch(format!(
356                "X has {} samples but y has {} samples",
357                n_samples, y_samples
358            )));
359        }
360
361        self.n_features = n_features;
362        self.label_names = (0..n_labels).map(|i| format!("label_{i}")).collect();
363
364        // Set default chain order if not specified
365        if self.chain_order.is_empty() {
366            self.chain_order = (0..n_labels).collect();
367        }
368
369        if self.chain_order.len() != n_labels {
370            return Err(MultiLabelError::InvalidInput(
371                "Chain order length must match number of labels".to_string(),
372            ));
373        }
374
375        self.classifiers.clear();
376
377        // Train classifiers in chain order
378        for (chain_pos, &label_idx) in self.chain_order.iter().enumerate() {
379            let y_binary = y.column(label_idx);
380
381            // Create augmented features by adding predictions from previous classifiers
382            let x_augmented = if chain_pos == 0 {
383                x.clone()
384            } else {
385                // Add predictions from previous classifiers as features
386                let mut augmented = Array2::zeros((n_samples, n_features + chain_pos));
387
388                // Copy original features
389                for i in 0..n_samples {
390                    for j in 0..n_features {
391                        augmented[[i, j]] = x[[i, j]];
392                    }
393                }
394
395                // Add predictions from previous classifiers
396                for prev_pos in 0..chain_pos {
397                    let prev_label_idx = self.chain_order[prev_pos];
398                    let y_prev_binary = y.column(prev_label_idx);
399
400                    for i in 0..n_samples {
401                        augmented[[i, n_features + prev_pos]] =
402                            if y_prev_binary[i] > 0.5 { 1.0 } else { 0.0 };
403                    }
404                }
405
406                augmented
407            };
408
409            // Create and configure binary SVM
410            let svm = match &self.kernel {
411                crate::svc::SvcKernel::Linear => crate::svc::SVC::new()
412                    .c(self.c)
413                    .linear()
414                    .tol(self.tolerance)
415                    .max_iter(self.max_iter),
416                crate::svc::SvcKernel::Rbf { gamma } => crate::svc::SVC::new()
417                    .c(self.c)
418                    .rbf(*gamma)
419                    .tol(self.tolerance)
420                    .max_iter(self.max_iter),
421                crate::svc::SvcKernel::Poly {
422                    degree,
423                    gamma,
424                    coef0,
425                } => crate::svc::SVC::new()
426                    .c(self.c)
427                    .poly(*degree, *gamma, *coef0)
428                    .tol(self.tolerance)
429                    .max_iter(self.max_iter),
430                crate::svc::SvcKernel::Sigmoid { gamma, coef0: _ } => {
431                    crate::svc::SVC::new()
432                        .c(self.c)
433                        .rbf(*gamma) // Using RBF as placeholder
434                        .tol(self.tolerance)
435                        .max_iter(self.max_iter)
436                }
437                crate::svc::SvcKernel::Custom(kernel_type) => crate::svc::SVC::new()
438                    .c(self.c)
439                    .kernel(kernel_type.clone())
440                    .tol(self.tolerance)
441                    .max_iter(self.max_iter),
442            };
443
444            // Convert to binary labels
445            let y_binary_vec: Vec<f64> = y_binary
446                .iter()
447                .map(|&val| if val > 0.5 { 1.0 } else { 0.0 })
448                .collect();
449            let y_binary_array = Array1::from_vec(y_binary_vec);
450
451            // Train the classifier using the Fit trait
452            use sklears_core::traits::Fit;
453            let fitted_svm = svm.fit(&x_augmented, &y_binary_array).map_err(|e| {
454                MultiLabelError::TrainingError(format!(
455                    "Failed to train classifier for label {} in chain: {:?}",
456                    label_idx, e
457                ))
458            })?;
459
460            self.classifiers.push(fitted_svm);
461        }
462
463        Ok(())
464    }
465
466    /// Predict labels for new data
467    pub fn predict(&self, x: &Array2<f64>) -> Result<MultiLabelPrediction, MultiLabelError> {
468        if self.classifiers.is_empty() {
469            return Err(MultiLabelError::PredictionError(
470                "Model not trained".to_string(),
471            ));
472        }
473
474        let (n_samples, n_features) = x.dim();
475        if n_features != self.n_features {
476            return Err(MultiLabelError::DimensionMismatch(format!(
477                "Expected {} features, got {}",
478                self.n_features, n_features
479            )));
480        }
481
482        let n_labels = self.chain_order.len();
483        let mut predictions = Array2::zeros((n_samples, n_labels));
484        let mut scores = Array2::zeros((n_samples, n_labels));
485
486        // Predict labels in chain order
487        for (chain_pos, &label_idx) in self.chain_order.iter().enumerate() {
488            // Create augmented features
489            let x_augmented = if chain_pos == 0 {
490                x.clone()
491            } else {
492                let mut augmented = Array2::zeros((n_samples, n_features + chain_pos));
493
494                // Copy original features
495                for i in 0..n_samples {
496                    for j in 0..n_features {
497                        augmented[[i, j]] = x[[i, j]];
498                    }
499                }
500
501                // Add predictions from previous classifiers
502                for prev_pos in 0..chain_pos {
503                    let prev_label_idx = self.chain_order[prev_pos];
504                    for i in 0..n_samples {
505                        augmented[[i, n_features + prev_pos]] = predictions[[i, prev_label_idx]];
506                    }
507                }
508
509                augmented
510            };
511
512            // Get prediction from current classifier
513            let classifier = &self.classifiers[chain_pos];
514            use sklears_core::traits::Predict;
515            let binary_pred = classifier.predict(&x_augmented).map_err(|e| {
516                MultiLabelError::PredictionError(format!(
517                    "Prediction failed for label {} in chain: {:?}",
518                    label_idx, e
519                ))
520            })?;
521
522            let decision_scores = classifier.decision_function(&x_augmented).map_err(|e| {
523                MultiLabelError::PredictionError(format!(
524                    "Decision function failed for label {} in chain: {:?}",
525                    label_idx, e
526                ))
527            })?;
528
529            // Store predictions
530            for (sample_idx, &pred) in binary_pred.iter().enumerate() {
531                predictions[[sample_idx, label_idx]] = if pred > 0.0 { 1.0 } else { 0.0 };
532                scores[[sample_idx, label_idx]] = decision_scores[sample_idx];
533            }
534        }
535
536        Ok(MultiLabelPrediction {
537            labels: predictions,
538            scores,
539            label_names: self.label_names.clone(),
540        })
541    }
542}
543
544/// Label Powerset Multi-Label SVM
545///
546/// Transforms the multi-label problem into a multi-class problem
547/// by treating each unique combination of labels as a separate class.
548#[derive(Debug, Clone)]
549pub struct LabelPowersetSVM {
550    /// Multi-class classifier
551    classifier: Option<crate::svc::SVC<sklears_core::traits::Trained>>,
552    /// One-vs-rest classifiers for multi-class fallback
553    ovr_classifiers: Vec<crate::svc::SVC<sklears_core::traits::Trained>>,
554    /// Ordering of class identifiers corresponding to one-vs-rest classifiers
555    ovr_class_order: Vec<usize>,
556    /// Single-class shortcut when only one powerset class exists
557    single_class_id: Option<usize>,
558    /// Mapping of negative/positive classes for binary fallback
559    binary_class_mapping: Option<(usize, usize)>,
560    /// Mapping from class index to label combination
561    class_to_labels: HashMap<usize, Vec<usize>>,
562    /// Mapping from label combination to class index
563    labels_to_class: HashMap<Vec<usize>, usize>,
564    /// Label names
565    label_names: Vec<String>,
566    /// Number of features
567    n_features: usize,
568    /// SVM hyperparameters
569    pub c: f64,
570    pub kernel: crate::svc::SvcKernel,
571    pub tolerance: f64,
572    pub max_iter: usize,
573}
574
575impl Default for LabelPowersetSVM {
576    fn default() -> Self {
577        Self::new()
578    }
579}
580
581impl LabelPowersetSVM {
582    /// Create a new Label Powerset Multi-Label SVM
583    pub fn new() -> Self {
584        Self {
585            classifier: None,
586            ovr_classifiers: Vec::new(),
587            ovr_class_order: Vec::new(),
588            single_class_id: None,
589            binary_class_mapping: None,
590            class_to_labels: HashMap::new(),
591            labels_to_class: HashMap::new(),
592            label_names: Vec::new(),
593            n_features: 0,
594            c: 1.0,
595            kernel: crate::svc::SvcKernel::Rbf { gamma: Some(1.0) },
596            tolerance: 1e-3,
597            max_iter: 1000,
598        }
599    }
600
601    /// Set the regularization parameter C
602    pub fn with_c(mut self, c: f64) -> Self {
603        self.c = c;
604        self
605    }
606
607    /// Set the kernel type
608    pub fn with_kernel(mut self, kernel: crate::svc::SvcKernel) -> Self {
609        self.kernel = kernel;
610        self
611    }
612
613    fn build_base_svc(&self) -> crate::svc::SVC<sklears_core::traits::Untrained> {
614        let base = crate::svc::SVC::new()
615            .c(self.c)
616            .tol(self.tolerance)
617            .max_iter(self.max_iter);
618
619        match &self.kernel {
620            crate::svc::SvcKernel::Linear => base.linear(),
621            crate::svc::SvcKernel::Rbf { gamma } => base.rbf(*gamma),
622            crate::svc::SvcKernel::Poly {
623                degree,
624                gamma,
625                coef0,
626            } => base.poly(*degree, *gamma, *coef0),
627            crate::svc::SvcKernel::Sigmoid { gamma, .. } => base.rbf(*gamma),
628            crate::svc::SvcKernel::Custom(kernel_type) => base.kernel(kernel_type.clone()),
629        }
630    }
631
632    /// Convert multi-label targets to powerset classes
633    fn create_powerset_mapping(&mut self, y: &Array2<f64>) -> Vec<usize> {
634        let (n_samples, n_labels) = y.dim();
635        let mut powerset_targets = Vec::with_capacity(n_samples);
636
637        self.class_to_labels.clear();
638        self.labels_to_class.clear();
639        let mut next_class_id = 0;
640
641        for sample_idx in 0..n_samples {
642            // Find active labels for this sample
643            let mut active_labels = Vec::new();
644            for label_idx in 0..n_labels {
645                if y[[sample_idx, label_idx]] > 0.5 {
646                    active_labels.push(label_idx);
647                }
648            }
649            // Sort to ensure consistent ordering
650            active_labels.sort();
651
652            // Get or create class ID for this label combination
653            let class_id = if let Some(&existing_class) = self.labels_to_class.get(&active_labels) {
654                existing_class
655            } else {
656                let new_class = next_class_id;
657                next_class_id += 1;
658                self.class_to_labels
659                    .insert(new_class, active_labels.clone());
660                self.labels_to_class.insert(active_labels, new_class);
661                new_class
662            };
663
664            powerset_targets.push(class_id);
665        }
666
667        powerset_targets
668    }
669
670    /// Fit the label powerset SVM to training data
671    pub fn fit(&mut self, x: &Array2<f64>, y: &Array2<f64>) -> Result<(), MultiLabelError> {
672        let (n_samples, n_features) = x.dim();
673        let (y_samples, n_labels) = y.dim();
674
675        if n_samples != y_samples {
676            return Err(MultiLabelError::DimensionMismatch(format!(
677                "X has {} samples but y has {} samples",
678                n_samples, y_samples
679            )));
680        }
681
682        self.n_features = n_features;
683        self.label_names = (0..n_labels).map(|i| format!("label_{i}")).collect();
684
685        // Create powerset mapping
686        let powerset_targets = self.create_powerset_mapping(y);
687        let mut class_ids: Vec<usize> = self.class_to_labels.keys().copied().collect();
688        class_ids.sort_unstable();
689
690        if class_ids.is_empty() {
691            return Err(MultiLabelError::InvalidInput(
692                "No label combinations found in training data".to_string(),
693            ));
694        }
695
696        self.classifier = None;
697        self.ovr_classifiers.clear();
698        self.ovr_class_order.clear();
699        self.single_class_id = None;
700        self.binary_class_mapping = None;
701
702        use sklears_core::traits::Fit;
703
704        if class_ids.len() == 1 {
705            self.single_class_id = Some(class_ids[0]);
706            return Ok(());
707        }
708
709        if class_ids.len() == 2 {
710            let negative_class = class_ids[0];
711            let positive_class = class_ids[1];
712            let binary_targets = Array1::from_vec(
713                powerset_targets
714                    .iter()
715                    .map(|&class_id| {
716                        if class_id == positive_class {
717                            1.0
718                        } else {
719                            -1.0
720                        }
721                    })
722                    .collect(),
723            );
724
725            let svm = self.build_base_svc();
726            let fitted = svm.fit(x, &binary_targets).map_err(|e| {
727                MultiLabelError::TrainingError(format!(
728                    "Failed to train binary powerset classifier: {:?}",
729                    e
730                ))
731            })?;
732
733            self.classifier = Some(fitted);
734            self.binary_class_mapping = Some((negative_class, positive_class));
735            return Ok(());
736        }
737
738        for &class_id in &class_ids {
739            let binary_targets = Array1::from_vec(
740                powerset_targets
741                    .iter()
742                    .map(|&target| if target == class_id { 1.0 } else { -1.0 })
743                    .collect(),
744            );
745
746            let svm = self.build_base_svc();
747            let fitted = svm.fit(x, &binary_targets).map_err(|e| {
748                MultiLabelError::TrainingError(format!(
749                    "Failed to train one-vs-rest classifier for class {class_id}: {:?}",
750                    e
751                ))
752            })?;
753
754            self.ovr_classifiers.push(fitted);
755            self.ovr_class_order.push(class_id);
756        }
757
758        Ok(())
759    }
760
761    /// Predict labels for new data
762    pub fn predict(&self, x: &Array2<f64>) -> Result<MultiLabelPrediction, MultiLabelError> {
763        let (n_samples, n_features) = x.dim();
764        if n_features != self.n_features {
765            return Err(MultiLabelError::DimensionMismatch(format!(
766                "Expected {} features, got {}",
767                self.n_features, n_features
768            )));
769        }
770
771        if self.class_to_labels.is_empty() {
772            return Err(MultiLabelError::PredictionError(
773                "Model not trained".to_string(),
774            ));
775        }
776
777        let n_labels = self.label_names.len();
778        let mut predictions = Array2::zeros((n_samples, n_labels));
779        let mut scores = Array2::zeros((n_samples, n_labels));
780
781        let mut predicted_classes: Vec<usize> = Vec::with_capacity(n_samples);
782        let mut decision_margins: Vec<f64> = vec![0.0; n_samples];
783
784        if let Some(class_id) = self.single_class_id {
785            predicted_classes.resize(n_samples, class_id);
786            decision_margins.fill(1.0);
787        } else if let Some((negative_class, positive_class)) = self.binary_class_mapping {
788            let classifier = self.classifier.as_ref().ok_or_else(|| {
789                MultiLabelError::PredictionError(
790                    "Binary classifier not available for prediction".to_string(),
791                )
792            })?;
793
794            let decision_scores = classifier.decision_function(x).map_err(|e| {
795                MultiLabelError::PredictionError(format!(
796                    "Decision function failed for binary powerset classifier: {:?}",
797                    e
798                ))
799            })?;
800
801            for (idx, &score) in decision_scores.iter().enumerate() {
802                decision_margins[idx] = score;
803                if score >= 0.0 {
804                    predicted_classes.push(positive_class);
805                } else {
806                    predicted_classes.push(negative_class);
807                }
808            }
809        } else if !self.ovr_classifiers.is_empty() {
810            let mut best_scores = vec![f64::NEG_INFINITY; n_samples];
811            let mut best_classes = vec![usize::MAX; n_samples];
812
813            for (classifier, &class_id) in
814                self.ovr_classifiers.iter().zip(self.ovr_class_order.iter())
815            {
816                let scores_vec = classifier.decision_function(x).map_err(|e| {
817                    MultiLabelError::PredictionError(format!(
818                        "Decision function failed for OvR classifier {class_id}: {:?}",
819                        e
820                    ))
821                })?;
822
823                for (sample_idx, &score) in scores_vec.iter().enumerate() {
824                    match score.partial_cmp(&best_scores[sample_idx]) {
825                        Some(Ordering::Greater) => {
826                            best_scores[sample_idx] = score;
827                            best_classes[sample_idx] = class_id;
828                        }
829                        Some(Ordering::Equal) if class_id < best_classes[sample_idx] => {
830                            best_scores[sample_idx] = score;
831                            best_classes[sample_idx] = class_id;
832                        }
833                        _ => {}
834                    }
835                }
836            }
837
838            for (idx, &class_id) in best_classes.iter().enumerate() {
839                if class_id == usize::MAX {
840                    return Err(MultiLabelError::PredictionError(
841                        "One-vs-rest classifiers produced no prediction".to_string(),
842                    ));
843                }
844                predicted_classes.push(class_id);
845                decision_margins[idx] = best_scores[idx];
846            }
847        } else {
848            let classifier = self.classifier.as_ref().ok_or_else(|| {
849                MultiLabelError::PredictionError(
850                    "No classifier available for prediction".to_string(),
851                )
852            })?;
853
854            use sklears_core::traits::Predict;
855            let powerset_pred = classifier.predict(x).map_err(|e| {
856                MultiLabelError::PredictionError(format!("Powerset prediction failed: {:?}", e))
857            })?;
858
859            let decision_scores = classifier.decision_function(x).map_err(|e| {
860                MultiLabelError::PredictionError(format!("Decision function failed: {:?}", e))
861            })?;
862
863            for (idx, &value) in powerset_pred.iter().enumerate() {
864                predicted_classes.push(value as usize);
865                decision_margins[idx] = decision_scores[idx];
866            }
867        }
868
869        for (sample_idx, &class_id) in predicted_classes.iter().enumerate() {
870            if let Some(labels) = self.class_to_labels.get(&class_id) {
871                for &label_idx in labels {
872                    predictions[[sample_idx, label_idx]] = 1.0;
873                    scores[[sample_idx, label_idx]] = decision_margins[sample_idx];
874                }
875            }
876        }
877
878        Ok(MultiLabelPrediction {
879            labels: predictions,
880            scores,
881            label_names: self.label_names.clone(),
882        })
883    }
884}
885
886#[allow(non_snake_case)]
887#[cfg(test)]
888mod tests {
889    use super::*;
890
891    fn create_test_data() -> (Array2<f64>, Array2<f64>) {
892        // Create simple multi-label test data
893        let X_var = Array2::from_shape_vec(
894            (6, 2),
895            vec![1.0, 2.0, 2.0, 3.0, 3.0, 1.0, 4.0, 5.0, 5.0, 4.0, 6.0, 6.0],
896        )
897        .expect("operation should succeed");
898
899        let y = Array2::from_shape_vec(
900            (6, 3),
901            vec![
902                1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0,
903                1.0, 1.0,
904            ],
905        )
906        .expect("operation should succeed");
907
908        (X_var, y)
909    }
910
911    #[test]
912    #[ignore]
913    fn test_binary_relevance_svm() {
914        let (X, y) = create_test_data();
915
916        let mut br_svm = BinaryRelevanceSVM::new()
917            .with_c(1.0)
918            .with_kernel(crate::svc::SvcKernel::Linear);
919
920        // Test fitting
921        assert!(br_svm.fit(&X, &y).is_ok());
922
923        // Test prediction
924        let pred = br_svm.predict(&X).expect("prediction should succeed");
925        assert_eq!(pred.labels.dim(), (6, 3));
926        assert_eq!(pred.scores.dim(), (6, 3));
927        assert_eq!(pred.label_names.len(), 3);
928
929        // Test predict_proba
930        let proba = br_svm
931            .predict_proba(&X)
932            .expect("probability prediction should succeed");
933        assert_eq!(proba.dim(), (6, 3));
934
935        // Check that probabilities are in [0, 1]
936        for &p in proba.iter() {
937            assert!((0.0..=1.0).contains(&p));
938        }
939    }
940
941    #[test]
942    #[ignore]
943    fn test_classifier_chains_svm() {
944        let (X, y) = create_test_data();
945
946        let mut cc_svm = ClassifierChainsSVM::new()
947            .with_c(1.0)
948            .with_kernel(crate::svc::SvcKernel::Linear);
949
950        // Test fitting
951        assert!(cc_svm.fit(&X, &y).is_ok());
952
953        // Test prediction
954        let pred = cc_svm.predict(&X).expect("prediction should succeed");
955        assert_eq!(pred.labels.dim(), (6, 3));
956        assert_eq!(pred.scores.dim(), (6, 3));
957        assert_eq!(pred.label_names.len(), 3);
958    }
959
960    #[test]
961    #[ignore = "timeout"]
962    fn test_label_powerset_svm() {
963        let (X, y) = create_test_data();
964
965        let mut lp_svm = LabelPowersetSVM::new()
966            .with_c(1.0)
967            .with_kernel(crate::svc::SvcKernel::Linear);
968
969        // Test fitting
970        assert!(lp_svm.fit(&X, &y).is_ok());
971
972        // Test prediction
973        let pred = lp_svm.predict(&X).expect("prediction should succeed");
974        assert_eq!(pred.labels.dim(), (6, 3));
975        assert_eq!(pred.scores.dim(), (6, 3));
976        assert_eq!(pred.label_names.len(), 3);
977    }
978
979    #[test]
980    fn test_invalid_input() {
981        let X_var = Array2::zeros((5, 2));
982        let y = Array2::zeros((6, 3)); // Wrong number of samples
983
984        let mut br_svm = BinaryRelevanceSVM::new();
985        assert!(br_svm.fit(&X_var, &y).is_err());
986    }
987
988    #[test]
989    fn test_prediction_before_training() {
990        let X_var = Array2::zeros((5, 2));
991        let br_svm = BinaryRelevanceSVM::new();
992        assert!(br_svm.predict(&X_var).is_err());
993    }
994}