geographdb_core/algorithms/
natural_grad.rs1pub fn softmax(logits: &[f32]) -> Vec<f32> {
24 let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
25 let exps: Vec<f32> = logits.iter().map(|&l| (l - max).exp()).collect();
26 let sum: f32 = exps.iter().sum();
27 exps.iter().map(|&e| e / sum).collect()
28}
29
30pub fn diagonal_fisher(probs: &[f32]) -> Vec<f32> {
34 probs.iter().map(|&p| p * (1.0 - p)).collect()
35}
36
37pub fn kl_divergence(p: &[f32], q: &[f32]) -> f32 {
43 assert_eq!(p.len(), q.len());
44 p.iter()
45 .zip(q.iter())
46 .map(|(&pi, &qi)| {
47 if pi <= 0.0 {
48 0.0
49 } else {
50 pi * (pi / qi.max(1e-30)).ln()
51 }
52 })
53 .sum()
54}
55
56pub fn fisher_rao_dist(p: &[f32], q: &[f32]) -> f32 {
63 assert_eq!(p.len(), q.len());
64 let bc: f32 = p
65 .iter()
66 .zip(q.iter())
67 .map(|(&pi, &qi)| (pi * qi).sqrt())
68 .sum();
69 2.0 * bc.clamp(-1.0, 1.0).acos()
70}
71
72pub fn natural_gradient(grad: &[f32], probs: &[f32], eps: f32) -> Vec<f32> {
81 assert_eq!(grad.len(), probs.len());
82 let fisher = diagonal_fisher(probs);
83 grad.iter()
84 .zip(fisher.iter())
85 .map(|(&g, &f)| g / (f + eps))
86 .collect()
87}
88
89pub struct StepComparison {
93 pub logits_before: Vec<f32>,
95 pub logits_vanilla: Vec<f32>,
97 pub logits_natural: Vec<f32>,
99 pub euclid_vanilla: f32,
101 pub euclid_natural: f32,
103 pub kl_vanilla: f32,
105 pub kl_natural: f32,
107 pub fisher_rao_vanilla: f32,
109 pub fisher_rao_natural: f32,
111}
112
113pub fn compare_steps(logits: &[f32], grad: &[f32], lr: f32, eps: f32) -> StepComparison {
119 let probs_before = softmax(logits);
120 let nat_grad = natural_gradient(grad, &probs_before, eps);
121
122 let logits_vanilla: Vec<f32> = logits.iter().zip(grad).map(|(&t, &g)| t - lr * g).collect();
123 let logits_natural: Vec<f32> = logits
124 .iter()
125 .zip(&nat_grad)
126 .map(|(&t, &ng)| t - lr * ng)
127 .collect();
128
129 let probs_vanilla = softmax(&logits_vanilla);
130 let probs_natural = softmax(&logits_natural);
131
132 let euclid_vanilla = logits
133 .iter()
134 .zip(&logits_vanilla)
135 .map(|(&a, &b)| (a - b).powi(2))
136 .sum::<f32>()
137 .sqrt();
138 let euclid_natural = logits
139 .iter()
140 .zip(&logits_natural)
141 .map(|(&a, &b)| (a - b).powi(2))
142 .sum::<f32>()
143 .sqrt();
144
145 StepComparison {
146 logits_before: logits.to_vec(),
147 logits_vanilla,
148 logits_natural,
149 euclid_vanilla,
150 euclid_natural,
151 kl_vanilla: kl_divergence(&probs_before, &probs_vanilla),
152 kl_natural: kl_divergence(&probs_before, &probs_natural),
153 fisher_rao_vanilla: fisher_rao_dist(&probs_before, &probs_vanilla),
154 fisher_rao_natural: fisher_rao_dist(&probs_before, &probs_natural),
155 }
156}
157
158#[cfg(test)]
161mod tests {
162 use super::*;
163
164 const EPS: f32 = 1e-5;
165
166 #[test]
167 fn test_softmax_sums_to_one() {
168 let logits = vec![1.0f32, 2.0, 0.5, -1.0, 3.0];
169 let p = softmax(&logits);
170 let sum: f32 = p.iter().sum();
171 assert!((sum - 1.0).abs() < EPS, "softmax must sum to 1, got {sum}");
172 }
173
174 #[test]
175 fn test_softmax_argmax_preserved() {
176 let logits = vec![0.1f32, 5.0, 0.3, -2.0];
177 let p = softmax(&logits);
178 let argmax = p
179 .iter()
180 .enumerate()
181 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
182 .map(|(i, _)| i)
183 .unwrap();
184 assert_eq!(argmax, 1, "largest logit must map to largest probability");
185 }
186
187 #[test]
188 fn test_softmax_uniform_logits() {
189 let logits = vec![0.0f32; 4];
190 let p = softmax(&logits);
191 for &pi in &p {
192 assert!((pi - 0.25).abs() < EPS, "uniform logits → uniform probs");
193 }
194 }
195
196 #[test]
197 fn test_diagonal_fisher_uniform() {
198 let probs = vec![0.25f32; 4];
200 let f = diagonal_fisher(&probs);
201 for &fi in &f {
202 assert!(
203 (fi - 0.1875).abs() < EPS,
204 "F_ii for uniform = 0.1875, got {fi}"
205 );
206 }
207 }
208
209 #[test]
210 fn test_diagonal_fisher_concentrated() {
211 let probs = vec![0.999f32, 0.0005, 0.0005];
213 let f = diagonal_fisher(&probs);
214 assert!(
215 f[0] < 0.01,
216 "Fisher near 1.0 should be near 0, got {}",
217 f[0]
218 );
219 }
220
221 #[test]
222 fn test_natural_gradient_scales_by_fisher_inverse() {
223 let probs = vec![0.5f32, 0.5];
225 let grad = vec![1.0f32, -1.0];
226 let nat = natural_gradient(&grad, &probs, 0.0);
227 assert!(
229 (nat[0] - 4.0).abs() < EPS,
230 "natural grad should be 4x vanilla at p=0.5"
231 );
232 assert!(
233 (nat[1] + 4.0).abs() < EPS,
234 "natural grad should be 4x vanilla at p=0.5"
235 );
236 }
237
238 #[test]
239 fn test_natural_gradient_eps_regularization() {
240 let probs = vec![0.0001f32, 0.9999];
242 let grad = vec![1.0f32, 0.0];
243 let nat_eps = natural_gradient(&grad, &probs, 1e-3);
244 let nat_no_eps = natural_gradient(&grad, &probs, 1e-30);
245 assert!(nat_eps[0].is_finite(), "eps should prevent NaN/inf");
247 assert!(
249 nat_no_eps[0] > nat_eps[0],
250 "eps reduces magnitude near boundary"
251 );
252 }
253
254 #[test]
255 fn test_kl_divergence_self_is_zero() {
256 let p = vec![0.2f32, 0.5, 0.3];
257 let kl = kl_divergence(&p, &p);
258 assert!(kl.abs() < EPS, "KL(p||p) must be zero, got {kl}");
259 }
260
261 #[test]
262 fn test_kl_divergence_positive() {
263 let p = vec![0.7f32, 0.3];
264 let q = vec![0.3f32, 0.7];
265 assert!(
266 kl_divergence(&p, &q) > 0.0,
267 "KL divergence between different distributions must be positive"
268 );
269 }
270
271 #[test]
272 fn test_fisher_rao_dist_self_is_zero() {
273 let p = vec![0.4f32, 0.4, 0.2];
274 let d = fisher_rao_dist(&p, &p);
275 assert!(
276 d.abs() < EPS,
277 "Fisher-Rao distance to self must be zero, got {d}"
278 );
279 }
280
281 #[test]
282 fn test_fisher_rao_dist_nonnegative() {
283 let p = vec![0.6f32, 0.4];
284 let q = vec![0.2f32, 0.8];
285 assert!(fisher_rao_dist(&p, &q) >= 0.0);
286 }
287
288 #[test]
289 fn test_natural_grad_smaller_fisher_rao_step() {
290 let logits = vec![3.0f32, 0.5, 0.5, 0.5];
298 let grad = vec![1.0f32, 0.1, 0.1, 0.1]; let lr = 0.3;
300
301 let cmp = compare_steps(&logits, &grad, lr, 1e-4);
302
303 assert!(cmp.kl_vanilla.is_finite() && cmp.kl_vanilla >= 0.0);
308 assert!(cmp.kl_natural.is_finite() && cmp.kl_natural >= 0.0);
309 assert!(cmp.fisher_rao_vanilla.is_finite());
310 assert!(cmp.fisher_rao_natural.is_finite());
311 }
312}