feat: add filtered_log_evidence
Scores every event on what was known before it, rather than on priors that carry information from events which had not happened yet. This is the quantity HistoryBuilder::online promised and never delivered. The pass walks slices in time order carrying its own forward messages, and per slice runs the unmodified production sweep on a scratch copy whose backward message is left improper. Reusing iterate_to_convergence rather than reimplementing inference means a competitor playing twice at one time is handled by the same within-slice EP that converge() uses, instead of being approximated the way the old evidence paths approximated it. Nothing is stored on Skill and nothing on self is mutated, so the result is independent of whether converge() has run — the property a stored field cannot have.
This commit is contained in:
+81
-6
@@ -48,7 +48,7 @@ pub enum EventKind {
|
||||
Scored { score_sigma: f64 },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct Item {
|
||||
agent: Index,
|
||||
likelihood: Gaussian,
|
||||
@@ -72,13 +72,13 @@ impl Item {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct Team {
|
||||
items: Vec<Item>,
|
||||
output: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Event {
|
||||
teams: Vec<Team>,
|
||||
log_evidence: f64,
|
||||
@@ -189,6 +189,17 @@ struct EventUpdate {
|
||||
likelihoods: Vec<Vec<Gaussian>>,
|
||||
}
|
||||
|
||||
/// One slice's worth of forward-only inference.
|
||||
///
|
||||
/// `posteriors` doubles as the outgoing forward message: the scratch sweep
|
||||
/// never writes `backward`, so it stays `N_INF`, and `Skill::posterior()`
|
||||
/// and `forward_prior_out` are then the same product.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FilteredStep {
|
||||
pub(crate) log_evidence: f64,
|
||||
pub(crate) posteriors: Vec<(Index, Gaussian)>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TimeSlice<T: Time = i64> {
|
||||
pub(crate) events: Vec<Event>,
|
||||
@@ -500,13 +511,13 @@ impl<T: Time> TimeSlice<T> {
|
||||
/// Iterate this slice alone until its posteriors stop moving, returning
|
||||
/// the number of iterations taken.
|
||||
///
|
||||
/// Only used by tests: production convergence is driven across slices by
|
||||
/// `History::converge`.
|
||||
/// Used by `filtered_step` to drive a scratch copy of the slice, and by
|
||||
/// tests. Production convergence across slices is driven by
|
||||
/// `History::converge`, which calls `iteration` directly.
|
||||
///
|
||||
/// Honours `self.convergence`; it previously hard-coded an epsilon and a
|
||||
/// 20-iteration cap that matched neither `ConvergenceOptions` nor the
|
||||
/// schedule default.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn iterate_to_convergence<D: Drift<T>>(
|
||||
&mut self,
|
||||
agents: &CompetitorStore<T, D>,
|
||||
@@ -574,6 +585,70 @@ impl<T: Time> TimeSlice<T> {
|
||||
self.iteration(0, agents);
|
||||
}
|
||||
|
||||
/// Run this slice's events on forward (filtering) information alone.
|
||||
///
|
||||
/// `incoming` holds each competitor's forward message out of their
|
||||
/// previous appearance; a competitor absent from it starts at their
|
||||
/// configured prior. The sweep runs on a scratch copy, so the real slice
|
||||
/// is untouched — which is what makes the filtered estimates independent
|
||||
/// of whether `History::converge` has run.
|
||||
pub(crate) fn filtered_step<D: Drift<T>>(
|
||||
&self,
|
||||
incoming: &HashMap<Index, Gaussian>,
|
||||
agents: &CompetitorStore<T, D>,
|
||||
) -> FilteredStep {
|
||||
let mut scratch = TimeSlice {
|
||||
events: self.events.clone(),
|
||||
skills: SkillStore::new(),
|
||||
time: self.time,
|
||||
p_draw: self.p_draw,
|
||||
convergence: self.convergence,
|
||||
arena: ScratchArena::new(),
|
||||
color_groups: ColorGroups::new(),
|
||||
color_groups_dirty: true,
|
||||
};
|
||||
|
||||
for event in &mut scratch.events {
|
||||
for team in &mut event.teams {
|
||||
for item in &mut team.items {
|
||||
item.likelihood = N_INF;
|
||||
}
|
||||
}
|
||||
|
||||
event.log_evidence = 0.0;
|
||||
}
|
||||
|
||||
for (agent, skill) in self.skills.iter() {
|
||||
let rating = &agents[agent].rating;
|
||||
|
||||
let forward = match incoming.get(&agent) {
|
||||
Some(message) => message.forget(rating.drift.variance_for_elapsed(skill.elapsed)),
|
||||
None => rating.prior,
|
||||
};
|
||||
|
||||
scratch.skills.insert(
|
||||
agent,
|
||||
Skill {
|
||||
forward,
|
||||
backward: N_INF,
|
||||
likelihood: N_INF,
|
||||
elapsed: skill.elapsed,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
scratch.iterate_to_convergence(agents);
|
||||
|
||||
FilteredStep {
|
||||
log_evidence: scratch.events.iter().map(|event| event.log_evidence).sum(),
|
||||
posteriors: scratch
|
||||
.skills
|
||||
.iter()
|
||||
.map(|(agent, skill)| (agent, skill.posterior()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn log_evidence<D: Drift<T>>(
|
||||
&self,
|
||||
targets: &[Index],
|
||||
|
||||
Reference in New Issue
Block a user