1use scirs2_core::ndarray::{Array1, Array2};
10use std::cmp::Ordering;
11use std::collections::HashMap;
12use std::fmt;
13
14#[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#[derive(Debug, Clone)]
38pub struct MultiLabelPrediction {
39 pub labels: Array2<f64>,
41 pub scores: Array2<f64>,
43 pub label_names: Vec<String>,
45}
46
47#[derive(Debug, Clone)]
49pub enum MultiLabelStrategy {
50 BinaryRelevance,
52 ClassifierChains,
54 LabelPowerset,
56 DirectOptimization,
58}
59
60#[derive(Debug, Clone)]
65pub struct BinaryRelevanceSVM {
66 classifiers: Vec<crate::svc::SVC<sklears_core::traits::Trained>>,
68 label_names: Vec<String>,
70 n_features: usize,
72 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 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 pub fn with_c(mut self, c: f64) -> Self {
101 self.c = c;
102 self
103 }
104
105 pub fn with_kernel(mut self, kernel: crate::svc::SvcKernel) -> Self {
107 self.kernel = kernel;
108 self
109 }
110
111 pub fn with_tolerance(mut self, tolerance: f64) -> Self {
113 self.tolerance = tolerance;
114 self
115 }
116
117 pub fn with_max_iter(mut self, max_iter: usize) -> Self {
119 self.max_iter = max_iter;
120 self
121 }
122
123 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 for label_idx in 0..n_labels {
141 let y_binary = y.column(label_idx);
142
143 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) .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 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
294pub struct ClassifierChainsSVM {
295 classifiers: Vec<crate::svc::SVC<sklears_core::traits::Trained>>,
297 label_names: Vec<String>,
299 chain_order: Vec<usize>,
301 n_features: usize,
303 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 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 pub fn with_c(mut self, c: f64) -> Self {
333 self.c = c;
334 self
335 }
336
337 pub fn with_kernel(mut self, kernel: crate::svc::SvcKernel) -> Self {
339 self.kernel = kernel;
340 self
341 }
342
343 pub fn with_chain_order(mut self, order: Vec<usize>) -> Self {
345 self.chain_order = order;
346 self
347 }
348
349 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 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 for (chain_pos, &label_idx) in self.chain_order.iter().enumerate() {
379 let y_binary = y.column(label_idx);
380
381 let x_augmented = if chain_pos == 0 {
383 x.clone()
384 } else {
385 let mut augmented = Array2::zeros((n_samples, n_features + chain_pos));
387
388 for i in 0..n_samples {
390 for j in 0..n_features {
391 augmented[[i, j]] = x[[i, j]];
392 }
393 }
394
395 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 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) .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 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 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 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 for (chain_pos, &label_idx) in self.chain_order.iter().enumerate() {
488 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 for i in 0..n_samples {
496 for j in 0..n_features {
497 augmented[[i, j]] = x[[i, j]];
498 }
499 }
500
501 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 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 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#[derive(Debug, Clone)]
549pub struct LabelPowersetSVM {
550 classifier: Option<crate::svc::SVC<sklears_core::traits::Trained>>,
552 ovr_classifiers: Vec<crate::svc::SVC<sklears_core::traits::Trained>>,
554 ovr_class_order: Vec<usize>,
556 single_class_id: Option<usize>,
558 binary_class_mapping: Option<(usize, usize)>,
560 class_to_labels: HashMap<usize, Vec<usize>>,
562 labels_to_class: HashMap<Vec<usize>, usize>,
564 label_names: Vec<String>,
566 n_features: usize,
568 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 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 pub fn with_c(mut self, c: f64) -> Self {
603 self.c = c;
604 self
605 }
606
607 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 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 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 active_labels.sort();
651
652 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 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 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 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 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 assert!(br_svm.fit(&X, &y).is_ok());
922
923 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 let proba = br_svm
931 .predict_proba(&X)
932 .expect("probability prediction should succeed");
933 assert_eq!(proba.dim(), (6, 3));
934
935 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 assert!(cc_svm.fit(&X, &y).is_ok());
952
953 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 assert!(lp_svm.fit(&X, &y).is_ok());
971
972 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)); 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}