//! Memory-Augmented Neural Networks (MANN) Implementation //! //! MANN extends neural networks with external memory for few-shot learning. //! Key features: //! - External memory matrix for storing past experiences //! - Content-based addressing using cosine similarity //! - LRU (Least Recently Used) memory management //! - Integration with N-way K-shot learning tasks //! - One-shot learning capability through memory use super::*; use rtx_tensor::{Tensor, Device, DType}; use rtx_autograd::{backward, clear_tape, NodeId}; use std::collections::{HashMap, VecDeque}; /// Configuration for Memory-Augmented Neural Networks #[derive(Debug, Clone)] pub struct MANNConfig { /// Size of external memory matrix (number of memory slots) pub memory_size: usize, /// Dimension of each memory slot pub memory_dim: usize, /// Learning rate for controller network pub learning_rate: f32, /// Focus parameter for content-based addressing (higher = more focused) pub focus_parameter: f32, /// Input feature dimension pub input_dim: usize, /// Output dimension (number of classes) pub output_dim: usize, /// Controller network hidden dimension pub controller_hidden_dim: usize, } impl Default for MANNConfig { fn default() -> Self { Self { memory_size: 128, memory_dim: 40, learning_rate: 0.001, focus_parameter: 1.0, input_dim: 784, output_dim: 5, controller_hidden_dim: 200, } } } /// Statistics for MANN training and evaluation #[derive(Debug, Clone, Default)] pub struct MANNStats { /// Number of episodes processed pub episodes_processed: usize, /// Average memory usage across episodes pub avg_memory_usage: f32, /// Number of memory reads performed pub memory_reads: usize, /// Number of memory writes performed pub memory_writes: usize, /// Average accuracy on query sets pub avg_accuracy: f32, /// Memory slots currently in use pub memory_slots_used: usize, } /// Memory-Augmented Neural Network for Few-Shot Learning #[derive(Debug)] pub struct MANN { /// External memory matrix (memory_size × memory_dim) memory: Tensor, /// Memory usage tracking (LRU order) memory_usage: VecDeque, /// Controller network weights controller: ControllerNetwork, /// Configuration config: MANNConfig, /// Training statistics stats: MANNStats, /// Device for computations device: Device, } /// Controller network for memory operations #[derive(Debug, Clone)] pub struct ControllerNetwork { /// Input to hidden layer weights pub input_hidden: Tensor, /// Hidden layer bias pub hidden_bias: Tensor, /// Hidden to output weights (for classification) pub hidden_output: Tensor, /// Output bias pub output_bias: Tensor, /// Key generation weights (for addressing) pub hidden_key: Tensor, /// Key bias pub key_bias: Tensor, } /// Memory addressing weights and operations #[derive(Debug, Clone)] pub struct MemoryAddressing { /// Read weights for each memory location pub read_weights: Tensor, /// Write weights for each memory location pub write_weights: Tensor, /// Key vector for content-based addressing pub key: Tensor, } /// Result of memory read operation #[derive(Debug, Clone)] pub struct MemoryReadResult { /// Read vector from memory pub read_vector: Tensor, /// Read weights used pub read_weights: Tensor, } /// Result of memory write operation #[derive(Debug, Clone)] pub struct MemoryWriteResult { /// Updated memory matrix pub updated_memory: Tensor, /// Write weights used pub write_weights: Tensor, } impl MANN { /// Create new MANN with given configuration pub fn new(config: MANNConfig, device: &Device) -> Result { // Initialize memory matrix with small random values let memory_data: Vec = (0..config.memory_size * config.memory_dim) .map(|i| (i as f32 * 0.01) % 0.2 - 0.1) .collect(); let memory = Tensor::from_vec( memory_data, &[config.memory_size, config.memory_dim], device.clone(), )?; // Initialize controller network let controller = ControllerNetwork::new(&config, device)?; // Initialize memory usage tracking (all slots available initially) let memory_usage = VecDeque::from_iter(0..config.memory_size); Ok(Self { memory, memory_usage, controller, config, stats: MANNStats::default(), device: device.clone(), }) } /// Process a single episode for few-shot learning pub fn process_episode(&mut self, episode: &Episode, device: &Device) -> Result { // Reset memory usage tracking for new episode self.memory_usage = VecDeque::from_iter(0..self.config.memory_size); let (support_x, support_y) = episode.support_batch(device)?; let (query_x, query_y) = episode.query_batch(device)?; // Process support set (write to memory) let mut support_predictions = Vec::new(); let support_batch_size = support_x.shape()[0]; for i in 0..support_batch_size { let input = support_x.slice(&[i..i+1, ..])?; let label = support_y.slice(&[i..i+1])?; // Forward pass through controller let controller_output = self.controller.forward(&input)?; // Generate key for content-based addressing let key = self.controller.generate_key(&input)?; // Write to memory (store input-label association) let write_result = self.write_to_memory(&input, &key, device)?; support_predictions.push(controller_output); } // Process query set (read from memory and classify) let mut query_predictions = Vec::new(); let query_batch_size = query_x.shape()[0]; for i in 0..query_batch_size { let input = query_x.slice(&[i..i+1, ..])?; // Generate key for content-based addressing let key = self.controller.generate_key(&input)?; // Read from memory let read_result = self.read_from_memory(&key, device)?; // Classify using controller + memory read let prediction = self.classify_with_memory(&input, &read_result.read_vector, device)?; query_predictions.push(prediction); } // Compute accuracy let accuracy = self.compute_accuracy(&query_predictions, &query_y, device)?; // Update statistics self.stats.episodes_processed += 1; self.stats.avg_accuracy = (self.stats.avg_accuracy * (self.stats.episodes_processed - 1) as f32 + accuracy) / self.stats.episodes_processed as f32; self.stats.memory_slots_used = self.config.memory_size - self.memory_usage.len(); Ok(MANNEpisodeResult { support_predictions, query_predictions, accuracy, memory_reads: query_batch_size, memory_writes: support_batch_size, }) } /// Content-based addressing using cosine similarity pub fn content_based_addressing(&self, key: &Tensor, device: &Device) -> Result { let memory_size = self.config.memory_size; let mut similarities = Vec::new(); // Compute cosine similarity between key and each memory slot for i in 0..memory_size { let memory_slot = self.memory.slice(&[i..i+1, ..])?; let similarity = self.cosine_similarity(key, &memory_slot, device)?; similarities.push(similarity); } // Convert to tensor let similarities_tensor = Tensor::from_vec(similarities, &[memory_size], device.clone())?; // Apply focus parameter and softmax let focused = similarities_tensor.mul(&Tensor::scalar(self.config.focus_parameter, device.clone())?)?; let weights = focused.softmax(0)?; Ok(weights) } /// Compute cosine similarity between two vectors pub fn cosine_similarity(&self, a: &Tensor, b: &Tensor, device: &Device) -> Result { let dot_product = a.mul(b)?.sum(None)?.to_vec()?[0]; let norm_a = a.pow_tensor(&Tensor::scalar(2.0, device.clone())?)?.sum(None)?.sqrt()?.to_vec()?[0]; let norm_b = b.pow_tensor(&Tensor::scalar(2.0, device.clone())?)?.sum(None)?.sqrt()?.to_vec()?[0]; let similarity = dot_product / (norm_a * norm_b + 1e-8); Ok(similarity) } /// Read from memory using content-based addressing pub fn read_from_memory(&mut self, key: &Tensor, device: &Device) -> Result { let read_weights = self.content_based_addressing(key, device)?; // Weighted sum of memory slots let read_vector = self.weighted_memory_read(&read_weights, device)?; self.stats.memory_reads += 1; Ok(MemoryReadResult { read_vector, read_weights, }) } /// Write to memory with LRU management pub fn write_to_memory(&mut self, input: &Tensor, key: &Tensor, device: &Device) -> Result { let write_weights = self.content_based_addressing(key, device)?; // Get least recently used memory slot let lru_slot = self.get_lru_slot(); // Update memory at LRU slot let input_data = input.to_vec()?; let memory_data = self.memory.to_vec()?; let mut new_memory_data = memory_data; let start_idx = lru_slot * self.config.memory_dim; let end_idx = start_idx + self.config.memory_dim.min(input_data.len()); for (i, &val) in input_data.iter().enumerate() { if start_idx + i < new_memory_data.len() && i < self.config.memory_dim { new_memory_data[start_idx + i] = val; } } self.memory = Tensor::from_vec( new_memory_data, &[self.config.memory_size, self.config.memory_dim], device.clone(), )?; // Update LRU tracking self.update_lru_tracking(lru_slot); self.stats.memory_writes += 1; Ok(MemoryWriteResult { updated_memory: self.memory.clone(), write_weights, }) } /// Get least recently used memory slot pub fn get_lru_slot(&self) -> usize { self.memory_usage.front().copied().unwrap_or(0) } /// Update LRU tracking for given slot pub fn update_lru_tracking(&mut self, slot: usize) { // Remove slot from current position self.memory_usage.retain(|&x| x != slot); // Add to back (most recently used) self.memory_usage.push_back(slot); } /// Perform weighted read from memory pub fn weighted_memory_read(&self, weights: &Tensor, device: &Device) -> Result { let memory_data = self.memory.to_vec()?; let weights_data = weights.to_vec()?; let memory_dim = self.config.memory_dim; let memory_size = self.config.memory_size; let mut read_data = vec![0.0f32; memory_dim]; for i in 0..memory_size { let weight = weights_data[i]; let start_idx = i * memory_dim; let end_idx = start_idx + memory_dim; for (j, &mem_val) in memory_data[start_idx..end_idx].iter().enumerate() { read_data[j] += weight * mem_val; } } Ok(Tensor::from_vec(read_data, &[memory_dim], device.clone())?) } /// Classify using controller network and memory read pub fn classify_with_memory(&self, input: &Tensor, memory_read: &Tensor, device: &Device) -> Result { // Concatenate input with memory read let input_data = input.to_vec()?; let memory_data = memory_read.to_vec()?; let mut combined_data = input_data; combined_data.extend(memory_data); let combined_input = Tensor::from_vec( combined_data, &[1, input.shape()[1] + memory_read.shape()[0]], device.clone(), )?; // Forward pass through controller for classification self.controller.forward(&combined_input) } /// Compute accuracy of predictions pub fn compute_accuracy(&self, predictions: &[Tensor], targets: &Tensor, device: &Device) -> Result { let targets_data = targets.to_vec()?; let mut correct = 0; let mut total = 0; for (pred, &target) in predictions.iter().zip(targets_data.iter()) { let pred_data = pred.to_vec()?; let predicted_class = pred_data .iter() .enumerate() .max_by(|(_, a), (_, b)| a.total_cmp(b)) .map(|(idx, _)| idx) .unwrap_or(0); if predicted_class == target as usize { correct += 1; } total += 1; } Ok(correct as f32 / total as f32) } /// Get current statistics pub fn get_stats(&self) -> MANNStats { self.stats.clone() } /// Clear memory and reset usage tracking pub fn clear_memory(&mut self) -> Result<()> { let memory_data = vec![0.0f32; self.config.memory_size * self.config.memory_dim]; self.memory = Tensor::from_vec( memory_data, &[self.config.memory_size, self.config.memory_dim], self.device.clone(), )?; self.memory_usage = VecDeque::from_iter(0..self.config.memory_size); Ok(()) } } /// Result of processing an episode with MANN #[derive(Debug, Clone)] pub struct MANNEpisodeResult { /// Predictions on support set pub support_predictions: Vec, /// Predictions on query set pub query_predictions: Vec, /// Accuracy on query set pub accuracy: f32, /// Number of memory reads performed pub memory_reads: usize, /// Number of memory writes performed pub memory_writes: usize, } impl ControllerNetwork { /// Create new controller network pub fn new(config: &MANNConfig, device: &Device) -> Result { let input_dim = config.input_dim + config.memory_dim; // Input + memory read let hidden_dim = config.controller_hidden_dim; // Initialize weights with Xavier initialization let input_hidden = Self::xavier_init(input_dim, hidden_dim, device)?; let hidden_bias = Tensor::zeros(&[hidden_dim], device.clone())?; let hidden_output = Self::xavier_init(hidden_dim, config.output_dim, device)?; let output_bias = Tensor::zeros(&[config.output_dim], device.clone())?; let hidden_key = Self::xavier_init(hidden_dim, config.memory_dim, device)?; let key_bias = Tensor::zeros(&[config.memory_dim], device.clone())?; Ok(Self { input_hidden, hidden_bias, hidden_output, output_bias, hidden_key, key_bias, }) } /// Xavier weight initialization fn xavier_init(input_dim: usize, output_dim: usize, device: &Device) -> Result { let limit = (6.0f32 / (input_dim + output_dim) as f32).sqrt(); let data: Vec = (0..input_dim * output_dim) .map(|i| ((i as f32 * 0.1234) % 2.0 - 1.0) * limit) .collect(); Ok(Tensor::from_vec(data, &[input_dim, output_dim], device.clone())?) } /// Forward pass through controller network pub fn forward(&self, input: &Tensor) -> Result { let hidden = input.matmul(&self.input_hidden)?.add(&self.hidden_bias)?.relu()?; let output = hidden.matmul(&self.hidden_output)?.add(&self.output_bias)?; Ok(output) } /// Generate key vector for memory addressing pub fn generate_key(&self, input: &Tensor) -> Result { let hidden = input.matmul(&self.input_hidden)?.add(&self.hidden_bias)?.relu()?; let key = hidden.matmul(&self.hidden_key)?.add(&self.key_bias)?.tanh()?; Ok(key) } } #[cfg(all(test, feature = "disabled_tests"))] mod tests { use super::*; #[test] fn test_mann_config_creation() { let config = MANNConfig::default(); assert_eq!(config.memory_size, 128); assert_eq!(config.memory_dim, 40); assert_eq!(config.focus_parameter, 1.0); } #[test] fn test_mann_creation() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig { memory_size: 10, memory_dim: 5, input_dim: 8, output_dim: 3, ..Default::default() }; let mann = MANN::new(config, &device).unwrap(); assert_eq!(mann.memory.shape(), &[10, 5]); assert_eq!(mann.memory_usage.len(), 10); } #[test] fn test_cosine_similarity_computation() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig::default(); let mann = MANN::new(config, &device).unwrap(); let a = Tensor::from_vec(vec![1.0, 0.0], &[1, 2], device.clone()).unwrap(); let b = Tensor::from_vec(vec![0.0, 1.0], &[1, 2], device.clone()).unwrap(); let c = Tensor::from_vec(vec![1.0, 0.0], &[1, 2], device.clone()).unwrap(); let sim_ab = mann.cosine_similarity(&a, &b, &device).unwrap(); let sim_ac = mann.cosine_similarity(&a, &c, &device).unwrap(); assert!((sim_ab - 0.0).abs() < 1e-6); // Orthogonal vectors assert!((sim_ac - 1.0).abs() < 1e-6); // Identical vectors } #[test] fn test_content_based_addressing() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig { memory_size: 3, memory_dim: 4, focus_parameter: 1.0, ..Default::default() }; let mann = MANN::new(config, &device).unwrap(); let key = Tensor::from_vec(vec![1.0, 0.0, 0.0, 0.0], &[1, 4], device.clone()).unwrap(); let weights = mann.content_based_addressing(&key, &device).unwrap(); assert_eq!(weights.shape(), &[3]); let weights_data = weights.to_vec().unwrap(); let sum: f32 = weights_data.iter().sum(); assert!((sum - 1.0).abs() < 1e-6); // Should sum to 1 (softmax) } #[test] fn test_memory_read_operation() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig { memory_size: 5, memory_dim: 3, ..Default::default() }; let mut mann = MANN::new(config, &device).unwrap(); let key = Tensor::from_vec(vec![1.0, 2.0, 3.0], &[1, 3], device.clone()).unwrap(); let result = mann.read_from_memory(&key, &device).unwrap(); assert_eq!(result.read_vector.shape(), &[3]); assert_eq!(result.read_weights.shape(), &[5]); assert_eq!(mann.stats.memory_reads, 1); } #[test] fn test_memory_write_operation() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig { memory_size: 4, memory_dim: 6, ..Default::default() }; let mut mann = MANN::new(config, &device).unwrap(); let input = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[1, 6], device.clone()).unwrap(); let key = Tensor::from_vec(vec![1.0, 1.0, 1.0, 1.0, 1.0, 1.0], &[1, 6], device.clone()).unwrap(); let old_memory_usage_len = mann.memory_usage.len(); let result = mann.write_to_memory(&input, &key, &device).unwrap(); assert_eq!(result.updated_memory.shape(), &[4, 6]); assert_eq!(result.write_weights.shape(), &[4]); assert_eq!(mann.stats.memory_writes, 1); assert_eq!(mann.memory_usage.len(), old_memory_usage_len); // LRU tracking updated } #[test] fn test_lru_memory_management() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig { memory_size: 3, memory_dim: 2, ..Default::default() }; let mut mann = MANN::new(config, &device).unwrap(); // Initially, slot 0 should be LRU assert_eq!(mann.get_lru_slot(), 0); // Use slot 0, should move to back mann.update_lru_tracking(0); assert_eq!(mann.get_lru_slot(), 1); // Use slot 2, should move to back mann.update_lru_tracking(2); assert_eq!(mann.get_lru_slot(), 1); } #[test] fn test_weighted_memory_read() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig { memory_size: 2, memory_dim: 3, ..Default::default() }; let mann = MANN::new(config, &device).unwrap(); let weights = Tensor::from_vec(vec![0.7, 0.3], &[2], device.clone()).unwrap(); let result = mann.weighted_memory_read(&weights, &device).unwrap(); assert_eq!(result.shape(), &[3]); } #[test] fn test_controller_network_creation() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig { input_dim: 10, memory_dim: 8, controller_hidden_dim: 16, output_dim: 5, ..Default::default() }; let controller = ControllerNetwork::new(&config, &device).unwrap(); assert_eq!(controller.input_hidden.shape(), &[18, 16]); // input_dim + memory_dim, hidden_dim assert_eq!(controller.hidden_bias.shape(), &[16]); assert_eq!(controller.hidden_output.shape(), &[16, 5]); assert_eq!(controller.output_bias.shape(), &[5]); } #[test] fn test_controller_forward_pass() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig { input_dim: 4, memory_dim: 3, controller_hidden_dim: 8, output_dim: 2, ..Default::default() }; let controller = ControllerNetwork::new(&config, &device).unwrap(); let input = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], &[1, 7], device.clone()).unwrap(); let output = controller.forward(&input).unwrap(); assert_eq!(output.shape(), &[1, 2]); } #[test] fn test_controller_key_generation() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig { input_dim: 5, memory_dim: 4, controller_hidden_dim: 10, output_dim: 3, ..Default::default() }; let controller = ControllerNetwork::new(&config, &device).unwrap(); let input = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], &[1, 9], device.clone()).unwrap(); let key = controller.generate_key(&input).unwrap(); assert_eq!(key.shape(), &[1, 4]); // Check that tanh activation was applied (values should be in [-1, 1]) let key_data = key.to_vec().unwrap(); for &val in &key_data { assert!(val >= -1.0 && val <= 1.0); } } #[test] fn test_mann_episode_processing() { let device = Device::cuda(0).unwrap_or(Device::default()); // Create synthetic dataset and episode let dataset = FewShotDataset::synthetic(3, 10, 20, &device).unwrap(); let episode = dataset.sample_episode(2, 2, 3, &device).unwrap(); let config = MANNConfig { memory_size: 16, memory_dim: 8, input_dim: 20, output_dim: 2, controller_hidden_dim: 32, ..Default::default() }; let mut mann = MANN::new(config, &device).unwrap(); let result = mann.process_episode(&episode, &device).unwrap(); assert_eq!(result.support_predictions.len(), 4); // 2 classes × 2 shots assert_eq!(result.query_predictions.len(), 6); // 2 classes × 3 queries assert!(result.accuracy >= 0.0 && result.accuracy <= 1.0); assert_eq!(result.memory_reads, 6); assert_eq!(result.memory_writes, 4); assert_eq!(mann.stats.episodes_processed, 1); } #[test] fn test_mann_accuracy_computation() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig::default(); let mann = MANN::new(config, &device).unwrap(); // Perfect predictions let pred1 = Tensor::from_vec(vec![0.9, 0.1], &[2], device.clone()).unwrap(); let pred2 = Tensor::from_vec(vec![0.2, 0.8], &[2], device.clone()).unwrap(); let predictions = vec![pred1, pred2]; let targets = Tensor::from_vec(vec![0.0, 1.0], &[2], device.clone()).unwrap(); let accuracy = mann.compute_accuracy(&predictions, &targets, &device).unwrap(); assert!((accuracy - 1.0).abs() < 1e-6); // Imperfect predictions let pred1 = Tensor::from_vec(vec![0.1, 0.9], &[2], device.clone()).unwrap(); // Wrong let pred2 = Tensor::from_vec(vec![0.2, 0.8], &[2], device.clone()).unwrap(); // Correct let predictions = vec![pred1, pred2]; let accuracy = mann.compute_accuracy(&predictions, &targets, &device).unwrap(); assert!((accuracy - 0.5).abs() < 1e-6); } #[test] fn test_mann_memory_clearing() { let device = Device::cuda(0).unwrap_or(Device::default()); let config = MANNConfig { memory_size: 5, memory_dim: 4, ..Default::default() }; let mut mann = MANN::new(config, &device).unwrap(); // Write something to memory first let input = Tensor::from_vec(vec![1.0, 2.0, 3.0, 4.0], &[1, 4], device.clone()).unwrap(); let key = Tensor::from_vec(vec![1.0, 1.0, 1.0, 1.0], &[1, 4], device.clone()).unwrap(); let _ = mann.write_to_memory(&input, &key, &device).unwrap(); // Memory should have non-zero values let memory_data = mann.memory.to_vec().unwrap(); let has_nonzero = memory_data.iter().any(|&x| x.abs() > 1e-6); assert!(has_nonzero); // Clear memory mann.clear_memory().unwrap(); // Memory should be all zeros let cleared_memory_data = mann.memory.to_vec().unwrap(); let all_zero = cleared_memory_data.iter().all(|&x| x.abs() < 1e-6); assert!(all_zero); // Memory usage should be reset assert_eq!(mann.memory_usage.len(), 5); assert_eq!(mann.get_lru_slot(), 0); } }