Skip to main content

torsh_optim/
neural_optimizer.rs

1//! Neural Optimizer - Research Feature
2//!
3//! This module implements neural network-based optimization strategies that learn
4//! to optimize functions. This is based on recent research in "learning to optimize"
5//! and includes implementations of:
6//! - Learning to learn by gradient descent by gradient descent (Andrychowicz et al., 2016)
7//! - Learned optimizers that scale and generalize (Metz et al., 2022)
8//! - MetaAdam and other learned adaptive optimizers
9//!
10//! WARNING: This is a research feature and may not be stable or performant
11//! for production use. It is intended for experimentation and research purposes.
12
13use crate::{Optimizer, OptimizerError, OptimizerResult};
14use parking_lot::RwLock;
15use std::collections::HashMap;
16use std::sync::Arc;
17use torsh_core::{
18    device::{CpuDevice, DeviceType},
19    DType,
20};
21use torsh_tensor::{creation::randn, Tensor};
22
23/// Configuration for neural optimizer
24#[derive(Debug, Clone)]
25pub struct NeuralOptimizerConfig {
26    /// Learning rate for the meta-optimizer (optimizer of the optimizer)
27    pub meta_learning_rate: f32,
28    /// Hidden size for the LSTM optimizer network
29    pub hidden_size: usize,
30    /// Number of layers in the optimizer network
31    pub num_layers: usize,
32    /// Device to run the neural optimizer on
33    pub device: Arc<CpuDevice>,
34    /// Maximum gradient norm for clipping
35    pub max_grad_norm: f32,
36    /// Whether to use coordinate-wise optimization
37    pub coordinate_wise: bool,
38    /// History length for the neural network
39    pub history_length: usize,
40}
41
42impl Default for NeuralOptimizerConfig {
43    fn default() -> Self {
44        Self {
45            meta_learning_rate: 0.001,
46            hidden_size: 20,
47            num_layers: 2,
48            device: Arc::new(CpuDevice::new()),
49            max_grad_norm: 10.0,
50            coordinate_wise: true,
51            history_length: 20,
52        }
53    }
54}
55
56/// Simple neural network for learning optimization updates
57/// This is a simplified implementation for demonstration purposes
58#[derive(Debug, Clone)]
59pub struct OptimizerNetwork {
60    /// LSTM-like state for each parameter
61    pub hidden_states: HashMap<String, Tensor>,
62    /// Cell states for LSTM
63    pub cell_states: HashMap<String, Tensor>,
64    /// Network weights
65    pub weights: NetworkWeights,
66    /// Configuration
67    pub config: NeuralOptimizerConfig,
68}
69
70/// Network weights for the neural optimizer
71#[derive(Debug, Clone)]
72pub struct NetworkWeights {
73    /// Input gate weights
74    pub w_input: Tensor,
75    /// Forget gate weights
76    pub w_forget: Tensor,
77    /// Output gate weights
78    pub w_output: Tensor,
79    /// Cell gate weights
80    pub w_cell: Tensor,
81    /// Output projection weights
82    pub w_output_proj: Tensor,
83    /// Biases
84    pub bias_input: Tensor,
85    pub bias_forget: Tensor,
86    pub bias_output: Tensor,
87    pub bias_cell: Tensor,
88    pub bias_output_proj: Tensor,
89}
90
91impl NetworkWeights {
92    /// Initialize network weights randomly
93    pub fn new(input_size: usize, hidden_size: usize, device: &CpuDevice) -> OptimizerResult<Self> {
94        let scale = (2.0 / (input_size + hidden_size) as f32).sqrt();
95
96        Ok(Self {
97            w_input: randn::<f32>(&[input_size + hidden_size, hidden_size])?.mul_scalar(scale)?,
98            w_forget: randn::<f32>(&[input_size + hidden_size, hidden_size])?.mul_scalar(scale)?,
99            w_output: randn::<f32>(&[input_size + hidden_size, hidden_size])?.mul_scalar(scale)?,
100            w_cell: randn::<f32>(&[input_size + hidden_size, hidden_size])?.mul_scalar(scale)?,
101            w_output_proj: randn::<f32>(&[hidden_size, 1])?.mul_scalar(scale)?,
102            bias_input: Tensor::zeros(&[hidden_size], DeviceType::Cpu)?,
103            bias_forget: Tensor::ones(&[hidden_size], DeviceType::Cpu)?, // Initialize forget bias to 1
104            bias_output: Tensor::zeros(&[hidden_size], DeviceType::Cpu)?,
105            bias_cell: Tensor::zeros(&[hidden_size], DeviceType::Cpu)?,
106            bias_output_proj: Tensor::zeros(&[1], DeviceType::Cpu)?,
107        })
108    }
109}
110
111impl OptimizerNetwork {
112    /// Create a new neural optimizer network
113    pub fn new(config: NeuralOptimizerConfig) -> OptimizerResult<Self> {
114        let input_size = if config.coordinate_wise {
115            2 // gradient and parameter value
116        } else {
117            config.history_length * 2 // history of gradients and parameters
118        };
119
120        let weights = NetworkWeights::new(input_size, config.hidden_size, &config.device)?;
121
122        Ok(Self {
123            hidden_states: HashMap::new(),
124            cell_states: HashMap::new(),
125            weights,
126            config,
127        })
128    }
129
130    /// Forward pass through the neural network to compute parameter update
131    pub fn forward(
132        &mut self,
133        param_id: &str,
134        gradient: &Tensor,
135        parameter: &Tensor,
136    ) -> OptimizerResult<Tensor> {
137        let device = self.config.device.clone();
138
139        // Prepare input: concatenate gradient and parameter information
140        let input = if self.config.coordinate_wise {
141            // For coordinate-wise optimization, process each element independently
142            let grad_norm = gradient.norm()?.unsqueeze(0)?;
143            let param_norm = parameter.norm()?.unsqueeze(0)?;
144            Tensor::cat(&[&grad_norm, &param_norm], 0)?
145        } else {
146            // Use full gradient and parameter vectors (simplified)
147            let grad_flat = gradient.flatten()?;
148            let param_flat = parameter.flatten()?;
149            Tensor::cat(&[&grad_flat, &param_flat], 0)?
150        };
151
152        // Get or initialize hidden and cell states
153        let hidden_shape = vec![self.config.hidden_size];
154        let hidden_state = self
155            .hidden_states
156            .entry(param_id.to_string())
157            .or_insert_with(|| {
158                Tensor::zeros(&hidden_shape, DeviceType::Cpu)
159                    .expect("tensor creation should succeed")
160            });
161        let cell_state = self
162            .cell_states
163            .entry(param_id.to_string())
164            .or_insert_with(|| {
165                Tensor::zeros(&hidden_shape, DeviceType::Cpu)
166                    .expect("tensor creation should succeed")
167            })
168            .clone();
169
170        // LSTM-like computation
171        let combined_input = Tensor::cat(&[&input, &hidden_state.clone()], 0)?;
172
173        // Gates computation
174        let input_gate = self.sigmoid(
175            &combined_input
176                .matmul(&self.weights.w_input)?
177                .add_op(&self.weights.bias_input)?,
178        )?;
179        let forget_gate = self.sigmoid(
180            &combined_input
181                .matmul(&self.weights.w_forget)?
182                .add_op(&self.weights.bias_forget)?,
183        )?;
184        let output_gate = self.sigmoid(
185            &combined_input
186                .matmul(&self.weights.w_output)?
187                .add_op(&self.weights.bias_output)?,
188        )?;
189        let cell_gate = self.tanh(
190            &combined_input
191                .matmul(&self.weights.w_cell)?
192                .add_op(&self.weights.bias_cell)?,
193        )?;
194
195        // Update cell state
196        let new_cell_state = forget_gate
197            .mul_op(&cell_state)?
198            .add_op(&input_gate.mul_op(&cell_gate)?)?;
199
200        // Update hidden state
201        let new_hidden_state = output_gate.mul_op(&self.tanh(&new_cell_state)?)?;
202
203        // Compute parameter update
204        let update_magnitude = new_hidden_state
205            .matmul(&self.weights.w_output_proj)?
206            .add_op(&self.weights.bias_output_proj)?;
207
208        // Apply update to parameter shape
209        let update = if self.config.coordinate_wise {
210            // Scale the gradient by the learned magnitude
211            gradient.mul_op(&update_magnitude.broadcast_to(gradient.shape().dims())?)?
212        } else {
213            // For non-coordinate-wise, we need more sophisticated reshaping
214            gradient.mul_scalar(update_magnitude.item()?)?
215        };
216
217        // Update states
218        *self
219            .hidden_states
220            .get_mut(param_id)
221            .expect("hidden_states should exist for param_id") = new_hidden_state;
222        *self
223            .cell_states
224            .get_mut(param_id)
225            .expect("cell_states should exist for param_id") = new_cell_state;
226
227        Ok(update)
228    }
229
230    /// Sigmoid activation function
231    fn sigmoid(&self, x: &Tensor) -> OptimizerResult<Tensor> {
232        // sigmoid(x) = 1 / (1 + exp(-x))
233        let neg_x = x.mul_scalar(-1.0)?;
234        let exp_neg_x = neg_x.exp()?;
235        let one_plus_exp = exp_neg_x.add_scalar(1.0)?;
236        Ok(one_plus_exp.reciprocal()?)
237    }
238
239    /// Tanh activation function
240    fn tanh(&self, x: &Tensor) -> OptimizerResult<Tensor> {
241        Ok(x.tanh()?)
242    }
243
244    /// Reset network state
245    pub fn reset_state(&mut self) {
246        self.hidden_states.clear();
247        self.cell_states.clear();
248    }
249
250    /// Get network parameters for meta-optimization
251    pub fn parameters(&self) -> Vec<&Tensor> {
252        vec![
253            &self.weights.w_input,
254            &self.weights.w_forget,
255            &self.weights.w_output,
256            &self.weights.w_cell,
257            &self.weights.w_output_proj,
258            &self.weights.bias_input,
259            &self.weights.bias_forget,
260            &self.weights.bias_output,
261            &self.weights.bias_cell,
262            &self.weights.bias_output_proj,
263        ]
264    }
265}
266
267/// Neural optimizer that uses a neural network to learn optimization updates
268pub struct NeuralOptimizer {
269    /// Neural network for computing updates
270    pub network: OptimizerNetwork,
271    /// Parameters being optimized
272    pub parameters: Vec<Tensor>,
273    /// Meta-optimizer for training the neural optimizer
274    pub meta_optimizer: Option<Box<dyn Optimizer>>,
275    /// Training mode flag
276    pub training: bool,
277    /// Step counter
278    pub step_count: usize,
279}
280
281impl NeuralOptimizer {
282    /// Create a new neural optimizer
283    pub fn new(
284        parameters: Vec<Tensor>,
285        config: Option<NeuralOptimizerConfig>,
286    ) -> OptimizerResult<Self> {
287        let config = config.unwrap_or_default();
288        let network = OptimizerNetwork::new(config)?;
289
290        Ok(Self {
291            network,
292            parameters,
293            meta_optimizer: None,
294            training: false,
295            step_count: 0,
296        })
297    }
298
299    /// Create a neural optimizer with meta-learning capabilities
300    pub fn with_meta_learning(
301        parameters: Vec<Tensor>,
302        config: Option<NeuralOptimizerConfig>,
303    ) -> OptimizerResult<Self> {
304        let mut optimizer = Self::new(parameters, config)?;
305        optimizer.training = true;
306
307        // Create meta-optimizer (Adam for the neural network parameters)
308        let network_params = optimizer
309            .network
310            .parameters()
311            .iter()
312            .map(|p| Arc::new(RwLock::new((*p).clone())))
313            .collect();
314
315        use crate::adam::Adam;
316        let meta_optimizer = Adam::new(
317            network_params,
318            Some(optimizer.network.config.meta_learning_rate),
319            None,
320            None,
321            None,
322            false,
323        );
324
325        optimizer.meta_optimizer = Some(Box::new(meta_optimizer));
326
327        Ok(optimizer)
328    }
329
330    /// Set training mode for meta-learning
331    pub fn train(&mut self, mode: bool) {
332        self.training = mode;
333    }
334
335    /// Reset the neural optimizer state
336    pub fn reset(&mut self) {
337        self.network.reset_state();
338        self.step_count = 0;
339    }
340
341    /// Compute loss for meta-learning (simplified objective)
342    pub fn compute_meta_loss(&self, target_loss: f32, actual_loss: f32) -> f32 {
343        (target_loss - actual_loss).powi(2)
344    }
345
346    /// Update the neural network parameters using meta-gradients
347    pub fn meta_step(&mut self, meta_loss: f32) -> OptimizerResult<()> {
348        if let Some(ref mut meta_optimizer) = self.meta_optimizer {
349            // Compute gradients of meta-loss with respect to network parameters
350            // This is a simplified implementation - in practice, you'd need proper backpropagation
351            for param in self.network.parameters() {
352                // Simplified meta-gradient (in practice, compute actual gradients)
353                let meta_grad =
354                    randn::<f32>(param.shape().dims())?.mul_scalar(meta_loss * 0.001)?;
355                param.set_grad(Some(meta_grad));
356            }
357
358            meta_optimizer.step()?;
359        }
360        Ok(())
361    }
362}
363
364impl Optimizer for NeuralOptimizer {
365    fn step(&mut self) -> OptimizerResult<()> {
366        self.step_count += 1;
367
368        for (i, param) in self.parameters.iter_mut().enumerate() {
369            if let Some(grad) = param.grad() {
370                let param_id = format!("param_{}", i);
371
372                // Compute update using neural network
373                let update = self.network.forward(&param_id, &grad, param)?;
374
375                // Apply gradient clipping
376                let update_norm = update.norm()?.item()?;
377                let clipped_update = if update_norm > self.network.config.max_grad_norm {
378                    update.mul_scalar(self.network.config.max_grad_norm / update_norm)?
379                } else {
380                    update
381                };
382
383                // Apply update to parameter
384                crate::param_update::sub_assign(&mut *param, &clipped_update)?;
385
386                // Clear gradients
387                param.set_grad(None);
388            }
389        }
390
391        Ok(())
392    }
393
394    fn zero_grad(&mut self) {
395        for param in &mut self.parameters {
396            // Neural optimizer manages gradients internally
397            // This is a placeholder implementation
398        }
399    }
400
401    fn get_lr(&self) -> Vec<f32> {
402        // Neural optimizer doesn't have a fixed learning rate
403        vec![self.network.config.meta_learning_rate]
404    }
405
406    fn set_lr(&mut self, lr: f32) {
407        // Update meta-learning rate
408        self.network.config.meta_learning_rate = lr;
409    }
410
411    fn state_dict(&self) -> OptimizerResult<crate::OptimizerState> {
412        let mut state = crate::OptimizerState::new("NeuralOptimizer".to_string());
413
414        // Add meta-learning rate to global state
415        state.global_state.insert(
416            "meta_learning_rate".to_string(),
417            self.network.config.meta_learning_rate,
418        );
419        state
420            .global_state
421            .insert("step_count".to_string(), self.step_count as f32);
422        state.global_state.insert(
423            "hidden_size".to_string(),
424            self.network.config.hidden_size as f32,
425        );
426        state.global_state.insert(
427            "num_layers".to_string(),
428            self.network.config.num_layers as f32,
429        );
430
431        // Note: Saving/loading network weights would require more sophisticated serialization
432
433        Ok(state)
434    }
435
436    fn add_param_group(
437        &mut self,
438        params: Vec<std::sync::Arc<parking_lot::RwLock<Tensor>>>,
439        options: std::collections::HashMap<String, f32>,
440    ) {
441        // Neural optimizer manages parameters differently
442        // This is a placeholder implementation
443    }
444
445    fn load_state_dict(&mut self, state: crate::OptimizerState) -> OptimizerResult<()> {
446        if let Some(&meta_lr) = state.global_state.get("meta_learning_rate") {
447            self.network.config.meta_learning_rate = meta_lr;
448        }
449
450        if let Some(&step_count) = state.global_state.get("step_count") {
451            self.step_count = step_count as usize;
452        }
453
454        Ok(())
455    }
456}
457
458/// Meta-learning trainer for neural optimizers
459pub struct NeuralOptimizerTrainer {
460    /// The neural optimizer being trained
461    pub optimizer: NeuralOptimizer,
462    /// Training tasks/problems
463    pub training_tasks: Vec<Box<dyn OptimizationTask>>,
464    /// Validation tasks
465    pub validation_tasks: Vec<Box<dyn OptimizationTask>>,
466    /// Training configuration
467    pub config: TrainingConfig,
468}
469
470/// Configuration for training neural optimizers
471#[derive(Debug, Clone)]
472pub struct TrainingConfig {
473    /// Number of meta-training iterations
474    pub meta_iterations: usize,
475    /// Number of inner optimization steps per task
476    pub inner_steps: usize,
477    /// Meta-learning rate
478    pub meta_lr: f32,
479    /// Device for training
480    pub device: Arc<CpuDevice>,
481}
482
483impl Default for TrainingConfig {
484    fn default() -> Self {
485        Self {
486            meta_iterations: 1000,
487            inner_steps: 100,
488            meta_lr: 0.001,
489            device: Arc::new(CpuDevice::new()),
490        }
491    }
492}
493
494/// Trait for optimization tasks used in meta-learning
495pub trait OptimizationTask {
496    /// Initialize parameters for this task
497    fn initialize_parameters(&self, device: &CpuDevice) -> OptimizerResult<Vec<Tensor>>;
498
499    /// Compute loss and gradients for given parameters
500    fn compute_loss_and_gradients(
501        &self,
502        parameters: &[Tensor],
503    ) -> OptimizerResult<(f32, Vec<Tensor>)>;
504
505    /// Get task name/description
506    fn name(&self) -> &str;
507}
508
509/// Simple quadratic task for testing neural optimizers
510pub struct QuadraticTask {
511    pub dimension: usize,
512    pub condition_number: f32,
513    pub name: String,
514}
515
516impl QuadraticTask {
517    pub fn new(dimension: usize, condition_number: f32) -> Self {
518        Self {
519            dimension,
520            condition_number,
521            name: format!("Quadratic_{}D_cond{:.1}", dimension, condition_number),
522        }
523    }
524}
525
526impl OptimizationTask for QuadraticTask {
527    fn initialize_parameters(&self, device: &CpuDevice) -> OptimizerResult<Vec<Tensor>> {
528        Ok(vec![randn::<f32>(&[self.dimension])?])
529    }
530
531    fn compute_loss_and_gradients(
532        &self,
533        parameters: &[Tensor],
534    ) -> OptimizerResult<(f32, Vec<Tensor>)> {
535        let param = &parameters[0];
536
537        // Create ill-conditioned quadratic: f(x) = 0.5 * x^T * A * x
538        // where A has eigenvalues ranging from 1 to condition_number
539        let mut hessian_diag = Vec::new();
540        for i in 0..self.dimension {
541            let eigenval =
542                1.0 + (self.condition_number - 1.0) * (i as f32) / (self.dimension as f32 - 1.0);
543            hessian_diag.push(eigenval);
544        }
545
546        let hessian_diag_tensor =
547            Tensor::from_data(hessian_diag, param.shape().dims().to_vec(), param.device())?;
548
549        // Loss: 0.5 * sum(hessian_diag * x^2)
550        let loss = param
551            .pow(2.0)?
552            .mul_op(&hessian_diag_tensor)?
553            .sum()?
554            .mul_scalar(0.5)?
555            .item()?;
556
557        // Gradient: hessian_diag * x
558        let grad = param.mul_op(&hessian_diag_tensor)?;
559
560        Ok((loss, vec![grad]))
561    }
562
563    fn name(&self) -> &str {
564        &self.name
565    }
566}
567
568impl NeuralOptimizerTrainer {
569    /// Create a new trainer
570    pub fn new(
571        optimizer: NeuralOptimizer,
572        training_tasks: Vec<Box<dyn OptimizationTask>>,
573        config: Option<TrainingConfig>,
574    ) -> Self {
575        Self {
576            optimizer,
577            training_tasks,
578            validation_tasks: Vec::new(),
579            config: config.unwrap_or_default(),
580        }
581    }
582
583    /// Train the neural optimizer on the given tasks
584    pub fn train(&mut self) -> OptimizerResult<Vec<f32>> {
585        let mut meta_losses = Vec::new();
586
587        for meta_iter in 0..self.config.meta_iterations {
588            let mut total_meta_loss = 0.0;
589
590            // Sample a random task
591            let task_idx = meta_iter % self.training_tasks.len();
592            let task = &self.training_tasks[task_idx];
593
594            // Initialize parameters for this task
595            let mut params = task.initialize_parameters(&CpuDevice::default())?;
596
597            // Perform inner optimization steps
598            let mut task_loss = 0.0;
599            for _ in 0..self.config.inner_steps {
600                let (loss, grads) = task.compute_loss_and_gradients(&params)?;
601                task_loss = loss;
602
603                // Set gradients
604                for (param, grad) in params.iter_mut().zip(grads.iter()) {
605                    param.set_grad(Some(grad.clone()));
606                }
607
608                // Update parameters using neural optimizer
609                // Note: This is simplified - in practice, you'd need to properly track the computation graph
610                self.optimizer.step()?;
611            }
612
613            // Compute meta-loss (how well did we optimize?)
614            let target_loss = 0.0; // Ideal target
615            let meta_loss = self.optimizer.compute_meta_loss(target_loss, task_loss);
616            total_meta_loss += meta_loss;
617
618            // Update neural optimizer parameters
619            self.optimizer.meta_step(meta_loss)?;
620
621            meta_losses.push(total_meta_loss);
622
623            if meta_iter % 100 == 0 {
624                println!(
625                    "Meta-iteration {}: Meta-loss = {:.6}, Task loss = {:.6} (Task: {})",
626                    meta_iter,
627                    meta_loss,
628                    task_loss,
629                    task.name()
630                );
631            }
632        }
633
634        Ok(meta_losses)
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641
642    #[test]
643    fn test_neural_optimizer_config() {
644        let config = NeuralOptimizerConfig::default();
645        assert_eq!(config.meta_learning_rate, 0.001);
646        assert_eq!(config.hidden_size, 20);
647        assert_eq!(config.num_layers, 2);
648        assert_eq!(config.max_grad_norm, 10.0);
649        assert!(config.coordinate_wise);
650        assert_eq!(config.history_length, 20);
651    }
652
653    #[test]
654    fn test_quadratic_task() {
655        let task = QuadraticTask::new(10, 100.0);
656        assert_eq!(task.dimension, 10);
657        assert_eq!(task.condition_number, 100.0);
658        assert_eq!(task.name(), "Quadratic_10D_cond100.0");
659    }
660
661    #[test]
662    fn test_training_config() {
663        let config = TrainingConfig::default();
664        assert_eq!(config.meta_iterations, 1000);
665        assert_eq!(config.inner_steps, 100);
666        assert_eq!(config.meta_lr, 0.001);
667    }
668}