use crate::nn::Param;
use crate::tensor::Tensor;
pub struct Ema {
params: Vec<Param>,
shadow: Vec<Tensor>,
decay: f32,
}
impl Ema {
pub fn new(params: Vec<Param>, decay: f32) -> Ema {
assert!(
(0.0..1.0).contains(&decay),
"decay must be in [0, 1), got {decay}"
);
let shadow = params.iter().map(|p| p.value().detach()).collect();
Ema {
params,
shadow,
decay,
}
}
pub fn update(&mut self) {
for (shadow, param) in self.shadow.iter_mut().zip(&self.params) {
let value = param.value();
*shadow = shadow
.mul_scalar(self.decay)
.add(&value.mul_scalar(1.0 - self.decay));
}
}
pub fn swap(&mut self) {
for (shadow, param) in self.shadow.iter_mut().zip(&self.params) {
let raw = param.value().detach();
param.set_value(shadow.clone());
*shadow = raw;
}
}
pub fn averaged(&self) -> &[Tensor] {
&self.shadow
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn update_follows_the_definition_exactly() {
let param = Param::new(Tensor::from_vec(vec![1.0], &[1]));
let mut ema = Ema::new(vec![param.clone()], 0.9);
param.set_value(Tensor::from_vec(vec![3.0], &[1]));
ema.update();
assert!((ema.averaged()[0].to_vec()[0] - 1.2).abs() < 1e-6);
}
#[test]
fn double_swap_is_identity() {
let param = Param::new(Tensor::from_vec(vec![5.0], &[1]));
let mut ema = Ema::new(vec![param.clone()], 0.99);
param.set_value(Tensor::from_vec(vec![7.0], &[1]));
ema.swap();
assert_eq!(
param.value().to_vec()[0],
5.0,
"first swap shows the average"
);
ema.swap();
assert_eq!(
param.value().to_vec()[0],
7.0,
"second swap restores training weights"
);
}
}