Interpretability Goals › Code: Exploring Model Internals

Michael BrenndoerferFebruary 2, 20267 min read
Language AI HandbookAll chapters
Part 59 of 67Interpretability
Chapter 446Interpretability Goals › Code: Exploring Model Internals
Reading section 7 of 14 · Code: Exploring Model Internals · 7 minSection 7 of 14 · 7 min

Book contents

Section 7 of 14 · 7 min read

Interpretability Goals › Code: Exploring Model InternalsOne section · Sections in this chapter7 / 14 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

To make interpretability concrete, let's implement a basic exploration of a small transformer model's internal representations. We will examine how activations change across layers for different types of inputs, a foundation for the probing and visualization techniques in later chapters. This code is exploratory rather than definitive: it demonstrates the setup and measurement tools, but four sentences is far too small a dataset to draw firm conclusions.

We start by loading a pretrained model and setting up extraction of intermediate activations.

In[3]:
Code
import warnings

from transformers import AutoModel, AutoTokenizer

warnings.filterwarnings("ignore")

# Use a lightweight model for demonstration
model_name = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name, output_hidden_states=True)
model.eval()
In[4]:
Code
# Define a set of sentences with different semantic properties
sentences = [
    "The cat sat on the mat.",  # simple, concrete
    "The president signed the bill.",  # factual, political
    "The matrix was singular.",  # technical, mathematical
    "She felt deeply melancholic.",  # emotional, abstract
]

# Tokenize all sentences
encoded = [
    tokenizer(s, return_tensors="pt", padding=True, truncation=True)
    for s in sentences
]
In[5]:
Code
import numpy as np
import torch

# Extract hidden states from all layers
all_hidden_states = []

with torch.no_grad():
    for enc in encoded:
        outputs = model(**enc)
        # hidden_states: tuple of (num_layers+1) tensors of shape (1, seq_len, hidden_size)
        # Index 0 is the embedding layer, rest are transformer layers
        hidden_states = torch.stack(
            outputs.hidden_states, dim=0
        )  # (layers, 1, seq, hidden)
        # Take the [CLS] token representation for each layer
        cls_states = hidden_states[:, 0, 0, :].numpy()  # (layers, hidden_size)
        all_hidden_states.append(cls_states)

all_hidden_states = np.array(
    all_hidden_states
)  # (sentences, layers, hidden_size)
num_layers = all_hidden_states.shape[1]
n_sentences = all_hidden_states.shape[0]
Out[6]:
Console
Model: distilbert-base-uncased
Hidden state shape: (sentences=4, layers=7, hidden_size=768)

Sentences analyzed: 4
  [0] The cat sat on the mat.
  [1] The president signed the bill.
  [2] The matrix was singular.
  [3] She felt deeply melancholic.

We now have hidden states for each sentence at each layer. A key interpretability question is how similar sentences are to each other as a function of layer depth. Do sentences become more or less similar as the model processes them? Intuitively, if the model is doing useful work, representations should become more differentiated and semantically meaningful as depth increases, diverging from the token-identity-driven representations at the input.

In[7]:
Code
from sklearn.metrics.pairwise import cosine_similarity

# Compute pairwise cosine similarity between all sentences at each layer
layer_similarities = np.zeros((num_layers, n_sentences, n_sentences))

for layer in range(num_layers):
    layer_reps = all_hidden_states[:, layer, :]  # (sentences, hidden_size)
    layer_similarities[layer] = cosine_similarity(layer_reps)

# Compute average off-diagonal similarity per layer
avg_sims = []
for layer in range(num_layers):
    sim_matrix = layer_similarities[layer]
    off_diag_indices = np.triu_indices(n_sentences, k=1)
    off_diag = sim_matrix[off_diag_indices]
    avg_sims.append(off_diag.mean())
Out[8]:
Console
Average inter-sentence cosine similarity across layers:
   Layer   Avg Similarity
----------------------------
   Embed           1.0000
 Layer 1           0.9876
 Layer 2           0.9920
 Layer 3           0.9457
 Layer 4           0.9309
 Layer 5           0.9021
 Layer 6           0.9334

The similarity values reveal something characteristic of transformer models: representations tend to be similar at the embedding layer (shared token embeddings dominate) and diversify at intermediate layers as contextual information is mixed in. This layer-by-layer transformation is exactly what probing classifiers exploit. If you train a classifier on early-layer representations to predict semantic categories, it tends to fail; if you train it on late-layer representations, it tends to succeed.

Out[9]:
Visualization
Line plot of average inter-sentence cosine similarity from embedding through final transformer layer.
Average cosine similarity between sentence representations across DistilBERT transformer layers. Similarity is highest at the embedding layer where token identity dominates, then decreases as contextual processing progressively differentiates representations. This trajectory illustrates how each transformer layer transforms the representation space to encode richer, more context-dependent information.

The plot shows how representations evolve through the network. At the embedding layer, all sentences are represented primarily by their token identities, producing high similarity because different sentences share many common words ("The", "the", "a"). As processing proceeds, contextual information differentiates the representations. This is the transformation that makes deeper layers better sources for probing higher-level semantic and syntactic properties. A classifier trained on embedding-layer representations would find it hard to distinguish sentence topics, but a classifier trained on final-layer representations would have access to rich contextual signal.

Now let's look at whether different types of sentences cluster together in activation space by using PCA to project the high-dimensional representations to 2D.

In[10]:
Code
from sklearn.decomposition import PCA

# We'll look at the last layer representations (most contextually rich)
last_layer_idx = num_layers - 1
last_layer_reps = all_hidden_states[:, last_layer_idx, :]  # (sentences, hidden)

# Also extract first real transformer layer
first_layer_reps = all_hidden_states[:, 1, :]  # (sentences, hidden)

# PCA to 2D - fit on combined data for consistent coordinate system
pca = PCA(n_components=2)
combined = np.vstack([first_layer_reps, last_layer_reps])
pca.fit(combined)

first_2d = pca.transform(first_layer_reps)
last_2d = pca.transform(last_layer_reps)
Out[11]:
Visualization
2D scatter plot of sentence embeddings at transformer layer 1 with labeled points.
PCA projection of sentence representations at the first transformer layer. Positions are driven primarily by token identities, so the relative arrangement of sentences reflects surface-level vocabulary overlap rather than semantic content.
2D scatter plot of sentence embeddings at the final transformer layer with labeled points.
PCA projection at the final transformer layer. Contextual processing has redistributed sentences in the space, and relative distances now reflect the model's learned semantic relationships, with each sentence occupying a more distinct position.

These projections illustrate one of the core findings of representation analysis: how sentences cluster in activation space changes across layers. While a six-layer model like DistilBERT-base shows modest differences with only four sentences, larger models like BERT-base and GPT-2 show dramatic stratification, with deeper layers encoding semantic neighborhoods that align closely with human-legible categories. With a larger dataset, you would observe that sentences with similar topics, styles, or syntactic structures group together in final-layer activation space, even when those sentences share no words in common.