Skip to main content

sklears_semi_supervised/deep_learning/
ladder_networks.rs

1//! Ladder Networks for Deep Semi-Supervised Learning
2//!
3//! This module implements Ladder Networks, a deep learning architecture that
4//! combines supervised learning with unsupervised learning through lateral
5//! connections and denoising objectives. Ladder networks achieve state-of-the-art
6//! performance on semi-supervised learning tasks by learning hierarchical
7//! representations at multiple levels.
8
9use scirs2_core::ndarray_ext::{Array1, Array2, ArrayView1, ArrayView2, Axis};
10use scirs2_core::random::Random;
11use sklears_core::{
12    error::{Result as SklResult, SklearsError},
13    traits::{Estimator, Fit, Predict, PredictProba, Untrained},
14    types::Float,
15};
16
17/// Ladder Networks for Deep Semi-Supervised Learning
18///
19/// Ladder Networks are neural networks that combine supervised and unsupervised
20/// learning objectives. They use lateral connections between encoder and decoder
21/// paths to enable effective learning from both labeled and unlabeled data.
22///
23/// The architecture consists of:
24/// - An encoder path that applies noise and nonlinearities
25/// - A decoder path that reconstructs clean representations
26/// - Lateral connections that help the decoder
27/// - Multiple reconstruction costs at different layers
28///
29/// # Parameters
30///
31/// * `layer_sizes` - Sizes of hidden layers (including input and output)
32/// * `noise_std` - Standard deviation of Gaussian noise added to each layer
33/// * `lambda_unsupervised` - Weight for unsupervised reconstruction loss
34/// * `lambda_supervised` - Weight for supervised classification loss
35/// * `denoising_cost_weights` - Weights for denoising costs at each layer
36/// * `learning_rate` - Learning rate for optimization
37/// * `max_iter` - Maximum number of training iterations
38/// * `batch_size` - Size of mini-batches for training
39///
40/// # Examples
41///
42/// ```rust,ignore
43/// use sklears_semi_supervised::LadderNetworks;
44/// use sklears_core::traits::{Predict, Fit};
45///
46///
47/// let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0]];
48/// let y = array![0, 1, -1, -1]; // -1 indicates unlabeled
49///
50/// let ln = LadderNetworks::new()
51///     .layer_sizes(vec![2, 4, 2])
52///     .noise_std(0.3)
53///     .lambda_unsupervised(1.0)
54///     .lambda_supervised(1.0);
55/// let fitted = ln.fit(&X.view(), &y.view()).unwrap();
56/// let predictions = fitted.predict(&X.view()).unwrap();
57/// ```
58#[derive(Debug, Clone)]
59pub struct LadderNetworks<S = Untrained> {
60    state: S,
61    layer_sizes: Vec<usize>,
62    noise_std: f64,
63    lambda_unsupervised: f64,
64    lambda_supervised: f64,
65    denoising_cost_weights: Vec<f64>,
66    learning_rate: f64,
67    max_iter: usize,
68    batch_size: usize,
69    beta1: f64,
70    beta2: f64,
71    epsilon: f64,
72    random_state: Option<u64>,
73}
74
75impl LadderNetworks<Untrained> {
76    /// Create a new LadderNetworks instance
77    pub fn new() -> Self {
78        Self {
79            state: Untrained,
80            layer_sizes: vec![10, 6, 4, 2], // Default architecture
81            noise_std: 0.3,
82            lambda_unsupervised: 1.0,
83            lambda_supervised: 1.0,
84            denoising_cost_weights: vec![1000.0, 10.0, 0.1, 0.1],
85            learning_rate: 0.002,
86            max_iter: 100,
87            batch_size: 32,
88            beta1: 0.9,
89            beta2: 0.999,
90            epsilon: 1e-8,
91            random_state: None,
92        }
93    }
94
95    /// Set the layer sizes (input size will be set automatically)
96    pub fn layer_sizes(mut self, sizes: Vec<usize>) -> Self {
97        self.layer_sizes = sizes;
98        self
99    }
100
101    /// Set the noise standard deviation
102    pub fn noise_std(mut self, std: f64) -> Self {
103        self.noise_std = std;
104        self
105    }
106
107    /// Set the unsupervised loss weight
108    pub fn lambda_unsupervised(mut self, lambda: f64) -> Self {
109        self.lambda_unsupervised = lambda;
110        self
111    }
112
113    /// Set the supervised loss weight
114    pub fn lambda_supervised(mut self, lambda: f64) -> Self {
115        self.lambda_supervised = lambda;
116        self
117    }
118
119    /// Set the denoising cost weights for each layer
120    pub fn denoising_cost_weights(mut self, weights: Vec<f64>) -> Self {
121        self.denoising_cost_weights = weights;
122        self
123    }
124
125    /// Set the learning rate
126    pub fn learning_rate(mut self, lr: f64) -> Self {
127        self.learning_rate = lr;
128        self
129    }
130
131    /// Set the maximum number of iterations
132    pub fn max_iter(mut self, max_iter: usize) -> Self {
133        self.max_iter = max_iter;
134        self
135    }
136
137    /// Set the batch size
138    pub fn batch_size(mut self, batch_size: usize) -> Self {
139        self.batch_size = batch_size;
140        self
141    }
142
143    /// Set Adam optimizer beta1 parameter
144    pub fn beta1(mut self, beta1: f64) -> Self {
145        self.beta1 = beta1;
146        self
147    }
148
149    /// Set Adam optimizer beta2 parameter
150    pub fn beta2(mut self, beta2: f64) -> Self {
151        self.beta2 = beta2;
152        self
153    }
154
155    /// Set random state for reproducibility
156    pub fn random_state(mut self, seed: u64) -> Self {
157        self.random_state = Some(seed);
158        self
159    }
160
161    fn initialize_weights(&self, input_size: usize) -> LadderWeights {
162        let mut layer_sizes = self.layer_sizes.clone();
163        layer_sizes[0] = input_size; // Set input size
164
165        let n_layers = layer_sizes.len();
166        let mut encoder_weights = Vec::with_capacity(n_layers - 1);
167        let mut encoder_biases = Vec::with_capacity(n_layers - 1);
168        let mut decoder_weights = Vec::with_capacity(n_layers - 1);
169        let mut decoder_biases = Vec::with_capacity(n_layers - 1);
170
171        // Xavier initialization
172        for i in 0..(n_layers - 1) {
173            let fan_in = layer_sizes[i];
174            let fan_out = layer_sizes[i + 1];
175            let xavier_std = (2.0 / (fan_in + fan_out) as f64).sqrt();
176
177            // Encoder weights (bottom-up)
178            let mut rng = Random::default();
179            let mut w_enc = Array2::zeros((fan_in, fan_out));
180            for i in 0..fan_in {
181                for j in 0..fan_out {
182                    w_enc[[i, j]] = rng.random_range(-3.0..3.0) / 3.0 * xavier_std;
183                }
184            }
185            let b_enc = Array1::zeros(fan_out);
186            encoder_weights.push(w_enc);
187            encoder_biases.push(b_enc);
188
189            // Decoder weights (top-down)
190            let mut w_dec = Array2::zeros((fan_out, fan_in));
191            for i in 0..fan_out {
192                for j in 0..fan_in {
193                    w_dec[[i, j]] = rng.random_range(-3.0..3.0) / 3.0 * xavier_std;
194                }
195            }
196            let b_dec = Array1::zeros(fan_in);
197            decoder_weights.push(w_dec);
198            decoder_biases.push(b_dec);
199        }
200
201        LadderWeights {
202            encoder_weights,
203            encoder_biases,
204            decoder_weights,
205            decoder_biases,
206            layer_sizes,
207        }
208    }
209
210    fn add_noise(&self, x: &Array2<f64>) -> Array2<f64> {
211        let mut rng = Random::default();
212        let mut noise = Array2::zeros(x.dim());
213        for i in 0..x.nrows() {
214            for j in 0..x.ncols() {
215                noise[[i, j]] = rng.random_range(-3.0..3.0) / 3.0 * self.noise_std;
216            }
217        }
218        x + &noise
219    }
220
221    fn relu(&self, x: &Array2<f64>) -> Array2<f64> {
222        x.mapv(|v| v.max(0.0))
223    }
224
225    #[allow(dead_code)]
226    pub(crate) fn relu_derivative(&self, x: &Array2<f64>) -> Array2<f64> {
227        x.mapv(|v| if v > 0.0 { 1.0 } else { 0.0 })
228    }
229
230    fn softmax(&self, x: &Array2<f64>) -> Array2<f64> {
231        let mut result = Array2::zeros(x.dim());
232        for (i, row) in x.axis_iter(Axis(0)).enumerate() {
233            let max_val = row.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
234            let exp_row: Array1<f64> = row.mapv(|v| (v - max_val).exp());
235            let sum_exp: f64 = exp_row.sum();
236            let softmax_row = exp_row / sum_exp;
237            result.row_mut(i).assign(&softmax_row);
238        }
239        result
240    }
241
242    fn forward_encoder(
243        &self,
244        x: &Array2<f64>,
245        weights: &LadderWeights,
246        add_noise: bool,
247    ) -> EncoderOutput {
248        let mut activations = Vec::new();
249        let mut noisy_activations = Vec::new();
250        let mut pre_activations = Vec::new();
251
252        let mut current = x.clone();
253        activations.push(current.clone());
254
255        if add_noise {
256            current = self.add_noise(&current);
257        }
258        noisy_activations.push(current.clone());
259
260        for i in 0..weights.encoder_weights.len() {
261            // Linear transformation
262            let z = current.dot(&weights.encoder_weights[i]) + &weights.encoder_biases[i];
263            pre_activations.push(z.clone());
264
265            // Apply activation function (ReLU for hidden layers, linear for output)
266            current = if i == weights.encoder_weights.len() - 1 {
267                z.clone() // Linear for output layer
268            } else {
269                self.relu(&z)
270            };
271
272            activations.push(current.clone());
273
274            // Add noise to hidden layers
275            if add_noise && i < weights.encoder_weights.len() - 1 {
276                current = self.add_noise(&current);
277            }
278            noisy_activations.push(current.clone());
279        }
280
281        EncoderOutput {
282            activations,
283            noisy_activations,
284            pre_activations,
285        }
286    }
287
288    fn forward_decoder(
289        &self,
290        top_activation: &Array2<f64>,
291        weights: &LadderWeights,
292    ) -> Vec<Array2<f64>> {
293        let mut decoder_outputs = Vec::new();
294        let mut current = top_activation.clone();
295
296        // Start from the top layer and work down
297        for i in (0..weights.decoder_weights.len()).rev() {
298            current = current.dot(&weights.decoder_weights[i]) + &weights.decoder_biases[i];
299
300            // Apply activation function for hidden layers
301            if i > 0 {
302                current = self.relu(&current);
303            }
304
305            decoder_outputs.push(current.clone());
306        }
307
308        decoder_outputs.reverse(); // Reverse to match layer order
309        decoder_outputs
310    }
311
312    fn compute_denoising_cost(
313        &self,
314        clean_activations: &[Array2<f64>],
315        reconstructed_activations: &[Array2<f64>],
316    ) -> f64 {
317        let mut total_cost = 0.0;
318
319        for (layer_idx, (clean, reconstructed)) in clean_activations
320            .iter()
321            .zip(reconstructed_activations.iter())
322            .enumerate()
323        {
324            if layer_idx < self.denoising_cost_weights.len() {
325                let diff = clean - reconstructed;
326                let mse = diff
327                    .mapv(|x| x * x)
328                    .mean()
329                    .expect("operation should succeed");
330                total_cost += self.denoising_cost_weights[layer_idx] * mse;
331            }
332        }
333
334        total_cost
335    }
336
337    fn compute_supervised_cost(&self, predictions: &Array2<f64>, targets: &Array2<f64>) -> f64 {
338        // Cross-entropy loss
339        let epsilon = 1e-15;
340        let clipped_predictions = predictions.mapv(|x| x.max(epsilon).min(1.0 - epsilon));
341        let log_predictions = clipped_predictions.mapv(|x| x.ln());
342
343        let mut cost = 0.0;
344        for i in 0..targets.nrows() {
345            for j in 0..targets.ncols() {
346                cost -= targets[[i, j]] * log_predictions[[i, j]];
347            }
348        }
349
350        cost / targets.nrows() as f64
351    }
352
353    fn create_target_matrix(&self, y: &Array1<i32>, classes: &[i32]) -> Array2<f64> {
354        let n_samples = y.len();
355        let n_classes = classes.len();
356        let mut targets = Array2::zeros((n_samples, n_classes));
357
358        for (i, &label) in y.iter().enumerate() {
359            if label != -1 {
360                if let Some(class_idx) = classes.iter().position(|&c| c == label) {
361                    targets[[i, class_idx]] = 1.0;
362                }
363            }
364        }
365
366        targets
367    }
368
369    fn update_weights_adam(
370        &self,
371        weights: &mut LadderWeights,
372        gradients: &LadderWeights,
373        momentum: &mut LadderWeights,
374        velocity: &mut LadderWeights,
375        iteration: usize,
376    ) {
377        let beta1_t = self.beta1.powi(iteration as i32);
378        let beta2_t = self.beta2.powi(iteration as i32);
379        let alpha_t = self.learning_rate * (1.0 - beta2_t).sqrt() / (1.0 - beta1_t);
380
381        // Update encoder weights
382        for i in 0..weights.encoder_weights.len() {
383            // Update momentum and velocity
384            momentum.encoder_weights[i] = self.beta1 * &momentum.encoder_weights[i]
385                + (1.0 - self.beta1) * &gradients.encoder_weights[i];
386            velocity.encoder_weights[i] = self.beta2 * &velocity.encoder_weights[i]
387                + (1.0 - self.beta2) * gradients.encoder_weights[i].mapv(|x| x * x);
388
389            // Update weights
390            let momentum_corrected = &momentum.encoder_weights[i] / (1.0 - beta1_t);
391            let velocity_corrected = &velocity.encoder_weights[i] / (1.0 - beta2_t);
392            let update = momentum_corrected / velocity_corrected.mapv(|x| x.sqrt() + self.epsilon);
393            weights.encoder_weights[i] = &weights.encoder_weights[i] - alpha_t * update;
394
395            // Update biases
396            momentum.encoder_biases[i] = self.beta1 * &momentum.encoder_biases[i]
397                + (1.0 - self.beta1) * &gradients.encoder_biases[i];
398            velocity.encoder_biases[i] = self.beta2 * &velocity.encoder_biases[i]
399                + (1.0 - self.beta2) * gradients.encoder_biases[i].mapv(|x| x * x);
400
401            let momentum_corrected_b = &momentum.encoder_biases[i] / (1.0 - beta1_t);
402            let velocity_corrected_b = &velocity.encoder_biases[i] / (1.0 - beta2_t);
403            let update_b =
404                momentum_corrected_b / velocity_corrected_b.mapv(|x| x.sqrt() + self.epsilon);
405            weights.encoder_biases[i] = &weights.encoder_biases[i] - alpha_t * update_b;
406        }
407
408        // Update decoder weights (similar process)
409        for i in 0..weights.decoder_weights.len() {
410            momentum.decoder_weights[i] = self.beta1 * &momentum.decoder_weights[i]
411                + (1.0 - self.beta1) * &gradients.decoder_weights[i];
412            velocity.decoder_weights[i] = self.beta2 * &velocity.decoder_weights[i]
413                + (1.0 - self.beta2) * gradients.decoder_weights[i].mapv(|x| x * x);
414
415            let momentum_corrected = &momentum.decoder_weights[i] / (1.0 - beta1_t);
416            let velocity_corrected = &velocity.decoder_weights[i] / (1.0 - beta2_t);
417            let update = momentum_corrected / velocity_corrected.mapv(|x| x.sqrt() + self.epsilon);
418            weights.decoder_weights[i] = &weights.decoder_weights[i] - alpha_t * update;
419
420            momentum.decoder_biases[i] = self.beta1 * &momentum.decoder_biases[i]
421                + (1.0 - self.beta1) * &gradients.decoder_biases[i];
422            velocity.decoder_biases[i] = self.beta2 * &velocity.decoder_biases[i]
423                + (1.0 - self.beta2) * gradients.decoder_biases[i].mapv(|x| x * x);
424
425            let momentum_corrected_b = &momentum.decoder_biases[i] / (1.0 - beta1_t);
426            let velocity_corrected_b = &velocity.decoder_biases[i] / (1.0 - beta2_t);
427            let update_b =
428                momentum_corrected_b / velocity_corrected_b.mapv(|x| x.sqrt() + self.epsilon);
429            weights.decoder_biases[i] = &weights.decoder_biases[i] - alpha_t * update_b;
430        }
431    }
432
433    fn compute_gradients(
434        &self,
435        x: &Array2<f64>,
436        targets: &Array2<f64>,
437        weights: &LadderWeights,
438    ) -> SklResult<LadderWeights> {
439        // Forward pass through noisy encoder
440        let _noisy_encoder_output = self.forward_encoder(x, weights, true);
441
442        // Initialize gradients
443        let mut gradient_weights = LadderWeights {
444            encoder_weights: weights
445                .encoder_weights
446                .iter()
447                .map(|w| Array2::zeros(w.dim()))
448                .collect(),
449            encoder_biases: weights
450                .encoder_biases
451                .iter()
452                .map(|b| Array1::zeros(b.len()))
453                .collect(),
454            decoder_weights: weights
455                .decoder_weights
456                .iter()
457                .map(|w| Array2::zeros(w.dim()))
458                .collect(),
459            decoder_biases: weights
460                .decoder_biases
461                .iter()
462                .map(|b| Array1::zeros(b.len()))
463                .collect(),
464            layer_sizes: weights.layer_sizes.clone(),
465        };
466
467        // Simplified gradient computation (in practice, this would use automatic differentiation)
468        // For demonstration, we compute approximate gradients using finite differences
469        let delta = 1e-5;
470
471        // Compute gradients for encoder weights
472        for i in 0..weights.encoder_weights.len() {
473            for j in 0..weights.encoder_weights[i].nrows() {
474                for k in 0..weights.encoder_weights[i].ncols() {
475                    // Perturb weight
476                    let mut weights_plus = weights.clone();
477                    let mut weights_minus = weights.clone();
478                    weights_plus.encoder_weights[i][[j, k]] += delta;
479                    weights_minus.encoder_weights[i][[j, k]] -= delta;
480
481                    // Compute costs
482                    let cost_plus = self.compute_total_cost(x, targets, &weights_plus)?;
483                    let cost_minus = self.compute_total_cost(x, targets, &weights_minus)?;
484
485                    // Approximate gradient
486                    gradient_weights.encoder_weights[i][[j, k]] =
487                        (cost_plus - cost_minus) / (2.0 * delta);
488                }
489            }
490        }
491
492        Ok(gradient_weights)
493    }
494
495    fn compute_total_cost(
496        &self,
497        x: &Array2<f64>,
498        targets: &Array2<f64>,
499        weights: &LadderWeights,
500    ) -> SklResult<f64> {
501        // Forward pass
502        let clean_encoder_output = self.forward_encoder(x, weights, false);
503        let noisy_encoder_output = self.forward_encoder(x, weights, true);
504        let decoder_outputs = self.forward_decoder(
505            noisy_encoder_output
506                .activations
507                .last()
508                .expect("operation should succeed"),
509            weights,
510        );
511
512        // Supervised cost
513        let predictions = self.softmax(
514            noisy_encoder_output
515                .activations
516                .last()
517                .expect("operation should succeed"),
518        );
519        let supervised_cost = self.compute_supervised_cost(&predictions, targets);
520
521        // Unsupervised denoising cost
522        let denoising_cost =
523            self.compute_denoising_cost(&clean_encoder_output.activations, &decoder_outputs);
524
525        let total_cost =
526            self.lambda_supervised * supervised_cost + self.lambda_unsupervised * denoising_cost;
527        Ok(total_cost)
528    }
529}
530
531#[derive(Debug, Clone)]
532pub struct LadderWeights {
533    /// encoder_weights
534    pub encoder_weights: Vec<Array2<f64>>,
535    /// encoder_biases
536    pub encoder_biases: Vec<Array1<f64>>,
537    /// decoder_weights
538    pub decoder_weights: Vec<Array2<f64>>,
539    /// decoder_biases
540    pub decoder_biases: Vec<Array1<f64>>,
541    /// layer_sizes
542    pub layer_sizes: Vec<usize>,
543}
544
545#[derive(Debug)]
546pub struct EncoderOutput {
547    /// activations
548    pub activations: Vec<Array2<f64>>,
549    /// noisy_activations
550    pub noisy_activations: Vec<Array2<f64>>,
551    /// pre_activations
552    pub pre_activations: Vec<Array2<f64>>,
553}
554
555impl Default for LadderNetworks<Untrained> {
556    fn default() -> Self {
557        Self::new()
558    }
559}
560
561impl Estimator for LadderNetworks<Untrained> {
562    type Config = ();
563    type Error = SklearsError;
564    type Float = Float;
565
566    fn config(&self) -> &Self::Config {
567        &()
568    }
569}
570
571impl Fit<ArrayView2<'_, Float>, ArrayView1<'_, i32>> for LadderNetworks<Untrained> {
572    type Fitted = LadderNetworks<LadderNetworksTrained>;
573
574    #[allow(non_snake_case)]
575    fn fit(self, X: &ArrayView2<'_, Float>, y: &ArrayView1<'_, i32>) -> SklResult<Self::Fitted> {
576        let X = X.to_owned();
577        let y = y.to_owned();
578        let (_n_samples, n_features) = X.dim();
579
580        // Identify classes
581        let mut classes = std::collections::HashSet::new();
582        for &label in y.iter() {
583            if label != -1 {
584                classes.insert(label);
585            }
586        }
587
588        if classes.is_empty() {
589            return Err(SklearsError::InvalidInput(
590                "No labeled samples provided".to_string(),
591            ));
592        }
593
594        let classes: Vec<i32> = classes.into_iter().collect();
595        let n_classes = classes.len();
596
597        // Adjust layer sizes
598        let mut layer_sizes = self.layer_sizes.clone();
599        layer_sizes[0] = n_features;
600        let last_idx = layer_sizes.len() - 1;
601        layer_sizes[last_idx] = n_classes;
602
603        // Initialize weights
604        let mut weights = self.initialize_weights(n_features);
605        weights.layer_sizes = layer_sizes;
606
607        // Initialize Adam optimizer state
608        let mut momentum = weights.clone();
609        let mut velocity = weights.clone();
610
611        // Zero initialize momentum and velocity
612        for i in 0..momentum.encoder_weights.len() {
613            momentum.encoder_weights[i].fill(0.0);
614            momentum.encoder_biases[i].fill(0.0);
615            velocity.encoder_weights[i].fill(0.0);
616            velocity.encoder_biases[i].fill(0.0);
617        }
618        for i in 0..momentum.decoder_weights.len() {
619            momentum.decoder_weights[i].fill(0.0);
620            momentum.decoder_biases[i].fill(0.0);
621            velocity.decoder_weights[i].fill(0.0);
622            velocity.decoder_biases[i].fill(0.0);
623        }
624
625        // Create target matrix
626        let targets = self.create_target_matrix(&y, &classes);
627
628        // Training loop (simplified for demonstration)
629        for iteration in 1..=self.max_iter {
630            // For simplicity, we'll train on the entire dataset
631            // In practice, you'd use mini-batches
632
633            let total_cost = self.compute_total_cost(&X, &targets, &weights)?;
634
635            if iteration % 10 == 0 {
636                println!("Iteration {}: Total cost = {:.6}", iteration, total_cost);
637            }
638
639            // Compute gradients (simplified - in practice use automatic differentiation)
640            let gradients = self.compute_gradients(&X, &targets, &weights)?;
641
642            // Update weights using Adam
643            self.update_weights_adam(
644                &mut weights,
645                &gradients,
646                &mut momentum,
647                &mut velocity,
648                iteration,
649            );
650        }
651
652        // Final forward pass to get label distributions
653        let final_encoder_output = self.forward_encoder(&X, &weights, false);
654        let final_predictions = self.softmax(
655            final_encoder_output
656                .activations
657                .last()
658                .expect("operation should succeed"),
659        );
660
661        Ok(LadderNetworks {
662            state: LadderNetworksTrained {
663                X_train: X,
664                y_train: y,
665                classes: Array1::from(classes),
666                weights,
667                label_distributions: final_predictions,
668            },
669            layer_sizes: self.layer_sizes,
670            noise_std: self.noise_std,
671            lambda_unsupervised: self.lambda_unsupervised,
672            lambda_supervised: self.lambda_supervised,
673            denoising_cost_weights: self.denoising_cost_weights,
674            learning_rate: self.learning_rate,
675            max_iter: self.max_iter,
676            batch_size: self.batch_size,
677            beta1: self.beta1,
678            beta2: self.beta2,
679            epsilon: self.epsilon,
680            random_state: self.random_state,
681        })
682    }
683}
684
685impl LadderNetworks<LadderNetworksTrained> {
686    fn forward_encoder(
687        &self,
688        x: &Array2<f64>,
689        weights: &LadderWeights,
690        add_noise: bool,
691    ) -> EncoderOutput {
692        let mut activations = Vec::new();
693        let mut noisy_activations = Vec::new();
694        let mut pre_activations = Vec::new();
695
696        let mut current = x.clone();
697        activations.push(current.clone());
698
699        if add_noise {
700            current = self.add_noise(&current);
701        }
702        noisy_activations.push(current.clone());
703
704        for i in 0..weights.encoder_weights.len() {
705            // Linear transformation
706            let z = current.dot(&weights.encoder_weights[i]) + &weights.encoder_biases[i];
707            pre_activations.push(z.clone());
708
709            // Apply activation function (ReLU for hidden layers, linear for output)
710            current = if i == weights.encoder_weights.len() - 1 {
711                z.clone() // Linear for output layer
712            } else {
713                self.relu(&z)
714            };
715
716            activations.push(current.clone());
717
718            // Add noise to hidden layers
719            if add_noise && i < weights.encoder_weights.len() - 1 {
720                current = self.add_noise(&current);
721            }
722            noisy_activations.push(current.clone());
723        }
724
725        EncoderOutput {
726            activations,
727            noisy_activations,
728            pre_activations,
729        }
730    }
731
732    fn add_noise(&self, x: &Array2<f64>) -> Array2<f64> {
733        let mut rng = Random::default();
734        let mut noise = Array2::zeros(x.dim());
735        for i in 0..x.nrows() {
736            for j in 0..x.ncols() {
737                noise[[i, j]] = rng.random_range(-3.0..3.0) / 3.0 * self.noise_std;
738            }
739        }
740        x + &noise
741    }
742
743    fn relu(&self, x: &Array2<f64>) -> Array2<f64> {
744        x.mapv(|v| v.max(0.0))
745    }
746
747    fn softmax(&self, x: &Array2<f64>) -> Array2<f64> {
748        let mut result = Array2::zeros(x.dim());
749        for (i, row) in x.axis_iter(Axis(0)).enumerate() {
750            let max_val = row.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
751            let exp_row: Array1<f64> = row.mapv(|v| (v - max_val).exp());
752            let sum_exp: f64 = exp_row.sum();
753            let softmax_row = exp_row / sum_exp;
754            result.row_mut(i).assign(&softmax_row);
755        }
756        result
757    }
758}
759
760impl Predict<ArrayView2<'_, Float>, Array1<i32>> for LadderNetworks<LadderNetworksTrained> {
761    #[allow(non_snake_case)]
762    fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array1<i32>> {
763        let X = X.to_owned();
764        let encoder_output = self.forward_encoder(&X, &self.state.weights, false);
765        let predictions = self.softmax(
766            encoder_output
767                .activations
768                .last()
769                .expect("operation should succeed"),
770        );
771
772        let mut result = Array1::zeros(X.nrows());
773        for i in 0..X.nrows() {
774            let max_idx = predictions
775                .row(i)
776                .iter()
777                .enumerate()
778                .max_by(|a, b| a.1.partial_cmp(b.1).expect("operation should succeed"))
779                .expect("operation should succeed")
780                .0;
781            result[i] = self.state.classes[max_idx];
782        }
783
784        Ok(result)
785    }
786}
787
788impl PredictProba<ArrayView2<'_, Float>, Array2<f64>> for LadderNetworks<LadderNetworksTrained> {
789    #[allow(non_snake_case)]
790    fn predict_proba(&self, X: &ArrayView2<'_, Float>) -> SklResult<Array2<f64>> {
791        let X = X.to_owned();
792        let encoder_output = self.forward_encoder(&X, &self.state.weights, false);
793        let predictions = self.softmax(
794            encoder_output
795                .activations
796                .last()
797                .expect("operation should succeed"),
798        );
799        Ok(predictions)
800    }
801}
802
803/// Trained state for LadderNetworks
804#[derive(Debug, Clone)]
805#[allow(non_snake_case)] // standard ML notation: X_train
806pub struct LadderNetworksTrained {
807    /// X_train
808    pub X_train: Array2<f64>,
809    /// y_train
810    pub y_train: Array1<i32>,
811    /// classes
812    pub classes: Array1<i32>,
813    /// weights
814    pub weights: LadderWeights,
815    /// label_distributions
816    pub label_distributions: Array2<f64>,
817}
818
819#[allow(non_snake_case)]
820#[cfg(test)]
821mod tests {
822    use super::*;
823    use scirs2_core::array;
824
825    #[test]
826    #[allow(non_snake_case)]
827    fn test_ladder_networks_basic() {
828        let X = array![
829            [1.0, 2.0, 0.5],
830            [2.0, 3.0, 1.0],
831            [3.0, 4.0, 1.5],
832            [4.0, 5.0, 2.0],
833            [5.0, 6.0, 2.5],
834            [6.0, 7.0, 3.0]
835        ];
836        let y = array![0, 1, 0, 1, -1, -1]; // -1 indicates unlabeled
837
838        let ln = LadderNetworks::new()
839            .layer_sizes(vec![3, 4, 2])
840            .noise_std(0.1)
841            .lambda_unsupervised(0.5)
842            .lambda_supervised(1.0)
843            .max_iter(5); // Reduced for testing
844        let fitted = ln
845            .fit(&X.view(), &y.view())
846            .expect("operation should succeed");
847
848        let predictions = fitted.predict(&X.view()).expect("operation should succeed");
849        assert_eq!(predictions.len(), 6);
850
851        let probas = fitted
852            .predict_proba(&X.view())
853            .expect("operation should succeed");
854        assert_eq!(probas.dim(), (6, 2));
855
856        // Check that probabilities sum to approximately 1
857        for i in 0..6 {
858            let sum: f64 = probas.row(i).sum();
859            assert!((sum - 1.0).abs() < 0.1); // Allow some numerical error
860        }
861    }
862
863    #[test]
864    fn test_ladder_networks_initialization() {
865        let ln = LadderNetworks::new()
866            .layer_sizes(vec![4, 6, 3])
867            .noise_std(0.2);
868
869        let weights = ln.initialize_weights(4);
870
871        // Check dimensions
872        assert_eq!(weights.layer_sizes, vec![4, 6, 3]);
873        assert_eq!(weights.encoder_weights.len(), 2);
874        assert_eq!(weights.encoder_weights[0].dim(), (4, 6));
875        assert_eq!(weights.encoder_weights[1].dim(), (6, 3));
876
877        assert_eq!(weights.decoder_weights.len(), 2);
878        assert_eq!(weights.decoder_weights[0].dim(), (6, 4));
879        assert_eq!(weights.decoder_weights[1].dim(), (3, 6));
880    }
881
882    #[test]
883    fn test_ladder_networks_noise_addition() {
884        let ln = LadderNetworks::new().noise_std(0.1);
885        let x = array![[1.0, 2.0], [3.0, 4.0]];
886
887        let noisy_x = ln.add_noise(&x);
888
889        // Check dimensions are preserved
890        assert_eq!(noisy_x.dim(), x.dim());
891
892        // Check that noise was added (values should be different)
893        let diff = (&noisy_x - &x).mapv(|v| v.abs()).sum();
894        assert!(diff > 0.0);
895    }
896
897    #[test]
898    fn test_ladder_networks_activations() {
899        let ln = LadderNetworks::new();
900
901        // Test ReLU
902        let x = array![[-1.0, 0.0, 1.0, 2.0]];
903        let relu_result = ln.relu(&x);
904        let expected = array![[0.0, 0.0, 1.0, 2.0]];
905
906        for i in 0..x.ncols() {
907            assert!((relu_result[[0, i]] - expected[[0, i]]).abs() < 1e-10);
908        }
909
910        // Test ReLU derivative
911        let relu_deriv = ln.relu_derivative(&x);
912        let expected_deriv = array![[0.0, 0.0, 1.0, 1.0]];
913
914        for i in 0..x.ncols() {
915            assert!((relu_deriv[[0, i]] - expected_deriv[[0, i]]).abs() < 1e-10);
916        }
917    }
918
919    #[test]
920    fn test_ladder_networks_softmax() {
921        let ln = LadderNetworks::new();
922        let x = array![[1.0, 2.0, 3.0], [0.0, 0.0, 0.0]];
923
924        let softmax_result = ln.softmax(&x);
925
926        // Check dimensions
927        assert_eq!(softmax_result.dim(), x.dim());
928
929        // Check that each row sums to 1
930        for i in 0..x.nrows() {
931            let sum: f64 = softmax_result.row(i).sum();
932            assert!((sum - 1.0).abs() < 1e-10);
933        }
934
935        // Check that all values are positive
936        for i in 0..x.nrows() {
937            for j in 0..x.ncols() {
938                assert!(softmax_result[[i, j]] > 0.0);
939            }
940        }
941    }
942
943    #[test]
944    fn test_ladder_networks_forward_encoder() {
945        let ln = LadderNetworks::new();
946        let weights = ln.initialize_weights(2);
947        let x = array![[1.0, 2.0], [3.0, 4.0]];
948
949        let encoder_output = ln.forward_encoder(&x, &weights, false);
950
951        // Check that we have activations for each layer
952        assert_eq!(encoder_output.activations.len(), weights.layer_sizes.len());
953        assert_eq!(
954            encoder_output.noisy_activations.len(),
955            weights.layer_sizes.len()
956        );
957
958        // Check input layer
959        assert_eq!(encoder_output.activations[0].dim(), x.dim());
960    }
961
962    #[test]
963    fn test_ladder_networks_target_matrix() {
964        let ln = LadderNetworks::new();
965        let y = array![0, 1, -1, 0]; // -1 indicates unlabeled
966        let classes = vec![0, 1];
967
968        let targets = ln.create_target_matrix(&y, &classes);
969
970        // Check dimensions
971        assert_eq!(targets.dim(), (4, 2));
972
973        // Check labeled samples
974        assert_eq!(targets[[0, 0]], 1.0); // First sample, class 0
975        assert_eq!(targets[[0, 1]], 0.0);
976        assert_eq!(targets[[1, 0]], 0.0); // Second sample, class 1
977        assert_eq!(targets[[1, 1]], 1.0);
978
979        // Check unlabeled sample (should be all zeros)
980        assert_eq!(targets[[2, 0]], 0.0);
981        assert_eq!(targets[[2, 1]], 0.0);
982    }
983}