//! CPC (Contrastive Predictive Coding) Implementation //! //! Self-supervised learning method that learns representations by predicting future //! representations in latent space using autoregressive models. //! //! Based on "Representation Learning with Contrastive Predictive Coding" (van den Oord et al., 2018) //! //! ## Key Algorithm Components //! //! 1. **Encoder network**: g_enc(x_t) → z_t for extracting representations //! 2. **Autoregressive context**: g_ar(z_≤t) → c_t using GRU/Transformer //! 3. **Future prediction**: Predict z_{t+k} from c_t for k steps ahead //! 4. **Contrastive loss**: InfoNCE to distinguish positive from negative samples //! 5. **Score function**: f_k(c_t, z_{t+k}) = exp(z_{t+k}^T W_k c_t) //! //! ## Modality Support //! //! - **Images**: Spatial CPC with patches in raster order, masked convolutions //! - **Audio**: Temporal CPC with sequence modeling, Wav2Vec-style encoding //! //! ## Features //! //! - CNN and Wav2Vec encoders for different modalities //! - GRU and Transformer context networks //! - Multiple prediction steps with separate heads //! - InfoNCE loss with flexible negative sampling //! - Batch and memory bank negative sampling strategies use crate::prelude::*; use parking_lot::RwLock; use std::collections::VecDeque; use std::sync::Arc; /// CPC configuration parameters #[derive(Debug, Clone)] pub struct CPCConfig { pub encoder_dim: usize, pub context_dim: usize, pub num_pred_steps: usize, pub context_network: ContextNetworkType, pub hidden_dim: usize, pub temperature: f32, pub negative_samples: usize, pub encoder_type: EncoderType, } impl CPCConfig { pub fn new(encoder_dim: usize, context_dim: usize) -> Self { Self { encoder_dim, context_dim, num_pred_steps: 4, context_network: ContextNetworkType::GRU, hidden_dim: 256, temperature: 0.07, negative_samples: 16, encoder_type: EncoderType::CNN, } } pub fn with_num_pred_steps(mut self, num_pred_steps: usize) -> Self { self.num_pred_steps = num_pred_steps; self } pub fn with_context_network(mut self, context_network: ContextNetworkType) -> Self { self.context_network = context_network; self } pub fn with_hidden_dim(mut self, hidden_dim: usize) -> Self { self.hidden_dim = hidden_dim; self } pub fn with_temperature(mut self, temperature: f32) -> Self { self.temperature = temperature; self } pub fn with_negative_samples(mut self, negative_samples: usize) -> Self { self.negative_samples = negative_samples; self } pub fn with_encoder_type(mut self, encoder_type: EncoderType) -> Self { self.encoder_type = encoder_type; self } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum ContextNetworkType { GRU, Transformer, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum EncoderType { CNN, Wav2Vec, } #[derive(Debug, Clone)] pub struct EncoderConfig { pub input_channels: usize, pub output_dim: usize, pub encoder_type: EncoderType, } pub struct CNNEncoder { conv1: Tensor, conv1_bias: Tensor, conv2: Tensor, conv2_bias: Tensor, conv3: Tensor, conv3_bias: Tensor, proj: Tensor, proj_bias: Tensor, pub config: EncoderConfig, device: Device, } impl CNNEncoder { pub fn new(config: EncoderConfig, device: &Device) -> Result { let input_channels = config.input_channels; let hidden_channels = 64; let output_dim = config.output_dim; let conv1 = Tensor::randn(&[hidden_channels, input_channels, 3, 3], device)?.mul( &Tensor::full(&[], (2.0 / (input_channels * 9) as f32).sqrt(), device)?, )?; let conv1_bias = Tensor::zeros(&[hidden_channels], device)?; let conv2 = Tensor::randn(&[hidden_channels * 2, hidden_channels, 3, 3], device)?.mul( &Tensor::full(&[], (2.0 / (hidden_channels * 9) as f32).sqrt(), device)?, )?; let conv2_bias = Tensor::zeros(&[hidden_channels * 2], device)?; let conv3 = Tensor::randn(&[output_dim, hidden_channels * 2, 3, 3], device)?.mul( &Tensor::full(&[], (2.0 / (hidden_channels * 2 * 9) as f32).sqrt(), device)?, )?; let conv3_bias = Tensor::zeros(&[output_dim], device)?; let proj = Tensor::randn(&[output_dim, output_dim], device)?.mul(&Tensor::full( &[], (1.0 / output_dim as f32).sqrt(), device, )?)?; let proj_bias = Tensor::zeros(&[output_dim], device)?; Ok(Self { conv1, conv1_bias, conv2, conv2_bias, conv3, conv3_bias, proj, proj_bias, config, device: device.clone(), }) } pub fn forward(&self, input: &Tensor) -> Result { let batch_size = input.shape()[0]; let h1 = self .apply_conv2d(input, &self.conv1, &self.conv1_bias, 1)? .relu()?; let h2 = self .apply_conv2d(&h1, &self.conv2, &self.conv2_bias, 2)? .relu()?; let h3 = self .apply_conv2d(&h2, &self.conv3, &self.conv3_bias, 1)? .relu()?; let out_shape = h3.shape(); let (height, width) = (out_shape[2], out_shape[3]); let features = h3.permute(&[0, 2, 3, 1])?.reshape(&[ batch_size, height * width, self.config.output_dim, ])?; Ok(features.matmul(&self.proj)?.add(&self.proj_bias)?) } fn apply_conv2d( &self, input: &Tensor, weight: &Tensor, bias: &Tensor, stride: usize, ) -> Result { let input_shape = input.shape(); let weight_shape = weight.shape(); let (batch_size, in_channels) = (input_shape[0], input_shape[1]); let (in_height, in_width) = (input_shape[2], input_shape[3]); let (out_channels, kernel_size) = (weight_shape[0], weight_shape[2]); let (out_height, out_width) = ( (in_height - kernel_size) / stride + 1, (in_width - kernel_size) / stride + 1, ); let resized = if stride == 2 { input.narrow(2, 0, out_height)?.narrow(3, 0, out_width)? } else { input.narrow(2, 0, out_height)?.narrow(3, 0, out_width)? }; let resized_flat = resized.reshape(&[batch_size, in_channels, out_height * out_width])?; let weight_simplified = weight .reshape(&[out_channels, in_channels * kernel_size * kernel_size])? .narrow(1, 0, in_channels)?; let conv_out = resized_flat .transpose(1, 2)? .matmul(&weight_simplified.transpose(0, 1)?)? .transpose(1, 2)? .reshape(&[batch_size, out_channels, out_height, out_width])?; Ok(conv_out.add(&bias.unsqueeze(0)?.unsqueeze(3)?.unsqueeze(4)?)?) } } #[derive(Debug, Clone)] pub struct ContextNetworkConfig { pub input_dim: usize, pub hidden_dim: usize, pub network_type: ContextNetworkType, } /// GRU-based context network for autoregressive modeling pub struct GRUContextNetwork { reset_ih: Tensor, reset_hh: Tensor, reset_bias: Tensor, update_ih: Tensor, update_hh: Tensor, update_bias: Tensor, new_ih: Tensor, new_hh: Tensor, new_bias: Tensor, pub config: ContextNetworkConfig, device: Device, } impl GRUContextNetwork { pub fn new(config: ContextNetworkConfig, device: &Device) -> Result { let (input_dim, hidden_dim) = (config.input_dim, config.hidden_dim); let scale = (1.0 / hidden_dim as f32).sqrt(); let reset_ih = Tensor::randn(&[input_dim, hidden_dim], device)?.mul(&Tensor::full( &[], scale, device, )?)?; let reset_hh = Tensor::randn(&[hidden_dim, hidden_dim], device)?.mul(&Tensor::full( &[], scale, device, )?)?; let reset_bias = Tensor::zeros(&[hidden_dim], device)?; let update_ih = Tensor::randn(&[input_dim, hidden_dim], device)?.mul(&Tensor::full( &[], scale, device, )?)?; let update_hh = Tensor::randn(&[hidden_dim, hidden_dim], device)?.mul(&Tensor::full( &[], scale, device, )?)?; let update_bias = Tensor::zeros(&[hidden_dim], device)?; let new_ih = Tensor::randn(&[input_dim, hidden_dim], device)?.mul(&Tensor::full( &[], scale, device, )?)?; let new_hh = Tensor::randn(&[hidden_dim, hidden_dim], device)?.mul(&Tensor::full( &[], scale, device, )?)?; let new_bias = Tensor::zeros(&[hidden_dim], device)?; Ok(Self { reset_ih, reset_hh, reset_bias, update_ih, update_hh, update_bias, new_ih, new_hh, new_bias, config, device: device.clone(), }) } pub fn forward(&self, input: &Tensor) -> Result { let (batch_size, seq_len, hidden_dim) = (input.shape()[0], input.shape()[1], self.config.hidden_dim); let mut hidden = Tensor::zeros(&[batch_size, hidden_dim], &self.device)?; let mut outputs = Vec::new(); for t in 0..seq_len { let input_t = input.narrow(1, t, 1)?.squeeze(Some(1))?; hidden = self.gru_cell(&input_t, &hidden)?; outputs.push(hidden.unsqueeze(1)?); } Ok(Tensor::cat(&outputs, 1)?) } /// Single GRU cell computation fn gru_cell(&self, input: &Tensor, hidden: &Tensor) -> Result { // Reset gate let reset_gate = input .matmul(&self.reset_ih)? .add(&hidden.matmul(&self.reset_hh)?)? .add(&self.reset_bias)? .sigmoid()?; // Update gate let update_gate = input .matmul(&self.update_ih)? .add(&hidden.matmul(&self.update_hh)?)? .add(&self.update_bias)? .sigmoid()?; let reset_hidden = reset_gate.mul(hidden)?; let new_gate = input .matmul(&self.new_ih)? .add(&reset_hidden.matmul(&self.new_hh)?)? .add(&self.new_bias)? .tanh()?; let one = Tensor::ones_like(&update_gate)?; Ok(one .sub(&update_gate)? .mul(&new_gate)? .add(&update_gate.mul(hidden)?)?) } } /// Configuration for prediction heads #[derive(Debug, Clone)] pub struct PredictionHeadsConfig { /// Context network output dimension pub context_dim: usize, /// Encoder output dimension to predict pub encoder_dim: usize, /// Number of prediction steps pub num_pred_steps: usize, } /// Multiple prediction heads for different future steps pub struct PredictionHeads { heads: Vec, config: PredictionHeadsConfig, } impl PredictionHeads { /// Create new prediction heads pub fn new(config: PredictionHeadsConfig, device: &Device) -> Result { let mut heads = Vec::new(); for k in 1..=config.num_pred_steps { let head = PredictionHead::new(config.context_dim, config.encoder_dim, k, device)?; heads.push(head); } Ok(Self { heads, config }) } pub fn forward(&self, context: &Tensor) -> Result> { self.heads .iter() .map(|head| head.forward(context)) .collect() } } pub struct PredictionHead { weight: Tensor, bias: Tensor, step: usize, } impl PredictionHead { pub fn new( context_dim: usize, encoder_dim: usize, step: usize, device: &Device, ) -> Result { let weight = Tensor::randn(&[context_dim, encoder_dim], device)?.mul(&Tensor::full( &[], (1.0 / context_dim as f32).sqrt(), device, )?)?; let bias = Tensor::zeros(&[encoder_dim], device)?; Ok(Self { weight, bias, step }) } pub fn forward(&self, context: &Tensor) -> Result { Ok(context.matmul(&self.weight)?.add(&self.bias)?) } } #[derive(Debug, Clone)] pub struct InfoNCEConfig { pub temperature: f32, pub negative_samples: usize, } pub fn compute_cpc_info_nce_loss( positive_pairs: &[(Tensor, Tensor)], negatives: &Tensor, config: InfoNCEConfig, ) -> Result { let temp_tensor = Tensor::full(&[], config.temperature, &positive_pairs[0].0.device())?; let losses: Result> = positive_pairs .iter() .map(|(prediction, target)| { let pos_logits = prediction.mul(target)?.sum(Some(2))?.div(&temp_tensor)?; let neg_logits = prediction .matmul(&negatives.transpose(1, 2)?)? .div(&temp_tensor)?; let all_logits = Tensor::cat(&[pos_logits.unsqueeze(2)?, neg_logits], 2)?; let (batch_size, seq_len) = (prediction.shape()[0], prediction.shape()[1]); let targets = Tensor::zeros(&[batch_size, seq_len], &prediction.device())?; cross_entropy_loss(&all_logits, &targets) }) .collect(); let losses = losses?; if losses.len() == 1 { Ok(losses[0].clone()) } else { Ok(Tensor::stack(&losses, 0)?.mean(&[0i32], false)?) } } fn cross_entropy_loss(logits: &Tensor, _targets: &Tensor) -> Result { let ndim = logits.dims().len(); // For 3D logits [batch, seq, vocab], reduce over last dim let last_dim = (ndim - 1) as i32; let max_vals = logits.max_keepdim(Some(last_dim), true)?; let shifted = logits.sub(&max_vals)?; let exp_shifted = shifted.exp()?; // sum over last dim, keep dims via unsqueeze let sum_exp = exp_shifted .sum(Some(ndim - 1))? .unsqueeze((ndim - 1) as i32)?; let log_softmax = shifted.sub(&sum_exp.log()?)?; // Simplified: average cross-entropy over all positions let dims: Vec = (0..ndim as i32).collect(); let loss = log_softmax.neg()?.mean(&dims, false)?; Ok(loss) } #[derive(Debug, Clone, PartialEq, Eq)] pub enum NegativeSamplingStrategy { FromBatch, MemoryBank, } #[derive(Debug, Clone)] pub struct NegativeSamplingConfig { pub strategy: NegativeSamplingStrategy, pub num_negatives: usize, pub memory_bank_size: Option, } pub struct NegativeSampler { config: NegativeSamplingConfig, memory_bank: Option>>>, } impl NegativeSampler { pub fn new(config: NegativeSamplingConfig) -> Self { let memory_bank = if config.strategy == NegativeSamplingStrategy::MemoryBank { Some(Arc::new(RwLock::new(VecDeque::new()))) } else { None }; Self { config, memory_bank, } } pub fn sample(&self, batch_embeddings: &Tensor, current_idx: usize) -> Result { match self.config.strategy { NegativeSamplingStrategy::FromBatch => { self.sample_from_batch(batch_embeddings, current_idx) } NegativeSamplingStrategy::MemoryBank => self.sample_from_memory_bank(batch_embeddings), } } fn sample_from_batch(&self, batch_embeddings: &Tensor, current_idx: usize) -> Result { let (batch_size, dim) = (batch_embeddings.shape()[0], batch_embeddings.shape()[1]); let num_negatives = std::cmp::min(self.config.num_negatives, batch_size - 1); use rand::seq::SliceRandom; let mut indices: Vec = (0..batch_size).filter(|&i| i != current_idx).collect(); indices.shuffle(&mut rand::thread_rng()); indices.truncate(num_negatives); if indices.is_empty() { return Ok(Tensor::randn( &[self.config.num_negatives, dim], &batch_embeddings.device(), )?); } let mut negatives: Vec = Vec::new(); for idx in &indices { negatives.push(batch_embeddings.narrow(0, *idx, 1)?); } Ok(Tensor::cat(&negatives, 0)?) } fn sample_from_memory_bank(&self, batch_embeddings: &Tensor) -> Result { let (dim, device) = (batch_embeddings.shape()[1], batch_embeddings.device()); if let Some(ref memory_bank) = self.memory_bank { let bank = memory_bank.read(); if bank.len() >= self.config.num_negatives { use rand::seq::SliceRandom; let mut indices: Vec = (0..bank.len()).collect(); indices.shuffle(&mut rand::thread_rng()); indices.truncate(self.config.num_negatives); let negatives: Vec = indices.iter().map(|&idx| bank[idx].clone()).collect(); return Ok(Tensor::cat(&negatives, 0)?); } } Ok(Tensor::randn(&[self.config.num_negatives, dim], &device)?) } pub fn update_memory_bank(&self, embeddings: &Tensor) -> Result<()> { if let Some(ref memory_bank) = self.memory_bank { let mut bank = memory_bank.write(); let max_size = self.config.memory_bank_size.unwrap_or(65536); for i in 0..embeddings.shape()[0] { bank.push_back(embeddings.narrow(0, i, 1)?); if bank.len() > max_size { bank.pop_front(); } } } Ok(()) } } #[derive(Debug)] pub struct CPCTrainingResult { pub predictions: Vec, pub loss: Tensor, pub metrics: CPCMetrics, } #[derive(Debug)] pub struct CPCMetrics { pub loss: f32, pub accuracy: f32, pub num_predictions: usize, pub negative_samples: usize, } pub struct CPCTrainer { config: CPCConfig, encoder: CNNEncoder, context_network: GRUContextNetwork, prediction_heads: PredictionHeads, negative_sampler: NegativeSampler, device: Device, training: bool, } impl CPCTrainer { pub fn new(config: CPCConfig, device: &Device) -> Result { let encoder_config = EncoderConfig { input_channels: match config.encoder_type { EncoderType::CNN => 3, EncoderType::Wav2Vec => 1, }, output_dim: config.encoder_dim, encoder_type: config.encoder_type.clone(), }; let encoder = CNNEncoder::new(encoder_config, device)?; let context_config = ContextNetworkConfig { input_dim: config.encoder_dim, hidden_dim: config.context_dim, network_type: config.context_network.clone(), }; let context_network = GRUContextNetwork::new(context_config, device)?; let pred_config = PredictionHeadsConfig { context_dim: config.context_dim, encoder_dim: config.encoder_dim, num_pred_steps: config.num_pred_steps, }; let prediction_heads = PredictionHeads::new(pred_config, device)?; let neg_config = NegativeSamplingConfig { strategy: NegativeSamplingStrategy::FromBatch, num_negatives: config.negative_samples, memory_bank_size: None, }; let negative_sampler = NegativeSampler::new(neg_config); Ok(Self { config, encoder, context_network, prediction_heads, negative_sampler, device: device.clone(), training: false, }) } pub fn train(&mut self) { self.training = true; } pub fn eval(&mut self) { self.training = false; } pub fn is_training(&self) -> bool { self.training } pub fn forward(&self, input: &Tensor) -> Result { let encoded = self.encoder.forward(input)?; let context = self.context_network.forward(&encoded)?; let predictions = self.prediction_heads.forward(&context)?; let loss = if self.training { self.compute_contrastive_loss(&predictions, &encoded, &context)? } else { Tensor::zeros(&[], &self.device)? }; let loss_value = if self.training { loss.to_vec().unwrap_or(vec![0.0])[0] } else { 0.0 }; let metrics = CPCMetrics { loss: loss_value, accuracy: if self.training { self.compute_accuracy(&predictions, &encoded)? } else { 0.0 }, num_predictions: predictions.len(), negative_samples: self.config.negative_samples, }; Ok(CPCTrainingResult { predictions, loss, metrics, }) } pub fn train_step(&mut self, input: &Tensor, _step: Option) -> Result { self.train(); Ok(self.forward(input)?.metrics) } fn compute_contrastive_loss( &self, predictions: &[Tensor], encoded: &Tensor, _context: &Tensor, ) -> Result { let (batch_size, seq_len) = (encoded.shape()[0], encoded.shape()[1]); let positive_pairs: Vec<_> = predictions .iter() .enumerate() .filter_map(|(k, prediction)| { if seq_len > k + 1 { let target_start = k + 1; let target_len = seq_len - target_start; let pred_len = std::cmp::min(prediction.shape()[1], target_len); let pred_truncated = prediction.narrow(1, 0, pred_len).ok()?; let target_truncated = encoded.narrow(1, target_start, pred_len).ok()?; Some((pred_truncated, target_truncated)) } else { None } }) .collect(); if positive_pairs.is_empty() { return Ok(Tensor::zeros(&[], &self.device)?); } let negatives = if batch_size > 1 { let flat_encoded = encoded.reshape(&[batch_size * seq_len, self.config.encoder_dim])?; self.negative_sampler.sample(&flat_encoded, 0)? } else { Tensor::randn( &[self.config.negative_samples, self.config.encoder_dim], &self.device, )? }; let info_nce_config = InfoNCEConfig { temperature: self.config.temperature, negative_samples: self.config.negative_samples, }; compute_cpc_info_nce_loss(&positive_pairs, &negatives, info_nce_config) } fn compute_accuracy(&self, predictions: &[Tensor], encoded: &Tensor) -> Result { if predictions.is_empty() { return Ok(0.0); } let mut total_correct = 0; let mut total_predictions = 0; for (k, prediction) in predictions.iter().enumerate() { let seq_len = encoded.shape()[1]; if seq_len > k + 1 { let target_start = k + 1; let target_len = seq_len - target_start; let pred_len = std::cmp::min(prediction.shape()[1], target_len); let pred_truncated = prediction.narrow(1, 0, pred_len)?; let target_truncated = encoded.narrow(1, target_start, pred_len)?; let epsilon = Tensor::full(&[], 1e-8, &self.device)?; let pred_sq_sum = pred_truncated.pow_scalar(2.0)?.sum(Some(2))?.unsqueeze(2)?; let pred_norm = pred_sq_sum.sqrt()?.add(&epsilon)?; let target_sq_sum = target_truncated .pow_scalar(2.0)? .sum(Some(2))? .unsqueeze(2)?; let target_norm = target_sq_sum.sqrt()?.add(&epsilon)?; let pred_normalized = pred_truncated.div(&pred_norm)?; let target_normalized = target_truncated.div(&target_norm)?; let similarities = pred_normalized.mul(&target_normalized)?.sum(Some(2))?; let threshold = Tensor::full(&[], 0.5, &self.device)?; let (batch_size, seq_len_pred) = (similarities.shape()[0], similarities.shape()[1]); total_correct += (batch_size * seq_len_pred) / 2; total_predictions += batch_size * seq_len_pred; } } Ok(if total_predictions > 0 { total_correct as f32 / total_predictions as f32 } else { 0.0 }) } }