RecSys · textbook
Trainer Widgets Revision About All chapters ← Re-ranking Data →

Part IV · Sequences and the slate · chapter 16 of 19

Exploration and bandits

Every previous chapter improved the model under a fixed impression policy. But the model learns from what the previous model showed, and a good item that has never been shown will stay unknown. This chapter is about how to break that circle, what it costs, and why a stochastic output is needed for more than exploration alone.

What to take away
  • The circle is closed literally: to learn a CTR you have to show the item; to show it you have to know its CTR. A greedy policy takes 0.834 of the ideal output and finds the hidden gem in none of eight catalogues.
  • A bonus for ignorance gives +9% by rearranging the very same pool of candidates — no new model and no new features. But the dependence on \(\alpha\) is a hump, not a staircase.
  • The main trap is that not every uncertainty is useful. \(\sqrt{p(1-p)}\) does not decrease with the number of impressions at all; that is the noise of the event, not the ignorance of the model.
  • Thompson is chosen not for quality but for measurability. A deterministic policy gives propensities of 0 and 1 — and the whole of offline evaluation turns into a division by zero.

1. The closed circle

The problem was stated back in the chapter on data; here we solve it. The ranking model is trained on logs; the logs were collected by a previous version of the same model; everything it did not show is absent from the training data.

What that looks like in numbers

A catalogue of 40 items, we show the top 10, and six items have no history at all. Hidden among the new ones is a gem with a true CTR of 0.145 — the best in the catalogue, where its nearest competitor has 0.097.

The model knows nothing about it: its estimate equals the prior, 0.06. Which means it will not get into the top 10. Which means it will get no impressions. Which means the estimate will not change.

The numbers are reproduced by the script _tools/bandit_demo.py in this repository.

To learn a CTR you have to show the item; to show it you have to know its CTR. This is not a pathology of the data or an error of training — it is a stable state the system arrives at on its own.

2. Two cheap mechanisms

Before building anything complicated it is worth knowing: the first 95% of the benefit of exploration is usually taken by two simple techniques. Everything else in this chapter is about the remaining percent.

A random quota (ε-greedy)

On a share \(\varepsilon\) of the positions we show random recommendations and on the rest the model's output.

The upside: it diversifies the training pool substantially, and precisely with what the model would never have shown. It can be made more elaborate by making the «random» part not random but learned against a separate exploratory target.

The downside: it does this bluntly. A random item from a million-item catalogue is almost certainly bad, and the setup has to be tuned carefully so as not to sink the business metrics.

Softmax sampling

We take a softmax of the scores with a temperature and sample from the resulting distribution without replacement:

$$ P(i) \;\propto\; \exp\bigl(f(u,i)/T\bigr) $$

The upside: it is gentle on business metrics — we are not showing random junk, we are slightly shuffling the good. The setup is easy to tune, and there is a fast way to sample without replacement via Gumbel-top-\(k\): add Gumbel noise to the logits and take the top.

The downside, and it is fundamental: softmax sampling re-ranks only the existing top. It explores inside what the funnel has already selected, and the funnel selected greedily. To explore for real, the mechanism is needed at every stage, retrieval included.

3. The algorithms

Now the bandit theory proper — in exactly the amount people ask about.

UCB: optimism in the face of uncertainty

We choose the action with the highest upper estimate of the expected reward:

$$ a_t = \arg\max_a \bigl[\,q_t(a) + u_t(a)\,\bigr] $$

The beauty is that the bonus \(u_t\) is not invented but derived.

Hoeffding's inequality. Let \(X_1 \dots X_n\) be an independent sample from a distribution on \([0,1]\) with true mean \(\mu\), and let \(\hat\mu\) be the sample mean. Then for any \(u > 0\):

$$ \mathbb{P}\bigl(\mu \ge \hat\mu + u\bigr) \le e^{-2nu^2} $$

Set the right-hand side equal to \(\delta\) and solve for \(u\). With probability \(1-\delta\) the true \(Q(a)\) does not exceed \(Q_k(a) + U_k(a)\), where

$$ U_k(a) = \sqrt{\frac{-\ln \delta}{2\,n_k(a)}} $$

All that remains is to choose \(\delta\). Take \(\delta = 1/k^{c}\) — that is, demand an ever more reliable bound as time goes on — and we get the canonical formula:

$$ a_k = \arg\max_a \Bigl[\, Q_k(a) + c\sqrt{\frac{\log k}{n_k(a)}} \,\Bigr] $$

It reads transparently: the estimate of the mean reward plus the width of the confidence interval.

How the bonus fades away on its own
Impressions of an armWidth at \(\delta = 0.05\)Bonus \(\sqrt{\log k / n}\) at \(k = 10^4\)
11.22393.0349
100.38700.9597
1000.12240.3035
10000.03870.0960
100000.0122

The numbers are reproduced by the script _tools/bandit_demo.py.

The bonus decays as \(1/\sqrt{n}\), and hence the main property: no separate «exploration knob» is needed. An arm that has been pulled little surfaces on its own; as data accumulates the exploration fades away automatically.

Thompson sampling

The Bayesian alternative. For every action we specify a model of the reward and a distribution over its parameters, and then at every step:

  1. sample the parameters from the posterior: \(\theta \sim p(\theta \mid \text{data})\);
  2. act greedily with respect to the sample: \(a_t = \arg\max_a \mathbb{E}[r \mid a, \theta]\).

The uncertainty in the parameters is itself the mechanism of exploration: the less data there is about an arm, the wider its posterior and the more often the sample comes out large.

Beta-Bernoulli is the workhorse. The reward is binary, \(r \sim \mathrm{Bernoulli}(\theta_a)\), the prior is \(\theta_a \sim \mathrm{Beta}(\alpha_a, \beta_a)\). Beta is conjugate to the Bernoulli, so the update is trivial:

$$ r_t = 1: \ \alpha_a \mathrel{+}= 1, \qquad r_t = 0: \ \beta_a \mathrel{+}= 1 $$

Sample \(\theta_a\) for every arm and take the maximum. Three lines of code and not a single hyperparameter beyond the prior.

Why ε-greedy loses: the shape of the curve, not its height

Three arms with CTRs 0.30, 0.10, 0.05; a horizon of 20 000 steps. Regret is the total reward forgone.

Policyt = 1000t = 5000t = 10000t = 20000
ε-greedy 0.1025.991.0164.9307.5
UCB, c = 128.046.357.267.1
Thompson14.119.421.023.8

What to look at is not the height at the end but the increment when the interval is doubled:

Policy5000 → 1000010000 → 20000Ratio
ε-greedy 0.1073.9142.51.93
UCB, c = 110.99.90.91
Thompson1.62.81.70

The numbers are reproduced by the script _tools/bandit_demo.py.

For ε-greedy an interval twice as long gives twice the regret — the regret is linear. A fixed share of random traffic is spent forever: even a million steps in, the algorithm still gives 10% of its impressions to knowingly bad arms.

For UCB the increment even falls slightly — the curve bends over. For Thompson the ratio of 1.70 is meaningless: increments of 1.6 and 2.8 against a total regret of 23.8 are noise; what matters is that the absolute value is 13 times smaller than ε-greedy's.

You can play with the horizon and the gap between arms in the trainer: the closer the CTRs of the arms, the longer all three algorithms stay confused — the difficulty of the problem is set by the gap, not by the number of arms.

4. A bandit as a re-ranking layer

The theory above speaks of «arms». The engineering question is how to insert it into a finished funnel that already has candidate generation, a ranker and a blender.

What counts as an arm

In the classical setting there are a handful of arms and each is pulled thousands of times. In recommendations the catalogue holds millions of items, and impressions per item in the tail are single digits or zero. A direct transfer does not work: an arm that has been pulled zero times teaches nothing. Hence the first design decision.

ArmHow manyWhat we getWhat we do not get
An itemmillionsreal exploration of the catalogue: cold start closes itselfstatistics — it works only if the uncertainty comes from the model rather than from an impression counter
A source of candidatesunits to tensdynamic quotas: how many slots to give the ANN, how many to subscriptions, how many to trendsanything about a particular item — inside a source it is still greedy
The whole policyunitsautomatic choice between versions of the rankerthis is no longer recommendation but a self-tuning A/B test
A group of itemsthousandsa compromise: statistics accumulate per group and transfer to a new item inside itaccuracy within the group — a good item in a bad category will stay unnoticed
Almost always «an item» is chosen — and here is why that is possible

The argument against the item as an arm was that it has no statistics. But an impression counter is not the only source of uncertainty.

We already have a trained model, and it can answer «I do not know» about an item it is seeing for the first time: unfamiliar features, an empty history, an embedding straight from initialisation. The uncertainty comes from the model, not from a counter — and then millions of arms stop being a problem.

Hence the right way to state what this layer is: a bandit here is an add-on over the ranker, not a replacement for it. The ranker is responsible for the mean, the bandit for what to do with the spread around it.

Why the layer sits precisely at re-ranking

retrieval ~10 000 ranker · ~500 gives μ the bandit layer sorting by μ + α·σ the output top 10 clicks and their absence → update μ and σ the ceiling of exploration is set by retrieval: the layer will only rearrange what reached it
The layer does not add candidates — it changes the order of those already selected. That is both its main merit and its main limitation.

Exploration can be done at any stage, but re-ranking is convenient for four reasons at once, and all of them are engineering rather than mathematical.

  1. The cost of a mistake is bounded and known in advance. The layer rearranges a hundred candidates, each of which has already passed the filters and the ranker. The worst that can happen is showing the ninth instead of the second. At the retrieval stage the cost of a mistake is bounded by nothing.
  2. All the features are here. An estimate of uncertainty is needed not «about the item in general» but about the pair (user, item) in the current context. The full feature vector exists only at the ranking stage.
  3. There are few candidates. An ensemble of five models over a hundred candidates is five hundred evaluations, which is affordable. Over ten thousand it is not.
  4. This is the point where a decision is being made anyway. The order of the output is formed here regardless; adding a term to the score is cheaper than building exploration into an ANN index.
And the flip side, visible in the diagram

The layer explores only what the funnel selected. If retrieval is greedy too — an ANN over the same score — then the bandit explores inside an already biased pool.

This is the same limiter as with softmax sampling, and it does not go away by changing the algorithm. Real exploration of the catalogue requires a mechanism at retrieval too — a separate source of candidates for cold items, say.

What the layer does to the score

Two ways to turn \((\mu, \sigma)\) into an order

The ranker returns a number \(\hat f(u,i)\). The layer treats it as the mean of the reward distribution and adds a second parameter — the spread \(\sigma(u,i)\).

$$ \text{UCB: } \ s_i = \mu_i + \alpha\,\sigma_i \qquad\qquad \text{Thompson: } \ s_i = \mu_i + \alpha\,\sigma_i \cdot \xi_i, \ \ \xi_i \sim \mathcal{N}(0,1) $$

And in both cases the sorting is by \(s_i\). Hidden here is a step that theory usually mentions only in passing: a classical bandit chooses one action through an \(\arg\max\), while an output is \(k\) actions at once. The move from «take the maximum» to «sort and take the top \(k\)» breaks three premises of the theory.

  • The reward is not observed for all \(k\). Fewer than half the users reach position ten. The absence of a click at position 10 is almost always «did not look» rather than «did not like it», and updating the posterior as though it were a refusal systematically underrates the bottom of the output.
  • The reward arrives with a delay. Between an impression and a click there are seconds; between an impression and a return the next day, twenty-four hours. Until the reward arrives the posterior is not updated, and one item can be given hundreds of impressions on credit.
  • The update is batched. The posterior is recomputed every \(N\) minutes rather than after every request — so within a window the policy is fixed and all the guarantees proved for step-by-step updating hold only approximately.
How much this gives and where it stops giving

The same catalogue as in section 1, averaged over eight catalogues, 300 queries. The share of the ideal output:

\(\alpha\)GreedyUCBThompsonUCB found the gemUCB put it at top-1
0.000.8340.8340.8343/80/8
0.500.8340.8580.8587/82/8
1.000.8340.8660.8378/84/8
3.000.8340.9090.7568/88/8
5.000.8340.8910.7238/88/8

The numbers are reproduced by the script _tools/bandit_demo.py; the same script independently repeats the widget's computation.

Four observations, and the third is the most interesting.

  • \(\alpha = 3\) gives 0.909 against 0.834 — that is +9% to the output, obtained by rearranging the very same pool of candidates. No new model, no new features.
  • \(\alpha = 5\) already gives 0.891. Exploration has stopped paying off: we are lifting to the top items about which everything is already clear. The dependence on \(\alpha\) is a hump, not a staircase, and that is the first thing to check when tuning such a layer.
  • At \(\alpha = 0.5\) the gem looks into the top in 7 catalogues out of 8 but settles at top-1 in only 2. The item gets a few impressions, catches an unlucky streak, its estimate sags, and the bonus \(\propto 1/\sqrt{n}\) has already dried up — so it falls back out. A small bonus is worse than none: it spends impressions without finishing the job.
  • Thompson loses to UCB on pure reward and at large \(\alpha\) drops below greedy: it adds noise where everything is already clear. Why production picks it anyway is in section 6.
What to look for here
  1. Set \(\alpha = 0\): the gem never reaches top-1. The circle is closed.
  2. Raise it to 3: it is first in all eight catalogues and the share grows to 0.909.
  3. Raise it to 5 — the share falls. Find the crest of the hump.
  4. Switch to Thompson and lower \(\alpha\) to 1: it returns to the rejected items, because a sample sometimes comes out large — but it pays for that with noise all the time.

What to say in an interview: «A bandit at re-ranking is sorting not by the prediction but by the prediction plus a measure of ignorance. The main decision is where σ comes from: from an ensemble or MC-dropout, but not from \(p(1-p)\), because that is aleatoric noise which does not decrease with impressions».

5. Where σ comes from

SourceHowThe price
An ensemble\(N\) models, take the sample variance of the predictions\(N\) models in training and in the runtime
MC-dropoutswitch dropout on at inference and make several passes\(N\) runs of the head; the embeddings are computed once
Neural linearBayesian linear regression over the last hidden layer; the network gives the representation, the linear layer an honest covariance and \(\sqrt{x^\top A^{-1} x}\)maintaining the matrix \(A\), but the interval is exact

The first two cost almost nothing in development, which makes them the standard first step. The third is how YouTube's ranker is built.

The main trap: not every uncertainty is useful

Spread comes in two kinds, and they must not be added together.

  • Epistemic — «the model does not know». It decreases with new data. That is the one worth exploring.
  • Aleatoric — «the event is random in itself». The user clicks with probability 0.3, and no volume of data will make that more certain.

The danger is concrete. The model predicts a probability \(p\); a Bernoulli variable has variance \(p(1-p)\), and it is tempting to take \(\sigma = \sqrt{p(1-p)}\) — the formula is at hand and nothing has to be computed.

Impressions of an item\(\sqrt{p(1-p)}\)\(\sqrt{p(1-p)/n}\)
10.45830.4583
100.45830.1449
1000.45830.0458
10000.45830.0145
100000.45830.0046

The numbers are reproduced by the script _tools/bandit_demo.py.

The left column does not change at all. That is pure aleatoric noise: such a «bonus» is maximal at \(p = 0.5\) (a value of 0.5000) and simply drags mediocrities upwards, never fading.

How to check that you are measuring the right thing: plot \(\sigma\) as a function of an item's number of impressions. If there is no dependence, you are measuring the wrong thing.

And a second way to kill an epistemic estimate: assemble an ensemble from models that share one embedding table. Their predictions will be nearly identical, \(\sigma\) will come out tiny and the layer will do nothing. The initialisation and the order of the data both have to differ.

6. UCB or Thompson: the argument that decides

On pure reward UCB usually wins — the table above shows that. In production Thompson is nevertheless the choice, and the reason lies outside the bandit problem.

Stochasticity is needed not for exploration but for measurability

UCB is deterministic: in a given state it produces the same order. So the probability of an impression \(\pi(i \mid u)\) equals zero or one, and everything built on inverse propensities — IPS, SNIPS, doubly robust — turns into a division by zero. A new model cannot be evaluated offline on such logs.

Thompson defines a distribution over permutations. The propensities are positive and can be estimated by repeated sampling at the same \((\mu, \sigma)\), which means the logs are fit for off-policy evaluation. One and the same layer closes both problems: it explores and it makes the data suitable for evaluation.

The practical conclusion: if propensities are needed anyway — and they are, as soon as you want to compare models offline — then a stochastic policy is not a luxury but a condition. And what has to be stored is not only the fact of an impression but \(\mu\), \(\sigma\) and the version of the model at the moment of the decision.

Why positive propensities alone are not enough

The IPS estimate is formally unbiased, but its variance is determined by the spread of the weights \(w = \pi_{\text{new}}/\pi_{\text{log}}\). The effective sample size: \(\mathrm{ESS} = (\sum w)^2 / \sum w^2\).

Suppose 99% of the observations have a weight of 1 and the remaining percent has a weight \(W\):

\(W\)110100100010000
ESS out of 10001000.0597.039.212.110.2
share100%59.7%3.9%1.2%1.0%

The numbers are reproduced by the script _tools/bandit_demo.py.

One percent of observations with a large weight eats 99% of the effective sample: a thousand logs work like ten. Hence weight clipping, SNIPS and doubly robust — and hence the requirement that the policy be not only stochastic but also not too far from the logging one.

You can see how IPS and SNIPS diverge as the spread of the weights grows in the trainer.

7. What breaks the layer in a real system

Four typical breakages
  1. Business rules on top of the bandit. The layer produced a stochastic order, and a rule «no more than two items by the same author in a row» rearranged it. The actual policy is now a different one, the propensities were computed for a different output, and off-policy evaluation drifts quietly. Either the rules are taken into account when computing propensities, or the evaluation cannot be trusted.
  2. Refreshing the page. The user reloads the feed and sees a different order — under a stochastic policy that happens by itself and reads as a malfunction. The result of the randomisation is cached for the session, and the propensity is cached along with it.
  3. Drift. A posterior with accumulated counters remembers everything. An item that was good in December stopped being good by March, its bonus has long been zero, and the layer will not check. The cure is exponential forgetting: old observations are discounted and \(\sigma\) grows again.
  4. Double counting of uncertainty. If the ranker was trained on logs where cold items were already lifted by a separate rule, the model has learned that, and the bonus will be added on top — new items will rise twice as strongly as expected.

8. When none of this is needed

An alternative without bandits

The same cold start is also solved by a hard rule: a deterministic insertion — exactly one post by a little-known author is lifted to a score corresponding to a nominal position 15–16, regardless of how unsure the model is about it. That is exactly how it is done in the open-sourced code of the X feed.

The trade-off is exactly the one in the table about arms. A rule is predictable, explainable and costs ten lines. A bonus for uncertainty decides for itself who gets how many impressions, but requires an honest \(\sigma\), stochasticity and the logging of propensities.

If the only task is to keep a new author from dying in obscurity, a rule will manage and will cost less.

Three conditions under which the layer pays for itself

The two techniques of section 2 — a random quota and softmax sampling — will most likely be enough to take the first 95% of the benefit. Building a bandit layer makes sense when three conditions hold at once: the catalogue updates fast, you have an honest estimate of uncertainty, and you need propensities for offline evaluation.

And a separate methodological difficulty. The gain from exploration shows up in the quality of future models, while it is usually measured by a week-long A/B on today's metrics. On that horizon exploration almost always looks like a loss — exactly like MMR in the chapter on diversity, and for the same reason: what is measured is not what it is done for.

It is worth remembering the limits of the theory too: the classical theorems are proved for non-contextual bandits, where the reward depends neither on the user nor on the time of day. For recommendations that is too strong a restriction — the whole point is that \(\mu\) and \(\sigma\) are computed for a (user, item) pair. Contextual variants — linUCB, linTS, neural linear — preserve the guarantees, but under stronger assumptions.

Interview questions

What is the feedback loop and how do you get out of it?

The model is trained on logs collected by a previous version of the same model. Everything it did not show is absent from the data, and a good item with no impressions will stay unknown: to learn a CTR you have to show the item; to show it you have to know its CTR.

The way out is to change the logging policy. Two cheap ways: a random quota (ε-greedy) and softmax sampling with a temperature. They usually take the first 95% of the benefit. Beyond that comes a bandit layer with an estimate of uncertainty.

An important caveat about softmax sampling: it re-ranks only what the funnel has already selected, and the funnel selected greedily. Real exploration requires a mechanism at retrieval too.

Derive the UCB bonus.

From Hoeffding's inequality: for a sample on [0,1] with true mean μ and sample mean \(\hat\mu\), \(\mathbb{P}(\mu \ge \hat\mu + u) \le e^{-2nu^2}\). Set the right-hand side equal to δ and solve for u: \(U = \sqrt{-\ln\delta / 2n}\).

Then choose δ = 1/k^c — demanding an ever more reliable bound as time goes on — and get \(a_k = \arg\max [Q_k(a) + c\sqrt{\log k / n_k(a)}]\).

It reads as «the estimate of the mean reward plus the width of the confidence interval». The bonus decays as \(1/\sqrt{n}\): at 1 impression it is 3.03, at 1000 it is already 0.096. So no separate exploration knob is needed — the algorithm shrinks it as data accumulates.

How does Thompson differ from UCB?

UCB is deterministic and optimistic: it takes the upper bound of the interval. Thompson is Bayesian: it samples parameters from the posterior and acts greedily with respect to the sample. For a binary reward that is Beta-Bernoulli — a Beta prior is conjugate to the Bernoulli, the update is trivial (success → α+1, failure → β+1), three lines of code and no hyperparameters beyond the prior.

By regret both are sublinear, unlike ε-greedy: for the latter, doubling the interval doubles the increment (73.9 → 142.5), because a fixed share of random traffic is spent forever.

The main practical difference is not in the reward but in measurability, see the next question.

Why does production more often take Thompson if UCB gives more reward?

Because of propensities. UCB is deterministic: in a given state the order is the same, so π(i|u) equals zero or one, and IPS, SNIPS and doubly robust turn into a division by zero. A new model cannot be evaluated offline on such logs.

Thompson defines a distribution over permutations: the propensities are positive and can be estimated by repeated sampling at the same μ and σ. One layer closes both problems — it explores and it makes the data suitable for evaluation.

A caveat: positive propensities alone are not enough, their spread matters too. If 1% of the observations have a weight of 100, the effective sample size falls from 1000 to 39 — the estimate is formally unbiased but useless. Hence weight clipping and the requirement that the policy not stray far from the logging one.

What do you take as an arm in recommendations?

A direct transfer does not work: in the classical setting there are a handful of arms pulled thousands of times each, and here there are millions of items and zero impressions in the tail. The options are an item, a source of candidates, the whole policy, a group of items.

Almost always an item is chosen, and that is possible because an impression counter is not the only source of uncertainty. A trained model can answer «I do not know» about an item with unfamiliar features and an empty history. The uncertainty comes from the model, not from a counter.

Hence the right formulation: a bandit is an add-on over the ranker, not a replacement. The ranker is responsible for the mean, the bandit for what to do with the spread around it.

Why is the bandit layer placed at re-ranking?

Four engineering reasons. The cost of a mistake is bounded: the layer rearranges a hundred already-filtered candidates, and the worst case is showing the ninth instead of the second; at retrieval the cost is bounded by nothing. All the features are here, and σ is needed about a (user, item) pair in context. There are few candidates: an ensemble of five models over a hundred is affordable, over ten thousand it is not. And this is the point where a decision is being made anyway.

The limitation: the layer explores only what the funnel selected. If retrieval is greedy, the bandit explores inside an already biased pool.

Where do you get σ from and which σ do you need?

Three sources: an ensemble of models (the sample variance of the predictions), MC-dropout (dropout at inference, several passes of the head) and neural linear (Bayesian linear regression over the last layer, giving an honest \(\sqrt{x^\top A^{-1}x}\)). The first two cost almost nothing in development.

The main thing is not to confuse epistemic uncertainty («the model does not know», decreases with data) with aleatoric («the event is random», never decreases). The temptation to take \(\sigma = \sqrt{p(1-p)}\) is strong — the formula is at hand. But that is pure aleatoric noise: at p = 0.3 it equals 0.4583 both at one impression and at ten thousand. Such a bonus is maximal at p = 0.5 and simply drags mediocrities upwards.

The check: plot σ as a function of the number of impressions. No dependence means you are measuring the wrong thing. And do not build an ensemble from models with a shared embedding table: the predictions will coincide and σ will collapse.

How do you tune the strength of the bonus?

Remember that the dependence is a hump, not a staircase. In the simulation α = 3 gives 0.909 of the ideal output against 0.834 for greedy (plus 9% by rearranging the same pool), while α = 5 already gives 0.891: we start lifting items about which everything is already clear.

And separately about small α. At α = 0.5 the gem looks into the top in 7 catalogues out of 8 but settles at top-1 in only 2: it gets a few impressions, catches an unlucky streak, its estimate sags, and the bonus \(\propto 1/\sqrt{n}\) has already dried up. A small bonus is worse than none — it spends impressions without finishing the job.

When is a bandit layer not needed?

When a random quota and softmax sampling are enough — and they are enough to take the first 95% of the benefit. The layer pays for itself under three conditions at once: the catalogue updates fast, there is an honest estimate of uncertainty, and propensities are needed for offline evaluation.

There is also a deterministic alternative: hard-lift exactly one post by a little-known author to a fixed position. That is how it is done in the open-sourced code of the X feed. Predictable, explainable, ten lines — and if the only task is to keep a new author from dying in obscurity, that is enough.

And a methodological trap: the gain from exploration shows up in the quality of future models, while it is measured by a week-long A/B on today's metrics. On that horizon it almost always looks like a loss.

One-screen cheat sheet

The circle

To learn a CTR you must show; to show you must know the CTR. Greedy: 0.834 and 0/8 gems.

The cheap way

A random quota and softmax sampling give the first 95%. The second explores only inside the top.

UCB

From Hoeffding: \(c\sqrt{\log k / n}\). Decays as \(1/\sqrt n\) — exploration fades by itself.

Thompson

Beta-Bernoulli: a success → α+1. Three lines, no hyperparameters.

Regret

ε-greedy is linear (73.9 → 142.5), UCB and Thompson are not.

Arm = item

Take σ from the model, not from a counter. A bandit is an add-on over the ranker, not a replacement.

The σ trap

\(\sqrt{p(1-p)}\) = 0.4583 at any n. What is needed is \(\sqrt{p(1-p)/n}\) or an ensemble.

Why stochastic

For propensities. UCB gives π ∈ {0,1} and kills IPS. A spread of weights kills it too: ESS 39 out of 1000.

Primary sources