search_ads_recsys / 05 · losses & calibration lesson 5 / 39

Losses & calibration — why the loss decides whether the auction works

The architecture is settled (lesson 4). Now: what objective do you minimize, and does its output mean P(click) or just a ranking score? The answer is the difference between an ads stack that bids correctly and one that's wrong by 2×.

Pointwise BCE — the workhorse

The default loss for CTR/CVR prediction is binary cross-entropy on per-impression labels. For logit z = f(x) and label y ∈ {0,1}:

LBCE(x, y) = −[ y · log σ(z) + (1 − y) · log(1 − σ(z)) ]

Why this and not something else? Because BCE is a proper scoring rule. The optimum of E(x,y)[LBCE] is achieved iff σ(f(x)) = P(y = 1 | x). The model is being asked to estimate the true posterior — calibration is the property BCE is solving for, not a separate goal you bolt on. Two practical consequences: (1) the output is directly usable as a probability — an auction can multiply it by a bid and get expected revenue with the right units; (2) shifting all logits by a constant changes the loss (σ is not shift-invariant), so the model is forced to learn the absolute level, not just the order.

Pairwise losses — the order is all that matters

If you only ever rank items relative to each other within a request, you don't need probabilities. You can train on preference pairs: an observed positive x⁺ (clicked) and a negative x⁻ (skipped or sampled). With scores s⁺ = f(x⁺), s⁻ = f(x⁻):

LBPR = −log σ(s⁺ − s⁻)

This is BPR (Rendle et al. 2009). RankNet (Burges et al. 2005, web search) is the same loss; BPR rediscovered it from the recsys community four years later.

The structural fact about pairwise losses: the loss depends only on the score difference. Add a million to every score and nothing changes. The output is a ranking signal, not a probability — it tells you "x⁺ beats x⁻" but emphatically does not tell you "x⁺ has CTR = 0.04". Post-hoc calibration is required if a downstream consumer wants probabilities.

Listwise losses — when the metric is a property of the whole list

Pairwise loss treats all pairs as equally important. But a swap at positions (1, 2) usually moves the ranking metric (NDCG, MRR) far more than a swap at (97, 98). LambdaRank (Burges, Ragno, Le, NIPS 2006; the 2010 paper is the unified survey) — and its GBDT incarnation LambdaMART, the historical state of the art for web search — fixes this by weighting each pair's gradient by the change in NDCG the swap would produce:

∂L / ∂si ∝ Σj [ −σ(sj − si) · |ΔNDCGij| ]

LightGBM's lambdarank objective is essentially this. The other listwise family treats the list as a softmax: Lsm = −log [ exp(s⁺) / Σj exp(sj) ]. This is the classic matching/retrieval loss — every positive competes against the rest of the in-batch (or sampled) negatives; lesson 6 picks up the sampled-negatives side. ListNet/ListMLE go further by defining a probability distribution over permutations and minimising KL — theoretically clean, rarely used in production because of cost.

Pointwise vs pairwise vs listwise — the decision rule

Pointwise BCEPairwise (BPR/RankNet)Listwise (Lambda/softmax)
OutputCalibrated probabilityRanking score (shift-invariant)Ranking score (shift-invariant)
CalibrationNative — proper scoring ruleNone — post-hoc onlyNone — post-hoc only
Sample efficiencyOK; one example, one gradientBetter on hard pairs; pair-mining helpsBest when metric is rank-aware
Gradient varianceLowModerate; depends on pair samplerDepends on listwise definition
Multi-task headsEasy — each head BCE on its own labelAwkward — pair structure differs per taskAwkward — list structure differs per task
Inference shapePer-item: probabilityPer-item: score; rank-onlyOften per-list scoring possible

Production reality at large platforms: pointwise BCE multi-task is the default. You get calibrated probabilities for every head (CTR, P(watch), P(convert | click)); a policy combines them. Listwise re-rankers may sit on top for inter-item effects — but the per-item scores feeding into them are still BCE-trained.

One production nuance worth naming: the pairwise and listwise families are not mutually exclusive in a real funnel. A common pattern (see lesson 04) is coarse-rank pairwise → fine-rank listwise: pairwise is cheap and prunes the candidate set fast (it only needs a score difference), then a listwise loss does the rank-aware fine-tuning on the survivors where the metric (NDCG) actually pays. BPR's two costs are why it stays out of fine-rank: it is sensitive to label noise (one mislabeled pair flips a gradient) and its compute grows with the number of pairs, not items.

The regression head is not "just MSE" — loss shapes per target

So far every loss above was for a binary label. But a modern multi-task ranker has heads whose target is not a coin flip: watch-time in seconds, play-depth, session duration. The lazy answer — "regression, so MSE" — is wrong often enough that the choice of loss shape per head is a senior-interview staple. The reason is always the same: the target distribution is not Gaussian, and MSE silently assumes it is (minimizing squared error is maximum likelihood under a fixed-variance Gaussian). When the truth is long-tailed, heavy-outlier, or really an ordering, MSE quietly optimizes the wrong thing. Here is the menu and when each earns its place.

Huber / smooth-MAE — outlier-robust watch-time

Watch-time per impression is dominated by a few enormous values: most sessions are seconds, but a binge-watcher leaves a video playing for 3 hours. MSE's gradient is the raw residual r = ŷ − y, so a single 3-hour session produces a gradient thousands of times larger than a normal one and drags the whole batch toward fitting that one outlier. Huber loss (a.k.a. smooth-MAE) fixes this by being quadratic near zero and linear past a transition point δ:

Lδ(r) = ½ r² if |r| ≤ δ; δ(|r| − ½δ) if |r| > δ

The point is the gradient: for |r| ≤ δ it is r (behaves like MSE — sensitive, fast convergence on the bulk); for |r| > δ it saturates at ±δ (behaves like MAE — a constant pull, so no single example can dominate).

Worked numbers — why MSE over-weights the 3-hour binge
Targets in seconds, transition δ = 60. A normal session: predicted 30s, actual 90sr = −60. A binge: predicted 30s, actual 10 800s (3 h) → r = −10 770.
MSE gradient = r Huber gradient (δ=60) normal (r = −60) −60 −60 (inside δ: same) binge (r = −10770) −10770 −60 (clipped to −δ) MSE: the binge contributes a gradient 10770/60 ≈ 180× the normal one. Huber: both contribute |grad| = 60. The binge has lost its veto power.
Under MSE the optimizer will happily mis-predict a thousand ordinary sessions to shave error off one binge, because that one residual is worth ~180 of them. Huber caps the binge's influence at one normal session's worth. The knob δ is the boundary between "trust the residual" and "this is an outlier, clip it" — set it around the 90–95th percentile of residuals so the bulk stays in the quadratic regime.

Gamma-distribution loss — long-tailed duration done right

Huber stops outliers from dominating, but it still implicitly models the target as roughly symmetric around the mean. Watch-time and dwell-time are neither symmetric nor outlier-y so much as genuinely long-tailed — strictly positive, right-skewed, variance that grows with the mean. The principled move is to stop pretending the target is Gaussian and fit the distribution that actually generates positive skewed data: the Gamma. The head predicts the Gamma mean μ = f(x) (and optionally a shape k), and the loss is the negative Gamma log-likelihood. For fixed shape k this reduces to a clean per-example form:

LGamma = k · ( y / μ + log μ ) + const

The mechanism that matters: the error is effectively measured in relative terms (the y/μ ratio), not absolute seconds. Mispredicting a 10s video by 5s and a 10 000s video by 5000s incur comparable loss — both are 50% off — whereas MSE would consider the second error a million times worse and ignore short videos entirely. This is exactly the "variance scales with the mean" property you want for duration. (Poisson log-loss, L = λ − y log λ, is the discrete-count sibling — used when the target is a non-negative count like number of interactions; the source uses it for a predicted like-count head.)

Ordinal play-depth buckets — turn regression into ordered classification

Often you do not actually need the watch-time in seconds — you need to know how far the user got. Then the cleanest reformulation is to discretize the continuous target into ordered buckets at the natural milestones (25% / 50% / 75% / 100% completion) and predict, with K−1 binary heads, the cumulative question "did the user pass threshold t?":

P(depth ≥ 25%), P(depth ≥ 50%), P(depth ≥ 75%), P(depth ≥ 100%)
Why ordinal beats both raw regression and plain multi-class
vs. regression: you stop fighting the heavy tail and the units problem entirely — each head is a calibrated BCE probability you can read and combine. vs. plain K-way softmax: a softmax treats "watched 25%" and "watched 100%" as unrelated classes, so predicting 100% when the truth was 75% is penalized exactly as much as predicting 0% — it throws away the order. The cumulative-threshold encoding is monotone by construction (passing 75% implies passing 50%), so the loss knows that being off by one bucket is a small error and being off by three is a big one. You also recover an expected play-depth for free: E[depth] ≈ Σt P(depth ≥ t) · Δt. The trade-off is granularity — four buckets cannot tell 26% from 49% — so pick milestones that map to product meaning (a 75% completion is a "satisfied watch").

Focal loss — sparse like / share / comment heads

Engagement heads other than click are brutally imbalanced: a share might fire on 0.1% of impressions. Plain BCE drowns in the easy negatives — the 99.9% of impressions the model already confidently calls "no share" each still contribute a small loss, and in aggregate they swamp the gradient from the rare positives. Focal loss (Lin et al. 2017) multiplies BCE by a modulating factor that fades out examples the model already gets right:

Lfocal = −(1 − pt)γ · log pt, where pt = p if y = 1 else 1 − p
Worked numbers — the (1−p)γ down-weighting, γ = 2
ExampleptBCE = −log ptfactor (1−pt)2focal loss
easy negative (model sure it's "no share")0.990.0100.00010.0000010
easy positive (model sure it's a share)0.900.1050.010.00105
hard positive (model unsure, truth = share)0.102.3030.811.865
The easy negative is down-weighted ≈ 10 000× and the easy positive 100×, while the hard positive keeps 81% of its loss. The gradient budget shifts off the millions of trivial negatives and onto the handful of rare, informative positives — without you having to hand-tune a resampling ratio. γ = 0 recovers plain BCE; γ = 2 is the usual default. (An α class-weight is often added on top for the residual positive/negative imbalance.)

YouTube watch-time-weighted logistic regression — the trick that makes a classifier predict a duration

There is an elegant alternative to a regression head entirely, from YouTube's ranking paper: keep a logistic-regression (BCE) classifier, but weight each positive example by its watch-time. Negatives (no click) get weight 1; a positive that was watched for Ti seconds gets weight Ti. The remarkable result is that the learned odds of this weighted classifier approximate E[\text{watch-time}] — so you serve e^{z} instead of σ(z) and read off expected watch-time directly, while keeping all the machinery (and calibration behavior) of a BCE classifier.

Why it works: the weighted logistic regression learns odds equal to the sum of positive weights over the count of negatives. With N impressions, k of them clicked-and-watched:

odds = ez = ( Σclicks Ti ) / ( N − k )
Worked arithmetic — odds ≈ E[watch-time]
Take N = 1000 impressions, k = 10 clicks, each watched 100s.
Σ positive weights = 10 × 100 = 1000 negatives = N − k = 990 learned odds e^z = 1000 / 990 = 1.0101 E[watch-time over ALL impressions] = (10 × 100) / 1000 = 1.0 s
The odds 1.0101 match expected watch-time 1.0s up to the factor N/(N−k) = 1000/990 ≈ 1.01. That factor is exactly 1/(1 − p) for click-rate p = k/N; since production click-rates are small (1–3%), e^{z} ≈ E[\text{watch-time}] with sub-3% bias. This is why a single BCE head, weighted by watch-time, can stand in for a full regression head — and inherits BCE's proper-scoring-rule calibration for free. The catch: it predicts watch-time conditioned on the click model being right, so a miscalibrated click prior leaks straight into the watch-time estimate.

A bridge to multi-objective fusion (lesson 12): uncertainty weighting

Every head above produces its own loss Li. The ranker trains on a single sum — but the heads are on wildly different scales (a Gamma watch-time loss in the tens, a BCE share loss near 0.01) and have wildly different noise (instantaneous swipe-speed is noisy; 7-day completion is clean). A fixed weighted sum Σ wi Li forces you to hand-tune the wi, and the noisy head will dominate the gradient just because its loss is numerically larger. Uncertainty weighting (Kendall et al. 2018) makes the weights learnable by treating each task's noise σi as a parameter:

L = Σi ( 1 / 2σi² ) · Li + Σi log σi

The effective weight on head i is 1/2σi²: a head the model finds hard (large σi) is automatically down-weighted. The log σi term is the regularizer that stops the cheat of sending every σi → ∞ to zero out all losses — growing σ costs log σ.

Worked example — a noisy head gets down-weighted
Two heads: a clean CTR head with loss L1 = 0.20 and a noisy watch-time head with loss L2 = 2.0. Suppose training settles at σ1 = 0.5 (confident) and σ2 = 2.0 (noisy).
headLiσieffective weight 1/2σi²weighted term + log σi
CTR (clean)0.200.52.002.00·0.20 + log 0.5 = 0.40 − 0.69 = −0.29
watch (noisy)2.02.00.1250.125·2.0 + log 2.0 = 0.25 + 0.69 = 0.94
Despite the watch head's raw loss being 10× larger, its effective weight is 0.125 versus the CTR head's 2.00 — a 16× down-weight, learned, not hand-set. A naive Σ Li would have done the opposite: let the loud, noisy head steer training. (Source caveat: this assumes Gaussian homoscedastic noise, so it is itself sensitive to outliers — which is exactly why you pair it with the Huber/Gamma head shapes above rather than raw MSE.)

This is only the doorway. The heavier machinery for trading off competing heads — MMoE / PLE gating, GradNorm (balancing by gradient magnitude), PCGrad / CAGrad (projecting away conflicting gradients), the seesaw effect, and online dynamic weighting — lives in lesson 12. The per-head loss shapes here are the inputs to that fusion; do not conflate the two.

Head / targetLoss shapeWhy this one
CTR, CVR (binary)BCEProper scoring rule → calibrated probability for the auction.
Like / share / comment (sparse binary)Focal (γ≈2) + α0.1% positives; fade out the easy negatives that swamp BCE.
Watch-time (heavy outliers)Huber / smooth-MAECap the 3-hour binge's gradient at δ; don't refit the bulk to it.
Watch-time / duration (long-tailed)Gamma NLL (or weighted-LR)Relative error; variance scales with the mean; strictly positive.
Play-depth (you want milestones)Ordinal cumulative BCEKeep the order; off-by-one is a small error, not a class swap.
Predicted count (interactions)Poisson log-lossNon-negative integer count; the discrete sibling of Gamma.

What "calibrated" actually means

A binary classifier p̂(x) is calibrated if, for every predicted probability level p:

P( y = 1 | p̂(x) ∈ [p, p + dp] ) ≈ p

Operationally: take all impressions where the model predicted between 0.29 and 0.31; compute the empirical click rate; if it's near 0.30, the model is calibrated in that bucket. Repeat for every bucket.

Reliability diagrams and ECE are the offline calibration metrics; how they sit next to AUC/GAUC, log-loss, and the online A/B counterparts (and why a head can improve offline yet move no online metric) is the subject of lesson 08.

The cleanest way to internalize it
Take a perfectly calibrated model and pass its output through p → p². AUC is unchanged (monotone). The reliability diagram has collapsed. Every probability is now under-estimated; an auction multiplying bid × p will under-bid. The ranker still ranks correctly, but the system around it is broken.

Why calibration matters for ads (and less for recsys)

The ads auction's expected revenue per impression is, roughly,

eCPM = bid × P(click) × P(conv | click) × 1000
Worked numbers — the negative-downsampling miscalibration that overbids 25×
The most common way a CTR model decalibrates: you downsample negatives to balance training. Production CTR is 2%, but you train on a 50/50 sampled set, so the model learns a prior of 50% and its average prediction is ~0.5 instead of ~0.02. Plug that into the auction: eCPM = bid × 0.5 instead of bid × 0.02 — you bid 25× too high and bleed money on every impression. Ranking is untouched (the order is monotone), so AUC looks fine and the bug is invisible until finance notices. The fix is a known log-odds correction: subtract \ln(\tfrac{0.5}{0.5}) − \ln(\tfrac{0.02}{0.98}) ≈ 3.9 from every logit before the sigmoid, or recalibrate against the true base rate. This is exactly why an ads stack cannot ship a pairwise/listwise score raw — it has no absolute level to correct.

If P(click) is off by a factor of 2 systematically, eCPM is off by a factor of 2. The auction picks the wrong winner; the second-price clearing rule charges the wrong CPC; pacing controllers (which use absolute CTR to bid-shade against a budget) misfire. Calibration is the difference between charging the right CPC and charging double or half — production ads stacks routinely have a team owning the calibration layer alone.

For a pure recsys feed, the only thing that matters downstream is the order. A miscalibrated model that ranks correctly still produces the correct feed, so recsys teams sometimes get away with pairwise/listwise losses where ads teams cannot. Said another way: ads stacks consume probabilities; recsys feeds consume permutations.

Why models miscalibrate in the first place

Post-hoc calibration techniques

MethodFormParametersWhen it shines
Platt scaling pcal = σ(a · z + b) Two scalars (slope, intercept) Monotonic miscalibration; small held-out set. Handles a prior shift via b and slope via a.
Isotonic regression Step function, monotone non-decreasing Non-parametric (≈ K knots) Non-monotonic shapes (S-curves with bends). Needs more data so the steps don't overfit.
Temperature scaling pcal = σ(z / T) One scalar Pure over/underconfidence on deep nets. Doesn't move the prior — only sharpens or softens.
Beta calibration (Kull et al. 2017) Three-parameter (a,b,c) logistic regression on (log p, log(1−p)) features Three scalars Sigmoid-output models where Platt is the wrong functional form.

A useful axis: what distortion can each method fix? Temperature only stretches the logit axis — it fixes overconfidence but cannot translate the curve to fix a prior shift. Platt has a bias term, so it can. Isotonic can fix non-monotone bends that Platt's two parameters can't bend around. The widget below makes this concrete.

Interactive · the calibration explorer

Pick a failure mode and a calibrator. Watch the reliability diagram and ECE update. The point: different distortions need different fixes.

Reliability diagram & calibrators
10 bins on [0,1]. Plotted: empirical click rate vs mean predicted probability. The "." diagonal is perfect calibration. Try: overconfident + temperature (fix). Prior-shift + temperature (does not fix). Prior-shift + Platt (fixes it).
FAILURE MODE
CALIBRATOR
failure
none
calibrator
raw
ECE (raw)
ECE (after)
Reading

Interview prompts you should be ready for

  1. "Why does an ads team train with BCE and a search-results team sometimes get away with pairwise loss?" (Probes: do you understand that ads consume probabilities — eCPM = bid × P(click) — while a feed only consumes order? Pairwise loss is shift-invariant; the model never learns the absolute level.)
  2. "Your CTR model has AUC 0.85 but the auction is bidding wildly. Diagnose." (Probes: AUC is invariant to monotone transforms of the score; the absolute level can be arbitrarily off while AUC stays high. The fix is a reliability diagram + ECE + a post-hoc calibrator. Then check upstream causes: missing log-Q correction, prior shift, feature staleness.)
  3. "Walk me through Platt scaling vs isotonic regression. When is each appropriate?" (Probes: parametric vs non-parametric, data requirements, the shape of distortions each can correct, the failure modes — Platt assumes a sigmoid distortion, isotonic needs enough data per knot.)
  4. "Your training data is 50% positive (downsampled), but production is 2% positive. What goes wrong if you don't correct?" (Probes: the model's predicted probabilities will sit near 0.5 on average. The auction multiplies these by bid and over-bids by ≈25×. The fix is the well-known log-odds correction: subtract log(ptrain / (1 − ptrain)) − log(pserve / (1 − pserve)) from the logit.)
  5. "When would you reach for LambdaRank instead of pointwise BCE?" (Probes: GBDT-based ranker for web search where the metric is rank-aware (NDCG), no downstream consumer needs a probability, dataset is judged not click-logged. Don't reach for it in an ads stack.)
  6. "Calibrated AUC vs uncalibrated AUC — same or different?" (Same. AUC is invariant to any monotone transform of the score, and every post-hoc calibrator is monotone. Calibration cannot change AUC. Junior candidates often think it does.)
  7. "Your watch-time head uses MSE and predictions are dominated by a few users who leave videos playing overnight. What's wrong and what do you switch to?" (Probes: MSE's gradient is the raw residual, so one 3-hour session contributes a gradient ~180× a normal one and the optimizer refits the bulk to chase that one outlier. Switch to Huber/smooth-MAE — quadratic inside δ, linear outside, so the outlier's gradient clips at δ. If the target is long-tailed rather than just outlier-y, a Gamma NLL measures relative error and lets variance scale with the mean. Bonus: YouTube's watch-time-weighted-LR, where the odds e^{z} ≈ E[\text{watch-time}].)
  8. "You have a sparse 'share' head (0.1% positive) and a noisy 'swipe-speed' head alongside a clean CTR head. How do you design the losses and combine them?" (Probes: per-head shape first — Focal loss for the sparse share head, the (1−p)^{γ} factor fading out the easy negatives that swamp BCE; Huber for the noisy swipe-speed. Then fusion — uncertainty weighting L = Σ (1/2σi²)Li + log σi auto-down-weights the noisy head via a learned σ, with log σ preventing the σ→∞ cheat. Don't reach straight for MMoE/GradNorm — that's lesson 12, and it's a separate axis from the per-head loss shape.)
Takeaway
The loss decides what the output means. BCE makes the output a probability (calibration is native); pairwise/listwise make the output a ranking score (calibration must be added post-hoc). Ads consume probabilities, so the ads stack uses BCE plus an explicit calibration layer; recsys feeds only consume order and can sometimes skip both. When debugging a "good AUC but bad bidding" story, the answer is always somewhere on the calibration axis, and the right calibrator depends on whether the distortion is over/underconfidence (temperature), prior-shift (Platt), or non-monotone bends (isotonic).