pub const FEATURES: usize = 5;
const BIAS: usize = FEATURES;
const PRIOR_PRECISION: f64 = 0.25;
pub struct LinkModel {
mean: [f64; FEATURES + 1],
precision: [f64; FEATURES + 1],
used: [f32; FEATURES],
z: f64,
}
impl LinkModel {
pub fn new(prior: [f32; FEATURES]) -> Self {
let mut mean = [0.0; FEATURES + 1];
for (m, p) in mean.iter_mut().zip(prior) {
*m = f64::from(p);
}
Self {
mean,
precision: [PRIOR_PRECISION; FEATURES + 1],
used: prior,
z: pagesift_stat::Delta::DEFAULT.z(),
}
}
pub fn weights(&self) -> &[f32; FEATURES] {
&self.used
}
pub fn observe(&mut self, x: &[f32; FEATURES], hit: bool) -> bool {
let mut xs = [1.0; FEATURES + 1];
for (a, b) in xs.iter_mut().zip(x) {
*a = f64::from(*b);
}
let logit: f64 = self.mean.iter().zip(&xs).map(|(m, x)| m * x).sum();
let p = 1.0 / (1.0 + (-logit).exp());
let y = if hit { 1.0 } else { 0.0 };
for (i, &x) in xs.iter().enumerate().take(BIAS + 1) {
if x == 0.0 {
continue;
}
self.precision[i] += p * (1.0 - p) * x * x;
self.mean[i] += (y - p) * x / self.precision[i];
}
let mut changed = false;
for i in 0..FEATURES {
let se = (1.0 / self.precision[i]).sqrt();
let m = self.mean[i].max(0.0);
if (m - f64::from(self.used[i])).abs() > self.z * se {
self.used[i] = m as f32;
changed = true;
}
}
changed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starts_at_the_prior_and_moves_only_on_strong_evidence() {
let prior = [2.0, 1.5, 1.0, 0.5, 0.5];
let mut m = LinkModel::new(prior);
assert_eq!(m.weights(), &prior);
for _ in 0..5 {
assert!(!m.observe(&[1.0, 0.0, 0.0, 0.0, 0.0], false));
}
assert_eq!(m.weights(), &prior);
for _ in 0..400 {
m.observe(&[1.0, 0.0, 0.0, 0.0, 0.0], false);
m.observe(&[0.0, 1.0, 0.0, 0.0, 0.0], true);
}
let w = m.weights();
assert!(w[0] < prior[0] && w[1] > prior[1], "{w:?}");
assert_eq!(w[2], prior[2], "features never seen keep the prior");
}
}