use scirs2_core::ndarray_ext::{Array1, Array2, ArrayView1, ArrayView2};
use sklears_core::{
error::{Result as SklResult, SklearsError},
traits::{Estimator, Fit, Predict, Untrained},
types::Float,
};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct EnhancedSelfTraining<S = Untrained> {
state: S,
threshold: f64,
criterion: String,
k_best: usize,
max_iter: usize,
verbose: bool,
confidence_method: String,
min_samples_per_class: usize,
}
impl EnhancedSelfTraining<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
threshold: 0.75,
criterion: "threshold".to_string(),
k_best: 10,
max_iter: 10,
verbose: false,
confidence_method: "max_proba".to_string(),
min_samples_per_class: 1,
}
}
pub fn threshold(mut self, threshold: f64) -> Self {
self.threshold = threshold;
self
}
pub fn criterion(mut self, criterion: String) -> Self {
self.criterion = criterion;
self
}
pub fn k_best(mut self, k_best: usize) -> Self {
self.k_best = k_best;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn confidence_method(mut self, method: String) -> Self {
self.confidence_method = method;
self
}
pub fn min_samples_per_class(mut self, min_samples: usize) -> Self {
self.min_samples_per_class = min_samples;
self
}
fn compute_confidence(&self, probabilities: &Array1<f64>) -> f64 {
match self.confidence_method.as_str() {
"max_proba" => probabilities.iter().fold(0.0, |a, &b| a.max(b)),
"entropy" => {
let entropy = -probabilities
.iter()
.filter(|&&p| p > 0.0)
.map(|&p| p * p.ln())
.sum::<f64>();
let max_entropy = (probabilities.len() as f64).ln();
if max_entropy > 0.0 {
1.0 - (entropy / max_entropy)
} else {
1.0
}
}
"margin" => {
let mut sorted_probs: Vec<f64> = probabilities.to_vec();
sorted_probs.sort_by(|a, b| b.partial_cmp(a).expect("operation should succeed"));
if sorted_probs.len() >= 2 {
sorted_probs[0] - sorted_probs[1]
} else {
sorted_probs[0]
}
}
_ => probabilities.iter().fold(0.0, |a, &b| a.max(b)),
}
}
}
impl Default for EnhancedSelfTraining<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for EnhancedSelfTraining<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, i32>> for EnhancedSelfTraining<Untrained> {
type Fitted = EnhancedSelfTraining<EnhancedSelfTrainingTrained>;
#[allow(non_snake_case)]
fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView1<'_, i32>) -> SklResult<Self::Fitted> {
let X = X.to_owned();
let mut y = y.to_owned();
let mut labeled_mask = Array1::from_elem(y.len(), false);
let mut classes = HashSet::new();
for (i, &label) in y.iter().enumerate() {
if label != -1 {
labeled_mask[i] = true;
classes.insert(label);
}
}
if labeled_mask.iter().all(|&x| !x) {
return Err(SklearsError::InvalidInput(
"No labeled samples provided".to_string(),
));
}
let classes: Vec<i32> = classes.into_iter().collect();
let n_classes = classes.len();
for iter in 0..self.max_iter {
let labeled_indices: Vec<usize> = labeled_mask
.iter()
.enumerate()
.filter(|(_, &is_labeled)| is_labeled)
.map(|(i, _)| i)
.collect();
if labeled_indices.len() == y.len() {
break;
}
let mut class_counts = HashMap::new();
for &idx in &labeled_indices {
*class_counts.entry(y[idx]).or_insert(0) += 1;
}
if class_counts
.values()
.any(|&count| count < self.min_samples_per_class)
{
if iter == 0 {
return Err(SklearsError::InvalidInput(
"Insufficient labeled samples per class".to_string(),
));
}
continue;
}
let mut candidates = Vec::new();
for (i, &is_labeled) in labeled_mask.iter().enumerate() {
if !is_labeled {
let mut distances: Vec<(usize, f64, i32)> = Vec::new();
for &labeled_idx in &labeled_indices {
let diff = &X.row(i) - &X.row(labeled_idx);
let dist = diff.mapv(|x| x * x).sum().sqrt();
distances.push((labeled_idx, dist, y[labeled_idx]));
}
distances
.sort_by(|a, b| a.1.partial_cmp(&b.1).expect("operation should succeed"));
let k = labeled_indices.len().clamp(1, 7);
let mut class_probs = vec![0.0; n_classes];
let mut total_weight = 0.0;
for &(_, dist, label) in distances.iter().take(k) {
let weight = if dist > 0.0 { 1.0 / (1.0 + dist) } else { 1.0 };
if let Some(class_idx) = classes.iter().position(|&c| c == label) {
class_probs[class_idx] += weight;
total_weight += weight;
}
}
if total_weight > 0.0 {
for prob in &mut class_probs {
*prob /= total_weight;
}
}
let proba_array = Array1::from(class_probs);
let confidence = self.compute_confidence(&proba_array);
let predicted_class_idx = proba_array
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).expect("operation should succeed"))
.expect("operation should succeed")
.0;
candidates.push((i, classes[predicted_class_idx], confidence));
}
}
let mut selected_indices = Vec::new();
match self.criterion.as_str() {
"threshold" => {
for (i, label, confidence) in candidates {
if confidence >= self.threshold {
selected_indices.push((i, label));
}
}
}
"k_best" => {
candidates
.sort_by(|a, b| b.2.partial_cmp(&a.2).expect("operation should succeed"));
for (i, label, _) in candidates.into_iter().take(self.k_best) {
selected_indices.push((i, label));
}
}
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown criterion: {}",
self.criterion
)));
}
}
if selected_indices.is_empty() {
if self.verbose {
println!("Iteration {}: No confident predictions, stopping", iter + 1);
}
break;
}
for (i, label) in selected_indices {
y[i] = label;
labeled_mask[i] = true;
}
if self.verbose {
let n_labeled = labeled_mask.iter().filter(|&&x| x).count();
println!("Iteration {}: {} labeled samples", iter + 1, n_labeled);
}
}
Ok(EnhancedSelfTraining {
state: EnhancedSelfTrainingTrained {
X_train: X.clone(),
y_train: y,
classes: Array1::from(classes),
labeled_mask,
},
threshold: self.threshold,
criterion: self.criterion,
k_best: self.k_best,
max_iter: self.max_iter,
verbose: self.verbose,
confidence_method: self.confidence_method,
min_samples_per_class: self.min_samples_per_class,
})
}
}
impl Predict<ArrayView2<'_, Float>, Array1<i32>>
for EnhancedSelfTraining<EnhancedSelfTrainingTrained>
{
#[allow(non_snake_case)]
fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array1<i32>> {
let X = X.to_owned();
let n_test = X.nrows();
let mut predictions = Array1::zeros(n_test);
let labeled_indices: Vec<usize> = self
.state
.labeled_mask
.iter()
.enumerate()
.filter(|(_, &is_labeled)| is_labeled)
.map(|(i, _)| i)
.collect();
for i in 0..n_test {
let mut min_dist = f64::INFINITY;
let mut best_label = 0;
for &labeled_idx in &labeled_indices {
let diff = &X.row(i) - &self.state.X_train.row(labeled_idx);
let dist = diff.mapv(|x| x * x).sum().sqrt();
if dist < min_dist {
min_dist = dist;
best_label = self.state.y_train[labeled_idx];
}
}
predictions[i] = best_label;
}
Ok(predictions)
}
}
#[derive(Debug, Clone)]
pub struct EnhancedSelfTrainingTrained {
pub X_train: Array2<f64>,
pub y_train: Array1<i32>,
pub classes: Array1<i32>,
pub labeled_mask: Array1<bool>,
}