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#[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 pub fn new(input_dim: usize, output_dim: usize, activation: Option<Activation>) -> Self {
25 let mut rng = scirs2_core::random::thread_rng();
26
27 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 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 for mut row in output.rows_mut() {
55 row += &self.biases;
56 }
57
58 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#[derive(Debug, Clone)]
73pub struct SimpleMLP<T: FloatBounds + ScalarOperand> {
74 layers: Vec<DenseLayer<T>>,
75}
76
77impl<T: FloatBounds + ScalarOperand> SimpleMLP<T> {
78 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 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(¤t)?;
105 }
106
107 Ok(current)
108 }
109}
110
111#[derive(Debug, Clone)]
116#[allow(dead_code)] pub struct ContrastiveLearner<T: FloatBounds + ScalarOperand> {
118 encoder: SimpleMLP<T>,
120 projection_head: SimpleMLP<T>,
122 temperature: T,
124 embedding_dim: usize,
126}
127
128#[derive(Debug, Clone)]
130pub struct ContrastiveConfig<T: Float> {
131 pub temperature: T,
133 pub embedding_dim: usize,
135 pub num_negatives: usize,
137 pub augmentation_prob: T,
139 pub encoder_lr: T,
141 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 pub fn new(
161 input_dim: usize,
162 hidden_dims: Vec<usize>,
163 config: ContrastiveConfig<T>,
164 ) -> Result<Self, SklearsError> {
165 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 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 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 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 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 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 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 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 pub fn encode(&mut self, input: &Array2<T>) -> Result<Array2<T>, SklearsError> {
251 self.encoder.forward(input)
252 }
253}
254
255#[derive(Debug, Clone)]
259#[allow(dead_code)] pub struct SelfSupervisedAutoencoder<T: FloatBounds + ScalarOperand> {
261 encoder: SimpleMLP<T>,
263 decoder: SimpleMLP<T>,
265 latent_dim: usize,
267 autoencoder_type: AutoencoderType,
269 config: AutoencoderConfig<T>,
271}
272
273#[derive(Debug, Clone)]
275pub enum AutoencoderType {
276 Vanilla,
278 Denoising,
280 Sparse,
282}
283
284#[derive(Debug, Clone)]
286pub struct AutoencoderConfig<T: Float> {
287 pub latent_dim: usize,
289 pub noise_level: T,
291 pub sparsity_penalty: T,
293 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 pub fn new(
311 input_dim: usize,
312 hidden_dims: Vec<usize>,
313 config: AutoencoderConfig<T>,
314 ) -> Result<Self, SklearsError> {
315 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 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)) .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 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 pub fn decode(&mut self, latent: &Array2<T>) -> Result<Array2<T>, SklearsError> {
360 self.decoder.forward(latent)
361 }
362
363 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 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 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 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#[derive(Debug, Clone)]
409pub struct SelfSupervisedTrainer<T: FloatBounds + ScalarOperand> {
410 method: SelfSupervisedMethod<T>,
412 config: TrainingConfig<T>,
414}
415
416#[derive(Debug, Clone)]
418pub enum SelfSupervisedMethod<T: FloatBounds + ScalarOperand> {
419 Contrastive(ContrastiveLearner<T>),
421 Autoencoder(SelfSupervisedAutoencoder<T>),
423}
424
425#[derive(Debug, Clone)]
427pub struct TrainingConfig<T: Float> {
428 pub epochs: usize,
430 pub batch_size: usize,
432 pub learning_rate: T,
434 pub validation_split: T,
436 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 pub fn new(method: SelfSupervisedMethod<T>, config: TrainingConfig<T>) -> Self {
455 Self { method, config }
456 }
457
458 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 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 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}