Skip to main content

trustformers_optim/
continual_learning.rs

1//! # Continual Learning Optimizers
2//!
3//! This module implements optimization algorithms specifically designed for
4//! continual learning scenarios where models must learn new tasks while
5//! retaining knowledge of previous tasks.
6//!
7//! ## Available Methods
8//!
9//! - **EWC (Elastic Weight Consolidation)**: Protects important weights from changes
10//! - **PackNet**: Progressive networks with parameter allocation
11//! - **L2 Regularization**: Simple regularization towards previous weights
12//! - **Memory Replay**: Gradient-based memory replay optimization
13//! - **Meta-Learning**: Model-Agnostic Meta-Learning for continual adaptation
14
15// reason: research-stage module — reserved API/scaffolding fields and methods
16// retained intentionally for in-progress features; not yet on active call paths.
17#![allow(dead_code)]
18
19use anyhow::{anyhow, Result};
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use trustformers_core::tensor::Tensor;
23
24/// Configuration for Elastic Weight Consolidation (EWC).
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct EWCConfig {
27    /// Base learning rate
28    pub learning_rate: f32,
29    /// Importance weight for Fisher information regularization
30    pub lambda: f32,
31    /// Method for computing Fisher information
32    pub fisher_method: FisherMethod,
33    /// Number of samples for Fisher information estimation
34    pub fisher_samples: usize,
35    /// Online vs offline EWC
36    pub online: bool,
37    /// Decay factor for online EWC
38    pub decay_factor: f32,
39}
40
41impl Default for EWCConfig {
42    fn default() -> Self {
43        Self {
44            learning_rate: 1e-3,
45            lambda: 1000.0,
46            fisher_method: FisherMethod::Empirical,
47            fisher_samples: 1000,
48            online: false,
49            decay_factor: 0.9,
50        }
51    }
52}
53
54/// Methods for computing Fisher information matrix.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub enum FisherMethod {
57    /// Empirical Fisher information
58    Empirical,
59    /// True Fisher information (computationally expensive)
60    True,
61    /// Diagonal approximation
62    Diagonal,
63}
64
65/// Configuration for Progressive Networks (PackNet).
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct PackNetConfig {
68    /// Base learning rate
69    pub learning_rate: f32,
70    /// Sparsity level for each task
71    pub sparsity_level: f32,
72    /// Number of tasks
73    pub num_tasks: usize,
74    /// Parameter allocation strategy
75    pub allocation_strategy: AllocationStrategy,
76}
77
78impl Default for PackNetConfig {
79    fn default() -> Self {
80        Self {
81            learning_rate: 1e-3,
82            sparsity_level: 0.5,
83            num_tasks: 10,
84            allocation_strategy: AllocationStrategy::Sequential,
85        }
86    }
87}
88
89/// Parameter allocation strategies for PackNet.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub enum AllocationStrategy {
92    /// Sequential allocation
93    Sequential,
94    /// Random allocation
95    Random,
96    /// Importance-based allocation
97    ImportanceBased,
98}
99
100/// Configuration for L2 regularization towards previous parameters.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct L2RegularizationConfig {
103    /// Base learning rate
104    pub learning_rate: f32,
105    /// Regularization strength
106    pub reg_strength: f32,
107    /// Update strategy for anchor parameters
108    pub update_strategy: UpdateStrategy,
109}
110
111impl Default for L2RegularizationConfig {
112    fn default() -> Self {
113        Self {
114            learning_rate: 1e-3,
115            reg_strength: 0.1,
116            update_strategy: UpdateStrategy::EMA,
117        }
118    }
119}
120
121/// Strategies for updating anchor parameters.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub enum UpdateStrategy {
124    /// No update (fixed anchors)
125    Fixed,
126    /// Exponential moving average
127    EMA,
128    /// Update at task boundaries
129    TaskBoundary,
130}
131
132/// Configuration for Memory Replay optimization.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct MemoryReplayConfig {
135    /// Base learning rate
136    pub learning_rate: f32,
137    /// Memory buffer size
138    pub memory_size: usize,
139    /// Replay frequency (every N steps)
140    pub replay_frequency: usize,
141    /// Replay batch size
142    pub replay_batch_size: usize,
143    /// Memory selection strategy
144    pub selection_strategy: MemorySelectionStrategy,
145}
146
147impl Default for MemoryReplayConfig {
148    fn default() -> Self {
149        Self {
150            learning_rate: 1e-3,
151            memory_size: 1000,
152            replay_frequency: 10,
153            replay_batch_size: 32,
154            selection_strategy: MemorySelectionStrategy::Random,
155        }
156    }
157}
158
159/// Memory selection strategies for replay.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub enum MemorySelectionStrategy {
162    /// Random selection
163    Random,
164    /// Gradient-based selection
165    GradientBased,
166    /// Uncertainty-based selection
167    UncertaintyBased,
168}
169
170/// Elastic Weight Consolidation optimizer.
171pub struct EWC {
172    config: EWCConfig,
173    parameters: Vec<Tensor>,
174    importance_weights: Vec<Tensor>,
175    anchor_parameters: Vec<Tensor>,
176    current_task: usize,
177    accumulated_importance: Vec<Tensor>,
178}
179
180impl EWC {
181    /// Create a new EWC optimizer.
182    pub fn new(config: EWCConfig, initial_parameters: Vec<Tensor>) -> Result<Self> {
183        let param_count = initial_parameters.len();
184
185        let importance_weights: Result<Vec<Tensor>> = (0..param_count)
186            .map(|i| {
187                Tensor::zeros(&initial_parameters[i].shape())
188                    .map_err(|e| anyhow::anyhow!("{:?}", e))
189            })
190            .collect();
191
192        let accumulated_importance: Result<Vec<Tensor>> = (0..param_count)
193            .map(|i| {
194                Tensor::zeros(&initial_parameters[i].shape())
195                    .map_err(|e| anyhow::anyhow!("{:?}", e))
196            })
197            .collect();
198
199        Ok(Self {
200            config,
201            parameters: initial_parameters.clone(),
202            importance_weights: importance_weights?,
203            anchor_parameters: initial_parameters.clone(),
204            current_task: 0,
205            accumulated_importance: accumulated_importance?,
206        })
207    }
208
209    /// Compute Fisher information matrix for current task.
210    pub fn compute_fisher_information(&mut self, gradients_samples: &[Vec<Tensor>]) -> Result<()> {
211        let num_samples = gradients_samples.len();
212        if num_samples == 0 {
213            return Err(anyhow!("No gradient samples provided"));
214        }
215
216        // Reset importance weights for new task
217        for importance in self.importance_weights.iter_mut() {
218            *importance = Tensor::zeros(&importance.shape())?;
219        }
220
221        // Compute empirical Fisher information
222        for gradient_sample in gradients_samples {
223            for (i, gradient) in gradient_sample.iter().enumerate() {
224                if i < self.importance_weights.len() {
225                    let squared_grad = gradient.mul(gradient)?;
226                    self.importance_weights[i] = self.importance_weights[i].add(&squared_grad)?;
227                }
228            }
229        }
230
231        // Average over samples
232        for importance in self.importance_weights.iter_mut() {
233            *importance = importance.div_scalar(num_samples as f32)?;
234        }
235
236        // Update accumulated importance for online EWC
237        if self.config.online {
238            for i in 0..self.accumulated_importance.len() {
239                let decayed =
240                    self.accumulated_importance[i].mul_scalar(self.config.decay_factor)?;
241                self.accumulated_importance[i] = decayed.add(&self.importance_weights[i])?;
242            }
243        }
244
245        Ok(())
246    }
247
248    /// Complete current task and prepare for next task.
249    pub fn finish_task(&mut self) -> Result<()> {
250        // Update anchor parameters to current parameters
251        self.anchor_parameters = self.parameters.clone();
252        self.current_task += 1;
253        Ok(())
254    }
255
256    /// Perform optimization step with EWC regularization.
257    pub fn step(&mut self, gradients: &[Tensor]) -> Result<()> {
258        for (i, gradient) in gradients.iter().enumerate() {
259            if i < self.parameters.len() {
260                // Compute EWC penalty gradient
261                let param_diff = self.parameters[i].sub(&self.anchor_parameters[i])?;
262                let importance = if self.config.online {
263                    &self.accumulated_importance[i]
264                } else {
265                    &self.importance_weights[i]
266                };
267                let ewc_grad = param_diff.mul(importance)?.mul_scalar(self.config.lambda)?;
268
269                // Combine original gradient with EWC penalty
270                let total_grad = gradient.add(&ewc_grad)?;
271
272                // Apply update
273                let update = total_grad.mul_scalar(self.config.learning_rate)?;
274                self.parameters[i] = self.parameters[i].sub(&update)?;
275            }
276        }
277        Ok(())
278    }
279
280    /// Get current parameters.
281    pub fn get_parameters(&self) -> &[Tensor] {
282        &self.parameters
283    }
284
285    /// Get importance weights.
286    pub fn get_importance_weights(&self) -> &[Tensor] {
287        &self.importance_weights
288    }
289}
290
291/// Progressive Networks (PackNet) optimizer.
292pub struct PackNet {
293    config: PackNetConfig,
294    parameters: Vec<Tensor>,
295    parameter_masks: Vec<Tensor>,
296    task_allocations: HashMap<usize, Vec<Tensor>>,
297    current_task: usize,
298    available_capacity: Vec<f32>,
299}
300
301impl PackNet {
302    /// Create a new PackNet optimizer.
303    pub fn new(config: PackNetConfig, initial_parameters: Vec<Tensor>) -> Result<Self> {
304        let param_count = initial_parameters.len();
305
306        let parameter_masks: Result<Vec<Tensor>> = (0..param_count)
307            .map(|i| {
308                Tensor::ones(&initial_parameters[i].shape()).map_err(|e| anyhow::anyhow!("{:?}", e))
309            })
310            .collect();
311
312        Ok(Self {
313            config,
314            parameters: initial_parameters.clone(),
315            parameter_masks: parameter_masks?,
316            task_allocations: HashMap::new(),
317            current_task: 0,
318            available_capacity: vec![1.0; param_count],
319        })
320    }
321
322    /// Allocate parameters for a new task.
323    pub fn allocate_task(&mut self, task_id: usize) -> Result<()> {
324        if self.available_capacity.iter().any(|&cap| cap < self.config.sparsity_level) {
325            return Err(anyhow!("Insufficient parameter capacity for new task"));
326        }
327
328        let mut task_masks = Vec::new();
329
330        for (i, param) in self.parameters.iter().enumerate() {
331            let shape = param.shape();
332            let total_params = shape.iter().product::<usize>();
333            let allocated_params = (total_params as f32 * self.config.sparsity_level) as usize;
334
335            // Create allocation mask
336            let mut mask_data = vec![0.0; total_params];
337
338            match self.config.allocation_strategy {
339                AllocationStrategy::Sequential => {
340                    let start_idx =
341                        ((1.0 - self.available_capacity[i]) * total_params as f32) as usize;
342                    let end_idx = (start_idx + allocated_params).min(total_params);
343                    for idx in start_idx..end_idx {
344                        mask_data[idx] = 1.0;
345                    }
346                },
347                AllocationStrategy::Random => {
348                    use scirs2_core::random::*; // SciRS2 Integration Policy
349                    let mut indices: Vec<usize> = (0..total_params).collect();
350                    let mut rng = thread_rng();
351                    indices.shuffle(rng.rng_mut());
352                    for &idx in indices.iter().take(allocated_params) {
353                        mask_data[idx] = 1.0;
354                    }
355                },
356                AllocationStrategy::ImportanceBased => {
357                    // Simplified importance-based allocation
358                    // In practice, this would use gradient magnitudes or other importance metrics
359                    for idx in 0..allocated_params.min(total_params) {
360                        mask_data[idx] = 1.0;
361                    }
362                },
363            }
364
365            let task_mask = Tensor::new(mask_data)?;
366            task_masks.push(task_mask);
367
368            // Update available capacity
369            self.available_capacity[i] -= self.config.sparsity_level;
370        }
371
372        self.task_allocations.insert(task_id, task_masks);
373        self.current_task = task_id;
374        Ok(())
375    }
376
377    /// Perform optimization step with parameter masking.
378    pub fn step(&mut self, gradients: &[Tensor]) -> Result<()> {
379        let task_masks = self
380            .task_allocations
381            .get(&self.current_task)
382            .ok_or_else(|| anyhow!("No allocation for current task"))?;
383
384        for (i, gradient) in gradients.iter().enumerate() {
385            if i < self.parameters.len() && i < task_masks.len() {
386                // Apply task-specific mask to gradient
387                let masked_grad = gradient.mul(&task_masks[i])?;
388
389                // Apply update
390                let update = masked_grad.mul_scalar(self.config.learning_rate)?;
391                self.parameters[i] = self.parameters[i].sub(&update)?;
392            }
393        }
394        Ok(())
395    }
396
397    /// Get current parameters.
398    pub fn get_parameters(&self) -> &[Tensor] {
399        &self.parameters
400    }
401
402    /// Get available capacity for new tasks.
403    pub fn get_available_capacity(&self) -> &[f32] {
404        &self.available_capacity
405    }
406}
407
408/// L2 Regularization optimizer for continual learning.
409pub struct L2Regularization {
410    config: L2RegularizationConfig,
411    parameters: Vec<Tensor>,
412    anchor_parameters: Vec<Tensor>,
413    ema_decay: f32,
414}
415
416impl L2Regularization {
417    /// Create a new L2 regularization optimizer.
418    pub fn new(config: L2RegularizationConfig, initial_parameters: Vec<Tensor>) -> Self {
419        Self {
420            config,
421            parameters: initial_parameters.clone(),
422            anchor_parameters: initial_parameters,
423            ema_decay: 0.999,
424        }
425    }
426
427    /// Perform optimization step with L2 regularization.
428    pub fn step(&mut self, gradients: &[Tensor]) -> Result<()> {
429        for (i, gradient) in gradients.iter().enumerate() {
430            if i < self.parameters.len() {
431                // Compute L2 regularization term
432                let param_diff = self.parameters[i].sub(&self.anchor_parameters[i])?;
433                let reg_grad = param_diff.mul_scalar(self.config.reg_strength)?;
434
435                // Combine gradient with regularization
436                let total_grad = gradient.add(&reg_grad)?;
437
438                // Apply update
439                let update = total_grad.mul_scalar(self.config.learning_rate)?;
440                self.parameters[i] = self.parameters[i].sub(&update)?;
441
442                // Update anchor parameters based on strategy
443                match self.config.update_strategy {
444                    UpdateStrategy::Fixed => {
445                        // Don't update anchors
446                    },
447                    UpdateStrategy::EMA => {
448                        // Exponential moving average update
449                        let anchor_update = self.parameters[i].mul_scalar(1.0 - self.ema_decay)?;
450                        let anchor_keep = self.anchor_parameters[i].mul_scalar(self.ema_decay)?;
451                        self.anchor_parameters[i] = anchor_update.add(&anchor_keep)?;
452                    },
453                    UpdateStrategy::TaskBoundary => {
454                        // Will be updated when finish_task() is called
455                    },
456                }
457            }
458        }
459        Ok(())
460    }
461
462    /// Finish current task (update anchors for TaskBoundary strategy).
463    pub fn finish_task(&mut self) -> Result<()> {
464        if matches!(self.config.update_strategy, UpdateStrategy::TaskBoundary) {
465            self.anchor_parameters = self.parameters.clone();
466        }
467        Ok(())
468    }
469
470    /// Get current parameters.
471    pub fn get_parameters(&self) -> &[Tensor] {
472        &self.parameters
473    }
474}
475
476/// Memory replay optimizer.
477pub struct MemoryReplay {
478    config: MemoryReplayConfig,
479    parameters: Vec<Tensor>,
480    memory_buffer: Vec<Vec<Tensor>>, // Stored gradients
481    step_count: usize,
482}
483
484impl MemoryReplay {
485    /// Create a new memory replay optimizer.
486    pub fn new(config: MemoryReplayConfig, initial_parameters: Vec<Tensor>) -> Self {
487        Self {
488            config,
489            parameters: initial_parameters,
490            memory_buffer: Vec::new(),
491            step_count: 0,
492        }
493    }
494
495    /// Add gradient to memory buffer.
496    pub fn store_gradient(&mut self, gradients: &[Tensor]) -> Result<()> {
497        if self.memory_buffer.len() >= self.config.memory_size {
498            // Remove oldest or least important gradient
499            match self.config.selection_strategy {
500                MemorySelectionStrategy::Random => {
501                    use scirs2_core::random::*; // SciRS2 Integration Policy
502                    let idx = thread_rng().random_range(0..self.memory_buffer.len());
503                    self.memory_buffer.remove(idx);
504                },
505                _ => {
506                    self.memory_buffer.remove(0); // FIFO for simplicity
507                },
508            }
509        }
510
511        self.memory_buffer.push(gradients.to_vec());
512        Ok(())
513    }
514
515    /// Perform optimization step with memory replay.
516    pub fn step(&mut self, gradients: &[Tensor]) -> Result<()> {
517        // Regular gradient update
518        for (i, gradient) in gradients.iter().enumerate() {
519            if i < self.parameters.len() {
520                let update = gradient.mul_scalar(self.config.learning_rate)?;
521                self.parameters[i] = self.parameters[i].sub(&update)?;
522            }
523        }
524
525        // Store current gradient
526        self.store_gradient(gradients)?;
527
528        // Replay from memory
529        if self.step_count.is_multiple_of(self.config.replay_frequency)
530            && !self.memory_buffer.is_empty()
531        {
532            self.replay_step()?;
533        }
534
535        self.step_count += 1;
536        Ok(())
537    }
538
539    fn replay_step(&mut self) -> Result<()> {
540        let batch_size = self.config.replay_batch_size.min(self.memory_buffer.len());
541
542        // Select random batch from memory
543        use scirs2_core::random::*; // SciRS2 Integration Policy
544        let mut indices: Vec<usize> = (0..self.memory_buffer.len()).collect();
545        let mut rng = thread_rng();
546        indices.shuffle(rng.rng_mut());
547
548        for &idx in indices.iter().take(batch_size) {
549            let replay_gradients = &self.memory_buffer[idx];
550
551            // Apply replay gradient with reduced learning rate
552            let replay_lr = self.config.learning_rate * 0.5;
553            for (i, gradient) in replay_gradients.iter().enumerate() {
554                if i < self.parameters.len() {
555                    let update = gradient.mul_scalar(replay_lr)?;
556                    self.parameters[i] = self.parameters[i].sub(&update)?;
557                }
558            }
559        }
560
561        Ok(())
562    }
563
564    /// Get current parameters.
565    pub fn get_parameters(&self) -> &[Tensor] {
566        &self.parameters
567    }
568
569    /// Get memory buffer size.
570    pub fn memory_size(&self) -> usize {
571        self.memory_buffer.len()
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    #[test]
580    fn test_ewc_config() {
581        let config = EWCConfig::default();
582        assert_eq!(config.learning_rate, 1e-3);
583        assert_eq!(config.lambda, 1000.0);
584        assert!(!config.online);
585    }
586
587    #[test]
588    fn test_packnet_config() {
589        let config = PackNetConfig::default();
590        assert_eq!(config.sparsity_level, 0.5);
591        assert_eq!(config.num_tasks, 10);
592    }
593
594    #[test]
595    fn test_l2_regularization_config() {
596        let config = L2RegularizationConfig::default();
597        assert_eq!(config.reg_strength, 0.1);
598        assert!(matches!(config.update_strategy, UpdateStrategy::EMA));
599    }
600
601    #[test]
602    fn test_memory_replay_config() {
603        let config = MemoryReplayConfig::default();
604        assert_eq!(config.memory_size, 1000);
605        assert_eq!(config.replay_frequency, 10);
606        assert!(matches!(
607            config.selection_strategy,
608            MemorySelectionStrategy::Random
609        ));
610    }
611
612    #[test]
613    fn test_fisher_methods() {
614        assert!(matches!(FisherMethod::Empirical, FisherMethod::Empirical));
615        assert!(matches!(FisherMethod::True, FisherMethod::True));
616        assert!(matches!(FisherMethod::Diagonal, FisherMethod::Diagonal));
617    }
618}