optirs_learned/gnn_optimizer/
meta_training.rs1use scirs2_core::ndarray::Array1;
14use scirs2_core::numeric::Float;
15use std::fmt::Debug;
16
17use super::{GnnOptimizer, GnnWeights, GruCell};
18use crate::error::Result;
19use crate::es_meta_training::{
20 expect_fully_consumed, pull_matrix, pull_scalar, pull_vector, push_matrix, push_scalar,
21 push_vector, MetaTrainable,
22};
23
24pub use crate::es_meta_training::EsMetaTrainer as GnnMetaTrainer;
27pub use crate::es_meta_training::{MetaTrainingConfig, MetaTrainingReport, SelectionMetric};
28
29impl<T: Float + Debug + Send + Sync + 'static> GruCell<T> {
30 fn extend_flat(&self, out: &mut Vec<f64>) {
32 push_matrix(&self.w_z, out);
33 push_matrix(&self.u_z, out);
34 push_vector(&self.b_z, out);
35 push_matrix(&self.w_r, out);
36 push_matrix(&self.u_r, out);
37 push_vector(&self.b_r, out);
38 push_matrix(&self.w_h, out);
39 push_matrix(&self.u_h, out);
40 push_vector(&self.b_h, out);
41 }
42
43 fn load_flat(&mut self, flat: &[f64], cursor: &mut usize) -> Result<()> {
45 pull_matrix(&mut self.w_z, flat, cursor)?;
46 pull_matrix(&mut self.u_z, flat, cursor)?;
47 pull_vector(&mut self.b_z, flat, cursor)?;
48 pull_matrix(&mut self.w_r, flat, cursor)?;
49 pull_matrix(&mut self.u_r, flat, cursor)?;
50 pull_vector(&mut self.b_r, flat, cursor)?;
51 pull_matrix(&mut self.w_h, flat, cursor)?;
52 pull_matrix(&mut self.u_h, flat, cursor)?;
53 pull_vector(&mut self.b_h, flat, cursor)?;
54 Ok(())
55 }
56}
57
58impl<T: Float + Debug + Send + Sync + 'static> GnnWeights<T> {
59 fn to_flat(&self) -> Vec<f64> {
61 let mut out = Vec::new();
62 push_matrix(&self.w_in, &mut out);
63 push_vector(&self.b_in, &mut out);
64 push_matrix(&self.w_msg, &mut out);
65 push_vector(&self.b_msg, &mut out);
66 self.gru.extend_flat(&mut out);
67 push_vector(&self.w_out, &mut out);
68 push_scalar(self.b_out, &mut out);
69 out
70 }
71
72 fn load_flat(&mut self, flat: &[f64]) -> Result<()> {
74 let mut cursor = 0usize;
75 pull_matrix(&mut self.w_in, flat, &mut cursor)?;
76 pull_vector(&mut self.b_in, flat, &mut cursor)?;
77 pull_matrix(&mut self.w_msg, flat, &mut cursor)?;
78 pull_vector(&mut self.b_msg, flat, &mut cursor)?;
79 self.gru.load_flat(flat, &mut cursor)?;
80 pull_vector(&mut self.w_out, flat, &mut cursor)?;
81 pull_scalar(&mut self.b_out, flat, &mut cursor)?;
82 expect_fully_consumed(flat, cursor)
83 }
84}
85
86impl<T: Float + Debug + Send + Sync + 'static> GnnOptimizer<T> {
87 pub fn weight_vector(&self) -> Vec<f64> {
93 self.weights.to_flat()
94 }
95
96 pub fn weight_count(&self) -> usize {
98 self.weights.to_flat().len()
99 }
100
101 pub fn set_weight_vector(&mut self, flat: &[f64]) -> Result<()> {
109 self.weights.load_flat(flat)
110 }
111
112 pub fn reset_state(&mut self) {
120 self.param_len = 0;
121 self.num_nodes = 0;
122 self.chunk_bounds.clear();
123 self.adjacency.clear();
124 self.grad_ema = Array1::zeros(0);
125 self.grad_sq_ema = Array1::zeros(0);
126 self.node_hidden.clear();
127 self.node_history = Array1::zeros(0);
128 self.step_count = 0;
129 self.grad_norm_ema = T::zero();
130 self.current_lr = self.base_lr;
131 }
132}
133
134impl<T: Float + Debug + Send + Sync + 'static> MetaTrainable<T> for GnnOptimizer<T> {
135 fn weight_vector(&self) -> Vec<f64> {
136 GnnOptimizer::weight_vector(self)
137 }
138
139 fn set_weight_vector(&mut self, flat: &[f64]) -> Result<()> {
140 GnnOptimizer::set_weight_vector(self, flat)
141 }
142
143 fn reset_state(&mut self) {
144 GnnOptimizer::reset_state(self)
145 }
146
147 fn weight_count(&self) -> usize {
148 GnnOptimizer::weight_count(self)
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::domain_objectives::{MetaObjective, QuadraticObjective};
156 use crate::domain_optimizers::AdvancedOptimizer;
157 use crate::gnn_optimizer::{GnnOptimizerConfig, GraphTopology};
158
159 fn optimizer(seed: u64) -> GnnOptimizer<f64> {
160 GnnOptimizer::new(GnnOptimizerConfig {
161 num_nodes: 4,
162 hidden_dim: 6,
163 num_message_rounds: 2,
164 topology: GraphTopology::Chain,
165 base_lr: 0.2,
166 seed,
167 ..Default::default()
168 })
169 .expect("optimizer")
170 }
171
172 fn training_tasks() -> Vec<QuadraticObjective<f64>> {
173 vec![
174 QuadraticObjective::isotropic(8, 1.0, 0.7).expect("task"),
175 QuadraticObjective::from_curvature(vec![1.0, 4.0, 1.0, 4.0, 1.0, 4.0, 1.0, 4.0], 0.5)
176 .expect("task"),
177 ]
178 }
179
180 #[test]
181 fn weight_vector_round_trips() {
182 let mut opt = optimizer(11);
183 let flat = opt.weight_vector();
184 assert!(!flat.is_empty());
185 assert_eq!(flat.len(), opt.weight_count());
186 assert!(
187 flat.iter().any(|v| v.abs() > 1e-9),
188 "the weight vector is all zeros, so it is not reading real weights"
189 );
190
191 opt.set_weight_vector(&flat).expect("round trip");
192 assert_eq!(opt.weight_vector(), flat);
193
194 assert!(opt.set_weight_vector(&flat[..flat.len() - 1]).is_err());
196 let mut too_long = flat.clone();
197 too_long.push(0.0);
198 assert!(opt.set_weight_vector(&too_long).is_err());
199 }
200
201 #[test]
204 fn the_weights_actually_drive_the_update() {
205 let mut opt = optimizer(3);
206 let params = Array1::from_vec(vec![1.0; 8]);
207 let gradient = Array1::from_vec(vec![0.4, -0.3, 0.2, 0.9, -0.1, 0.5, -0.7, 0.15]);
208
209 let before = opt.step(¶ms, &gradient).expect("step");
210
211 let mut flat = opt.weight_vector();
212 let last = flat.len() - 1;
215 flat[last] = -8.0;
216 let mut shrunk = optimizer(3);
217 shrunk.set_weight_vector(&flat).expect("set");
218 let after = shrunk.step(¶ms, &gradient).expect("step");
219
220 let moved_before = (&before - ¶ms).mapv(f64::abs).sum();
221 let moved_after = (&after - ¶ms).mapv(f64::abs).sum();
222 assert!(
223 moved_after < moved_before * 0.5,
224 "a strongly negative readout bias must shrink the update: \
225 {moved_before} -> {moved_after}"
226 );
227 }
228
229 #[test]
230 fn meta_loss_is_a_real_measurement() {
231 let tasks = training_tasks();
232 let refs: Vec<&dyn MetaObjective<f64>> =
233 tasks.iter().map(|t| t as &dyn MetaObjective<f64>).collect();
234 let trainer = GnnMetaTrainer::new(MetaTrainingConfig::default()).expect("trainer");
235
236 let a = trainer.meta_loss(&optimizer(1), &refs).expect("loss a");
237 let b = trainer.meta_loss(&optimizer(2), &refs).expect("loss b");
238 assert!(a.is_finite() && b.is_finite());
239 assert!(
240 a > 0.0 && b > 0.0,
241 "normalised loss curves must be positive"
242 );
243 assert!(
244 (a - b).abs() > 1e-9,
245 "two different weight draws scored identically ({a} vs {b}), so meta_loss is not \
246 reading the weights"
247 );
248
249 let opt = optimizer(1);
251 let snapshot = opt.weight_vector();
252 let repeat = trainer.meta_loss(&opt, &refs).expect("repeat");
253 assert_eq!(opt.weight_vector(), snapshot);
254 assert!((repeat - a).abs() < 1e-12, "meta_loss is not deterministic");
255 }
256
257 #[test]
258 fn meta_loss_rejects_an_empty_task_set() {
259 let trainer = GnnMetaTrainer::new(MetaTrainingConfig::default()).expect("trainer");
260 let empty: [&dyn MetaObjective<f64>; 0] = [];
261 assert!(trainer.meta_loss(&optimizer(1), &empty).is_err());
262 assert!(trainer.train(&mut optimizer(1), &empty).is_err());
263 }
264
265 #[test]
268 fn reset_state_clears_the_persistent_state() {
269 let params = Array1::from_vec(vec![1.0; 8]);
270 let gradient = Array1::from_vec(vec![0.5; 8]);
271
272 let mut fresh = optimizer(21);
273 let baseline = fresh.step(¶ms, &gradient).expect("step");
274
275 let mut used = optimizer(21);
276 for _ in 0..5 {
277 used.step(¶ms, &gradient).expect("warm-up step");
278 }
279 let warm = used.step(¶ms, &gradient).expect("step");
280 assert!(
281 (&warm - &baseline).mapv(f64::abs).sum() > 1e-12,
282 "five warm-up steps left no trace, so there is no state to reset"
283 );
284
285 used.reset_state();
286 let after_reset = used.step(¶ms, &gradient).expect("step");
287 assert!(
288 (&after_reset - &baseline).mapv(f64::abs).sum() < 1e-12,
289 "reset_state did not restore the fresh-optimizer behaviour"
290 );
291 assert_eq!(used.weight_vector(), fresh.weight_vector());
293 }
294
295 #[test]
301 fn every_accessor_stays_safe_after_a_bare_reset() {
302 let mut opt = optimizer(31);
303 let params = Array1::from_vec(vec![1.0; 8]);
305 let gradient = Array1::from_vec(vec![0.25; 8]);
306 for _ in 0..3 {
307 opt.step(¶ms, &gradient).expect("warm-up");
308 }
309
310 opt.reset_state();
311
312 let state = opt.get_state();
314 assert_eq!(state.step_count, 0);
315 assert!(state.current_lr.is_finite());
316 assert!(state.grad_norm_ema.is_finite());
317 assert_eq!(opt.get_learning_rate(), state.current_lr);
318 assert_eq!(opt.num_nodes(), 0);
320 assert!(opt.hidden_dim() > 0);
321 assert_eq!(opt.seed(), 31);
322 assert_eq!(opt.name(), "GNNOptimizer");
323 assert!(!opt.weight_vector().is_empty());
324 assert!(opt.weight_count() > 0);
325 opt.reset_state();
327 assert_eq!(opt.num_nodes(), 0);
328 assert!(opt.step(¶ms, &gradient).is_ok());
330 assert!(opt.num_nodes() > 0);
331 }
332}