optirs_learned/ntm_optimizer/
meta_training.rs1use scirs2_core::ndarray::Array1;
30use scirs2_core::numeric::Float;
31use std::fmt::Debug;
32
33use super::{NtmController, NtmOptimizer};
34use crate::error::Result;
35use crate::es_meta_training::{
36 expect_fully_consumed, pull_matrix, pull_scalar, pull_vector, push_matrix, push_scalar,
37 push_vector, MetaTrainable,
38};
39
40pub use crate::es_meta_training::EsMetaTrainer as NtmMetaTrainer;
43pub use crate::es_meta_training::{MetaTrainingConfig, MetaTrainingReport, SelectionMetric};
44
45impl<T: Float + Debug + Send + Sync + 'static> NtmController<T> {
46 fn to_flat(&self) -> Vec<f64> {
48 let mut out = Vec::new();
49 push_matrix(&self.w_in, &mut out);
50 push_vector(&self.b_in, &mut out);
51 push_matrix(&self.w_key, &mut out);
52 push_vector(&self.b_key, &mut out);
53 push_vector(&self.w_beta, &mut out);
54 push_scalar(self.b_beta, &mut out);
55 push_vector(&self.w_gate, &mut out);
56 push_scalar(self.b_gate, &mut out);
57 push_matrix(&self.w_shift, &mut out);
58 push_vector(&self.b_shift, &mut out);
59 push_vector(&self.w_gamma, &mut out);
60 push_scalar(self.b_gamma, &mut out);
61 push_matrix(&self.w_erase, &mut out);
62 push_vector(&self.b_erase, &mut out);
63 push_matrix(&self.w_add, &mut out);
64 push_vector(&self.b_add, &mut out);
65 push_vector(&self.w_out, &mut out);
66 push_vector(&self.v_out, &mut out);
67 push_scalar(self.b_out, &mut out);
68 out
69 }
70
71 fn load_flat(&mut self, flat: &[f64]) -> Result<()> {
73 let mut cursor = 0usize;
74 pull_matrix(&mut self.w_in, flat, &mut cursor)?;
75 pull_vector(&mut self.b_in, flat, &mut cursor)?;
76 pull_matrix(&mut self.w_key, flat, &mut cursor)?;
77 pull_vector(&mut self.b_key, flat, &mut cursor)?;
78 pull_vector(&mut self.w_beta, flat, &mut cursor)?;
79 pull_scalar(&mut self.b_beta, flat, &mut cursor)?;
80 pull_vector(&mut self.w_gate, flat, &mut cursor)?;
81 pull_scalar(&mut self.b_gate, flat, &mut cursor)?;
82 pull_matrix(&mut self.w_shift, flat, &mut cursor)?;
83 pull_vector(&mut self.b_shift, flat, &mut cursor)?;
84 pull_vector(&mut self.w_gamma, flat, &mut cursor)?;
85 pull_scalar(&mut self.b_gamma, flat, &mut cursor)?;
86 pull_matrix(&mut self.w_erase, flat, &mut cursor)?;
87 pull_vector(&mut self.b_erase, flat, &mut cursor)?;
88 pull_matrix(&mut self.w_add, flat, &mut cursor)?;
89 pull_vector(&mut self.b_add, flat, &mut cursor)?;
90 pull_vector(&mut self.w_out, flat, &mut cursor)?;
91 pull_vector(&mut self.v_out, flat, &mut cursor)?;
92 pull_scalar(&mut self.b_out, flat, &mut cursor)?;
93 expect_fully_consumed(flat, cursor)
94 }
95}
96
97impl<T: Float + Debug + Send + Sync + 'static> NtmOptimizer<T> {
98 pub fn weight_vector(&self) -> Vec<f64> {
105 self.controller.to_flat()
106 }
107
108 pub fn weight_count(&self) -> usize {
110 self.controller.to_flat().len()
111 }
112
113 pub fn set_weight_vector(&mut self, flat: &[f64]) -> Result<()> {
121 self.controller.load_flat(flat)
122 }
123
124 pub fn reset_state(&mut self) {
133 self.memory = self.initial_memory.clone();
134 let uniform: T = scirs2_core::numeric::NumCast::from(1.0 / self.num_locations as f64)
135 .unwrap_or_else(|| T::zero());
136 self.prev_weights = Array1::from_elem(self.num_locations, uniform);
137 self.last_weights = self.prev_weights.clone();
138 self.prev_read = Array1::zeros(self.mem_width);
139 self.param_len = 0;
140 self.num_slots = 0;
141 self.chunk_bounds.clear();
142 self.grad_ema = Array1::zeros(0);
143 self.grad_sq_ema = Array1::zeros(0);
144 self.step_count = 0;
145 self.grad_norm_ema = T::zero();
146 self.current_lr = self.base_lr;
147 }
148}
149
150impl<T: Float + Debug + Send + Sync + 'static> MetaTrainable<T> for NtmOptimizer<T> {
151 fn weight_vector(&self) -> Vec<f64> {
152 NtmOptimizer::weight_vector(self)
153 }
154
155 fn set_weight_vector(&mut self, flat: &[f64]) -> Result<()> {
156 NtmOptimizer::set_weight_vector(self, flat)
157 }
158
159 fn reset_state(&mut self) {
160 NtmOptimizer::reset_state(self)
161 }
162
163 fn weight_count(&self) -> usize {
164 NtmOptimizer::weight_count(self)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::domain_objectives::{MetaObjective, QuadraticObjective};
172 use crate::domain_optimizers::AdvancedOptimizer;
173 use crate::ntm_optimizer::NtmOptimizerConfig;
174
175 fn optimizer(seed: u64) -> NtmOptimizer<f64> {
176 NtmOptimizer::new(NtmOptimizerConfig {
177 num_slots: 4,
178 num_locations: 6,
179 mem_width: 4,
180 hidden_dim: 6,
181 shift_range: 1,
182 base_lr: 0.2,
183 seed,
184 ..Default::default()
185 })
186 .expect("optimizer")
187 }
188
189 fn training_tasks() -> Vec<QuadraticObjective<f64>> {
190 vec![
191 QuadraticObjective::isotropic(8, 1.0, 0.7).expect("task"),
192 QuadraticObjective::from_curvature(vec![1.0, 4.0, 1.0, 4.0, 1.0, 4.0, 1.0, 4.0], 0.5)
193 .expect("task"),
194 ]
195 }
196
197 #[test]
198 fn weight_vector_round_trips() {
199 let mut opt = optimizer(13);
200 let flat = opt.weight_vector();
201 assert!(!flat.is_empty());
202 assert_eq!(flat.len(), opt.weight_count());
203 assert!(
204 flat.iter().any(|v| v.abs() > 1e-9),
205 "the weight vector is all zeros, so it is not reading real weights"
206 );
207
208 opt.set_weight_vector(&flat).expect("round trip");
209 assert_eq!(opt.weight_vector(), flat);
210
211 assert!(opt.set_weight_vector(&flat[..flat.len() - 1]).is_err());
212 let mut too_long = flat.clone();
213 too_long.push(0.0);
214 assert!(opt.set_weight_vector(&too_long).is_err());
215 }
216
217 #[test]
220 fn the_memory_matrix_is_not_part_of_the_weight_vector() {
221 let opt = optimizer(13);
222 let weights = opt.weight_count();
223 let memory_cells = opt.memory().len();
224 assert!(memory_cells > 0);
225
226 let mut mutated = opt.clone();
232 let params = Array1::from_vec(vec![1.0; 8]);
233 let gradient = Array1::from_vec(vec![0.3; 8]);
234 for _ in 0..4 {
235 mutated.step(¶ms, &gradient).expect("step");
236 }
237 assert!(
238 (mutated.memory() - opt.memory()).mapv(f64::abs).sum() > 1e-12,
239 "stepping did not write to memory, so there is nothing to distinguish"
240 );
241 assert_eq!(
242 mutated.weight_count(),
243 weights,
244 "the weight count changed when only the memory did"
245 );
246 assert_eq!(
247 mutated.weight_vector(),
248 opt.weight_vector(),
249 "writing to memory changed the weight vector, so memory is being \
250 treated as a learned parameter"
251 );
252 }
253
254 #[test]
257 fn reset_state_restores_the_seeded_memory_and_clears_momentum() {
258 let params = Array1::from_vec(vec![1.0; 8]);
259 let gradient = Array1::from_vec(vec![0.5; 8]);
260
261 let mut fresh = optimizer(29);
262 let pristine_memory = fresh.memory().clone();
263 let baseline = fresh.step(¶ms, &gradient).expect("step");
264
265 let mut used = optimizer(29);
266 for _ in 0..5 {
267 used.step(¶ms, &gradient).expect("warm-up step");
268 }
269 assert!(
270 (used.memory() - &pristine_memory).mapv(f64::abs).sum() > 1e-12,
271 "the warm-up left the memory untouched"
272 );
273
274 used.reset_state();
275 assert!(
276 (used.memory() - &pristine_memory).mapv(f64::abs).sum() < 1e-12,
277 "reset_state did not restore the seeded memory"
278 );
279 let after_reset = used.step(¶ms, &gradient).expect("step");
280 assert!(
281 (&after_reset - &baseline).mapv(f64::abs).sum() < 1e-12,
282 "reset_state did not restore the fresh-optimizer behaviour"
283 );
284 assert_eq!(used.weight_vector(), fresh.weight_vector());
285 }
286
287 #[test]
294 fn every_accessor_stays_safe_after_a_bare_reset() {
295 let mut opt = optimizer(37);
296 let params = Array1::from_vec(vec![1.0; 8]);
297 let gradient = Array1::from_vec(vec![0.25; 8]);
298 for _ in 0..3 {
299 opt.step(¶ms, &gradient).expect("warm-up");
300 }
301
302 opt.reset_state();
303
304 let state = opt.get_state();
305 assert_eq!(state.step_count, 0);
306 assert!(state.current_lr.is_finite());
307 assert!(state.grad_norm_ema.is_finite());
308 assert_eq!(opt.get_learning_rate(), state.current_lr);
309 assert_eq!(opt.num_slots(), 0);
311 assert_eq!(opt.num_locations(), 6);
312 assert_eq!(opt.mem_width(), 4);
313 assert!(opt.hidden_dim() > 0);
314 assert_eq!(opt.shift_range(), 1);
315 assert_eq!(opt.seed(), 37);
316 assert_eq!(opt.name(), "NtmOptimizer");
317 let weights = opt.last_address_weights();
319 assert_eq!(weights.len(), opt.num_locations());
320 let total: f64 = weights.iter().sum();
321 assert!(
322 (total - 1.0).abs() < 1e-12,
323 "addressing weights should sum to 1 after a reset, got {total}"
324 );
325 assert_eq!(opt.memory().dim(), (6, 4));
326 assert!(!opt.weight_vector().is_empty());
327
328 opt.reset_state();
330 assert_eq!(opt.num_slots(), 0);
331 assert!(opt.step(¶ms, &gradient).is_ok());
332 assert!(opt.num_slots() > 0);
333 }
334
335 #[test]
336 fn meta_loss_is_a_real_measurement() {
337 let tasks = training_tasks();
338 let refs: Vec<&dyn MetaObjective<f64>> =
339 tasks.iter().map(|t| t as &dyn MetaObjective<f64>).collect();
340 let trainer = NtmMetaTrainer::new(MetaTrainingConfig::default()).expect("trainer");
341
342 let a = trainer.meta_loss(&optimizer(1), &refs).expect("loss a");
343 let b = trainer.meta_loss(&optimizer(2), &refs).expect("loss b");
344 assert!(a.is_finite() && b.is_finite() && a > 0.0 && b > 0.0);
345 assert!(
346 (a - b).abs() > 1e-9,
347 "two different controller draws scored identically ({a} vs {b})"
348 );
349
350 let opt = optimizer(1);
351 let snapshot = opt.weight_vector();
352 let repeat = trainer.meta_loss(&opt, &refs).expect("repeat");
353 assert_eq!(opt.weight_vector(), snapshot);
354 assert!((repeat - a).abs() < 1e-12, "meta_loss is not deterministic");
355 }
356}