Focal Loss Viz

Plot of the focal loss FL(p_t) = -(1 - p_t)^γ · log(p_t) against model confidence, with the cross-entropy curve underneath for comparison. Drag γ and easy examples on the right side of the chart collapse toward zero loss while hard examples on the left hold nearly full weight — the same lever RetinaNet pulls to make dense object detection actually train.

Focal loss versus standard cross-entropy.Focal loss with γ = 2.00. At p_t = 0.9 the modulating factor is 0.010, suppressing the cross-entropy loss by 100×.
FL = -(1 - p_t)^γ · log(p_t)γ = 2.00
0123450.00.20.40.60.81.0p_t (model confidence on the true class)loss
Modulating factor (1 - p_t)^γ0 ≤ (1 - p_t)^γ ≤ 1
10
2.00
Customize
Focusing parameter
2.00
Display

Installation

npx shadcn@latest add https://craftbits.dev/r/focal-loss-viz.json

Usage

import { FocalLossViz } from "@craft-bits/core";
 
<FocalLossViz defaultGamma={2} />

Show the modulating factor on a second mini-chart so the user can read off (1 - p_t)^γ at any p_t:

<FocalLossViz defaultGamma={2} showModulator />

Drive γ from the outside:

const [gamma, setGamma] = useState(2);
 
<FocalLossViz
  gamma={gamma}
  onGammaChange={setGamma}
  gammaRange={[0, 8]}
/>

Understanding the component

  1. The focal-loss recipe. Cross-entropy -log(p_t) punishes wrong predictions hard but keeps a non-trivial gradient on already-confident examples — useless on imbalanced datasets where most examples are easy negatives. Multiplying by (1 - p_t)^γ drives that gradient toward zero as p_t → 1, concentrating training on the few hard examples that still look like noise to the model.
  2. γ as a focusing dial. At γ = 0 the modulating factor is identically 1 and focal loss is exactly cross-entropy — the curves overlap. As γ grows the focal curve peels away from CE on the easy (right) side. The hard (left) side barely moves because (1 - p_t)^γ ≈ 1 when p_t is small.
  3. Numerical stability. log(p_t) is computed via log(max(p_t, 1e-10)) so the left edge of the chart stays finite. Curve sampling clips segments that would shoot past the y-axis ceiling, drawing a clean break instead of a wild jump.
  4. Spring transitions on γ changes. The focal curve animates with SPRINGS.smooth so dragging γ produces a continuous reshape rather than a chatter of redraws. Reduced-motion users snap.
  5. Controlled + uncontrolled. Pass gamma + onGammaChange to drive γ from outside, or omit both and let the component own its state via defaultGamma.
  6. Optional modulator subplot. Setting showModulator adds a narrow second chart of (1 - p_t)^γ on a fixed 0..1 axis. Useful when teaching what's being multiplied in — the loss curve alone hides the magnitude of the down-weighting at any given p_t.

Props

PropTypeDefaultDescription
gammanumberControlled γ ≥ 0. Pair with onGammaChange.
defaultGammanumber2Uncontrolled initial γ.
onGammaChange(γ: number) => voidFires whenever the γ slider commits a new value.
gammaRangereadonly [number, number][0, 5]Slider extents. Both must be ≥ 0, min < max.
showModulatorbooleanfalseRender the (1 - p_t)^γ curve on a second mini-chart.
transitionTransitionSPRINGS.smoothSpring for the focal-curve reshape.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • The chart is wrapped in role="figure" with a dynamic aria-describedby ("Focal loss with γ = 2.00. At p_t = 0.9 the modulating factor is 0.010, suppressing the cross-entropy loss by 100×.") so screen-reader users get the same headline as sighted users.
  • The γ slider is a LabeledSlider backed by a native <input type="range"> — full keyboard support out of the box (Arrow keys step, Page Up / Page Down step by 10%, Home / End jump to the extents).
  • The summary uses aria-live="polite" so changes are announced as the user drags.
  • Curve transitions use SPRINGS.smooth; prefers-reduced-motion: reduce snaps instantly.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/viz/FocalLossViz.tsx). Stripped the lesson-specific Explore / Predict / Challenge mode strip, Widget chrome, p_t scrubber, suppression-ratio chip, and undo / redo history; generalised to a single visualisation primitive with controlled / uncontrolled γ. Added the optional modulator subplot, replaced the hand-rolled pointer-event slider with LabeledSlider (free keyboard + a11y), and swapped inline springs for SPRINGS.smooth from @craft-bits/core/motion so reshapes feel continuous on slow drags.
  • Upstream paper: Lin et al., Focal Loss for Dense Object Detection (2017).