AI Grimoire
Sheet
statuscommon
difficultyadvanced
timeO(d·m)
described2023
revised7d ago

Sparse Autoencoders

Fit an overcomplete dictionary to a layer’s activations under a sparsity penalty, on the hypothesis that features are more numerous than dimensions and superposed.

[sparse][features]Commonly used

Standing

Commonly usedEstablished and frequently the right choice, but competing with live alternatives rather than having settled the question.

The dominant method for decomposing activations, and under active criticism — feature absorption and reconstruction-quality objections are unresolved.

judged as of 2026-09 · what the labels mean

Theory

The superposition hypothesis holds that a network represents more features than it has dimensions by placing them in almost-orthogonal directions and relying on their sparsity to keep interference low. If true, the activations are sparse combinations of an overcomplete dictionary, and that dictionary is recoverable.

x≈b+∑ifi(x) di,f(x)=ReLU(We(x−b)+be)x \approx b + \sum_i f_i(x)\, d_i, \qquad f(x) = \mathrm{ReLU}\bigl( W_e (x - b) + b_e \bigr)
eq. 1 — encode, then reconstruct from the dictionary
L=∥x−x^∥22+λ∑ifi(x) ∥di∥2L = \bigl\| x - \hat{x} \bigr\|_2^2 + \lambda \sum_i f_i(x)\, \| d_i \|_2
eq. 2 — L1 scaled by decoder normBricken et al.

Weighting the L1 by the decoder column norm is not cosmetic: without it the penalty is trivially gamed by shrinking ff and growing dd proportionally. The alternative is to constrain the columns to unit norm and project after every step.

Shrinkage

An L1 penalty biases every active feature toward zero, so reconstructions are systematically too small. Top-k and JumpReLU encoders replace the penalty with a hard sparsity constraint, which removes the bias at the cost of a non-differentiable activation that needs a straight-through estimator.

Implementation

python · torch ≥ 2.1
import torch
from torch import Tensor, nn


class TopKSAE(nn.Module):
    """Top-k encoder: sparsity is a constraint, not a penalty,
    so there is no shrinkage to correct for."""

    def __init__(self, d_model: int, d_hidden: int, k: int = 32):
        super().__init__()
        self.enc = nn.Linear(d_model, d_hidden)
        self.dec = nn.Linear(d_hidden, d_model, bias=False)
        self.pre_bias = nn.Parameter(torch.zeros(d_model))
        self.k = k

    def forward(self, x: Tensor) -> tuple[Tensor, Tensor]:
        centred = x - self.pre_bias
        acts = self.enc(centred)
        vals, idx = acts.topk(self.k, dim=-1)
        f = torch.zeros_like(acts).scatter_(-1, idx, vals.relu())
        return self.dec(f) + self.pre_bias, f

    @torch.no_grad()
    def normalise_decoder_(self) -> None:
        self.dec.weight /= self.dec.weight.norm(dim=0, keepdim=True)

Subtracting a learned pre-bias before encoding and adding it back after decoding lets the dictionary model deviations from the mean activation rather than the mean itself — without it the first few features are spent representing a constant. Call normalise_decoder_ after each optimiser step, and remove the component of the gradient parallel to each column beforehand if you want the step to be exact rather than approximately tangent.

Dictionary
16 384
L0
32
Dead latents
3.1%
residual stream, layer 12 of 24, d = 1024, 400M tokens

Related

References

[1]Bricken et al. — Towards Monosemanticity: Decomposing Language Models With Dictionary Learning (2023)transformer-circuits
[2]Elhage et al. — Toy Models of Superposition (2022)transformer-circuits
[3]Gao et al. — Scaling and Evaluating Sparse Autoencoders (2024)arXiv:2406.04093
[4]Rajamanoharan et al. — Jumping Ahead: Improving Reconstruction Fidelity with JumpReLU SAEs (2024)arXiv:2407.14435