Skip to main content

sklears_neural/
self_supervised.rs

1use scirs2_core::ndarray::{Array1, Array2, ScalarOperand};
2use scirs2_core::numeric::Float;
3use std::fmt::Debug;
4
5use crate::activation::Activation;
6use sklears_core::error::SklearsError;
7use sklears_core::types::FloatBounds;
8
9// Self-supervised learning methods for neural networks.
10// This module provides implementations of various self-supervised learning techniques
11// including contrastive learning, masked modeling, and autoencoding approaches.
12
13/// Simple Dense Layer for Self-Supervised Models
14#[derive(Debug, Clone)]
15pub struct DenseLayer<T: FloatBounds + ScalarOperand> {
16    weights: Array2<T>,
17    biases: Array1<T>,
18    activation: Option<Activation>,
19    last_input: Option<Array2<T>>,
20}
21
22impl<T: FloatBounds + ScalarOperand> DenseLayer<T> {
23    /// Create a new dense layer
24    pub fn new(input_dim: usize, output_dim: usize, activation: Option<Activation>) -> Self {
25        let mut rng = scirs2_core::random::thread_rng();
26
27        // Xavier initialization
28        let scale = T::from(2.0).unwrap_or_else(|| T::zero())
29            / T::from(input_dim + output_dim).unwrap_or_else(|| T::zero());
30        let std_dev = scale.sqrt();
31
32        let weights = Array2::from_shape_fn((input_dim, output_dim), |_| {
33            let val: f32 = rng.gen_range(-1.0..1.0);
34            T::from(val).unwrap_or_else(|| T::zero()) * std_dev
35        });
36
37        let biases = Array1::zeros(output_dim);
38
39        Self {
40            weights,
41            biases,
42            activation,
43            last_input: None,
44        }
45    }
46
47    /// Forward pass
48    pub fn forward(&mut self, input: &Array2<T>) -> Result<Array2<T>, SklearsError> {
49        self.last_input = Some(input.clone());
50
51        let mut output = input.dot(&self.weights);
52
53        // Add bias
54        for mut row in output.rows_mut() {
55            row += &self.biases;
56        }
57
58        // Apply activation
59        if let Some(ref activation) = self.activation {
60            for element in output.iter_mut() {
61                let x_f64 = element.to_f64().unwrap_or(0.0);
62                let result_f64 = activation.forward(x_f64);
63                *element = T::from(result_f64).unwrap_or_else(|| T::zero());
64            }
65        }
66
67        Ok(output)
68    }
69}
70
71/// Simple Multi-Layer Perceptron for Self-Supervised Learning
72#[derive(Debug, Clone)]
73pub struct SimpleMLP<T: FloatBounds + ScalarOperand> {
74    layers: Vec<DenseLayer<T>>,
75}
76
77impl<T: FloatBounds + ScalarOperand> SimpleMLP<T> {
78    /// Create a new MLP
79    pub fn new(layer_sizes: &[usize], activations: &[Option<Activation>]) -> Self {
80        let mut layers = Vec::new();
81
82        for i in 0..layer_sizes.len() - 1 {
83            let activation = if i < activations.len() {
84                activations[i]
85            } else {
86                None
87            };
88
89            layers.push(DenseLayer::new(
90                layer_sizes[i],
91                layer_sizes[i + 1],
92                activation,
93            ));
94        }
95
96        Self { layers }
97    }
98
99    /// Forward pass through all layers
100    pub fn forward(&mut self, input: &Array2<T>) -> Result<Array2<T>, SklearsError> {
101        let mut current = input.clone();
102
103        for layer in &mut self.layers {
104            current = layer.forward(&current)?;
105        }
106
107        Ok(current)
108    }
109}
110
111/// Contrastive Learning Framework
112///
113/// Implements SimCLR-style contrastive learning with configurable augmentations
114/// and temperature-scaled cross-entropy loss.
115#[derive(Debug, Clone)]
116#[allow(dead_code)] // embedding_dim retained for representation shape validation
117pub struct ContrastiveLearner<T: FloatBounds + ScalarOperand> {
118    /// Encoder network
119    encoder: SimpleMLP<T>,
120    /// Projection head for contrastive learning
121    projection_head: SimpleMLP<T>,
122    /// Temperature parameter for contrastive loss
123    temperature: T,
124    /// Embedding dimension
125    embedding_dim: usize,
126}
127
128/// Configuration for the contrastive learning algorithm
129#[derive(Debug, Clone)]
130pub struct ContrastiveConfig<T: Float> {
131    /// Temperature for contrastive loss
132    pub temperature: T,
133    /// Embedding dimension
134    pub embedding_dim: usize,
135    /// Number of negative samples
136    pub num_negatives: usize,
137    /// Augmentation probability
138    pub augmentation_prob: T,
139    /// Learning rate for encoder
140    pub encoder_lr: T,
141    /// Projection head learning rate
142    pub projection_lr: T,
143}
144
145impl<T: Float> Default for ContrastiveConfig<T> {
146    fn default() -> Self {
147        Self {
148            temperature: T::from(0.1).unwrap_or_else(|| T::zero()),
149            embedding_dim: 128,
150            num_negatives: 256,
151            augmentation_prob: T::from(0.5).unwrap_or_else(|| T::zero()),
152            encoder_lr: T::from(0.001).unwrap_or_else(|| T::zero()),
153            projection_lr: T::from(0.001).unwrap_or_else(|| T::zero()),
154        }
155    }
156}
157
158impl<T: FloatBounds + ScalarOperand + Debug> ContrastiveLearner<T> {
159    /// Create a new contrastive learner
160    pub fn new(
161        input_dim: usize,
162        hidden_dims: Vec<usize>,
163        config: ContrastiveConfig<T>,
164    ) -> Result<Self, SklearsError> {
165        // Build encoder network
166        let mut encoder_sizes = vec![input_dim];
167        encoder_sizes.extend_from_slice(&hidden_dims);
168
169        let encoder_activations: Vec<Option<Activation>> = (0..hidden_dims.len())
170            .map(|_| Some(Activation::Relu))
171            .collect();
172
173        let encoder = SimpleMLP::new(&encoder_sizes, &encoder_activations);
174
175        // Build projection head
176        let proj_sizes = vec![hidden_dims[hidden_dims.len() - 1], config.embedding_dim];
177        let proj_activations = vec![None];
178        let projection_head = SimpleMLP::new(&proj_sizes, &proj_activations);
179
180        Ok(Self {
181            encoder,
182            projection_head,
183            temperature: config.temperature,
184            embedding_dim: config.embedding_dim,
185        })
186    }
187
188    /// Compute contrastive loss
189    pub fn contrastive_loss(&self, embeddings: &Array2<T>) -> Result<T, SklearsError> {
190        let batch_size = embeddings.nrows();
191        let mut total_loss = T::zero();
192
193        for i in 0..batch_size {
194            let anchor = embeddings.row(i);
195            let mut negative_sims = Vec::new();
196
197            // Find positive pair (next sample in batch as positive)
198            let positive_idx = (i + 1) % batch_size;
199            let positive = embeddings.row(positive_idx);
200            let positive_sim = self.cosine_similarity(&anchor, &positive)?;
201
202            // Compute negative similarities
203            for j in 0..batch_size {
204                if j != i && j != positive_idx {
205                    let negative = embeddings.row(j);
206                    let neg_sim = self.cosine_similarity(&anchor, &negative)?;
207                    negative_sims.push(neg_sim);
208                }
209            }
210
211            // Compute contrastive loss
212            let pos_exp = (positive_sim / self.temperature).exp();
213            let neg_exp_sum: T = negative_sims
214                .iter()
215                .map(|&sim| (sim / self.temperature).exp())
216                .fold(T::zero(), |acc, x| acc + x);
217
218            let loss = -(pos_exp / (pos_exp + neg_exp_sum)).ln();
219            total_loss += loss;
220        }
221
222        Ok(total_loss / T::from(batch_size).unwrap_or_else(|| T::zero()))
223    }
224
225    /// Compute cosine similarity between two vectors
226    fn cosine_similarity(
227        &self,
228        a: &scirs2_core::ndarray::ArrayView1<T>,
229        b: &scirs2_core::ndarray::ArrayView1<T>,
230    ) -> Result<T, SklearsError> {
231        let dot_product = a.dot(b);
232        let norm_a = a.mapv(|x| x * x).sum().sqrt();
233        let norm_b = b.mapv(|x| x * x).sum().sqrt();
234
235        if norm_a == T::zero() || norm_b == T::zero() {
236            return Ok(T::zero());
237        }
238
239        Ok(dot_product / (norm_a * norm_b))
240    }
241
242    /// Forward pass through encoder and projection head
243    pub fn forward(&mut self, input: &Array2<T>) -> Result<Array2<T>, SklearsError> {
244        let encoded = self.encoder.forward(input)?;
245        let projected = self.projection_head.forward(&encoded)?;
246        Ok(projected)
247    }
248
249    /// Get encoder representations (without projection head)
250    pub fn encode(&mut self, input: &Array2<T>) -> Result<Array2<T>, SklearsError> {
251        self.encoder.forward(input)
252    }
253}
254
255/// Autoencoder for Self-Supervised Representation Learning
256///
257/// Implements simple autoencoder architecture for unsupervised feature learning.
258#[derive(Debug, Clone)]
259#[allow(dead_code)] // latent_dim retained for bottleneck description and future VAE-style sampling
260pub struct SelfSupervisedAutoencoder<T: FloatBounds + ScalarOperand> {
261    /// Encoder network
262    encoder: SimpleMLP<T>,
263    /// Decoder network
264    decoder: SimpleMLP<T>,
265    /// Latent dimension
266    latent_dim: usize,
267    /// Autoencoder type
268    autoencoder_type: AutoencoderType,
269    /// Configuration
270    config: AutoencoderConfig<T>,
271}
272
273/// Variant of self-supervised autoencoder reconstruction objective
274#[derive(Debug, Clone)]
275pub enum AutoencoderType {
276    /// Standard autoencoder minimizing reconstruction loss
277    Vanilla,
278    /// Autoencoder trained to reconstruct clean inputs from corrupted inputs
279    Denoising,
280    /// Autoencoder with a sparsity penalty on the latent activations
281    Sparse,
282}
283
284/// Configuration for the self-supervised autoencoder
285#[derive(Debug, Clone)]
286pub struct AutoencoderConfig<T: Float> {
287    /// Latent dimension
288    pub latent_dim: usize,
289    /// Noise level for denoising autoencoder
290    pub noise_level: T,
291    /// Sparsity penalty coefficient
292    pub sparsity_penalty: T,
293    /// Autoencoder type
294    pub autoencoder_type: AutoencoderType,
295}
296
297impl<T: Float> Default for AutoencoderConfig<T> {
298    fn default() -> Self {
299        Self {
300            latent_dim: 128,
301            noise_level: T::from(0.1).unwrap_or_else(|| T::zero()),
302            sparsity_penalty: T::from(0.01).unwrap_or_else(|| T::zero()),
303            autoencoder_type: AutoencoderType::Vanilla,
304        }
305    }
306}
307
308impl<T: FloatBounds + ScalarOperand + Debug> SelfSupervisedAutoencoder<T> {
309    /// Create a new self-supervised autoencoder
310    pub fn new(
311        input_dim: usize,
312        hidden_dims: Vec<usize>,
313        config: AutoencoderConfig<T>,
314    ) -> Result<Self, SklearsError> {
315        // Build encoder
316        let mut encoder_sizes = vec![input_dim];
317        encoder_sizes.extend_from_slice(&hidden_dims);
318        encoder_sizes.push(config.latent_dim);
319
320        let encoder_activations: Vec<Option<Activation>> = (0..encoder_sizes.len() - 1)
321            .map(|_| Some(Activation::Relu))
322            .collect();
323
324        let encoder = SimpleMLP::new(&encoder_sizes, &encoder_activations);
325
326        // Build decoder (reverse of encoder)
327        let mut decoder_sizes = vec![config.latent_dim];
328        decoder_sizes.extend(hidden_dims.iter().rev().cloned());
329        decoder_sizes.push(input_dim);
330
331        let decoder_activations: Vec<Option<Activation>> = (0..decoder_sizes.len() - 2)
332            .map(|_| Some(Activation::Relu))
333            .chain(std::iter::once(None)) // No activation for final layer
334            .collect();
335
336        let decoder = SimpleMLP::new(&decoder_sizes, &decoder_activations);
337
338        Ok(Self {
339            encoder,
340            decoder,
341            latent_dim: config.latent_dim,
342            autoencoder_type: config.autoencoder_type.clone(),
343            config,
344        })
345    }
346
347    /// Encode input to latent representation
348    pub fn encode(&mut self, input: &Array2<T>) -> Result<Array2<T>, SklearsError> {
349        match self.autoencoder_type {
350            AutoencoderType::Denoising => {
351                let noisy_input = self.add_noise(input)?;
352                self.encoder.forward(&noisy_input)
353            }
354            _ => self.encoder.forward(input),
355        }
356    }
357
358    /// Decode latent representation to output
359    pub fn decode(&mut self, latent: &Array2<T>) -> Result<Array2<T>, SklearsError> {
360        self.decoder.forward(latent)
361    }
362
363    /// Forward pass through autoencoder
364    pub fn forward(&mut self, input: &Array2<T>) -> Result<Array2<T>, SklearsError> {
365        let encoded = self.encode(input)?;
366        let decoded = self.decode(&encoded)?;
367        Ok(decoded)
368    }
369
370    /// Add noise for denoising autoencoder
371    fn add_noise(&self, input: &Array2<T>) -> Result<Array2<T>, SklearsError> {
372        let mut rng = scirs2_core::random::thread_rng();
373        let mut noisy_input = input.clone();
374
375        for element in noisy_input.iter_mut() {
376            let noise =
377                T::from(rng.random::<f32>()).unwrap_or_else(|| T::zero()) * self.config.noise_level;
378            *element += noise;
379        }
380
381        Ok(noisy_input)
382    }
383
384    /// Compute reconstruction loss
385    pub fn reconstruction_loss(
386        &self,
387        input: &Array2<T>,
388        output: &Array2<T>,
389    ) -> Result<T, SklearsError> {
390        let diff = input - output;
391        let mse = diff
392            .mapv(|x| x * x)
393            .mean()
394            .expect("mean should not fail on non-empty array");
395        Ok(mse)
396    }
397
398    /// Compute sparsity penalty
399    pub fn sparsity_penalty(&self, latent: &Array2<T>) -> Result<T, SklearsError> {
400        let l1_norm = latent.mapv(|x| x.abs()).sum();
401        Ok(self.config.sparsity_penalty * l1_norm)
402    }
403}
404
405/// Self-supervised learning trainer
406///
407/// Combines different self-supervised methods with a unified training interface.
408#[derive(Debug, Clone)]
409pub struct SelfSupervisedTrainer<T: FloatBounds + ScalarOperand> {
410    /// Learning method
411    method: SelfSupervisedMethod<T>,
412    /// Training configuration
413    config: TrainingConfig<T>,
414}
415
416/// The self-supervised learning algorithm to use inside [`SelfSupervisedTrainer`]
417#[derive(Debug, Clone)]
418pub enum SelfSupervisedMethod<T: FloatBounds + ScalarOperand> {
419    /// Contrastive learning using positive/negative pair discrimination
420    Contrastive(ContrastiveLearner<T>),
421    /// Reconstruction-based learning via an autoencoder
422    Autoencoder(SelfSupervisedAutoencoder<T>),
423}
424
425/// Training loop configuration for self-supervised learning
426#[derive(Debug, Clone)]
427pub struct TrainingConfig<T: Float> {
428    /// Number of epochs
429    pub epochs: usize,
430    /// Batch size
431    pub batch_size: usize,
432    /// Learning rate
433    pub learning_rate: T,
434    /// Validation split
435    pub validation_split: T,
436    /// Early stopping patience
437    pub patience: usize,
438}
439
440impl<T: Float> Default for TrainingConfig<T> {
441    fn default() -> Self {
442        Self {
443            epochs: 100,
444            batch_size: 32,
445            learning_rate: T::from(0.001).unwrap_or_else(|| T::zero()),
446            validation_split: T::from(0.1).unwrap_or_else(|| T::zero()),
447            patience: 10,
448        }
449    }
450}
451
452impl<T: FloatBounds + ScalarOperand + Debug + std::iter::Sum> SelfSupervisedTrainer<T> {
453    /// Create a new self-supervised trainer
454    pub fn new(method: SelfSupervisedMethod<T>, config: TrainingConfig<T>) -> Self {
455        Self { method, config }
456    }
457
458    /// Train the self-supervised model
459    pub fn fit(&mut self, data: &Array2<T>) -> Result<Vec<T>, SklearsError> {
460        let mut losses = Vec::new();
461
462        for _epoch in 0..self.config.epochs {
463            let epoch_loss = match &mut self.method {
464                SelfSupervisedMethod::Contrastive(learner) => {
465                    let embeddings = learner.forward(data)?;
466                    learner.contrastive_loss(&embeddings)?
467                }
468                SelfSupervisedMethod::Autoencoder(autoencoder) => {
469                    let output = autoencoder.forward(data)?;
470                    autoencoder.reconstruction_loss(data, &output)?
471                }
472            };
473
474            losses.push(epoch_loss);
475
476            // Simple convergence check
477            if losses.len() > 10 {
478                let recent_avg = losses[losses.len() - 10..].iter().cloned().sum::<T>()
479                    / T::from(10.0).unwrap_or_else(|| T::zero());
480                if epoch_loss < recent_avg * T::from(0.001).unwrap_or_else(|| T::zero()) {
481                    break;
482                }
483            }
484        }
485
486        Ok(losses)
487    }
488
489    /// Transform data using the trained model
490    pub fn transform(&mut self, data: &Array2<T>) -> Result<Array2<T>, SklearsError> {
491        match &mut self.method {
492            SelfSupervisedMethod::Contrastive(learner) => learner.encode(data),
493            SelfSupervisedMethod::Autoencoder(autoencoder) => autoencoder.encode(data),
494        }
495    }
496}
497
498#[allow(non_snake_case)]
499#[cfg(test)]
500mod tests {
501    use super::*;
502    use approx::assert_abs_diff_eq;
503
504    #[test]
505    fn test_dense_layer_creation() {
506        let layer = DenseLayer::<f32>::new(10, 5, Some(Activation::Relu));
507        assert_eq!(layer.weights.dim(), (10, 5));
508        assert_eq!(layer.biases.len(), 5);
509    }
510
511    #[test]
512    fn test_simple_mlp_creation() {
513        let mlp = SimpleMLP::<f32>::new(&[10, 8, 5], &[Some(Activation::Relu), None]);
514        assert_eq!(mlp.layers.len(), 2);
515    }
516
517    #[test]
518    fn test_contrastive_learner_creation() {
519        let config = ContrastiveConfig::default();
520        let learner = ContrastiveLearner::<f32>::new(100, vec![64, 32], config);
521        assert!(learner.is_ok());
522    }
523
524    #[test]
525    fn test_contrastive_loss_computation() {
526        let config = ContrastiveConfig::default();
527        let learner = ContrastiveLearner::<f32>::new(10, vec![8], config)
528            .expect("construction should succeed");
529
530        let embeddings =
531            Array2::from_shape_vec((4, 128), vec![0.0; 4 * 128]).expect("array shape mismatch");
532
533        let loss = learner.contrastive_loss(&embeddings);
534        assert!(loss.is_ok());
535    }
536
537    #[test]
538    fn test_autoencoder_creation() {
539        let config = AutoencoderConfig::default();
540        let autoencoder = SelfSupervisedAutoencoder::<f32>::new(100, vec![64, 32], config);
541        assert!(autoencoder.is_ok());
542    }
543
544    #[test]
545    fn test_autoencoder_forward_pass() {
546        let config = AutoencoderConfig::default();
547        let mut autoencoder = SelfSupervisedAutoencoder::<f32>::new(10, vec![8], config)
548            .expect("construction should succeed");
549
550        let input = Array2::from_shape_vec((2, 10), (0..20).map(|x| x as f32).collect())
551            .expect("array shape mismatch");
552        let output = autoencoder.forward(&input);
553
554        assert!(output.is_ok());
555        assert_eq!(output.expect("operation should succeed").dim(), input.dim());
556    }
557
558    #[test]
559    fn test_self_supervised_trainer() {
560        let config = ContrastiveConfig::default();
561        let learner = ContrastiveLearner::<f32>::new(10, vec![8], config)
562            .expect("construction should succeed");
563        let method = SelfSupervisedMethod::Contrastive(learner);
564
565        let training_config = TrainingConfig {
566            epochs: 5,
567            batch_size: 4,
568            learning_rate: 0.01,
569            validation_split: 0.2,
570            patience: 3,
571        };
572
573        let mut trainer = SelfSupervisedTrainer::new(method, training_config);
574        let data = Array2::from_shape_vec((4, 10), (0..40).map(|x| x as f32).collect())
575            .expect("array shape mismatch");
576
577        let losses = trainer.fit(&data);
578        assert!(losses.is_ok());
579        assert!(losses.expect("operation should succeed").len() <= 5);
580    }
581
582    #[test]
583    fn test_cosine_similarity() {
584        let config = ContrastiveConfig::default();
585        let learner = ContrastiveLearner::<f32>::new(10, vec![8], config)
586            .expect("construction should succeed");
587
588        let a = Array1::from_vec(vec![1.0, 0.0, 0.0]);
589        let b = Array1::from_vec(vec![0.0, 1.0, 0.0]);
590        let c = Array1::from_vec(vec![1.0, 0.0, 0.0]);
591
592        let sim_ab = learner
593            .cosine_similarity(&a.view(), &b.view())
594            .expect("operation should succeed");
595        let sim_ac = learner
596            .cosine_similarity(&a.view(), &c.view())
597            .expect("operation should succeed");
598
599        assert_abs_diff_eq!(sim_ab, 0.0, epsilon = 1e-6);
600        assert_abs_diff_eq!(sim_ac, 1.0, epsilon = 1e-6);
601    }
602
603    #[test]
604    fn test_reconstruction_loss() {
605        let config = AutoencoderConfig::default();
606        let autoencoder = SelfSupervisedAutoencoder::<f32>::new(10, vec![8], config)
607            .expect("construction should succeed");
608
609        let input = Array2::from_shape_vec((2, 10), (0..20).map(|x| x as f32).collect())
610            .expect("array shape mismatch");
611        let output = input.clone();
612
613        let loss = autoencoder
614            .reconstruction_loss(&input, &output)
615            .expect("operation should succeed");
616        assert_abs_diff_eq!(loss, 0.0, epsilon = 1e-6);
617    }
618}