1use scirs2_core::ndarray::{Array1, Array2};
18use scirs2_core::numeric::Float;
19use std::collections::HashMap;
20use std::fmt::Debug;
21
22use crate::error::{OptimError, Result};
23
24pub struct ElasticWeightConsolidation<T: Float + Debug + Send + Sync + 'static> {
38 lambda: T,
40 fisher_diagonal: HashMap<String, Array1<T>>,
42 anchor_parameters: HashMap<String, Array1<T>>,
44 task_fisher_diagonals: Vec<HashMap<String, Array1<T>>>,
46 task_anchor_parameters: Vec<HashMap<String, Array1<T>>>,
48 num_samples_fisher: usize,
50 online: bool,
52 gamma: T,
54}
55
56impl<T: Float + Debug + Send + Sync + 'static> ElasticWeightConsolidation<T> {
57 pub fn new(lambda: T) -> Self {
61 Self {
62 lambda,
63 fisher_diagonal: HashMap::new(),
64 anchor_parameters: HashMap::new(),
65 task_fisher_diagonals: Vec::new(),
66 task_anchor_parameters: Vec::new(),
67 num_samples_fisher: 100,
68 online: false,
69 gamma: T::from(0.95).unwrap_or_else(|| T::one()),
70 }
71 }
72
73 pub fn with_num_samples(mut self, n: usize) -> Self {
75 self.num_samples_fisher = n;
76 self
77 }
78
79 pub fn with_online(mut self, online: bool) -> Self {
81 self.online = online;
82 self
83 }
84
85 pub fn with_gamma(mut self, gamma: T) -> Self {
87 self.gamma = gamma;
88 self
89 }
90
91 pub fn compute_fisher_diagonal(
106 &mut self,
107 parameters: &HashMap<String, Array1<T>>,
108 gradients_fn: impl Fn(&HashMap<String, Array1<T>>, usize) -> Result<HashMap<String, Array1<T>>>,
109 ) -> Result<()> {
110 if parameters.is_empty() {
111 return Err(OptimError::InsufficientData(
112 "parameters map is empty".to_string(),
113 ));
114 }
115
116 if self.num_samples_fisher == 0 {
117 return Err(OptimError::InvalidConfig(
118 "num_samples_fisher must be > 0".to_string(),
119 ));
120 }
121
122 let mut fisher_accum: HashMap<String, Array1<T>> = HashMap::new();
124 for (name, param) in parameters {
125 fisher_accum.insert(name.clone(), Array1::from_elem(param.len(), T::zero()));
126 }
127
128 let n_samples = T::from(self.num_samples_fisher).unwrap_or_else(|| T::one());
129
130 for sample_idx in 0..self.num_samples_fisher {
131 let grads = gradients_fn(parameters, sample_idx)?;
132 for (name, grad) in &grads {
133 let accum = fisher_accum.get_mut(name).ok_or_else(|| {
134 OptimError::InvalidConfig(format!(
135 "gradient reported for unknown parameter '{}'; \
136 known parameters: {:?}",
137 name,
138 {
139 let mut keys: Vec<&str> =
140 parameters.keys().map(|s| s.as_str()).collect();
141 keys.sort_unstable();
142 keys
143 }
144 ))
145 })?;
146 if accum.len() != grad.len() {
147 return Err(OptimError::ComputationError(format!(
148 "gradient dimension mismatch for '{}': expected {}, got {}",
149 name,
150 accum.len(),
151 grad.len()
152 )));
153 }
154 for (a, g) in accum.iter_mut().zip(grad.iter()) {
156 *a = *a + (*g) * (*g);
157 }
158 }
159 }
160
161 let mut fisher = HashMap::new();
163 for (name, mut accum) in fisher_accum {
164 for a in accum.iter_mut() {
165 *a = *a / n_samples;
166 }
167 fisher.insert(name, accum);
168 }
169
170 self.fisher_diagonal = fisher;
171 Ok(())
172 }
173
174 pub fn consolidate(&mut self, parameters: &HashMap<String, Array1<T>>) -> Result<()> {
180 if self.fisher_diagonal.is_empty() {
181 return Err(OptimError::InvalidState(
182 "Fisher diagonal not computed; call compute_fisher_diagonal first".to_string(),
183 ));
184 }
185
186 self.anchor_parameters = parameters.clone();
188 self.task_anchor_parameters.push(parameters.clone());
189
190 if self.online {
191 let mut merged = HashMap::new();
193 for (name, new_fisher) in &self.fisher_diagonal {
194 let updated = if let Some(old_fisher) = self
195 .task_fisher_diagonals
196 .last()
197 .and_then(|map| map.get(name))
198 {
199 Self::require_same_len(
204 name,
205 "previous Fisher diagonal",
206 new_fisher.len(),
207 old_fisher.len(),
208 )?;
209 let mut result = Array1::from_elem(new_fisher.len(), T::zero());
210 for i in 0..result.len() {
211 result[i] = self.gamma * old_fisher[i] + new_fisher[i];
212 }
213 result
214 } else {
215 new_fisher.clone()
216 };
217 merged.insert(name.clone(), updated);
218 }
219 self.task_fisher_diagonals.push(merged);
220 } else {
221 self.task_fisher_diagonals
223 .push(self.fisher_diagonal.clone());
224 }
225
226 Ok(())
227 }
228
229 pub fn ewc_penalty(&self, parameters: &HashMap<String, Array1<T>>) -> Result<T> {
236 if self.anchor_parameters.is_empty() {
237 return Err(OptimError::InvalidState(
238 "no anchor parameters stored; call consolidate first".to_string(),
239 ));
240 }
241
242 let half = T::from(0.5).unwrap_or_else(|| T::one());
243 let mut total_penalty = T::zero();
244
245 if self.online {
246 let fisher_map = self.task_fisher_diagonals.last().ok_or_else(|| {
248 OptimError::InvalidState("no consolidated Fisher available".to_string())
249 })?;
250
251 for (name, anchor) in &self.anchor_parameters {
252 let current = parameters.get(name).ok_or_else(|| {
253 OptimError::InvalidState(format!("parameter '{}' not found in input", name))
254 })?;
255 let fisher = fisher_map.get(name).ok_or_else(|| {
256 OptimError::InvalidState(format!("Fisher information for '{}' not found", name))
257 })?;
258
259 Self::require_same_len(name, "current parameter", anchor.len(), current.len())?;
260 Self::require_same_len(name, "Fisher diagonal", anchor.len(), fisher.len())?;
261
262 for i in 0..anchor.len() {
263 let diff = current[i] - anchor[i];
264 total_penalty = total_penalty + fisher[i] * diff * diff;
265 }
266 }
267 } else {
268 for (task_idx, task_fisher) in self.task_fisher_diagonals.iter().enumerate() {
270 let task_anchor = self.task_anchor_parameters.get(task_idx).ok_or_else(|| {
275 OptimError::InvalidState(format!(
276 "no anchor parameters stored for task {task_idx}: {} anchors for {} Fisher \
277 diagonals",
278 self.task_anchor_parameters.len(),
279 self.task_fisher_diagonals.len()
280 ))
281 })?;
282 for (name, anchor) in task_anchor {
283 let current = parameters.get(name).ok_or_else(|| {
284 OptimError::InvalidState(format!("parameter '{}' not found in input", name))
285 })?;
286 if let Some(fisher) = task_fisher.get(name) {
287 Self::require_same_len(
288 name,
289 "current parameter",
290 anchor.len(),
291 current.len(),
292 )?;
293 Self::require_same_len(
294 name,
295 "Fisher diagonal",
296 anchor.len(),
297 fisher.len(),
298 )?;
299 for i in 0..anchor.len() {
300 let diff = current[i] - anchor[i];
301 total_penalty = total_penalty + fisher[i] * diff * diff;
302 }
303 }
304 }
305 }
306 }
307
308 Ok(self.lambda * half * total_penalty)
309 }
310
311 pub fn ewc_gradient(
315 &self,
316 parameters: &HashMap<String, Array1<T>>,
317 ) -> Result<HashMap<String, Array1<T>>> {
318 if self.anchor_parameters.is_empty() {
319 return Err(OptimError::InvalidState(
320 "no anchor parameters stored; call consolidate first".to_string(),
321 ));
322 }
323
324 let mut gradients: HashMap<String, Array1<T>> = HashMap::new();
325
326 for (name, param) in parameters {
328 gradients.insert(name.clone(), Array1::from_elem(param.len(), T::zero()));
329 }
330
331 if self.online {
332 let fisher_map = self.task_fisher_diagonals.last().ok_or_else(|| {
333 OptimError::InvalidState("no consolidated Fisher available".to_string())
334 })?;
335
336 for (name, anchor) in &self.anchor_parameters {
337 let current = parameters.get(name).ok_or_else(|| {
338 OptimError::InvalidState(format!("parameter '{}' not found in input", name))
339 })?;
340 let fisher = fisher_map.get(name).ok_or_else(|| {
341 OptimError::InvalidState(format!("Fisher information for '{}' not found", name))
342 })?;
343 let grad = gradients.get_mut(name).ok_or_else(|| {
344 OptimError::InvalidState(format!("gradient entry for '{}' not found", name))
345 })?;
346
347 Self::require_same_len(name, "current parameter", anchor.len(), current.len())?;
348 Self::require_same_len(name, "Fisher diagonal", anchor.len(), fisher.len())?;
349 Self::require_same_len(name, "gradient buffer", anchor.len(), grad.len())?;
350
351 for i in 0..anchor.len() {
352 grad[i] = grad[i] + self.lambda * fisher[i] * (current[i] - anchor[i]);
353 }
354 }
355 } else {
356 for (task_idx, task_fisher) in self.task_fisher_diagonals.iter().enumerate() {
357 let task_anchor = self.task_anchor_parameters.get(task_idx).ok_or_else(|| {
362 OptimError::InvalidState(format!(
363 "no anchor parameters stored for task {task_idx}: {} anchors for {} Fisher \
364 diagonals",
365 self.task_anchor_parameters.len(),
366 self.task_fisher_diagonals.len()
367 ))
368 })?;
369 for (name, anchor) in task_anchor {
370 let current = parameters.get(name).ok_or_else(|| {
371 OptimError::InvalidState(format!("parameter '{}' not found in input", name))
372 })?;
373 if let Some(fisher) = task_fisher.get(name) {
374 let grad = gradients.get_mut(name).ok_or_else(|| {
375 OptimError::InvalidState(format!(
376 "gradient entry for '{}' not found",
377 name
378 ))
379 })?;
380
381 Self::require_same_len(
382 name,
383 "current parameter",
384 anchor.len(),
385 current.len(),
386 )?;
387 Self::require_same_len(
388 name,
389 "Fisher diagonal",
390 anchor.len(),
391 fisher.len(),
392 )?;
393 Self::require_same_len(name, "gradient buffer", anchor.len(), grad.len())?;
394 for i in 0..anchor.len() {
395 grad[i] = grad[i] + self.lambda * fisher[i] * (current[i] - anchor[i]);
396 }
397 }
398 }
399 }
400 }
401
402 Ok(gradients)
403 }
404
405 fn require_same_len(name: &str, what: &str, expected: usize, got: usize) -> Result<()> {
415 if expected != got {
416 return Err(OptimError::InvalidState(format!(
417 "{what} for '{name}' has length {got} but its anchor has length {expected}; \
418 EWC needs matching shapes"
419 )));
420 }
421 Ok(())
422 }
423
424 pub fn num_tasks(&self) -> usize {
426 self.task_fisher_diagonals.len()
427 }
428}
429
430#[derive(Debug, Clone)]
436pub struct NetworkColumn<T: Float + Debug + Send + Sync + 'static> {
437 pub weights: Vec<Array2<T>>,
439 pub biases: Vec<Array1<T>>,
441 pub frozen: bool,
443}
444
445impl<T: Float + Debug + Send + Sync + 'static> NetworkColumn<T> {
446 pub fn num_layers(&self) -> usize {
448 self.weights.len()
449 }
450
451 fn forward_layer(&self, input: &Array1<T>, layer_idx: usize) -> Result<(Array1<T>, Array1<T>)> {
454 if layer_idx >= self.weights.len() {
455 return Err(OptimError::NetworkError(format!(
456 "layer index {} out of range (column has {} layers)",
457 layer_idx,
458 self.weights.len()
459 )));
460 }
461 let w = &self.weights[layer_idx];
462 let b = &self.biases[layer_idx];
463
464 let out_dim = w.nrows();
466 let in_dim = w.ncols();
467 if input.len() != in_dim {
468 return Err(OptimError::NetworkError(format!(
469 "input dimension mismatch at layer {}: weight expects {}, got {}",
470 layer_idx,
471 in_dim,
472 input.len()
473 )));
474 }
475
476 let mut z = b.clone();
477 for i in 0..out_dim {
478 let mut sum = T::zero();
479 for j in 0..in_dim {
480 sum = sum + w[[i, j]] * input[j];
481 }
482 z[i] = z[i] + sum;
483 }
484
485 let a = self.apply_activation(&z, layer_idx);
486 Ok((z, a))
487 }
488
489 fn apply_activation(&self, z: &Array1<T>, layer_idx: usize) -> Array1<T> {
496 let is_last_layer = layer_idx + 1 == self.weights.len();
497 if is_last_layer {
498 z.clone()
499 } else {
500 z.mapv(|v| if v > T::zero() { v } else { T::zero() })
501 }
502 }
503}
504
505pub struct ProgressiveNetworks<T: Float + Debug + Send + Sync + 'static> {
511 columns: Vec<NetworkColumn<T>>,
513 lateral_connections: Vec<Vec<Array2<T>>>,
517 active_column: usize,
519 hidden_sizes: Vec<usize>,
521}
522
523impl<T: Float + Debug + Send + Sync + 'static> ProgressiveNetworks<T> {
524 pub fn new(hidden_sizes: Vec<usize>) -> Self {
528 Self {
529 columns: Vec::new(),
530 lateral_connections: Vec::new(),
531 active_column: 0,
532 hidden_sizes,
533 }
534 }
535
536 pub fn add_task_column(&mut self, input_size: usize, output_size: usize) -> Result<usize> {
544 if input_size == 0 || output_size == 0 {
545 return Err(OptimError::InvalidConfig(
546 "input_size and output_size must be > 0".to_string(),
547 ));
548 }
549
550 let col_id = self.columns.len();
551
552 for col in &mut self.columns {
554 col.frozen = true;
555 }
556
557 let mut layer_sizes = Vec::with_capacity(self.hidden_sizes.len() + 2);
559 layer_sizes.push(input_size);
560 layer_sizes.extend_from_slice(&self.hidden_sizes);
561 layer_sizes.push(output_size);
562
563 let mut weights = Vec::new();
565 let mut biases = Vec::new();
566
567 for l in 0..layer_sizes.len() - 1 {
568 let fan_in = layer_sizes[l];
569 let fan_out = layer_sizes[l + 1];
570
571 let scale_val = T::from(2.0 / ((fan_in + fan_out) as f64)).unwrap_or_else(|| T::one());
573 let scale = Float::sqrt(scale_val);
574
575 let mut w = Array2::from_elem((fan_out, fan_in), T::zero());
577 for i in 0..fan_out {
578 for j in 0..fan_in {
579 let idx = (i * fan_in + j) as f64;
581 let sign = if (i + j) % 2 == 0 {
582 T::one()
583 } else {
584 -T::one()
585 };
586 let magnitude =
587 scale * T::from((idx + 1.0).recip()).unwrap_or_else(|| T::one());
588 w[[i, j]] = sign * magnitude;
589 }
590 }
591
592 let b = Array1::from_elem(fan_out, T::zero());
593
594 weights.push(w);
595 biases.push(b);
596 }
597
598 let column = NetworkColumn {
599 weights,
600 biases,
601 frozen: false,
602 };
603
604 self.columns.push(column);
605
606 let mut laterals_for_col = Vec::new();
608 if col_id > 0 {
609 for l in 0..layer_sizes.len() - 1 {
610 let lateral_in = if l == 0 {
616 0
618 } else {
619 col_id * layer_sizes[l]
621 };
622 let lateral_out = layer_sizes[l + 1];
623
624 if lateral_in == 0 {
625 laterals_for_col.push(Array2::from_elem((lateral_out, 1), T::zero()));
627 } else {
628 let scale_val = T::from(0.1 / (lateral_in as f64)).unwrap_or_else(|| T::one());
630 let scale = Float::sqrt(scale_val);
631 let mut lat_w = Array2::from_elem((lateral_out, lateral_in), T::zero());
632 for i in 0..lateral_out {
633 for j in 0..lateral_in {
634 let sign = if (i + j) % 3 == 0 {
635 T::one()
636 } else if (i + j) % 3 == 1 {
637 -T::one()
638 } else {
639 T::zero()
640 };
641 lat_w[[i, j]] = sign * scale;
642 }
643 }
644 laterals_for_col.push(lat_w);
645 }
646 }
647 }
648
649 self.lateral_connections.push(laterals_for_col);
650 self.active_column = col_id;
651
652 Ok(col_id)
653 }
654
655 pub fn forward(&self, input: &Array1<T>, task_id: usize) -> Result<Array1<T>> {
660 let (output, _) = self.forward_with_laterals(input, task_id)?;
661 Ok(output)
662 }
663
664 pub fn forward_with_laterals(
670 &self,
671 input: &Array1<T>,
672 task_id: usize,
673 ) -> Result<(Array1<T>, Vec<Array1<T>>)> {
674 if task_id >= self.columns.len() {
675 return Err(OptimError::InvalidState(format!(
676 "task_id {} out of range; only {} columns exist",
677 task_id,
678 self.columns.len()
679 )));
680 }
681
682 let mut all_activations: Vec<Vec<Array1<T>>> = Vec::new();
685
686 for col in 0..=task_id {
687 let column = &self.columns[col];
688 let num_layers = column.num_layers();
689 let mut col_activations: Vec<Array1<T>> = Vec::new();
690 let mut h = input.clone();
691
692 for l in 0..num_layers {
693 let lateral_contribution = if col > 0 && l > 0 {
707 let laterals = &self.lateral_connections[col];
708 if !laterals.is_empty() && l < laterals.len() {
709 let lat_w = &laterals[l];
710 let mut lateral_input_parts: Vec<T> = Vec::new();
711 for prev_col_acts in all_activations.iter().take(col) {
712 if l - 1 < prev_col_acts.len() {
713 let prev_act = &prev_col_acts[l - 1];
714 lateral_input_parts.extend(prev_act.iter().copied());
715 }
716 }
717
718 if lateral_input_parts.is_empty() {
719 None
720 } else {
721 let lateral_input = Array1::from_vec(lateral_input_parts);
722 if lat_w.ncols() != lateral_input.len() {
723 return Err(OptimError::NetworkError(format!(
724 "lateral weight at column {col} layer {l} expects \
725 {} inputs but the previous columns supplied {}",
726 lat_w.ncols(),
727 lateral_input.len()
728 )));
729 }
730 let lat_out_dim = lat_w.nrows();
731 let mut contribution = Array1::from_elem(lat_out_dim, T::zero());
732 for i in 0..lat_out_dim {
733 let mut sum = T::zero();
734 for j in 0..lat_w.ncols() {
735 sum = sum + lat_w[[i, j]] * lateral_input[j];
736 }
737 contribution[i] = sum;
738 }
739 Some(contribution)
740 }
741 } else {
742 None
743 }
744 } else {
745 None
746 };
747
748 let (mut pre, post_without_lateral) = column.forward_layer(&h, l)?;
749 let post = match lateral_contribution {
750 Some(contribution) => {
751 if contribution.len() != pre.len() {
752 return Err(OptimError::NetworkError(format!(
753 "lateral contribution at column {col} layer {l} has \
754 {} entries but the pre-activation has {}",
755 contribution.len(),
756 pre.len()
757 )));
758 }
759 for i in 0..pre.len() {
760 pre[i] = pre[i] + contribution[i];
761 }
762 column.apply_activation(&pre, l)
763 }
764 None => post_without_lateral,
765 };
766 col_activations.push(post.clone());
767 h = post;
768 }
769
770 all_activations.push(col_activations);
771 }
772
773 let target_activations = &all_activations[task_id];
775 let output = target_activations
776 .last()
777 .ok_or_else(|| OptimError::NetworkError("column has no layers".to_string()))?
778 .clone();
779
780 let mut intermediates: Vec<Array1<T>> = Vec::new();
782 for col_acts in &all_activations {
783 for act in col_acts {
784 intermediates.push(act.clone());
785 }
786 }
787
788 Ok((output, intermediates))
789 }
790
791 pub fn freeze_column(&mut self, column_id: usize) -> Result<()> {
793 if column_id >= self.columns.len() {
794 return Err(OptimError::InvalidState(format!(
795 "column_id {} out of range; only {} columns exist",
796 column_id,
797 self.columns.len()
798 )));
799 }
800 self.columns[column_id].frozen = true;
801 Ok(())
802 }
803
804 pub fn num_columns(&self) -> usize {
806 self.columns.len()
807 }
808
809 pub fn get_column_parameters(&self, column_id: usize) -> Result<&NetworkColumn<T>> {
811 self.columns.get(column_id).ok_or_else(|| {
812 OptimError::InvalidState(format!(
813 "column_id {} out of range; only {} columns exist",
814 column_id,
815 self.columns.len()
816 ))
817 })
818 }
819}
820
821#[cfg(test)]
826mod tests {
827 use super::*;
828 use scirs2_core::ndarray::Array1;
829
830 type F = f64;
831
832 fn make_params(names: &[&str], size: usize, value: f64) -> HashMap<String, Array1<F>> {
834 let mut map = HashMap::new();
835 for name in names {
836 map.insert(name.to_string(), Array1::from_elem(size, value));
837 }
838 map
839 }
840
841 #[test]
851 fn ewc_reports_length_mismatches_instead_of_panicking() {
852 let build = |online: bool| -> ElasticWeightConsolidation<F> {
854 let mut ewc: ElasticWeightConsolidation<F> = ElasticWeightConsolidation::new(1.0)
855 .with_num_samples(2)
856 .with_online(online);
857 let wide = make_params(&["w"], 4, 1.0);
858 ewc.compute_fisher_diagonal(&wide, |params, _| {
859 Ok(params
860 .iter()
861 .map(|(name, value)| (name.clone(), value.mapv(|v| v * 0.5)))
862 .collect())
863 })
864 .expect("fisher");
865 ewc.consolidate(&wide).expect("consolidate");
866 ewc
867 };
868
869 for online in [false, true] {
870 let ewc = build(online);
871 let narrow = make_params(&["w"], 2, 1.0);
872
873 let penalty = ewc.ewc_penalty(&narrow);
874 assert!(
875 penalty.is_err(),
876 "online={online}: a width-2 parameter against a width-4 anchor must be an error"
877 );
878 let text = penalty.expect_err("checked above").to_string();
879 assert!(
880 text.contains("length") && text.contains('w'),
881 "online={online}: unhelpful error {text}"
882 );
883
884 let gradient = ewc.ewc_gradient(&narrow);
885 assert!(
886 gradient.is_err(),
887 "online={online}: ewc_gradient must reject the mismatch too"
888 );
889
890 let wide = make_params(&["w"], 4, 2.0);
893 let ok_penalty = ewc.ewc_penalty(&wide).expect("matching widths");
894 assert!(
895 ok_penalty > 0.0,
896 "online={online}: a displaced parameter must incur a positive penalty"
897 );
898 let grads = ewc.ewc_gradient(&wide).expect("matching widths");
899 assert_eq!(grads["w"].len(), 4);
900 assert!(
901 grads["w"].iter().all(|g| *g > 0.0),
902 "online={online}: gradient must push back toward the anchor"
903 );
904 }
905 }
906
907 #[test]
910 fn online_consolidation_reports_a_fisher_length_change() {
911 let mut ewc: ElasticWeightConsolidation<F> = ElasticWeightConsolidation::new(1.0)
912 .with_num_samples(2)
913 .with_online(true);
914 let grads = |params: &HashMap<String, Array1<F>>, _: usize| {
915 Ok(params
916 .iter()
917 .map(|(name, value)| (name.clone(), value.mapv(|v| v * 0.5)))
918 .collect())
919 };
920
921 let narrow = make_params(&["w"], 2, 1.0);
922 ewc.compute_fisher_diagonal(&narrow, grads).expect("fisher");
923 ewc.consolidate(&narrow).expect("first consolidate");
924
925 let wide = make_params(&["w"], 5, 1.0);
927 ewc.compute_fisher_diagonal(&wide, grads)
928 .expect("fisher for the wider task");
929 let err = ewc
930 .consolidate(&wide)
931 .expect_err("a Fisher length change must be reported, not panic");
932 assert!(err.to_string().contains("length"), "{}", err);
933 }
934
935 #[test]
936 fn test_ewc_creation_and_configuration() {
937 let ewc: ElasticWeightConsolidation<F> = ElasticWeightConsolidation::new(1000.0)
938 .with_num_samples(50)
939 .with_online(true)
940 .with_gamma(0.9);
941
942 assert_eq!(ewc.num_tasks(), 0);
943 assert!(ewc.online);
944 assert!((ewc.gamma - 0.9).abs() < 1e-12);
945 assert!((ewc.lambda - 1000.0).abs() < 1e-12);
946 assert_eq!(ewc.num_samples_fisher, 50);
947 }
948
949 #[test]
950 fn test_fisher_diagonal_computation() {
951 let mut ewc: ElasticWeightConsolidation<F> =
952 ElasticWeightConsolidation::new(1.0).with_num_samples(10);
953
954 let params = make_params(&["w1", "w2"], 3, 1.0);
955
956 let grad_fn = |_p: &HashMap<String, Array1<F>>,
959 _sample: usize|
960 -> Result<HashMap<String, Array1<F>>> {
961 let mut g = HashMap::new();
962 g.insert("w1".to_string(), Array1::from_vec(vec![1.0, 2.0, 3.0]));
963 g.insert("w2".to_string(), Array1::from_vec(vec![0.5, 0.5, 0.5]));
964 Ok(g)
965 };
966
967 ewc.compute_fisher_diagonal(¶ms, grad_fn)
968 .expect("compute_fisher_diagonal should succeed");
969
970 let f1 = ewc
972 .fisher_diagonal
973 .get("w1")
974 .expect("w1 Fisher should exist");
975 assert!((f1[0] - 1.0).abs() < 1e-12);
976 assert!((f1[1] - 4.0).abs() < 1e-12);
977 assert!((f1[2] - 9.0).abs() < 1e-12);
978
979 let f2 = ewc
980 .fisher_diagonal
981 .get("w2")
982 .expect("w2 Fisher should exist");
983 assert!((f2[0] - 0.25).abs() < 1e-12);
984 }
985
986 #[test]
987 fn test_ewc_penalty_computation() {
988 let mut ewc: ElasticWeightConsolidation<F> =
989 ElasticWeightConsolidation::new(2.0).with_num_samples(5);
990
991 let anchor = make_params(&["w"], 2, 1.0);
992
993 let grad_fn = |_p: &HashMap<String, Array1<F>>,
995 _sample: usize|
996 -> Result<HashMap<String, Array1<F>>> {
997 let mut g = HashMap::new();
998 g.insert("w".to_string(), Array1::from_vec(vec![1.0, 1.0]));
999 Ok(g)
1000 };
1001
1002 ewc.compute_fisher_diagonal(&anchor, grad_fn)
1003 .expect("compute Fisher should succeed");
1004 ewc.consolidate(&anchor)
1005 .expect("consolidate should succeed");
1006
1007 let mut current = HashMap::new();
1009 current.insert("w".to_string(), Array1::from_vec(vec![2.0, 3.0]));
1010
1011 let penalty = ewc
1014 .ewc_penalty(¤t)
1015 .expect("ewc_penalty should succeed");
1016 assert!(
1017 (penalty - 5.0).abs() < 1e-12,
1018 "expected penalty 5.0, got {}",
1019 penalty
1020 );
1021 }
1022
1023 #[test]
1024 fn test_ewc_gradient_computation() {
1025 let mut ewc: ElasticWeightConsolidation<F> =
1026 ElasticWeightConsolidation::new(2.0).with_num_samples(5);
1027
1028 let anchor = make_params(&["w"], 2, 1.0);
1029
1030 let grad_fn = |_p: &HashMap<String, Array1<F>>,
1031 _sample: usize|
1032 -> Result<HashMap<String, Array1<F>>> {
1033 let mut g = HashMap::new();
1034 g.insert("w".to_string(), Array1::from_vec(vec![1.0, 1.0]));
1035 Ok(g)
1036 };
1037
1038 ewc.compute_fisher_diagonal(&anchor, grad_fn)
1039 .expect("compute Fisher should succeed");
1040 ewc.consolidate(&anchor)
1041 .expect("consolidate should succeed");
1042
1043 let mut current = HashMap::new();
1044 current.insert("w".to_string(), Array1::from_vec(vec![2.0, 3.0]));
1045
1046 let grads = ewc
1049 .ewc_gradient(¤t)
1050 .expect("ewc_gradient should succeed");
1051 let gw = grads.get("w").expect("gradient for w should exist");
1052 assert!((gw[0] - 2.0).abs() < 1e-12, "expected 2.0, got {}", gw[0]);
1053 assert!((gw[1] - 4.0).abs() < 1e-12, "expected 4.0, got {}", gw[1]);
1054 }
1055
1056 #[test]
1057 fn test_consolidation_workflow() {
1058 let mut ewc: ElasticWeightConsolidation<F> =
1059 ElasticWeightConsolidation::new(1.0).with_num_samples(5);
1060
1061 let params_task1 = make_params(&["w"], 2, 1.0);
1062
1063 let grad_fn = |_p: &HashMap<String, Array1<F>>,
1064 _sample: usize|
1065 -> Result<HashMap<String, Array1<F>>> {
1066 let mut g = HashMap::new();
1067 g.insert("w".to_string(), Array1::from_vec(vec![1.0, 2.0]));
1068 Ok(g)
1069 };
1070
1071 ewc.compute_fisher_diagonal(¶ms_task1, grad_fn)
1072 .expect("Fisher computation should succeed");
1073 ewc.consolidate(¶ms_task1)
1074 .expect("consolidation should succeed");
1075 assert_eq!(ewc.num_tasks(), 1);
1076
1077 let params_task2 = make_params(&["w"], 2, 2.0);
1079 ewc.compute_fisher_diagonal(¶ms_task2, grad_fn)
1080 .expect("Fisher computation for task 2 should succeed");
1081 ewc.consolidate(¶ms_task2)
1082 .expect("consolidation for task 2 should succeed");
1083 assert_eq!(ewc.num_tasks(), 2);
1084
1085 let penalty = ewc
1087 .ewc_penalty(¶ms_task2)
1088 .expect("penalty should succeed");
1089 assert!(
1093 penalty > 0.0,
1094 "penalty should be positive due to task1 Fisher, got {}",
1095 penalty
1096 );
1097 }
1098
1099 #[test]
1100 fn test_online_ewc_multiple_tasks() {
1101 let mut ewc: ElasticWeightConsolidation<F> = ElasticWeightConsolidation::new(1.0)
1102 .with_num_samples(5)
1103 .with_online(true)
1104 .with_gamma(0.5);
1105
1106 let params1 = make_params(&["w"], 2, 0.0);
1108 let grad_fn1 = |_p: &HashMap<String, Array1<F>>,
1109 _sample: usize|
1110 -> Result<HashMap<String, Array1<F>>> {
1111 let mut g = HashMap::new();
1112 g.insert("w".to_string(), Array1::from_vec(vec![1.0, 1.0]));
1113 Ok(g)
1114 };
1115 ewc.compute_fisher_diagonal(¶ms1, grad_fn1)
1116 .expect("Fisher 1 should succeed");
1117 ewc.consolidate(¶ms1)
1118 .expect("consolidate 1 should succeed");
1119 assert_eq!(ewc.num_tasks(), 1);
1120
1121 let fisher1 = ewc.task_fisher_diagonals[0]
1123 .get("w")
1124 .expect("Fisher should exist");
1125 assert!((fisher1[0] - 1.0).abs() < 1e-12);
1126
1127 let params2 = make_params(&["w"], 2, 1.0);
1129 let grad_fn2 = |_p: &HashMap<String, Array1<F>>,
1130 _sample: usize|
1131 -> Result<HashMap<String, Array1<F>>> {
1132 let mut g = HashMap::new();
1133 g.insert("w".to_string(), Array1::from_vec(vec![2.0, 2.0]));
1134 Ok(g)
1135 };
1136 ewc.compute_fisher_diagonal(¶ms2, grad_fn2)
1137 .expect("Fisher 2 should succeed");
1138 ewc.consolidate(¶ms2)
1139 .expect("consolidate 2 should succeed");
1140 assert_eq!(ewc.num_tasks(), 2);
1141
1142 let fisher2 = ewc.task_fisher_diagonals[1]
1144 .get("w")
1145 .expect("Fisher should exist");
1146 assert!(
1147 (fisher2[0] - 4.5).abs() < 1e-12,
1148 "expected 4.5, got {}",
1149 fisher2[0]
1150 );
1151
1152 let mut test_params = HashMap::new();
1154 test_params.insert("w".to_string(), Array1::from_vec(vec![2.0, 2.0]));
1155
1156 let penalty = ewc
1157 .ewc_penalty(&test_params)
1158 .expect("penalty should succeed");
1159 assert!(
1161 (penalty - 4.5).abs() < 1e-12,
1162 "expected 4.5, got {}",
1163 penalty
1164 );
1165 }
1166
1167 #[test]
1172 fn test_progressive_network_creation_and_columns() {
1173 let mut pn: ProgressiveNetworks<F> = ProgressiveNetworks::new(vec![16, 8]);
1174
1175 assert_eq!(pn.num_columns(), 0);
1176
1177 let col0 = pn
1178 .add_task_column(4, 2)
1179 .expect("add column 0 should succeed");
1180 assert_eq!(col0, 0);
1181 assert_eq!(pn.num_columns(), 1);
1182
1183 let params0 = pn
1185 .get_column_parameters(0)
1186 .expect("get column 0 should succeed");
1187 assert_eq!(params0.num_layers(), 3);
1188 assert!(!params0.frozen);
1189
1190 let col1 = pn
1192 .add_task_column(4, 2)
1193 .expect("add column 1 should succeed");
1194 assert_eq!(col1, 1);
1195 assert_eq!(pn.num_columns(), 2);
1196
1197 let params0_after = pn
1198 .get_column_parameters(0)
1199 .expect("get column 0 should succeed");
1200 assert!(params0_after.frozen, "column 0 should be frozen");
1201
1202 let params1 = pn
1203 .get_column_parameters(1)
1204 .expect("get column 1 should succeed");
1205 assert!(!params1.frozen, "column 1 should not be frozen");
1206 }
1207
1208 #[test]
1209 fn test_progressive_network_forward_single_column() {
1210 let mut pn: ProgressiveNetworks<F> = ProgressiveNetworks::new(vec![8]);
1211
1212 pn.add_task_column(4, 2).expect("add column should succeed");
1213
1214 let input = Array1::from_vec(vec![1.0, 0.5, -0.3, 0.8]);
1215 let output = pn.forward(&input, 0).expect("forward should succeed");
1216
1217 assert_eq!(output.len(), 2, "output should have 2 elements");
1219
1220 for val in output.iter() {
1222 assert!(val.is_finite(), "output should be finite, got {}", val);
1223 }
1224 }
1225
1226 #[test]
1227 fn test_progressive_network_forward_with_laterals() {
1228 let mut pn: ProgressiveNetworks<F> = ProgressiveNetworks::new(vec![8]);
1229
1230 pn.add_task_column(4, 2)
1231 .expect("add column 0 should succeed");
1232 pn.add_task_column(4, 2)
1233 .expect("add column 1 should succeed");
1234
1235 let input = Array1::from_vec(vec![1.0, 0.5, -0.3, 0.8]);
1236
1237 let output0 = pn
1239 .forward(&input, 0)
1240 .expect("forward task 0 should succeed");
1241 assert_eq!(output0.len(), 2);
1242
1243 let (output1, intermediates) = pn
1245 .forward_with_laterals(&input, 1)
1246 .expect("forward_with_laterals task 1 should succeed");
1247 assert_eq!(output1.len(), 2);
1248
1249 assert!(
1251 !intermediates.is_empty(),
1252 "should have intermediate activations"
1253 );
1254
1255 let diff: f64 = output0
1258 .iter()
1259 .zip(output1.iter())
1260 .map(|(a, b)| (a - b).abs())
1261 .sum();
1262 for val in output1.iter() {
1265 assert!(val.is_finite(), "output should be finite, got {}", val);
1266 }
1267 let _ = diff; }
1269
1270 #[test]
1271 fn test_progressive_network_freeze_column() {
1272 let mut pn: ProgressiveNetworks<F> = ProgressiveNetworks::new(vec![8]);
1273
1274 pn.add_task_column(4, 2).expect("add column should succeed");
1275
1276 assert!(
1277 !pn.get_column_parameters(0).expect("get params").frozen,
1278 "column should not be frozen initially"
1279 );
1280
1281 pn.freeze_column(0).expect("freeze should succeed");
1282
1283 assert!(
1284 pn.get_column_parameters(0).expect("get params").frozen,
1285 "column should be frozen after freeze_column"
1286 );
1287
1288 let err = pn.freeze_column(99);
1290 assert!(err.is_err(), "freezing out-of-range column should fail");
1291 }
1292
1293 #[test]
1303 fn lateral_connections_actually_contribute_with_varying_layer_widths() {
1304 let mut pn: ProgressiveNetworks<F> = ProgressiveNetworks::new(vec![8, 4]);
1305 pn.add_task_column(4, 2).expect("column 0");
1306 pn.add_task_column(4, 2).expect("column 1");
1307
1308 let input = Array1::from_vec(vec![0.6, -0.4, 0.9, -1.1]);
1309 let with_laterals = pn.forward(&input, 1).expect("forward with laterals");
1310
1311 let lateral_magnitude = pn.lateral_connections[1]
1313 .iter()
1314 .flat_map(|m| m.iter())
1315 .fold(0.0_f64, |acc, v| acc.max(v.abs()));
1316 assert!(
1317 lateral_magnitude > 0.0,
1318 "column 1 has no lateral weights to contribute"
1319 );
1320
1321 for matrix in pn.lateral_connections[1].iter_mut() {
1323 matrix.fill(0.0);
1324 }
1325 let without_laterals = pn.forward(&input, 1).expect("forward without laterals");
1326
1327 let delta = with_laterals
1328 .iter()
1329 .zip(without_laterals.iter())
1330 .map(|(a, b)| (a - b).abs())
1331 .fold(0.0_f64, f64::max);
1332 assert!(
1333 delta > 1e-9,
1334 "zeroing the lateral weights changed nothing (max delta {delta}); \
1335 the lateral contribution is still being silently dropped"
1336 );
1337
1338 let col0 = pn.forward(&input, 0).expect("forward column 0");
1340 assert_eq!(col0.len(), 2);
1341 }
1342
1343 #[test]
1346 fn a_mis_shaped_lateral_weight_is_an_error() {
1347 let mut pn: ProgressiveNetworks<F> = ProgressiveNetworks::new(vec![8, 4]);
1348 pn.add_task_column(4, 2).expect("column 0");
1349 pn.add_task_column(4, 2).expect("column 1");
1350
1351 pn.lateral_connections[1][1] = Array2::from_elem((4, 3), 0.1);
1353 let input = Array1::from_vec(vec![0.6, -0.4, 0.9, -1.1]);
1354 assert!(
1355 pn.forward(&input, 1).is_err(),
1356 "a lateral weight whose width does not match the concatenated \
1357 previous activations must be reported, not ignored"
1358 );
1359 }
1360
1361 #[test]
1362 fn test_progressive_network_multiple_tasks_forward() {
1363 let mut pn: ProgressiveNetworks<F> = ProgressiveNetworks::new(vec![8, 4]);
1364
1365 for _ in 0..3 {
1367 pn.add_task_column(4, 2).expect("add column should succeed");
1368 }
1369 assert_eq!(pn.num_columns(), 3);
1370
1371 let input = Array1::from_vec(vec![0.5, -0.5, 1.0, -1.0]);
1372
1373 for task_id in 0..3 {
1375 let output = pn
1376 .forward(&input, task_id)
1377 .unwrap_or_else(|_| panic!("forward task {} should succeed", task_id));
1378 assert_eq!(
1379 output.len(),
1380 2,
1381 "output for task {} should have 2 elements",
1382 task_id
1383 );
1384 for val in output.iter() {
1385 assert!(
1386 val.is_finite(),
1387 "output for task {} should be finite",
1388 task_id
1389 );
1390 }
1391 }
1392
1393 let err = pn.forward(&input, 10);
1395 assert!(err.is_err(), "forward with invalid task_id should fail");
1396 }
1397}