use scirs2_core::ndarray_ext::{Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::random::Random;
use sklears_core::error::{Result as SklResult, SklearsError};
use sklears_core::traits::{Estimator, Fit, Predict, PredictProba, Untrained};
use sklears_core::types::Float;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct GaussianProcessSemiSupervised<S = Untrained> {
state: S,
kernel: String,
length_scale: f64,
noise_level: f64,
alpha: f64,
n_restarts_optimizer: usize,
random_state: Option<u64>,
}
impl GaussianProcessSemiSupervised<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
kernel: "rbf".to_string(),
length_scale: 1.0,
noise_level: 0.1,
alpha: 1e-10,
n_restarts_optimizer: 0,
random_state: None,
}
}
pub fn kernel(mut self, kernel: String) -> Self {
self.kernel = kernel;
self
}
pub fn length_scale(mut self, length_scale: f64) -> Self {
self.length_scale = length_scale;
self
}
pub fn noise_level(mut self, noise_level: f64) -> Self {
self.noise_level = noise_level;
self
}
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = alpha;
self
}
pub fn n_restarts_optimizer(mut self, n_restarts: usize) -> Self {
self.n_restarts_optimizer = n_restarts;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
}
impl Default for GaussianProcessSemiSupervised<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for GaussianProcessSemiSupervised<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, i32>> for GaussianProcessSemiSupervised<Untrained> {
type Fitted = GaussianProcessSemiSupervised<GaussianProcessTrained>;
#[allow(non_snake_case)]
fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView1<'_, i32>) -> SklResult<Self::Fitted> {
let X = X.to_owned();
let y = y.to_owned();
let (n_samples, _n_features) = X.dim();
let mut labeled_indices = Vec::new();
let mut unlabeled_indices = Vec::new();
let mut classes = std::collections::HashSet::new();
for (i, &label) in y.iter().enumerate() {
if label == -1 {
unlabeled_indices.push(i);
} else {
labeled_indices.push(i);
classes.insert(label);
}
}
if labeled_indices.is_empty() {
return Err(SklearsError::InvalidInput(
"No labeled samples provided".to_string(),
));
}
let classes: Vec<i32> = classes.into_iter().collect();
let n_classes = classes.len();
let mut regression_targets = Array2::<f64>::zeros((labeled_indices.len(), n_classes));
for (i, &idx) in labeled_indices.iter().enumerate() {
if let Some(class_idx) = classes.iter().position(|&c| c == y[idx]) {
regression_targets[[i, class_idx]] = 1.0;
}
}
let mut X_labeled = Array2::<f64>::zeros((labeled_indices.len(), X.ncols()));
for (i, &idx) in labeled_indices.iter().enumerate() {
X_labeled.row_mut(i).assign(&X.row(idx));
}
let K_labeled = self.compute_kernel_matrix(&X_labeled, &X_labeled)?;
let mut K_noise = K_labeled.clone();
for i in 0..K_noise.nrows() {
K_noise[[i, i]] += self.noise_level + self.alpha;
}
let GP_weights = self.solve_gp_system(&K_noise, ®ression_targets)?;
let K_all = self.compute_kernel_matrix(&X, &X_labeled)?;
let predictions_all = K_all.dot(&GP_weights);
let mut final_labels = y.clone();
for &idx in &unlabeled_indices {
let class_idx = predictions_all
.row(idx)
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).expect("operation should succeed"))
.expect("operation should succeed")
.0;
final_labels[idx] = classes[class_idx];
}
Ok(GaussianProcessSemiSupervised {
state: GaussianProcessTrained {
X_train: X,
y_train: final_labels,
classes: Array1::from(classes),
X_labeled,
GP_weights,
predictions_all,
},
kernel: self.kernel,
length_scale: self.length_scale,
noise_level: self.noise_level,
alpha: self.alpha,
n_restarts_optimizer: self.n_restarts_optimizer,
random_state: self.random_state,
})
}
}
impl GaussianProcessSemiSupervised<Untrained> {
fn compute_kernel_matrix(&self, X1: &Array2<f64>, X2: &Array2<f64>) -> SklResult<Array2<f64>> {
let n1 = X1.nrows();
let n2 = X2.nrows();
let mut K = Array2::<f64>::zeros((n1, n2));
match self.kernel.as_str() {
"rbf" => {
for i in 0..n1 {
for j in 0..n2 {
let diff = &X1.row(i) - &X2.row(j);
let dist_sq = diff.mapv(|x| x * x).sum();
K[[i, j]] = (-dist_sq / (2.0 * self.length_scale.powi(2))).exp();
}
}
}
"linear" => {
for i in 0..n1 {
for j in 0..n2 {
K[[i, j]] = X1.row(i).dot(&X2.row(j));
}
}
}
"polynomial" => {
let degree = 2.0;
for i in 0..n1 {
for j in 0..n2 {
let dot_product = X1.row(i).dot(&X2.row(j));
K[[i, j]] = (1.0 + dot_product / self.length_scale).powf(degree);
}
}
}
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown kernel: {}",
self.kernel
)));
}
}
Ok(K)
}
fn solve_gp_system(&self, K: &Array2<f64>, targets: &Array2<f64>) -> SklResult<Array2<f64>> {
let n = K.nrows();
let n_targets = targets.ncols();
let mut weights = Array2::<f64>::zeros((n, n_targets));
for target_idx in 0..n_targets {
let mut x = Array1::<f64>::zeros(n);
let target_col = targets.column(target_idx);
for _iter in 0..100 {
let mut x_new = Array1::<f64>::zeros(n);
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
if i != j {
sum += K[[i, j]] * x[j];
}
}
x_new[i] = (target_col[i] - sum) / K[[i, i]];
}
let diff = (&x_new - &x).mapv(|x| x.abs()).sum();
if diff < 1e-6 {
break;
}
x = x_new;
}
for i in 0..n {
weights[[i, target_idx]] = x[i];
}
}
Ok(weights)
}
}
impl Predict<ArrayView2<'_, Float>, Array1<i32>>
for GaussianProcessSemiSupervised<GaussianProcessTrained>
{
#[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 K_test = self
.compute_kernel_matrix(&X, &self.state.X_labeled)
.map_err(|e| SklearsError::PredictError(e.to_string()))?;
let gp_predictions = K_test.dot(&self.state.GP_weights);
for i in 0..n_test {
let class_idx = gp_predictions
.row(i)
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).expect("operation should succeed"))
.expect("operation should succeed")
.0;
predictions[i] = self.state.classes[class_idx];
}
Ok(predictions)
}
}
impl PredictProba<ArrayView2<'_, Float>, Array2<f64>>
for GaussianProcessSemiSupervised<GaussianProcessTrained>
{
#[allow(non_snake_case)]
fn predict_proba(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<f64>> {
let X = X.to_owned();
let n_test = X.nrows();
let n_classes = self.state.classes.len();
let K_test = self
.compute_kernel_matrix(&X, &self.state.X_labeled)
.map_err(|e| SklearsError::PredictError(e.to_string()))?;
let gp_predictions = K_test.dot(&self.state.GP_weights);
let mut probabilities = Array2::<f64>::zeros((n_test, n_classes));
for i in 0..n_test {
let row = gp_predictions.row(i);
let max_val = row.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let mut exp_sum = 0.0;
for j in 0..n_classes {
let exp_val = (row[j] - max_val).exp();
probabilities[[i, j]] = exp_val;
exp_sum += exp_val;
}
if exp_sum > 0.0 {
for j in 0..n_classes {
probabilities[[i, j]] /= exp_sum;
}
} else {
for j in 0..n_classes {
probabilities[[i, j]] = 1.0 / n_classes as f64;
}
}
}
Ok(probabilities)
}
}
impl GaussianProcessSemiSupervised<GaussianProcessTrained> {
fn compute_kernel_matrix(&self, X1: &Array2<f64>, X2: &Array2<f64>) -> SklResult<Array2<f64>> {
let n1 = X1.nrows();
let n2 = X2.nrows();
let mut K = Array2::<f64>::zeros((n1, n2));
match self.kernel.as_str() {
"rbf" => {
for i in 0..n1 {
for j in 0..n2 {
let diff = &X1.row(i) - &X2.row(j);
let dist_sq = diff.mapv(|x| x * x).sum();
K[[i, j]] = (-dist_sq / (2.0 * self.length_scale.powi(2))).exp();
}
}
}
"linear" => {
for i in 0..n1 {
for j in 0..n2 {
K[[i, j]] = X1.row(i).dot(&X2.row(j));
}
}
}
"polynomial" => {
let degree = 2.0;
for i in 0..n1 {
for j in 0..n2 {
let dot_product = X1.row(i).dot(&X2.row(j));
K[[i, j]] = (1.0 + dot_product / self.length_scale).powf(degree);
}
}
}
_ => {
return Err(SklearsError::InvalidInput(format!(
"Unknown kernel: {}",
self.kernel
)));
}
}
Ok(K)
}
}
#[derive(Debug, Clone)]
pub struct VariationalBayesianSemiSupervised<S = Untrained> {
state: S,
n_components: usize,
max_iter: usize,
tol: f64,
reg_covar: f64,
alpha_prior: f64,
random_state: Option<u64>,
}
impl VariationalBayesianSemiSupervised<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
n_components: 2,
max_iter: 100,
tol: 1e-4,
reg_covar: 1e-6,
alpha_prior: 1.0,
random_state: None,
}
}
pub fn n_components(mut self, n_components: usize) -> Self {
self.n_components = n_components;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn tol(mut self, tol: f64) -> Self {
self.tol = tol;
self
}
pub fn reg_covar(mut self, reg_covar: f64) -> Self {
self.reg_covar = reg_covar;
self
}
pub fn alpha_prior(mut self, alpha_prior: f64) -> Self {
self.alpha_prior = alpha_prior;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
}
impl Default for VariationalBayesianSemiSupervised<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for VariationalBayesianSemiSupervised<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, i32>>
for VariationalBayesianSemiSupervised<Untrained>
{
type Fitted = VariationalBayesianSemiSupervised<VariationalBayesianTrained>;
#[allow(non_snake_case)]
fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView1<'_, i32>) -> SklResult<Self::Fitted> {
let X = X.to_owned();
let y = y.to_owned();
let (n_samples, n_features) = X.dim();
let mut labeled_indices = Vec::new();
let mut unlabeled_indices = Vec::new();
let mut classes = std::collections::HashSet::new();
for (i, &label) in y.iter().enumerate() {
if label == -1 {
unlabeled_indices.push(i);
} else {
labeled_indices.push(i);
classes.insert(label);
}
}
if labeled_indices.is_empty() {
return Err(SklearsError::InvalidInput(
"No labeled samples provided".to_string(),
));
}
let classes: Vec<i32> = classes.into_iter().collect();
let mut rng = if let Some(seed) = self.random_state {
Random::seed(seed)
} else {
Random::seed(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("operation should succeed")
.as_secs(),
)
};
let mut means = Array2::<f64>::zeros((self.n_components, n_features));
let mut covariances = Vec::new();
let mut mixing_weights = Array1::<f64>::ones(self.n_components) / self.n_components as f64;
for k in 0..self.n_components {
for j in 0..n_features {
means[[k, j]] = rng.random_range(-1.0..1.0);
}
}
for _k in 0..self.n_components {
let mut cov = Array2::<f64>::zeros((n_features, n_features));
for i in 0..n_features {
cov[[i, i]] = 1.0 + self.reg_covar;
}
covariances.push(cov);
}
let mut responsibilities = Array2::<f64>::zeros((n_samples, self.n_components));
for _iter in 0..self.max_iter {
let prev_means = means.clone();
for i in 0..n_samples {
let mut log_prob_norm = f64::NEG_INFINITY;
for k in 0..self.n_components {
let log_prob =
self.compute_log_probability(&X.row(i), &means.row(k), &covariances[k]);
let log_resp = log_prob + mixing_weights[k].ln();
if log_resp > log_prob_norm {
log_prob_norm = log_resp;
}
}
let mut exp_sum = 0.0;
for k in 0..self.n_components {
let log_prob =
self.compute_log_probability(&X.row(i), &means.row(k), &covariances[k]);
let log_resp = log_prob + mixing_weights[k].ln() - log_prob_norm;
responsibilities[[i, k]] = log_resp.exp();
exp_sum += responsibilities[[i, k]];
}
if exp_sum > 0.0 {
for k in 0..self.n_components {
responsibilities[[i, k]] /= exp_sum;
}
}
}
for k in 0..self.n_components {
let n_k: f64 = responsibilities.column(k).sum();
if n_k > 1e-10 {
let mut new_mean = Array1::<f64>::zeros(n_features);
for i in 0..n_samples {
for j in 0..n_features {
new_mean[j] += responsibilities[[i, k]] * X[[i, j]];
}
}
new_mean /= n_k;
means.row_mut(k).assign(&new_mean);
mixing_weights[k] = (n_k + self.alpha_prior - 1.0)
/ (n_samples as f64 + self.n_components as f64 * self.alpha_prior
- self.n_components as f64);
}
}
let diff = (&means - &prev_means).mapv(|x| x.abs()).sum();
if diff < self.tol {
break;
}
}
let mut final_labels = y.clone();
for &idx in &unlabeled_indices {
let best_component = responsibilities
.row(idx)
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).expect("operation should succeed"))
.expect("operation should succeed")
.0;
let predicted_class = classes[best_component % classes.len()];
final_labels[idx] = predicted_class;
}
Ok(VariationalBayesianSemiSupervised {
state: VariationalBayesianTrained {
X_train: X,
y_train: final_labels,
classes: Array1::from(classes),
means,
covariances,
mixing_weights,
responsibilities,
},
n_components: self.n_components,
max_iter: self.max_iter,
tol: self.tol,
reg_covar: self.reg_covar,
alpha_prior: self.alpha_prior,
random_state: self.random_state,
})
}
}
impl VariationalBayesianSemiSupervised<Untrained> {
fn compute_log_probability(
&self,
x: &ArrayView1<f64>,
mean: &ArrayView1<f64>,
covariance: &Array2<f64>,
) -> f64 {
let d = x.len() as f64;
let diff = x.to_owned() - mean.to_owned();
let mut log_prob = -0.5 * d * (2.0 * PI).ln();
for i in 0..diff.len() {
let var = covariance[[i, i]];
log_prob -= 0.5 * (var.ln() + diff[i] * diff[i] / var);
}
log_prob
}
}
impl Predict<ArrayView2<'_, Float>, Array1<i32>>
for VariationalBayesianSemiSupervised<VariationalBayesianTrained>
{
#[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);
for i in 0..n_test {
let mut max_log_prob = f64::NEG_INFINITY;
let mut best_component = 0;
for k in 0..self.n_components {
let log_prob = self.compute_log_probability(
&X.row(i),
&self.state.means.row(k),
&self.state.covariances[k],
) + self.state.mixing_weights[k].ln();
if log_prob > max_log_prob {
max_log_prob = log_prob;
best_component = k;
}
}
let predicted_class = self.state.classes[best_component % self.state.classes.len()];
predictions[i] = predicted_class;
}
Ok(predictions)
}
}
impl VariationalBayesianSemiSupervised<VariationalBayesianTrained> {
fn compute_log_probability(
&self,
x: &ArrayView1<f64>,
mean: &ArrayView1<f64>,
covariance: &Array2<f64>,
) -> f64 {
let d = x.len() as f64;
let diff = x.to_owned() - mean.to_owned();
let mut log_prob = -0.5 * d * (2.0 * PI).ln();
for i in 0..diff.len() {
let var = covariance[[i, i]];
log_prob -= 0.5 * (var.ln() + diff[i] * diff[i] / var);
}
log_prob
}
}
#[derive(Debug, Clone)]
pub struct GaussianProcessTrained {
pub X_train: Array2<f64>,
pub y_train: Array1<i32>,
pub classes: Array1<i32>,
pub X_labeled: Array2<f64>,
pub GP_weights: Array2<f64>,
pub predictions_all: Array2<f64>,
}
#[derive(Debug, Clone)]
pub struct VariationalBayesianTrained {
pub X_train: Array2<f64>,
pub y_train: Array1<i32>,
pub classes: Array1<i32>,
pub means: Array2<f64>,
pub covariances: Vec<Array2<f64>>,
pub mixing_weights: Array1<f64>,
pub responsibilities: Array2<f64>,
}
#[derive(Debug, Clone)]
pub struct BayesianActiveLearning<S = Untrained> {
state: S,
n_queries: usize,
kernel: String,
length_scale: f64,
noise_level: f64,
acquisition: String,
random_state: Option<u64>,
}
impl BayesianActiveLearning<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
n_queries: 10,
kernel: "rbf".to_string(),
length_scale: 1.0,
noise_level: 0.1,
acquisition: "uncertainty".to_string(),
random_state: None,
}
}
pub fn n_queries(mut self, n_queries: usize) -> Self {
self.n_queries = n_queries;
self
}
pub fn kernel(mut self, kernel: String) -> Self {
self.kernel = kernel;
self
}
pub fn length_scale(mut self, length_scale: f64) -> Self {
self.length_scale = length_scale;
self
}
pub fn noise_level(mut self, noise_level: f64) -> Self {
self.noise_level = noise_level;
self
}
pub fn acquisition(mut self, acquisition: String) -> Self {
self.acquisition = acquisition;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
}
impl Default for BayesianActiveLearning<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for BayesianActiveLearning<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, i32>> for BayesianActiveLearning<Untrained> {
type Fitted = BayesianActiveLearning<BayesianActiveTrained>;
#[allow(non_snake_case)]
fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView1<'_, i32>) -> SklResult<Self::Fitted> {
let X = X.to_owned();
let y = y.to_owned();
let mut labeled_indices = Vec::new();
let mut unlabeled_indices = Vec::new();
let mut classes = std::collections::HashSet::new();
for (i, &label) in y.iter().enumerate() {
if label == -1 {
unlabeled_indices.push(i);
} else {
labeled_indices.push(i);
classes.insert(label);
}
}
if labeled_indices.is_empty() {
return Err(SklearsError::InvalidInput(
"No labeled samples provided".to_string(),
));
}
let classes: Vec<i32> = classes.into_iter().collect();
let mut uncertainties = Vec::new();
for &idx in &unlabeled_indices {
let mut min_dist = f64::INFINITY;
for &labeled_idx in &labeled_indices {
let diff = &X.row(idx) - &X.row(labeled_idx);
let dist = diff.mapv(|x| x * x).sum().sqrt();
if dist < min_dist {
min_dist = dist;
}
}
uncertainties.push((idx, min_dist));
}
uncertainties.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
let query_indices: Vec<usize> = uncertainties
.iter()
.take(self.n_queries.min(unlabeled_indices.len()))
.map(|(idx, _)| *idx)
.collect();
Ok(BayesianActiveLearning {
state: BayesianActiveTrained {
X_train: X,
y_train: y,
classes: Array1::from(classes),
query_indices,
uncertainties: uncertainties.iter().map(|(_, u)| *u).collect(),
},
n_queries: self.n_queries,
kernel: self.kernel,
length_scale: self.length_scale,
noise_level: self.noise_level,
acquisition: self.acquisition,
random_state: self.random_state,
})
}
}
impl Predict<ArrayView2<'_, Float>, Array1<i32>> for BayesianActiveLearning<BayesianActiveTrained> {
#[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);
for i in 0..n_test {
let mut min_dist = f64::INFINITY;
let mut best_label = self.state.classes[0];
for j in 0..self.state.X_train.nrows() {
if self.state.y_train[j] != -1 {
let diff = &X.row(i) - &self.state.X_train.row(j);
let dist = diff.mapv(|x| x * x).sum().sqrt();
if dist < min_dist {
min_dist = dist;
best_label = self.state.y_train[j];
}
}
}
predictions[i] = best_label;
}
Ok(predictions)
}
}
#[derive(Debug, Clone)]
pub struct HierarchicalBayesianSemiSupervised<S = Untrained> {
state: S,
n_levels: usize,
n_components: usize,
max_iter: usize,
prior_strength: f64,
random_state: Option<u64>,
}
impl HierarchicalBayesianSemiSupervised<Untrained> {
pub fn new() -> Self {
Self {
state: Untrained,
n_levels: 2,
n_components: 2,
max_iter: 100,
prior_strength: 1.0,
random_state: None,
}
}
pub fn n_levels(mut self, n_levels: usize) -> Self {
self.n_levels = n_levels;
self
}
pub fn n_components(mut self, n_components: usize) -> Self {
self.n_components = n_components;
self
}
pub fn max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn prior_strength(mut self, prior_strength: f64) -> Self {
self.prior_strength = prior_strength;
self
}
pub fn random_state(mut self, random_state: u64) -> Self {
self.random_state = Some(random_state);
self
}
}
impl Default for HierarchicalBayesianSemiSupervised<Untrained> {
fn default() -> Self {
Self::new()
}
}
impl Estimator for HierarchicalBayesianSemiSupervised<Untrained> {
type Config = ();
type Error = SklearsError;
type Float = Float;
fn config(&self) -> &Self::Config {
&()
}
}
impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, i32>>
for HierarchicalBayesianSemiSupervised<Untrained>
{
type Fitted = HierarchicalBayesianSemiSupervised<HierarchicalBayesianTrained>;
#[allow(non_snake_case)]
fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView1<'_, i32>) -> SklResult<Self::Fitted> {
let X = X.to_owned();
let y = y.to_owned();
let (n_samples, n_features) = X.dim();
let mut labeled_indices = Vec::new();
let mut unlabeled_indices = Vec::new();
let mut classes = std::collections::HashSet::new();
for (i, &label) in y.iter().enumerate() {
if label == -1 {
unlabeled_indices.push(i);
} else {
labeled_indices.push(i);
classes.insert(label);
}
}
if labeled_indices.is_empty() {
return Err(SklearsError::InvalidInput(
"No labeled samples provided".to_string(),
));
}
let classes: Vec<i32> = classes.into_iter().collect();
let n_classes = classes.len();
let mut rng = if let Some(seed) = self.random_state {
Random::seed(seed)
} else {
Random::seed(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("operation should succeed")
.as_secs(),
)
};
let mut level_means = Vec::new();
for _ in 0..self.n_levels {
let mut means = Array2::<f64>::zeros((self.n_components, n_features));
for i in 0..self.n_components {
for j in 0..n_features {
means[[i, j]] = rng.random_range(-1.0..1.0);
}
}
level_means.push(means);
}
for _iter in 0..self.max_iter {
let mean = X
.mean_axis(scirs2_core::ndarray::Axis(0))
.expect("operation should succeed");
#[allow(clippy::needless_range_loop)]
for level_idx in 0..self.n_levels {
for comp_idx in 0..self.n_components {
for feat_idx in 0..n_features {
level_means[level_idx][[comp_idx, feat_idx]] = 0.9
* level_means[level_idx][[comp_idx, feat_idx]]
+ 0.1 * mean[feat_idx];
}
}
}
}
let mut final_labels = y.clone();
for &idx in &unlabeled_indices {
let mut min_dist = f64::INFINITY;
let mut best_component = 0;
for comp_idx in 0..self.n_components {
let diff = &X.row(idx) - &level_means[0].row(comp_idx);
let dist = diff.mapv(|x| x * x).sum().sqrt();
if dist < min_dist {
min_dist = dist;
best_component = comp_idx;
}
}
let predicted_class = classes[best_component % n_classes];
final_labels[idx] = predicted_class;
}
Ok(HierarchicalBayesianSemiSupervised {
state: HierarchicalBayesianTrained {
X_train: X,
y_train: final_labels,
classes: Array1::from(classes),
level_means,
},
n_levels: self.n_levels,
n_components: self.n_components,
max_iter: self.max_iter,
prior_strength: self.prior_strength,
random_state: self.random_state,
})
}
}
impl Predict<ArrayView2<'_, Float>, Array1<i32>>
for HierarchicalBayesianSemiSupervised<HierarchicalBayesianTrained>
{
#[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);
for i in 0..n_test {
let mut min_dist = f64::INFINITY;
let mut best_label = self.state.classes[0];
for j in 0..self.state.X_train.nrows() {
let diff = &X.row(i) - &self.state.X_train.row(j);
let dist = diff.mapv(|x| x * x).sum().sqrt();
if dist < min_dist {
min_dist = dist;
best_label = self.state.y_train[j];
}
}
predictions[i] = best_label;
}
Ok(predictions)
}
}
#[derive(Debug, Clone)]
pub struct BayesianActiveTrained {
pub X_train: Array2<f64>,
pub y_train: Array1<i32>,
pub classes: Array1<i32>,
pub query_indices: Vec<usize>,
pub uncertainties: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct HierarchicalBayesianTrained {
pub X_train: Array2<f64>,
pub y_train: Array1<i32>,
pub classes: Array1<i32>,
pub level_means: Vec<Array2<f64>>,
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::array;
#[test]
#[allow(non_snake_case)]
fn test_gaussian_process_semi_supervised() {
let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
let y = array![0, 1, -1, -1];
let gp = GaussianProcessSemiSupervised::new()
.kernel("rbf".to_string())
.length_scale(1.0)
.noise_level(0.1)
.random_state(42);
let fitted = gp
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted.predict(&X.view()).expect("operation should succeed");
let probas = fitted
.predict_proba(&X.view())
.expect("operation should succeed");
assert_eq!(predictions.len(), 4);
assert_eq!(probas.dim(), (4, 2));
assert!(predictions.iter().all(|&p| p >= 0 && p <= 1));
for i in 0..4 {
let sum: f64 = probas.row(i).sum();
assert!((sum - 1.0).abs() < 1e-6);
}
assert_eq!(predictions[0], 0);
assert_eq!(predictions[1], 1);
}
#[test]
#[allow(non_snake_case)]
fn test_variational_bayesian_semi_supervised() {
let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
let y = array![0, 1, -1, -1];
let vb = VariationalBayesianSemiSupervised::new()
.n_components(2)
.max_iter(10)
.random_state(42);
let fitted = vb
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted.predict(&X.view()).expect("operation should succeed");
assert_eq!(predictions.len(), 4);
assert!(predictions.iter().all(|&p| p >= 0 && p <= 1));
}
#[test]
fn test_gaussian_process_parameters() {
let gp = GaussianProcessSemiSupervised::new()
.kernel("linear".to_string())
.length_scale(2.0)
.noise_level(0.2)
.alpha(1e-8)
.n_restarts_optimizer(5);
assert_eq!(gp.kernel, "linear");
assert_eq!(gp.length_scale, 2.0);
assert_eq!(gp.noise_level, 0.2);
assert_eq!(gp.alpha, 1e-8);
assert_eq!(gp.n_restarts_optimizer, 5);
}
#[test]
fn test_variational_bayesian_parameters() {
let vb = VariationalBayesianSemiSupervised::new()
.n_components(4)
.max_iter(200)
.tol(1e-6)
.reg_covar(1e-4)
.alpha_prior(2.0);
assert_eq!(vb.n_components, 4);
assert_eq!(vb.max_iter, 200);
assert_eq!(vb.tol, 1e-6);
assert_eq!(vb.reg_covar, 1e-4);
assert_eq!(vb.alpha_prior, 2.0);
}
#[test]
#[allow(non_snake_case)]
fn test_kernel_matrix_computation() {
let gp = GaussianProcessSemiSupervised::new()
.kernel("rbf".to_string())
.length_scale(1.0);
let X = array![[1.0, 2.0], [3.0, 4.0]];
let K = gp
.compute_kernel_matrix(&X, &X)
.expect("operation should succeed");
assert_eq!(K.dim(), (2, 2));
assert!((K[[0, 0]] - 1.0).abs() < 1e-10); assert!((K[[1, 1]] - 1.0).abs() < 1e-10);
assert!(K[[0, 1]] > 0.0 && K[[0, 1]] < 1.0); assert!((K[[0, 1]] - K[[1, 0]]).abs() < 1e-10); }
#[test]
#[allow(non_snake_case)]
fn test_linear_kernel() {
let gp = GaussianProcessSemiSupervised::new().kernel("linear".to_string());
let X = array![[1.0, 2.0], [3.0, 4.0]];
let K = gp
.compute_kernel_matrix(&X, &X)
.expect("operation should succeed");
assert_eq!(K.dim(), (2, 2));
assert!((K[[0, 0]] - 5.0).abs() < 1e-10); assert!((K[[1, 1]] - 25.0).abs() < 1e-10); assert!((K[[0, 1]] - 11.0).abs() < 1e-10); }
#[test]
#[allow(non_snake_case)]
fn test_polynomial_kernel() {
let gp = GaussianProcessSemiSupervised::new()
.kernel("polynomial".to_string())
.length_scale(1.0);
let X = array![[1.0, 1.0]];
let K = gp
.compute_kernel_matrix(&X, &X)
.expect("operation should succeed");
assert_eq!(K.dim(), (1, 1));
assert!((K[[0, 0]] - 9.0).abs() < 1e-10); }
#[test]
#[allow(non_snake_case)]
fn test_empty_labeled_samples_error() {
let X = array![[1.0, 2.0], [2.0, 3.0]];
let y = array![-1, -1];
let gp = GaussianProcessSemiSupervised::new();
let result = gp.fit(&X.view(), &y.view());
assert!(result.is_err());
let vb = VariationalBayesianSemiSupervised::new();
let result = vb.fit(&X.view(), &y.view());
assert!(result.is_err());
}
#[test]
#[allow(non_snake_case)]
fn test_single_labeled_sample() {
let X = array![[1.0, 2.0], [2.0, 3.0]];
let y = array![0, -1];
let gp = GaussianProcessSemiSupervised::new()
.noise_level(0.1)
.random_state(42);
let fitted = gp
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted.predict(&X.view()).expect("operation should succeed");
assert_eq!(predictions.len(), 2);
assert_eq!(predictions[0], 0); }
#[test]
fn test_log_probability_computation() {
let vb = VariationalBayesianSemiSupervised::new();
let x = array![1.0, 2.0];
let mean = array![1.0, 2.0];
let mut covar = Array2::<f64>::zeros((2, 2));
covar[[0, 0]] = 1.0;
covar[[1, 1]] = 1.0;
let log_prob = vb.compute_log_probability(&x.view(), &mean.view(), &covar);
assert!(log_prob.is_finite());
assert!(log_prob < 0.0); }
#[test]
#[allow(non_snake_case)]
fn test_bayesian_active_learning() {
let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
let y = array![0, 1, -1, -1];
let bal = BayesianActiveLearning::new().n_queries(2).random_state(42);
let fitted = bal
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted.predict(&X.view()).expect("operation should succeed");
assert_eq!(predictions.len(), 4);
assert!(predictions.iter().all(|&p| p >= 0 && p <= 1));
assert_eq!(fitted.state.query_indices.len(), 2);
}
#[test]
fn test_bayesian_active_learning_parameters() {
let bal = BayesianActiveLearning::new()
.n_queries(5)
.kernel("rbf".to_string())
.length_scale(2.0)
.noise_level(0.2)
.acquisition("entropy".to_string());
assert_eq!(bal.n_queries, 5);
assert_eq!(bal.kernel, "rbf");
assert_eq!(bal.length_scale, 2.0);
assert_eq!(bal.noise_level, 0.2);
assert_eq!(bal.acquisition, "entropy");
}
#[test]
#[allow(non_snake_case)]
fn test_hierarchical_bayesian() {
let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
let y = array![0, 1, -1, -1];
let hb = HierarchicalBayesianSemiSupervised::new()
.n_levels(2)
.n_components(2)
.max_iter(10)
.random_state(42);
let fitted = hb
.fit(&X.view(), &y.view())
.expect("operation should succeed");
let predictions = fitted.predict(&X.view()).expect("operation should succeed");
assert_eq!(predictions.len(), 4);
assert!(predictions.iter().all(|&p| p >= 0 && p <= 1));
assert_eq!(fitted.state.level_means.len(), 2);
}
#[test]
fn test_hierarchical_bayesian_parameters() {
let hb = HierarchicalBayesianSemiSupervised::new()
.n_levels(3)
.n_components(4)
.max_iter(200)
.prior_strength(2.0);
assert_eq!(hb.n_levels, 3);
assert_eq!(hb.n_components, 4);
assert_eq!(hb.max_iter, 200);
assert_eq!(hb.prior_strength, 2.0);
}
}