pub fn softmax(logits: &[f32]) -> Vec<f32> {
let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = logits.iter().map(|&l| (l - max).exp()).collect();
let sum: f32 = exps.iter().sum();
exps.iter().map(|&e| e / sum).collect()
}
pub fn diagonal_fisher(probs: &[f32]) -> Vec<f32> {
probs.iter().map(|&p| p * (1.0 - p)).collect()
}
pub fn kl_divergence(p: &[f32], q: &[f32]) -> f32 {
assert_eq!(p.len(), q.len());
p.iter()
.zip(q.iter())
.map(|(&pi, &qi)| {
if pi <= 0.0 {
0.0
} else {
pi * (pi / qi.max(1e-30)).ln()
}
})
.sum()
}
pub fn fisher_rao_dist(p: &[f32], q: &[f32]) -> f32 {
assert_eq!(p.len(), q.len());
let bc: f32 = p
.iter()
.zip(q.iter())
.map(|(&pi, &qi)| (pi * qi).sqrt())
.sum();
2.0 * bc.clamp(-1.0, 1.0).acos()
}
pub fn natural_gradient(grad: &[f32], probs: &[f32], eps: f32) -> Vec<f32> {
assert_eq!(grad.len(), probs.len());
let fisher = diagonal_fisher(probs);
grad.iter()
.zip(fisher.iter())
.map(|(&g, &f)| g / (f + eps))
.collect()
}
pub struct StepComparison {
pub logits_before: Vec<f32>,
pub logits_vanilla: Vec<f32>,
pub logits_natural: Vec<f32>,
pub euclid_vanilla: f32,
pub euclid_natural: f32,
pub kl_vanilla: f32,
pub kl_natural: f32,
pub fisher_rao_vanilla: f32,
pub fisher_rao_natural: f32,
}
pub fn compare_steps(logits: &[f32], grad: &[f32], lr: f32, eps: f32) -> StepComparison {
let probs_before = softmax(logits);
let nat_grad = natural_gradient(grad, &probs_before, eps);
let logits_vanilla: Vec<f32> = logits.iter().zip(grad).map(|(&t, &g)| t - lr * g).collect();
let logits_natural: Vec<f32> = logits
.iter()
.zip(&nat_grad)
.map(|(&t, &ng)| t - lr * ng)
.collect();
let probs_vanilla = softmax(&logits_vanilla);
let probs_natural = softmax(&logits_natural);
let euclid_vanilla = logits
.iter()
.zip(&logits_vanilla)
.map(|(&a, &b)| (a - b).powi(2))
.sum::<f32>()
.sqrt();
let euclid_natural = logits
.iter()
.zip(&logits_natural)
.map(|(&a, &b)| (a - b).powi(2))
.sum::<f32>()
.sqrt();
StepComparison {
logits_before: logits.to_vec(),
logits_vanilla,
logits_natural,
euclid_vanilla,
euclid_natural,
kl_vanilla: kl_divergence(&probs_before, &probs_vanilla),
kl_natural: kl_divergence(&probs_before, &probs_natural),
fisher_rao_vanilla: fisher_rao_dist(&probs_before, &probs_vanilla),
fisher_rao_natural: fisher_rao_dist(&probs_before, &probs_natural),
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f32 = 1e-5;
#[test]
fn test_softmax_sums_to_one() {
let logits = vec![1.0f32, 2.0, 0.5, -1.0, 3.0];
let p = softmax(&logits);
let sum: f32 = p.iter().sum();
assert!((sum - 1.0).abs() < EPS, "softmax must sum to 1, got {sum}");
}
#[test]
fn test_softmax_argmax_preserved() {
let logits = vec![0.1f32, 5.0, 0.3, -2.0];
let p = softmax(&logits);
let argmax = p
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(i, _)| i)
.unwrap();
assert_eq!(argmax, 1, "largest logit must map to largest probability");
}
#[test]
fn test_softmax_uniform_logits() {
let logits = vec![0.0f32; 4];
let p = softmax(&logits);
for &pi in &p {
assert!((pi - 0.25).abs() < EPS, "uniform logits → uniform probs");
}
}
#[test]
fn test_diagonal_fisher_uniform() {
let probs = vec![0.25f32; 4];
let f = diagonal_fisher(&probs);
for &fi in &f {
assert!(
(fi - 0.1875).abs() < EPS,
"F_ii for uniform = 0.1875, got {fi}"
);
}
}
#[test]
fn test_diagonal_fisher_concentrated() {
let probs = vec![0.999f32, 0.0005, 0.0005];
let f = diagonal_fisher(&probs);
assert!(
f[0] < 0.01,
"Fisher near 1.0 should be near 0, got {}",
f[0]
);
}
#[test]
fn test_natural_gradient_scales_by_fisher_inverse() {
let probs = vec![0.5f32, 0.5];
let grad = vec![1.0f32, -1.0];
let nat = natural_gradient(&grad, &probs, 0.0);
assert!(
(nat[0] - 4.0).abs() < EPS,
"natural grad should be 4x vanilla at p=0.5"
);
assert!(
(nat[1] + 4.0).abs() < EPS,
"natural grad should be 4x vanilla at p=0.5"
);
}
#[test]
fn test_natural_gradient_eps_regularization() {
let probs = vec![0.0001f32, 0.9999];
let grad = vec![1.0f32, 0.0];
let nat_eps = natural_gradient(&grad, &probs, 1e-3);
let nat_no_eps = natural_gradient(&grad, &probs, 1e-30);
assert!(nat_eps[0].is_finite(), "eps should prevent NaN/inf");
assert!(
nat_no_eps[0] > nat_eps[0],
"eps reduces magnitude near boundary"
);
}
#[test]
fn test_kl_divergence_self_is_zero() {
let p = vec![0.2f32, 0.5, 0.3];
let kl = kl_divergence(&p, &p);
assert!(kl.abs() < EPS, "KL(p||p) must be zero, got {kl}");
}
#[test]
fn test_kl_divergence_positive() {
let p = vec![0.7f32, 0.3];
let q = vec![0.3f32, 0.7];
assert!(
kl_divergence(&p, &q) > 0.0,
"KL divergence between different distributions must be positive"
);
}
#[test]
fn test_fisher_rao_dist_self_is_zero() {
let p = vec![0.4f32, 0.4, 0.2];
let d = fisher_rao_dist(&p, &p);
assert!(
d.abs() < EPS,
"Fisher-Rao distance to self must be zero, got {d}"
);
}
#[test]
fn test_fisher_rao_dist_nonnegative() {
let p = vec![0.6f32, 0.4];
let q = vec![0.2f32, 0.8];
assert!(fisher_rao_dist(&p, &q) >= 0.0);
}
#[test]
fn test_natural_grad_smaller_fisher_rao_step() {
let logits = vec![3.0f32, 0.5, 0.5, 0.5];
let grad = vec![1.0f32, 0.1, 0.1, 0.1]; let lr = 0.3;
let cmp = compare_steps(&logits, &grad, lr, 1e-4);
assert!(cmp.kl_vanilla.is_finite() && cmp.kl_vanilla >= 0.0);
assert!(cmp.kl_natural.is_finite() && cmp.kl_natural >= 0.0);
assert!(cmp.fisher_rao_vanilla.is_finite());
assert!(cmp.fisher_rao_natural.is_finite());
}
}