LoRA Rank Explorer

Plot how the reconstruction quality of W ≈ B·A varies with the LoRA rank r. The y-axis is the reconstruction residual ||W − Bᵣ·Aᵣ||; the x-axis is r ∈ [1, min(rows, cols)]. Drag the slider and watch the marker walk along the curve — as r grows, the rank-r approximation approaches W, and the curve makes that approach legible.

Different from LoRaFactorizationViz, which renders the factorization geometrically as three blocks (ΔW = B · A). LoRaRankExplorer plots reconstruction quality — same idea, different lens.

LoRA rank explorer: reconstruction error vs rank curve.LoRA reconstruction curve. Target is 8×8. Frobenius residual at r=1: 4.572.
Reconstruction vs rank8×8 · Frobenius · r=1
01.0002.0003.0004.00012345678rank r‖W − BA‖_F
1
Frobenius residual
4.572
captured energy
90.8%
Customize
Rank
1
Metric

Installation

npx shadcn@latest add https://craftbits.dev/r/lora-rank-explorer.json

Usage

import { LoRaRankExplorer } from "@craft-bits/core";
 
<LoRaRankExplorer defaultR={1} />

Provide your own target matrix W:

<LoRaRankExplorer
  targetMatrix={[
    [4, 3, 2, 1],
    [3, 2, 1, 0],
    [2, 1, 0, -1],
    [1, 0, -1, -2],
  ]}
  defaultR={1}
/>

Drive the rank from outside the component:

const [r, setR] = useState(2);
 
<LoRaRankExplorer r={r} onRChange={setR} />

Switch to the spectral metric for a sharper "step" curve when W has a few dominant directions:

<LoRaRankExplorer metric="spectral" defaultR={1} />

Understanding the component

  1. One curve, one slider. Each x-tick is an integer rank. Each y-value is the reconstruction residual at that rank — the gap between W and its best rank-r approximation Wᵣ = Bᵣ · Aᵣ. The marker dot and the dashed vertical cursor mark the current r. Drag the slider, the marker walks along the curve.
  2. Curve shape tells the LoRA story. The default synthetic target has two dominant singular directions plus low-amplitude noise — the curve drops sharply through r = 1, 2, then plateaus. That's the "you only need a handful of ranks" lesson in a single picture. Hand it a different targetMatrix and the curve tells a different story (a noise-y matrix decays slowly; a clean rank-k matrix hits zero at r = k).
  3. Two metrics, one picture. metric="frobenius" plots ||W − Wᵣ||_F = sqrt(Σⱼ>ᵣ σⱼ²) — total squared residual energy. metric="spectral" plots σ_{r+1} directly — the largest singular value of the residual, per Eckart–Young. Spectral curves drop in visible "steps" between each σⱼ; Frobenius curves are smoother.
  4. Captured-energy readout. Below the curve, a Σⱼ≤ᵣ σⱼ² / ||W||_F² panel renders the fraction of the matrix's energy the rank-r approximation captures. The number stays meaningful in both metrics — it always answers "how much of W does rank-r reproduce?"
  5. Pure / deterministic math. Truncated SVD via seeded power-iteration deflation. Same targetMatrix always produces the same curve — no Math.random, no hydration drift. Singular values are computed once per targetMatrix; the curve is O(rMax) after that.
  6. Controlled + uncontrolled. Pass r + onRChange to drive the rank from a parent; omit them and the component owns its own state via defaultR. The rank clamps to [1, min(rows, cols)].
  7. Reduced motion. When prefers-reduced-motion: reduce is set, the cursor + marker springs collapse to duration: 0 — they jump to the new position instead of springing.

Props

PropTypeDefaultDescription
targetMatrixreadonly (readonly number[])[]synthetic 8×8The matrix W the rank-r factorization approximates. Rows must be the same length; non-finite values fall back to the synthetic default.
rnumberControlled rank. Pair with onRChange. Clamps to [1, min(rows, cols)].
defaultRnumber1Uncontrolled initial rank.
onRChange(r) => voidFires when the rank changes.
metric'frobenius' | 'spectral''frobenius'Reconstruction-quality norm. Frobenius is sqrt(Σⱼ>ᵣ σⱼ²); spectral is σ_{r+1}.
transitionTransitionSPRINGS.snapSpring for the marker + cursor. Curve animation always uses SPRINGS.smooth.
classNamestringMerged onto the root via cn().

Accessibility

  • The root is role="figure" with a visually-hidden aria-live="polite" summary that re-announces the metric and the current residual whenever r changes.
  • The rank slider is a native <input type="range"> via LabeledSlider — keyboard arrows, screen-reader value announcements, and :focus-visible rings come for free.
  • The plot uses role="img" with a labelled aria-labelledby so SVG-aware screen readers describe the figure.
  • Color is never the only signal — every axis carries a label (rank r, ‖W − BA‖_F / σ_{r+1}) and the readout panel renders the numbers in font-variant-numeric: tabular-nums.
  • prefers-reduced-motion: reduce collapses every spring to an instant swap.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/viz/LoRaRankExplorer.tsx). The source was a lesson-mode widget with Widget chrome (undo/redo history, bookmarks, formula bar), an Explore / Predict / Challenge ModeStrip with quiz rounds and challenge progression, three side-by-side heatmaps (frozen W, A·B update, merged W + α·A·B), an alpha-scaling knob, and a hard-coded 6 × 6 target plus frozen matrix. The library extract is the pure rank-vs-reconstruction primitive — one curve, one slider, two metrics, a captured-energy readout. The modes, heatmaps, alpha control, and lesson scaffolding live in the consuming lesson, not the primitive.