Sparse Probing¶
Sparse probing measures how strongly binary-label information is concentrated in a small set of activation coordinates. TransformerLens provides a model-independent fit and sweep API; callers decide how to collect and aggregate activations.
Probe performance establishes held-out decodability under the chosen split, preprocessing, regularization, and sparsity. It does not establish causal model use, neuron monosemanticity, or superposition.
Fit one probe¶
import torch
from transformer_lens.tools.analysis import fit_sparse_probe
features = torch.randn(200, 768)
labels = torch.arange(200) % 2
result = fit_sparse_probe(
features,
labels,
k=8,
positive_label=1,
test_fraction=0.3,
preprocess="none",
class_weight="balanced",
l2_strength=1e-2,
seed=0,
)
print(result.selected_features)
print(result.metrics.f1)
features must have shape [example, feature] and dtype float16, bfloat16, float32, or float64.
Labels must be a one-dimensional Boolean or integer tensor containing exactly two values, one equal
to positive_label. Each class needs at least two examples.
Selection and split contract¶
The function creates a deterministic stratified split before computing any learned statistic. For each class $c$, its realized test count is
$$ n_{\mathrm{test},c} = \operatorname{clamp}(\lceil f n_c \rceil, 1, n_c - 1), $$
where $f$ is test_fraction. Realized train and test counts are returned because small classes
can differ materially from the requested aggregate fraction.
On the training split only, feature $j$ receives the signed score
$$ s_j = \mathbb{E}[X_j \mid y=\mathrm{positive}] - \mathbb{E}[X_j \mid y=\mathrm{negative}]. $$
The selected support contains the $k$ largest $|s_j|$. Equal scores are resolved by increasing
feature index. preprocess="none" fits the selected raw coordinates. With
preprocess="standardize", selected columns are centered and scaled using training statistics.
Each column’s scale is its training standard deviation raised to at least std_floor (default
1e-3), so a near-constant column is not amplified to unit scale; zero-variance columns receive
scale one, and std_floor=0 disables the floor. The same transform is then applied to held-out
values.
Because L2 regularization is scale-sensitive, preprocessing can change the fitted probe and the resulting k-curve. A sweep therefore fixes preprocessing and L2 strength across every k.
Logistic objective¶
For selected training features $X$, labels $y \in {0,1}$, coefficients $w$, and intercept $b$, the optimizer minimizes
$$ \frac{1}{n}\sum_i \alpha_{y_i} \operatorname{BCEWithLogits}(X_i w + b, y_i)
\frac{\lambda}{2}\lVert w\rVert_2^2, \qquad \alpha_c = \frac{n}{2n_c}. $$
The displayed weights apply to the default class_weight="balanced"; pass None to use
$\alpha_c=1$. The intercept is not regularized, so under class_weight=None the fitted
intercept of an uninformative probe converges to the training log-odds
$\log(n_{\mathrm{pos}}/n_{\mathrm{neg}})$, not to zero; the balanced weights remove that
imbalance offset. Positive predictions have nonnegative logits.
Accuracy, precision, recall, F1, and all four confusion counts are returned; precision or F1 is
zero when its denominator is zero. F1 is the primary sparse-probing metric.
F1 and the other threshold metrics describe a single operating point, logit >= 0, so they can
reward a probe that carries no information. When every selected coordinate is constant on the
training rows, the fit returns zero coefficients and a zero intercept, every held-out logit is
exactly 0.0, and every held-out example is predicted positive. F1 then equals $2p/(1+p)$ for
the held-out positive rate $p$ (0.667 at $p = 0.5$) even though the probe cannot rank examples.
roc_auc and average_precision are also returned, computed from the held-out logits without a
threshold. Both are tie-aware: tied logits share their average rank for ROC-AUC and form one
threshold for average precision. The degenerate probe above therefore scores ROC-AUC 0.5 and
average precision $p$, the chance levels. Check constant_features and compare against these
threshold-free metrics before reading a high F1 as decodability.
Feature-score reductions use float64 for float64 inputs and float32 otherwise. Selected matrices
move to CPU float64, where LBFGS (at most max_iter iterations, stopping once function
evaluations reach max_iter * 5 // 4, which the strong-Wolfe line search can exceed by one, and
with a fixed internal gradient stop) is followed by up to max_refinement_steps damped Newton
steps on the (k+1)-square
Hessian of the objective. A fit is accepted only when the Newton decrement $\tfrac{1}{2}
g^\top H^{-1} g$, an estimate of the objective gap to the optimum in nats, is at most
decrement_tolerance (default 1e-12, which must lie in (0, 1)); the decrement is checked
before the first refinement step, so a budget of zero only checks. The fit raises when that
tolerance is not met within the budget, when the Hessian cannot be factorised, or when any
output is non-finite. All result tensors are detached CPU tensors.
Results retain the requested k, max_iter, max_refinement_steps, and decrement_tolerance
alongside the realized objective, the Newton decrement, the refinement-step count, the
objective-gradient infinity norm after refinement (a diagnostic, not the acceptance rule), the
LBFGS iteration and function-evaluation counts, and the LBFGS stop reason: "tolerance_grad",
"max_iter", "max_eval", or "line_search" when the strong-Wolfe search made no progress.
There is no convergence flag: a fit that misses the acceptance threshold raises instead of
returning.
Sweep and controls¶
from transformer_lens.tools.analysis import sweep_sparse_probe
sweep = sweep_sparse_probe(
features,
labels,
ks=[1, 2, 4, 8, 16],
n_random_subsets=20,
n_label_shuffles=20,
seed=0,
)
for k, probe, random_control in zip(
sweep.ks,
sweep.results,
sweep.random_coordinate_controls,
strict=True,
):
print(
k,
probe.metrics.f1,
probe.metrics.roc_auc,
random_control.f1.median(),
random_control.roc_auc.median(),
)
Every k uses the same split, preprocessing mode, and L2 strength. ks must be strictly
increasing and unique.
Random-coordinate controls sample k distinct coordinates and fit the same classifier.
Label-shuffle controls permute training labels, repeat selection and fitting, and evaluate against
the untouched held-out labels. Each control carries per-repeat accuracy, precision, recall, F1,
ROC-AUC, and average precision. A random-coordinate control that lands on a dead coordinate keeps
the inflated F1 described above, so compare controls on ROC-AUC as well. The API returns raw
control supports and metric distributions; it
does not convert them into p-values or representation labels. A repeat count of zero disables that
control. Each control draw depends only on seed, k, its arm, and its repeat index, so a k’s
controls are independent of the rest of the k-grid and of the other arm’s repeat count, and the
draws for a smaller repeat count are a prefix of a larger one’s.
Unlike the main results, control distributions carry only raw metrics — no per-fit convergence
diagnostics. A control fit that fails to converge is not fatal to the sweep: it is excluded from
its control distribution and recorded in sweep.rejections (a SparseProbeRejection naming the
arm, k, repeat, support, and reason), so the completed main probes and other controls survive.
Each control’s rows therefore cover only the repeats that converged. Main-probe fits are not made
partial this way — a main fit that misses the acceptance threshold still raises, as does
fit_sparse_probe.
Controls can be expensive: the sweep performs one main fit plus both requested control counts for every k. Start with small grids and repeat counts.
Composing with cached activations¶
Use run_with_cache to construct the feature matrix separately so token, position, batching, and
aggregation choices remain explicit:
tokens = model.to_tokens(prompts)
_, cache = model.run_with_cache(tokens, names_filter=[hook_name])
features = cache[hook_name][:, -1, :]
result = fit_sparse_probe(features, labels, k=8)
The example selects the final sequence position, which is not appropriate for every dataset.
Choose the hook and position policy before interpreting selected coordinates. The API cannot detect
leakage already introduced into caller-provided features. When several rows come from one source
prompt, for example multiple positions of the same sequence, keep all of those rows on one side of
the split; the row-level split is label-independent and can otherwise place rows of one prompt on
both sides, inflating held-out accuracy on grouped data.
Reference¶
The raw mean-difference selector and sparse-probing framing follow Wes Gurnee et al., “Finding Neurons in a Haystack: Case Studies with Sparse Probing,” TMLR 2023, with reference code.
TransformerLens intentionally adds stratification, stable tie-breaking, explicit objective and
convergence diagnostics, and deterministic controls. Its optional centered standardization and
Torch LBFGS solver are not exact reproductions of the reference implementation. The default
std_floor=1e-3 matches the reference’s standard-deviation floor, except that zero-variance
columns keep scale one.