1use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
5use scirs2_core::random::thread_rng;
6use sklears_core::{
7 error::{Result as SklResult, SklearsError},
8 traits::{Estimator, Fit, Predict, Untrained},
9 types::Float,
10};
11
12#[derive(Debug, Clone)]
17pub struct IndependentLabelPrediction<S = Untrained> {
18 state: S,
19 threshold_strategy: ThresholdStrategy,
20 optimize_thresholds: bool,
21 class_weight: Option<String>, random_state: Option<u64>,
23}
24
25#[derive(Debug, Clone)]
27pub enum ThresholdStrategy {
28 Fixed(Float), PerLabel(Vec<Float>), Optimal, FScore, }
37
38#[derive(Debug, Clone)]
40pub struct IndependentLabelPredictionTrained {
41 binary_classifiers: Vec<BinaryClassifierModel>,
42 thresholds: Vec<Float>,
43 n_labels: usize,
44}
45
46#[derive(Debug, Clone)]
48pub struct BinaryClassifierModel {
49 weights: Array1<Float>,
50 bias: Float,
51 feature_means: Array1<Float>,
52 feature_stds: Array1<Float>,
53}
54
55impl IndependentLabelPrediction<Untrained> {
56 pub fn new() -> Self {
58 Self {
59 state: Untrained,
60 threshold_strategy: ThresholdStrategy::Fixed(0.5),
61 optimize_thresholds: false,
62 class_weight: None,
63 random_state: None,
64 }
65 }
66
67 pub fn threshold_strategy(mut self, strategy: ThresholdStrategy) -> Self {
69 self.threshold_strategy = strategy;
70 self
71 }
72
73 pub fn optimize_thresholds(mut self, optimize: bool) -> Self {
75 self.optimize_thresholds = optimize;
76 self
77 }
78
79 pub fn class_weight(mut self, weight: Option<String>) -> Self {
81 self.class_weight = weight;
82 self
83 }
84
85 pub fn random_state(mut self, random_state: Option<u64>) -> Self {
87 self.random_state = random_state;
88 self
89 }
90}
91
92impl Default for IndependentLabelPrediction<Untrained> {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98impl Estimator for IndependentLabelPrediction<Untrained> {
99 type Config = ();
100 type Error = SklearsError;
101 type Float = Float;
102
103 fn config(&self) -> &Self::Config {
104 &()
105 }
106}
107
108impl Fit<ArrayView2<'_, Float>, ArrayView2<'_, i32>> for IndependentLabelPrediction<Untrained> {
109 type Fitted = IndependentLabelPrediction<IndependentLabelPredictionTrained>;
110
111 fn fit(self, x: &ArrayView2<'_, Float>, y: &ArrayView2<'_, i32>) -> SklResult<Self::Fitted> {
112 let (n_samples, _n_features) = x.dim();
113 let (y_samples, n_labels) = y.dim();
114
115 if n_samples != y_samples {
116 return Err(SklearsError::InvalidInput(
117 "Number of samples in X and y must match".to_string(),
118 ));
119 }
120
121 if n_samples < 2 {
122 return Err(SklearsError::InvalidInput(
123 "Need at least 2 samples for training".to_string(),
124 ));
125 }
126
127 let mut rng = thread_rng();
129
130 let mut binary_classifiers = Vec::new();
132 for label_idx in 0..n_labels {
133 let label_column = y.column(label_idx);
134 let classifier = self.train_binary_classifier(x, &label_column, &mut rng)?;
135 binary_classifiers.push(classifier);
136 }
137
138 let thresholds = match &self.threshold_strategy {
140 ThresholdStrategy::Fixed(threshold) => vec![*threshold; n_labels],
141 ThresholdStrategy::PerLabel(thresholds) => {
142 if thresholds.len() != n_labels {
143 return Err(SklearsError::InvalidInput(
144 "Number of thresholds must match number of labels".to_string(),
145 ));
146 }
147 thresholds.clone()
148 }
149 ThresholdStrategy::Optimal => {
150 self.optimize_thresholds_for_accuracy(x, y, &binary_classifiers)?
151 }
152 ThresholdStrategy::FScore => {
153 self.optimize_thresholds_for_fscore(x, y, &binary_classifiers)?
154 }
155 };
156
157 Ok(IndependentLabelPrediction {
158 state: IndependentLabelPredictionTrained {
159 binary_classifiers,
160 thresholds,
161 n_labels,
162 },
163 threshold_strategy: self.threshold_strategy,
164 optimize_thresholds: self.optimize_thresholds,
165 class_weight: self.class_weight,
166 random_state: self.random_state,
167 })
168 }
169}
170
171impl IndependentLabelPrediction<Untrained> {
172 fn train_binary_classifier(
173 &self,
174 x: &ArrayView2<'_, Float>,
175 y_label: &ArrayView1<'_, i32>,
176 _rng: &mut scirs2_core::random::CoreRandom,
177 ) -> SklResult<BinaryClassifierModel> {
178 let (n_samples, n_features) = x.dim();
179
180 let feature_means = x
182 .mean_axis(Axis(0))
183 .expect("array should have elements for mean computation");
184 let feature_stds = x
185 .mapv(|val| val * val)
186 .mean_axis(Axis(0))
187 .expect("array should have elements for mean computation")
188 - &feature_means.mapv(|mean| mean * mean);
189 let feature_stds = feature_stds.mapv(|var| (var.max(1e-10)).sqrt());
190
191 let mut x_normalized = x.to_owned();
193 for mut row in x_normalized.rows_mut().into_iter() {
194 row -= &feature_means;
195 row /= &feature_stds;
196 }
197
198 let class_weights = if self.class_weight.as_deref() == Some("balanced") {
200 let pos_count = y_label.iter().filter(|&&y| y == 1).count();
201 let neg_count = n_samples - pos_count;
202
203 if pos_count == 0 || neg_count == 0 {
204 (1.0, 1.0)
205 } else {
206 let pos_weight = n_samples as Float / (2.0 * pos_count as Float);
207 let neg_weight = n_samples as Float / (2.0 * neg_count as Float);
208 (neg_weight, pos_weight)
209 }
210 } else {
211 (1.0, 1.0)
212 };
213
214 let mut weights = Array1::<Float>::zeros(n_features);
216 let mut bias = 0.0;
217
218 let learning_rate = 0.01;
219 let max_iter = 1000;
220 let tolerance = 1e-6;
221
222 for iteration in 0..max_iter {
223 let mut weight_gradient = Array1::<Float>::zeros(n_features);
224 let mut bias_gradient = 0.0;
225 let mut total_loss = 0.0;
226
227 for sample_idx in 0..n_samples {
228 let x_sample = x_normalized.row(sample_idx);
229 let y_true = y_label[sample_idx] as Float;
230
231 let logits = x_sample.dot(&weights) + bias;
233 let prediction = 1.0 / (1.0 + (-logits).exp());
234
235 let sample_weight = if y_true > 0.5 {
237 class_weights.1
238 } else {
239 class_weights.0
240 };
241 let loss = -sample_weight
242 * (y_true * prediction.ln() + (1.0 - y_true) * (1.0 - prediction).ln());
243 total_loss += loss;
244
245 let error = sample_weight * (prediction - y_true);
247 weight_gradient += &(x_sample.to_owned() * error);
248 bias_gradient += error;
249 }
250
251 weights -= &(weight_gradient * (learning_rate / n_samples as Float));
253 bias -= bias_gradient * (learning_rate / n_samples as Float);
254
255 if iteration > 10 {
257 let avg_loss = total_loss / n_samples as Float;
258 if avg_loss < tolerance {
259 break;
260 }
261 }
262 }
263
264 Ok(BinaryClassifierModel {
265 weights,
266 bias,
267 feature_means,
268 feature_stds,
269 })
270 }
271
272 fn optimize_thresholds_for_accuracy(
273 &self,
274 x: &ArrayView2<'_, Float>,
275 y: &ArrayView2<'_, i32>,
276 classifiers: &[BinaryClassifierModel],
277 ) -> SklResult<Vec<Float>> {
278 let n_labels = y.ncols();
279 let mut thresholds = Vec::new();
280
281 for (label_idx, classifier) in classifiers.iter().enumerate().take(n_labels) {
282 let y_true = y.column(label_idx);
283 let y_scores = self.predict_probabilities_single_label(x, classifier)?;
284
285 let mut best_threshold = 0.5;
286 let mut best_accuracy = 0.0;
287
288 for threshold_int in 1..100 {
290 let threshold = threshold_int as Float / 100.0;
291
292 let mut correct = 0;
293 for sample_idx in 0..x.nrows() {
294 let predicted = if y_scores[sample_idx] >= threshold {
295 1
296 } else {
297 0
298 };
299 if predicted == y_true[sample_idx] {
300 correct += 1;
301 }
302 }
303
304 let accuracy = correct as Float / x.nrows() as Float;
305 if accuracy > best_accuracy {
306 best_accuracy = accuracy;
307 best_threshold = threshold;
308 }
309 }
310
311 thresholds.push(best_threshold);
312 }
313
314 Ok(thresholds)
315 }
316
317 fn optimize_thresholds_for_fscore(
318 &self,
319 x: &ArrayView2<'_, Float>,
320 y: &ArrayView2<'_, i32>,
321 classifiers: &[BinaryClassifierModel],
322 ) -> SklResult<Vec<Float>> {
323 let n_labels = y.ncols();
324 let mut thresholds = Vec::new();
325
326 for (label_idx, classifier) in classifiers.iter().enumerate().take(n_labels) {
327 let y_true = y.column(label_idx);
328 let y_scores = self.predict_probabilities_single_label(x, classifier)?;
329
330 let mut best_threshold = 0.5;
331 let mut best_fscore = 0.0;
332
333 for threshold_int in 1..100 {
335 let threshold = threshold_int as Float / 100.0;
336
337 let mut tp = 0;
338 let mut fp = 0;
339 let mut fn_count = 0;
340
341 for sample_idx in 0..x.nrows() {
342 let predicted = if y_scores[sample_idx] >= threshold {
343 1
344 } else {
345 0
346 };
347 let actual = y_true[sample_idx];
348
349 match (actual, predicted) {
350 (1, 1) => tp += 1,
351 (0, 1) => fp += 1,
352 (1, 0) => fn_count += 1,
353 _ => {}
354 }
355 }
356
357 let precision = if tp + fp > 0 {
358 tp as Float / (tp + fp) as Float
359 } else {
360 0.0
361 };
362 let recall = if tp + fn_count > 0 {
363 tp as Float / (tp + fn_count) as Float
364 } else {
365 0.0
366 };
367 let fscore = if precision + recall > 0.0 {
368 2.0 * precision * recall / (precision + recall)
369 } else {
370 0.0
371 };
372
373 if fscore > best_fscore {
374 best_fscore = fscore;
375 best_threshold = threshold;
376 }
377 }
378
379 thresholds.push(best_threshold);
380 }
381
382 Ok(thresholds)
383 }
384
385 fn predict_probabilities_single_label(
386 &self,
387 x: &ArrayView2<'_, Float>,
388 classifier: &BinaryClassifierModel,
389 ) -> SklResult<Array1<Float>> {
390 let n_samples = x.nrows();
391 let mut probabilities = Array1::<Float>::zeros(n_samples);
392
393 for sample_idx in 0..n_samples {
394 let x_sample = x.row(sample_idx);
395
396 let x_normalized =
398 (&x_sample.to_owned() - &classifier.feature_means) / &classifier.feature_stds;
399
400 let logits = x_normalized.dot(&classifier.weights) + classifier.bias;
402 let probability = 1.0 / (1.0 + (-logits).exp());
403
404 probabilities[sample_idx] = probability;
405 }
406
407 Ok(probabilities)
408 }
409}
410
411impl Predict<ArrayView2<'_, Float>, Array2<i32>>
412 for IndependentLabelPrediction<IndependentLabelPredictionTrained>
413{
414 fn predict(&self, x: &ArrayView2<'_, Float>) -> SklResult<Array2<i32>> {
415 let (n_samples, _n_features) = x.dim();
416 let mut predictions = Array2::<i32>::zeros((n_samples, self.state.n_labels));
417
418 for label_idx in 0..self.state.n_labels {
419 let classifier = &self.state.binary_classifiers[label_idx];
420 let threshold = self.state.thresholds[label_idx];
421
422 for sample_idx in 0..n_samples {
423 let x_sample = x.row(sample_idx);
424
425 let x_normalized =
427 (&x_sample.to_owned() - &classifier.feature_means) / &classifier.feature_stds;
428
429 let logits = x_normalized.dot(&classifier.weights) + classifier.bias;
431 let probability = 1.0 / (1.0 + (-logits).exp());
432
433 predictions[[sample_idx, label_idx]] = if probability >= threshold { 1 } else { 0 };
435 }
436 }
437
438 Ok(predictions)
439 }
440}
441
442impl IndependentLabelPrediction<IndependentLabelPredictionTrained> {
443 pub fn predict_proba(&self, x: &ArrayView2<'_, Float>) -> SklResult<Array2<Float>> {
445 let (n_samples, _) = x.dim();
446 let mut probabilities = Array2::<Float>::zeros((n_samples, self.state.n_labels));
447
448 for label_idx in 0..self.state.n_labels {
449 let classifier = &self.state.binary_classifiers[label_idx];
450
451 for sample_idx in 0..n_samples {
452 let x_sample = x.row(sample_idx);
453
454 let x_normalized =
456 (&x_sample.to_owned() - &classifier.feature_means) / &classifier.feature_stds;
457
458 let logits = x_normalized.dot(&classifier.weights) + classifier.bias;
460 let probability = 1.0 / (1.0 + (-logits).exp());
461
462 probabilities[[sample_idx, label_idx]] = probability;
463 }
464 }
465
466 Ok(probabilities)
467 }
468
469 pub fn thresholds(&self) -> &[Float] {
471 &self.state.thresholds
472 }
473
474 pub fn feature_importances(&self) -> Vec<Array1<Float>> {
476 self.state
477 .binary_classifiers
478 .iter()
479 .map(|classifier| classifier.weights.mapv(|w| w.abs()))
480 .collect()
481 }
482}