Dictionary of Applied Machine Learning · transformer

transformer — Python demo

Numerical companion to the entry transformer: it recomputes what the entry states and prints one line per check

One block per paragraph of the entry (marked [P...]): each block verifies numerically what the corresponding statement asserts. Self-contained (numpy/matplotlib only), fixed seed.

Run it with python3 transformer.py, from any directory — it writes its output files into the current directory. Requires NumPy and Matplotlib only, and uses fixed seeds, so the printed numbers reproduce exactly. Download transformer.py

The script, block by block

One cell per block of the script: the code, and what that code printed when it last ran here

setup

"""
transformer.py — numerical companion to the glossary entry 'transformer'.

One block per paragraph of the entry (marked [P...]): each block verifies
numerically what the corresponding statement asserts. Self-contained
(numpy/matplotlib only), fixed seed.

Blocks
------
[P-shape]  A transformer composes layers that map the matrix X in
           R^{n x d} of stacked token feature vectors to one of the SAME
           shape — which is exactly what lets layers compose freely: a
           stack of 6 alternating layers applies without any shape
           bookkeeping.
[P-block]  The two layer types differ in information flow: a
           token-mixing layer (softmax self-attention) lets a
           perturbation of token j change the output rows of OTHER
           tokens, while the position-wise MLP is row-local — perturbing
           token j leaves every other output row untouched. Residual
           connections preserve the shape as well.
[P-alt]    Token mixing need not be attention: a fixed Fourier mixing
           layer (FNet-style, real part of the FFT across the token
           axis) is a same-shape, cross-token-propagating replacement
           that computes NO pairwise query-key scores, whereas the
           attention layer forms an n x n score matrix (n^2 pairwise
           comparisons) by construction.

Outputs
-------
transformer.png : preview figure (checking only).

Data generated by pythondemos/transformer.py.
"""

import numpy as np
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
report = []


def check(name, ok):
    report.append((name, bool(ok)))
    print(f"  [{'ok' if ok else 'FAIL'}] {name}")


n, dm = 8, 16                                      # tokens x features
softmax = lambda S: np.exp(S - S.max(1, keepdims=True)) / \
    np.exp(S - S.max(1, keepdims=True)).sum(1, keepdims=True)

def attention_layer(X, Wq, Wk, Wv):
    S = (X @ Wq) @ (X @ Wk).T / np.sqrt(dm)        # n x n scores
    return softmax(S) @ (X @ Wv), S

def mlp_layer(X, W1, W2):                          # position-wise
    return np.maximum(X @ W1, 0) @ W2

P-shape

A transformer composes layers that map the matrix X in R^{n x d} of stacked token feature vectors to one of the SAME shape — which is exactly what lets layers compose freely: a stack of 6 alternating layers applies without any shape bookkeeping.

print("[P-shape] same-shape layers compose freely")
params = [(rng.normal(size=(dm, dm)) / np.sqrt(dm),
           rng.normal(size=(dm, dm)) / np.sqrt(dm),
           rng.normal(size=(dm, dm)) / np.sqrt(dm),
           rng.normal(size=(dm, dm)) / np.sqrt(dm),
           rng.normal(size=(dm, dm)) / np.sqrt(dm)) for _ in range(3)]
X = rng.normal(size=(n, dm))
Z = X.copy()
for Wq, Wk, Wv, W1, W2 in params:                  # 3 blocks = 6 layers
    A, _ = attention_layer(Z, Wq, Wk, Wv)
    Z = Z + A                                      # residual
    Z = Z + mlp_layer(Z, W1, W2)                   # residual
    check("layer output keeps the n x d shape", Z.shape == (n, dm))
check("the 6-layer composition ran without shape bookkeeping",
      Z.shape == X.shape)
[P-shape] same-shape layers compose freely
  [ok] layer output keeps the n x d shape
  [ok] layer output keeps the n x d shape
  [ok] layer output keeps the n x d shape
  [ok] the 6-layer composition ran without shape bookkeeping

P-block

The two layer types differ in information flow: a token-mixing layer (softmax self-attention) lets a perturbation of token j change the output rows of OTHER tokens, while the position-wise MLP is row-local — perturbing token j leaves every other output row untouched. Residual connections preserve the shape as well.

print("[P-block] token mixing vs position-wise transformation")
Wq, Wk, Wv, W1, W2 = params[0]
Xp = X.copy()
Xp[3] += 0.5 * rng.normal(size=dm)                 # perturb token 3
A0, S0 = attention_layer(X, Wq, Wk, Wv)
A1, _ = attention_layer(Xp, Wq, Wk, Wv)
other = [i for i in range(n) if i != 3]
check("token mixing: perturbing token 3 changes OTHER tokens' outputs",
      np.max(np.abs(A1[other] - A0[other])) > 1e-6)
M0, M1 = mlp_layer(X, W1, W2), mlp_layer(Xp, W1, W2)
check("position-wise MLP: other rows are untouched (row-local)",
      np.allclose(M1[other], M0[other]))
check("position-wise MLP: only row 3 changed",
      not np.allclose(M1[3], M0[3]))
[P-block] token mixing vs position-wise transformation
  [ok] token mixing: perturbing token 3 changes OTHER tokens' outputs
  [ok] position-wise MLP: other rows are untouched (row-local)
  [ok] position-wise MLP: only row 3 changed

P-alt

Token mixing need not be attention: a fixed Fourier mixing layer (FNet-style, real part of the FFT across the token axis) is a same-shape, cross-token-propagating replacement that computes NO pairwise query-key scores, whereas the attention layer forms an n x n score matrix (n^2 pairwise comparisons) by construction.

print("[P-alt] Fourier mixing replaces attention without pairwise scores")
fourier_layer = lambda X: np.real(np.fft.fft(X, axis=0))   # mix tokens
F0, F1 = fourier_layer(X), fourier_layer(Xp)
check("Fourier layer keeps the n x d shape", F0.shape == (n, dm))
check("Fourier layer propagates information across tokens",
      np.max(np.abs(F1[other] - F0[other])) > 1e-6)
check("attention forms an n x n score matrix (n^2 = 64 pairwise "
      "comparisons) by construction; Fourier mixing forms none",
      S0.shape == (n, n))
stack = fourier_layer(X) + mlp_layer(fourier_layer(X), W1, W2)
check("the alternative mixing layer composes in the same block "
      "structure", stack.shape == (n, dm))

# ------------------------------------------------------------ preview
fig, ax = plt.subplots(1, 2, figsize=(7.6, 3.0))
ax[0].imshow(softmax(S0), cmap="gray")
ax[0].set_title("[P-block] attention weights (n x n)")
ax[1].imshow(np.abs(A1 - A0) > 1e-9, cmap="gray", aspect="auto")
ax[1].set_title("rows changed by perturbing token 3")
fig.tight_layout()
fig.savefig("transformer.png", dpi=110)
print(f"\n{sum(ok for _, ok in report)}/{len(report)} checks passed")
assert all(ok for _, ok in report)
[P-alt] Fourier mixing replaces attention without pairwise scores
  [ok] Fourier layer keeps the n x d shape
  [ok] Fourier layer propagates information across tokens
  [ok] attention forms an n x n score matrix (n^2 = 64 pairwise comparisons) by construction; Fourier mixing forms none
  [ok] the alternative mixing layer composes in the same block structure

11/11 checks passed
Preview figure produced by transformer.py
The preview figure the block P-alt writes when the script runs