Navigation

Introduction to AI

Machine Learning

Deep Learning

Generative AI

Tools & Frameworks

General

Sparse Autoencoders (SAEs) for Interpretability

Neural networks represent concepts as directions in activation space. However, because the number of concepts a model must learn exceeds its dimensionality (e.g., a 4096-dimensional residual stream representing millions of real-world concepts), networks squeeze multiple concepts into the same dimensions—a phenomenon known as superposition. This makes raw activations highly polysemantic and difficult for humans to interpret.

Sparse Autoencoders (SAEs) address this issue. By training a shallow autoencoder with an L1 regularization penalty on neural activations, SAEs decompose polysemantic activations into a sparse set of interpretable, monosemantic features.


The Superposition Problem

Consider an activation vector $h \in \mathbb{R}^d$. If a model represents a concept $i$ as a unit direction vector $v_i$, we can write the activation as:

$$h = \sum_{i} f_i v_i$$

Where $f_i \ge 0$ is the activation value of concept $i$.

  • If the concepts are orthogonal ($v_i^T v_j = 0$), we can represent at most $d$ concepts.
  • To represent $M \gg d$ concepts, the model uses non-orthogonal directions. This introduces interference (cross-talk) between concepts, which the model suppresses by ensuring that only a small subset of concepts are active at any time (sparsity).

Because of superposition, looking at individual activations or neurons directly is confusing; a single neuron might fire for “clinical medical trials,” “conversations about soccer,” and “JavaScript syntax.”


Decomposing Activations with SAEs

An SAE is a neural network with a single hidden layer that is trained to reconstruct activation vectors $h$.

Activation Vector (h) ---> Encoder (W_enc) ---> ReLU/Sparsity ---> Latent Features (f)
                                                                      |
Activation Reconstruction (h_hat) <--- Decoder (W_dec) <-------------+

1. The Encoder

The encoder projects the activation vector $h$ into a higher-dimensional space ($D \gg d$, typically $8\text{x}$ to $32\text{x}$ larger than the model’s residual stream) and applies a ReLU activation to enforce non-negativity:

$$f = \text{ReLU}\left( W_{\text{enc}} (h - b_{\text{dec}}) + b_{\text{enc}} \right)$$

Where $f \in \mathbb{R}^D$ is the sparse feature activation vector, $W_{\text{enc}} \in \mathbb{R}^{D \times d}$ is the encoder weight matrix, and $b_{\text{enc}}$ is the bias.

2. The Decoder

The decoder attempts to reconstruct the original activation vector $h$ from the sparse latent features $f$:

$$\hat{h} = W_{\text{dec}} f + b_{\text{dec}}$$

Where $W_{\text{dec}} \in \mathbb{R}^{d \times D}$ is the decoder weight matrix.


The Loss Function: Enforcing Sparsity

To ensure that the hidden layer $f$ represents concepts monosemantically, we must force it to be sparse (most elements of $f$ should be exactly $0$). This is achieved by combining reconstruction loss with an L1 regularization penalty:

$$\mathcal{L}_{\text{SAE}}(h) = |h - \hat{h}|^2_2 + \lambda |f|_1$$

Where:

  • $|h - \hat{h}|^2_2$ is the Mean Squared Error (MSE) reconstruction loss.
  • $|f|1 = \sum{i} |f_i|$ is the L1 norm of the latent features, which penalizes the sum of active feature values.
  • $\lambda$ is a hyperparameter balancing reconstruction fidelity against sparsity.

Training Enhancements

Standard SAEs suffer from dead latents—features that stop firing during training because their weights are updated in a way that prevents the ReLU from activating. To address this, researchers use two main techniques:

  • Latent Resampling: Periodically identifying dead latents and resetting their weights to match activation vectors that the SAE currently reconstructs poorly.
  • Top-K SAEs: Instead of using L1 regularization, Top-K SAEs explicitly keep only the $K$ largest activations in $f$ and set the remaining $D-K$ features to $0$. This eliminates the need to tune $\lambda$.

Code Concept: A Basic SAE Module

Below is a PyTorch implementation of a standard Sparse Autoencoder with L1 regularization.

import torch
import torch.nn as nn

class SparseAutoencoder(nn.Module):
    def __init__(self, activation_dim, dict_size, l1_coeff=1e-3):
        super().__init__()
        self.activation_dim = activation_dim
        self.dict_size = dict_size # D: typically activation_dim * 8 or 32
        self.l1_coeff = l1_coeff
        
        # Encoder: projects activation to high-dimensional space
        self.encoder = nn.Linear(activation_dim, dict_size)
        self.relu = nn.ReLU()
        
        # Decoder: reconstructs activation
        self.decoder = nn.Linear(dict_size, activation_dim, bias=False)
        self.dec_bias = nn.Parameter(torch.zeros(activation_dim))
        
        # Normalize decoder columns to unit norm to prevent L1 scaling tricks
        self.normalize_decoder_weights()

    @torch.no_grad()
    def normalize_decoder_weights(self):
        # Enforce unit norm on columns of decoder weight matrix
        norms = torch.norm(self.decoder.weight, p=2, dim=0, keepdim=True)
        self.decoder.weight.div_(norms)

    def forward(self, h):
        # h: [batch, activation_dim]
        # Step 1: Center activations and encode
        h_centered = h - self.dec_bias
        features = self.relu(self.encoder(h_centered)) # [batch, dict_size]
        
        # Step 2: Reconstruct
        h_reconstructed = self.decoder(features) + self.dec_bias # [batch, activation_dim]
        
        # Step 3: Compute Loss
        reconstruction_loss = nn.functional.mse_loss(h_reconstructed, h)
        l1_loss = torch.norm(features, p=1, dim=-1).mean()
        total_loss = reconstruction_loss + self.l1_coeff * l1_loss
        
        return h_reconstructed, total_loss, features