Utility as a max of experts
A candidate is as good as its best style thinks it is. Two more obvious designs cannot represent a cross-island comparison at all.
The form
A candidate has the feature vector , which the standardizer maps to with per-feature means and scales . The taste model holds styles, and style is a weight vector : a linear functional on . Utility is the maximum over styles, not a weighted mixture:
At this reduces exactly to Bayesian linear regression on , which is a useful property: the mixture is a strict generalization with no special-casing at the boundary.
One posterior draw’s utility (TasteSample::utility_mix):
pub fn utility_mix(&self, phi: &[f64]) -> f64 {
self.theta.iter()
.map(|t| dot(t, phi))
.fold(f64::NEG_INFINITY, f64::max)
}
Why a maximum
Taste is multi-modal. One person can love dark drones and bright plucks (“ambient-me” and “acid-me”), and those are not points on one axis. A single linear utility would average them into a preference for neither, and would then be confidently wrong about both.
The max form gives each island its own style, and every judgment, including a duel across two islands, compares candidates on the shared scale . A dark drone and a bright pluck are both scored, each by whichever style rates it highest, and the comparison is well-formed.
Two rejected designs, and why
A per-session style latent
“One mood per session: sample which style is active, then use it.”
Fails because it cannot represent several islands inside a session. A user who auditions a pad, then a bass, then a pad in one sitting is not switching moods; they have two tastes at once. Whenever the session’s latent is wrong for the current candidate, every observation in that session is scored by the wrong style.
A per-observation marginalized style
“Marginalize over which style judges each observation.”
Fails on a sharper point: it forces both duel items through the same style, so a cross-island comparison is unrepresentable. There is no style under which “the drone beats the pluck” is a sensible statement if the drone lives in style 1 and the pluck in style 2, and a duel between them is exactly the question the acquisition rule will ask.
This is not a theoretical objection. A synthetic bimodal user exposed it: the marginalized mixture failed to beat . Adding capacity made the model no better, which is the signature of capacity the likelihood cannot use.
What max-utility buys structurally
There are no discrete latent sites at all. No style assignment to sample,
no categorical variables, no label-switching during inference to fight. Every
site in the model is an f64, which means fugue’s generic adaptive single-site
MH applies unchanged, with no custom kernel and no Rao-Blackwellization.
Label permutation is resolved post hoc instead, by
TastePosterior::aligned.
is an upper bound, not a claim
by default (SessionConfig::k_styles, in engine.rs), and the fitted
number of live styles grows with evidence.
Nothing enforces that; it falls out. A style with no evidence to explain stays
near its prior, and style_share reports what fraction of the pool each style
actually claims as its best. A style claiming ≈0% is idle: the player’s
taste has fewer islands than , and the app dims it rather than inventing a
name for it.
So is capacity, and the data decides how much gets used.
The prior, and the correction forces
The factor is standard: with for a standardized vector, it makes the prior utility of a candidate roughly unit-variance, so likelihood scales stay sane at any feature count.
The factor is the correction the max form forces, and it is easy to miss.
Under the prior each is marginally , so is the maximum of iid standard normals, whose standard deviation
falls with (MAX_NORMAL_SD, in auracle-taste’s model.rs):
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| 1.000 | 0.826 | 0.748 | 0.701 | 0.669 |
The mean shift cancels in duels (both sides shift equally) and is absorbed by and the cutpoints elsewhere. The variance shrinkage does not cancel. Left uncorrected, drops from 2.0 at to 0.90 at , so growing mid-session would quietly make the model less able to express a strong preference.
That is the opposite of what adding capacity should do, and a player would meet it as a model that grows vaguer the longer they play.
Dividing by restores invariance: is the same at every .
What the interface reads off this
| Quantity | Is |
|---|---|
utility_mix(z) | of over posterior draws: the glow and size on the taste map |
utility(z, k) | Style ’s rating specifically |
best_style(z) | Which style claims this candidate: the hue on the map |
responsibilities(z) | Posterior probability that each style is the best one for this candidate |
slope(z, ê) | The slope of along a direction at , through the style that claims in each draw: PERFORM’s lean on a control |
style_share(pool) | Per-style share of the pool, averaged over candidates |
prob_prefers(a, b) | : the forecast the app shows for a pair |
responsibilities is a posterior distribution over which style applies, which
is strictly more informative than an argmax and is what lets a candidate sit
visibly between two islands.