1use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis};
9use sklears_core::{
10 error::{Result as SklResult, SklearsError},
11 types::Float,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum ReconstructionMethod {
17 Linear,
19 IterativeThresholding,
21 OrthogonalMatchingPursuit,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum PruningStrategy {
28 Default,
30 Similarity,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum ClassificationCriterion {
37 Gini,
39 Entropy,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45pub enum ThresholdStrategy {
46 Fixed,
48 PerLabel,
50 Optimal,
52 FScore,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq)]
58pub enum CalibrationMethod {
59 Sigmoid,
61 Isotonic,
63}
64
65#[derive(Debug, Clone)]
67pub struct SimpleLinearClassifier {
68 pub weights: Array1<Float>,
70 pub bias: Float,
72}
73
74#[derive(Debug, Clone)]
76pub struct SimpleBinaryModel {
77 pub weights: Array1<Float>,
79 pub bias: Float,
81 pub accuracy: Float,
83}
84
85#[derive(Debug, Clone)]
87pub struct BayesianBinaryModel {
88 pub weight_mean: Array1<Float>,
90 pub weight_cov: Array2<Float>,
92 pub bias_mean: Float,
94 pub bias_var: Float,
96 pub noise_precision: Float,
98}
99
100#[derive(Debug, Clone)]
102pub struct CostMatrix {
103 pub fp_costs: Vec<Float>,
105 pub fn_costs: Vec<Float>,
107}
108
109impl CostMatrix {
110 pub fn from_fp_fn_costs(fp_costs: Vec<Float>, fn_costs: Vec<Float>) -> SklResult<Self> {
112 if fp_costs.len() != fn_costs.len() {
113 return Err(SklearsError::InvalidInput(
114 "False positive and false negative cost vectors must have the same length"
115 .to_string(),
116 ));
117 }
118
119 if fp_costs.is_empty() {
120 return Err(SklearsError::InvalidInput(
121 "Cost vectors cannot be empty".to_string(),
122 ));
123 }
124
125 for &cost in fp_costs.iter().chain(fn_costs.iter()) {
127 if cost < 0.0 {
128 return Err(SklearsError::InvalidInput(
129 "All costs must be non-negative".to_string(),
130 ));
131 }
132 }
133
134 Ok(Self { fp_costs, fn_costs })
135 }
136
137 pub fn balanced(n_labels: usize) -> Self {
139 Self {
140 fp_costs: vec![1.0; n_labels],
141 fn_costs: vec![1.0; n_labels],
142 }
143 }
144
145 pub fn get_threshold(&self, label_idx: usize) -> Float {
147 if label_idx >= self.fp_costs.len() {
148 return 0.5; }
150
151 let fp_cost = self.fp_costs[label_idx];
152 let fn_cost = self.fn_costs[label_idx];
153
154 if fp_cost + fn_cost > 0.0 {
156 fp_cost / (fp_cost + fn_cost)
157 } else {
158 0.5
159 }
160 }
161}
162
163pub fn euclidean_distance(x1: &ArrayView1<Float>, x2: &ArrayView1<Float>) -> Float {
165 x1.iter()
166 .zip(x2.iter())
167 .map(|(a, b)| (a - b).powi(2))
168 .sum::<Float>()
169 .sqrt()
170}
171
172#[allow(non_snake_case)] pub fn standardize_features_simple(
175 X: &ArrayView2<Float>,
176 means: &Array1<Float>,
177 stds: &Array1<Float>,
178) -> Array2<Float> {
179 let mut X_standardized = X.to_owned();
180
181 for (mut col, (&mean, &std)) in X_standardized
182 .axis_iter_mut(Axis(1))
183 .zip(means.iter().zip(stds.iter()))
184 {
185 col.mapv_inplace(|x| (x - mean) / std);
186 }
187
188 X_standardized
189}
190
191#[allow(non_snake_case)] pub fn train_binary_classifier(
194 X: &ArrayView2<Float>,
195 y: &Array1<i32>,
196) -> SklResult<SimpleBinaryModel> {
197 let (n_samples, n_features) = X.dim();
198
199 if n_samples != y.len() {
200 return Err(SklearsError::InvalidInput(
201 "X and y must have the same number of samples".to_string(),
202 ));
203 }
204
205 let y_float: Array1<Float> = y.mapv(|x| x as Float);
207
208 let x_means = X
210 .mean_axis(Axis(0))
211 .expect("array should have elements for mean computation");
212 let y_mean = y_float
213 .mean()
214 .expect("array should have elements for mean computation");
215
216 let mut weights = Array1::<Float>::zeros(n_features);
218
219 for (i, weight) in weights.iter_mut().enumerate() {
220 let x_col = X.column(i);
221 let x_mean = x_means[i];
222
223 let numerator: Float = x_col
224 .iter()
225 .zip(y_float.iter())
226 .map(|(&x, &y)| (x - x_mean) * (y - y_mean))
227 .sum();
228
229 let denominator: Float = x_col
230 .iter()
231 .map(|&x| (x - x_mean).powi(2))
232 .sum::<Float>()
233 .sqrt()
234 * y_float
235 .iter()
236 .map(|&y| (y - y_mean).powi(2))
237 .sum::<Float>()
238 .sqrt();
239
240 *weight = if denominator > 1e-10 {
241 numerator / denominator
242 } else {
243 0.0
244 };
245 }
246
247 let bias = y_mean - weights.dot(&x_means);
249
250 let mut correct = 0;
252 for i in 0..n_samples {
253 let prediction = if weights.dot(&X.row(i)) + bias > 0.0 {
254 1
255 } else {
256 0
257 };
258 if prediction == y[i] {
259 correct += 1;
260 }
261 }
262 let accuracy = correct as Float / n_samples as Float;
263
264 Ok(SimpleBinaryModel {
265 weights,
266 bias,
267 accuracy,
268 })
269}
270
271#[allow(non_snake_case)] pub fn train_simple_linear_classifier(
274 X: &ArrayView2<Float>,
275 y: &Array1<Float>,
276) -> SklResult<SimpleLinearClassifier> {
277 let (n_samples, n_features) = X.dim();
278
279 if n_samples != y.len() {
280 return Err(SklearsError::InvalidInput(
281 "X and y must have the same number of samples".to_string(),
282 ));
283 }
284
285 let mut X_with_bias = Array2::ones((n_samples, n_features + 1));
287 X_with_bias.slice_mut(s![.., ..n_features]).assign(X);
288
289 let xtx = X_with_bias.t().dot(&X_with_bias);
291 let xty = X_with_bias.t().dot(y);
292
293 let weights_with_bias = solve_linear_system(&xtx, &xty)?;
294
295 let weights = weights_with_bias.slice(s![..n_features]).to_owned();
296 let bias = weights_with_bias[n_features];
297
298 Ok(SimpleLinearClassifier { weights, bias })
299}
300
301#[allow(non_snake_case)] pub fn predict_simple_linear(
304 X: &ArrayView2<Float>,
305 classifier: &SimpleLinearClassifier,
306) -> Array1<Float> {
307 X.dot(&classifier.weights) + classifier.bias
308}
309
310#[allow(non_snake_case)] pub fn solve_linear_system(A: &Array2<Float>, b: &Array1<Float>) -> SklResult<Array1<Float>> {
313 let n = A.nrows();
314 if A.ncols() != n || b.len() != n {
315 return Err(SklearsError::InvalidInput(
316 "Matrix must be square and vector must match matrix size".to_string(),
317 ));
318 }
319
320 let mut aug = Array2::<Float>::zeros((n, n + 1));
321 aug.slice_mut(s![.., ..n]).assign(A);
322 aug.slice_mut(s![.., n]).assign(b);
323
324 for i in 0..n {
326 let mut max_row = i;
328 for k in (i + 1)..n {
329 if aug[[k, i]].abs() > aug[[max_row, i]].abs() {
330 max_row = k;
331 }
332 }
333
334 if max_row != i {
336 for j in 0..=n {
337 let temp = aug[[i, j]];
338 aug[[i, j]] = aug[[max_row, j]];
339 aug[[max_row, j]] = temp;
340 }
341 }
342
343 if aug[[i, i]].abs() < 1e-10 {
345 aug[[i, i]] += 1e-8;
347 }
348
349 for k in (i + 1)..n {
351 let factor = aug[[k, i]] / aug[[i, i]];
352 for j in i..=n {
353 aug[[k, j]] -= factor * aug[[i, j]];
354 }
355 }
356 }
357
358 let mut x = Array1::<Float>::zeros(n);
360 for i in (0..n).rev() {
361 x[i] = aug[[i, n]];
362 for j in (i + 1)..n {
363 x[i] -= aug[[i, j]] * x[j];
364 }
365 x[i] /= aug[[i, i]];
366 }
367
368 Ok(x)
369}
370
371pub fn generate_random_projection_matrix(
373 n_compressed: usize,
374 n_labels: usize,
375 random_state: Option<u64>,
376) -> Array2<Float> {
377 let mut rng_state = random_state.unwrap_or(42);
379
380 let mut matrix = Array2::<Float>::zeros((n_compressed, n_labels));
381
382 for i in 0..n_compressed {
383 for j in 0..n_labels {
384 rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
386 let random_val = (rng_state as Float) / (u64::MAX as Float);
387 matrix[[i, j]] = (random_val - 0.5) * 2.0; }
389 }
390
391 for mut row in matrix.rows_mut() {
393 let norm = row.iter().map(|x| x * x).sum::<Float>().sqrt();
394 if norm > 1e-10 {
395 row /= norm;
396 }
397 }
398
399 matrix
400}
401
402pub fn reconstruct_labels(
404 compressed_labels: &Array1<Float>,
405 projection_matrix: &Array2<Float>,
406 method: ReconstructionMethod,
407) -> SklResult<Array1<Float>> {
408 match method {
409 ReconstructionMethod::Linear => {
410 let pinv = compute_pseudoinverse(projection_matrix)?;
412 Ok(pinv.dot(compressed_labels))
413 }
414 ReconstructionMethod::IterativeThresholding => {
415 iterative_thresholding_reconstruction(compressed_labels, projection_matrix)
416 }
417 ReconstructionMethod::OrthogonalMatchingPursuit => {
418 omp_reconstruction(compressed_labels, projection_matrix)
419 }
420 }
421}
422
423fn compute_pseudoinverse(matrix: &Array2<Float>) -> SklResult<Array2<Float>> {
425 let (m, n) = matrix.dim();
426
427 if m >= n {
428 let ata = matrix.t().dot(matrix);
430 let ata_inv = matrix_inverse(&ata)?;
431 Ok(ata_inv.dot(&matrix.t()))
432 } else {
433 let aat = matrix.dot(&matrix.t());
435 let aat_inv = matrix_inverse(&aat)?;
436 Ok(matrix.t().dot(&aat_inv))
437 }
438}
439
440fn matrix_inverse(matrix: &Array2<Float>) -> SklResult<Array2<Float>> {
442 let n = matrix.nrows();
443 if matrix.ncols() != n {
444 return Err(SklearsError::InvalidInput(
445 "Matrix must be square".to_string(),
446 ));
447 }
448
449 let mut aug = Array2::<Float>::zeros((n, 2 * n));
450 aug.slice_mut(s![.., ..n]).assign(matrix);
451
452 for i in 0..n {
454 aug[[i, n + i]] = 1.0;
455 }
456
457 for i in 0..n {
459 let mut max_row = i;
461 for k in (i + 1)..n {
462 if aug[[k, i]].abs() > aug[[max_row, i]].abs() {
463 max_row = k;
464 }
465 }
466
467 if max_row != i {
469 for j in 0..(2 * n) {
470 let temp = aug[[i, j]];
471 aug[[i, j]] = aug[[max_row, j]];
472 aug[[max_row, j]] = temp;
473 }
474 }
475
476 if aug[[i, i]].abs() < 1e-10 {
478 return Err(SklearsError::InvalidInput("Matrix is singular".to_string()));
479 }
480
481 let pivot = aug[[i, i]];
483 for j in 0..(2 * n) {
484 aug[[i, j]] /= pivot;
485 }
486
487 for k in 0..n {
489 if k != i {
490 let factor = aug[[k, i]];
491 for j in 0..(2 * n) {
492 aug[[k, j]] -= factor * aug[[i, j]];
493 }
494 }
495 }
496 }
497
498 Ok(aug.slice(s![.., n..]).to_owned())
499}
500
501pub fn iterative_thresholding_reconstruction(
503 compressed_labels: &Array1<Float>,
504 projection_matrix: &Array2<Float>,
505) -> SklResult<Array1<Float>> {
506 let n_labels = projection_matrix.ncols();
507 let mut x = Array1::<Float>::zeros(n_labels);
508 let step_size = 0.1;
509 let threshold = 0.1;
510 let max_iterations = 100;
511
512 for _ in 0..max_iterations {
513 let residual = projection_matrix.dot(&x) - compressed_labels;
515 let gradient = projection_matrix.t().dot(&residual);
516 x = &x - step_size * &gradient;
517
518 x.mapv_inplace(|xi| {
520 if xi > threshold {
521 xi - threshold
522 } else if xi < -threshold {
523 xi + threshold
524 } else {
525 0.0
526 }
527 });
528 }
529
530 Ok(x)
531}
532
533pub fn omp_reconstruction(
535 compressed_labels: &Array1<Float>,
536 projection_matrix: &Array2<Float>,
537) -> SklResult<Array1<Float>> {
538 let n_labels = projection_matrix.ncols();
539 let mut selected_indices = Vec::new();
540 let mut residual = compressed_labels.clone();
541 let max_iterations = std::cmp::min(10, n_labels); for _ in 0..max_iterations {
544 let mut max_corr = 0.0;
546 let mut best_idx = 0;
547
548 for j in 0..n_labels {
549 if !selected_indices.contains(&j) {
550 let column = projection_matrix.column(j);
551 let corr = column.dot(&residual).abs();
552 if corr > max_corr {
553 max_corr = corr;
554 best_idx = j;
555 }
556 }
557 }
558
559 if max_corr < 1e-6 {
560 break;
561 }
562
563 selected_indices.push(best_idx);
564
565 if let Ok(coeffs) =
567 solve_least_squares_subset(compressed_labels, projection_matrix, &selected_indices)
568 {
569 let mut reconstruction = Array1::<Float>::zeros(projection_matrix.nrows());
571 for (i, &idx) in selected_indices.iter().enumerate() {
572 let column = projection_matrix.column(idx);
573 reconstruction = reconstruction + coeffs[i] * &column;
574 }
575 residual = compressed_labels - &reconstruction;
576 }
577 }
578
579 let mut x = Array1::<Float>::zeros(n_labels);
581 if let Ok(coeffs) =
582 solve_least_squares_subset(compressed_labels, projection_matrix, &selected_indices)
583 {
584 for (i, &idx) in selected_indices.iter().enumerate() {
585 x[idx] = coeffs[i];
586 }
587 }
588
589 Ok(x)
590}
591
592#[allow(non_snake_case)] pub fn solve_least_squares_subset(
595 y: &Array1<Float>,
596 A: &Array2<Float>,
597 indices: &[usize],
598) -> SklResult<Array1<Float>> {
599 if indices.is_empty() {
600 return Err(SklearsError::InvalidInput(
601 "No indices provided".to_string(),
602 ));
603 }
604
605 let n_rows = A.nrows();
606 let n_selected = indices.len();
607 let mut A_subset = Array2::<Float>::zeros((n_rows, n_selected));
608
609 for (j, &idx) in indices.iter().enumerate() {
610 A_subset.column_mut(j).assign(&A.column(idx));
611 }
612
613 let ata = A_subset.t().dot(&A_subset);
615 let aty = A_subset.t().dot(y);
616
617 solve_linear_system(&ata, &aty)
618}
619
620#[allow(non_snake_case)] pub fn train_weighted_binary_classifier_simple(
623 X: &ArrayView2<Float>,
624 y: &Array1<i32>,
625 sample_weights: &Array1<Float>,
626) -> SklResult<SimpleBinaryModel> {
627 let (n_samples, n_features) = X.dim();
628
629 if n_samples != y.len() || n_samples != sample_weights.len() {
630 return Err(SklearsError::InvalidInput(
631 "X, y, and sample_weights must have the same number of samples".to_string(),
632 ));
633 }
634
635 let total_weight = sample_weights.sum();
637 let mut x_means = Array1::<Float>::zeros(n_features);
638 let mut y_mean = 0.0;
639
640 for i in 0..n_samples {
641 let weight = sample_weights[i];
642 y_mean += weight * y[i] as Float;
643 for j in 0..n_features {
644 x_means[j] += weight * X[[i, j]];
645 }
646 }
647
648 x_means /= total_weight;
649 y_mean /= total_weight;
650
651 let mut weights = Array1::<Float>::zeros(n_features);
653
654 for j in 0..n_features {
655 let mut numerator = 0.0;
656 let mut x_var = 0.0;
657 let mut y_var = 0.0;
658
659 for i in 0..n_samples {
660 let weight = sample_weights[i];
661 let x_diff = X[[i, j]] - x_means[j];
662 let y_diff = y[i] as Float - y_mean;
663
664 numerator += weight * x_diff * y_diff;
665 x_var += weight * x_diff * x_diff;
666 y_var += weight * y_diff * y_diff;
667 }
668
669 let denominator = (x_var * y_var).sqrt();
670 weights[j] = if denominator > 1e-10 {
671 numerator / denominator
672 } else {
673 0.0
674 };
675 }
676
677 let bias = y_mean - weights.dot(&x_means);
678
679 let mut correct_weight = 0.0;
681 for i in 0..n_samples {
682 let prediction = if weights.dot(&X.row(i)) + bias > 0.0 {
683 1
684 } else {
685 0
686 };
687 if prediction == y[i] {
688 correct_weight += sample_weights[i];
689 }
690 }
691 let accuracy = correct_weight / total_weight;
692
693 Ok(SimpleBinaryModel {
694 weights,
695 bias,
696 accuracy,
697 })
698}
699
700#[allow(non_snake_case)] pub fn predict_binary_probabilities(
703 X: &ArrayView2<Float>,
704 model: &SimpleBinaryModel,
705) -> Array1<Float> {
706 let raw_scores = X.dot(&model.weights) + model.bias;
707 raw_scores.mapv(|x| 1.0 / (1.0 + (-x).exp()))
708}
709
710pub fn compute_cost_sensitive_weights(y: &Array2<i32>, cost_matrix: &CostMatrix) -> Array1<Float> {
712 let n_samples = y.nrows();
713 let mut weights = Array1::ones(n_samples);
714
715 for i in 0..n_samples {
716 let mut sample_weight = 1.0;
717
718 for (j, &label) in y.row(i).iter().enumerate() {
719 if j < cost_matrix.fp_costs.len() {
720 if label == 1 {
722 sample_weight *= cost_matrix.fn_costs[j];
723 } else {
724 sample_weight *= cost_matrix.fp_costs[j];
725 }
726 }
727 }
728
729 weights[i] = sample_weight;
730 }
731
732 let total_weight = weights.sum();
734 if total_weight > 0.0 {
735 weights *= n_samples as Float / total_weight;
736 }
737
738 weights
739}
740
741pub fn random_normal() -> Float {
743 use std::cell::RefCell;
744
745 thread_local! {
746 static SAVED: RefCell<Option<Float>> = const { RefCell::new(None) };
747 }
748
749 SAVED.with(|saved| {
750 if let Some(value) = saved.borrow_mut().take() {
751 return value;
752 }
753
754 let u1 = (rand_u32() as Float) / (u32::MAX as Float);
756 let u2 = (rand_u32() as Float) / (u32::MAX as Float);
757
758 let mag = (-2.0 * u1.ln()).sqrt();
760 let z0 = mag * (2.0 * std::f64::consts::PI * u2).cos();
761 let z1 = mag * (2.0 * std::f64::consts::PI * u2).sin();
762
763 *saved.borrow_mut() = Some(z1);
764 z0
765 })
766}
767
768fn rand_u32() -> u32 {
770 use std::cell::RefCell;
771
772 thread_local! {
773 static STATE: RefCell<u32> = const { RefCell::new(42) };
774 }
775
776 STATE.with(|state| {
777 let mut s = state.borrow_mut();
778 *s = s.wrapping_mul(1664525).wrapping_add(1013904223);
779 *s
780 })
781}
782
783#[allow(non_snake_case)] pub fn train_bayesian_binary_classifier(
786 X: &Array2<Float>,
787 y: &Array1<i32>,
788 alpha: Float,
789) -> SklResult<BayesianBinaryModel> {
790 let (n_samples, n_features) = X.dim();
791
792 if n_samples != y.len() {
793 return Err(SklearsError::InvalidInput(
794 "X and y must have the same number of samples".to_string(),
795 ));
796 }
797
798 let y_float: Array1<Float> = y.mapv(|x| if x == 1 { 1.0 } else { -1.0 });
800
801 let prior_precision = Array2::<Float>::eye(n_features) * alpha;
803
804 let noise_precision = 1.0;
806
807 let xtx = X.t().dot(X);
809 let posterior_precision = &prior_precision + &xtx * noise_precision;
810
811 let weight_cov = matrix_inverse(&posterior_precision)?;
813
814 let xty = X.t().dot(&y_float);
816 let weight_mean = weight_cov.dot(&(&xty * noise_precision));
817
818 let bias_mean = 0.0;
820 let bias_var = 1.0 / alpha;
821
822 Ok(BayesianBinaryModel {
823 weight_mean,
824 weight_cov,
825 bias_mean,
826 bias_var,
827 noise_precision,
828 })
829}
830
831#[allow(non_snake_case)] pub fn predict_bayesian_binary(
834 X: &ArrayView2<Float>,
835 model: &BayesianBinaryModel,
836) -> Array1<Float> {
837 let raw_scores = X.dot(&model.weight_mean) + model.bias_mean;
838 raw_scores.mapv(|x| 1.0 / (1.0 + (-x).exp()))
839}
840
841#[allow(non_snake_case)] pub fn predict_bayesian_uncertainty(
844 X: &ArrayView2<Float>,
845 model: &BayesianBinaryModel,
846) -> SklResult<(Array1<Float>, Array1<Float>)> {
847 let n_samples = X.nrows();
848 let mut means = Array1::<Float>::zeros(n_samples);
849 let mut variances = Array1::<Float>::zeros(n_samples);
850
851 for i in 0..n_samples {
852 let x = X.row(i);
853
854 let mean_score = x.dot(&model.weight_mean) + model.bias_mean;
856 means[i] = 1.0 / (1.0 + (-mean_score).exp());
857
858 let score_var =
860 x.dot(&model.weight_cov.dot(&x)) + model.bias_var + 1.0 / model.noise_precision;
861 variances[i] = score_var;
862 }
863
864 Ok((means, variances))
865}
866
867#[allow(non_snake_case)] pub fn predict_bayesian_mean(X: &ArrayView2<Float>, model: &BayesianBinaryModel) -> Array1<Float> {
870 X.dot(&model.weight_mean) + model.bias_mean
871}