thegovind's picture
Publish the weight-space programming labs
bb102c0 verified
Raw History Blame Contribute Delete
4.17 kB
#!/usr/bin/env python
"""Lab 0: weight-space programming on a toy layer small enough to print.
uv run --with numpy --with matplotlib python labs/lab0_toy.py # seconds, any laptop
An MLP's last matrix, down_proj, is W (here 4 x 6; in Qwen3.5-0.8B it is 1,024 x 3,584). Neuron j fires with
strength a[j] and writes its column W[:, j] into the stream, so the layer adds y = W a = sum_j a[j] * W[:, j].
The paper's edit picks a direction u and some neurons S and sets W' = W - alpha * u (u^T W[:, S]) on those
columns. Every claim below is checked with an assert, so if this script finishes, they all hold.
"""
import os
import numpy as np
np.set_printoptions(precision=3, suppress=True)
rng = np.random.default_rng(0)
d, n = 4, 6
W = rng.normal(size=(d, n))
u = rng.normal(size=d)
u /= np.linalg.norm(u) # the direction we care about (in the paper: "toward looping")
S = [1, 4] # the neurons we allow ourselves to touch (in the paper: 64 of 3,584)
others = [j for j in range(n) if j not in S]
def edit(W, u, alpha, S=None):
g = u @ W # g[j] = how much neuron j writes along u, per unit of activation
if S is not None:
mask = np.zeros(W.shape[1])
mask[S] = 1
g = g * mask
return W - alpha * np.outer(u, g)
print("1. How much each neuron writes along u (u . W[:, j]):", u @ W)
print("\n2. The edit scales the chosen neurons' push along u by (1 - alpha) and leaves everything else alone")
P = np.eye(d) - np.outer(u, u) # removes the u part of any vector
for alpha, name in [(0, "no edit"), (1, "erase"), (2, "mirror"), (6, "flip and amplify 5x (the paper)"),
(-4, "amplify 5x the other way")]:
W2 = edit(W, u, alpha, S)
before, after = (u @ W)[S], (u @ W2)[S]
assert np.allclose(after, (1 - alpha) * before)
assert np.allclose(W2[:, others], W[:, others]) # untouched neurons stay identical
assert np.allclose(P @ W2, P @ W) # nothing changes outside the direction u
print(f" alpha={alpha:>2} ({name:31s}) push along u of neurons {S}: {before} -> {after}")
print("\n3. The change is rank one: one direction out (u), one pattern in (u^T W on the chosen columns)")
D = edit(W, u, 6, S) - W
sv = np.linalg.svd(D, compute_uv=False)
assert sv[1] < 1e-12 * sv[0]
print(" singular values of W' - W:", sv)
print("\n4. The weight edit is steering: W' a = W a - alpha * s * u, with s = u . (W[:, S] a[S])")
alpha = 6
for name, a in [("chosen neurons quiet", np.where(np.isin(np.arange(n), S), 0.0, rng.normal(size=n))),
("chosen neurons firing", np.where(np.isin(np.arange(n), S), 3.0, rng.normal(size=n)))]:
s = u @ (W[:, S] @ a[S])
y_edit, y_steer = edit(W, u, alpha, S) @ a, W @ a - alpha * s * u
assert np.allclose(y_edit, y_steer)
print(f" {name:22s} s = {s:+.3f} change to the output |W'a - Wa| = {np.linalg.norm(y_edit - W @ a):.3f}")
print(" Same weights, different tokens, different push: the edit only acts when the chosen neurons fire.")
import matplotlib # noqa: E402
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
fig, ax = plt.subplots(1, 2, figsize=(11, 3.6))
im = ax[0].imshow(edit(W, u, 6, S) - W, cmap="RdBu", vmin=-6, vmax=6)
ax[0].set_title("W' - W: only the chosen columns change")
ax[0].set_xlabel("neuron (column)")
ax[0].set_ylabel("stream dimension (row)")
fig.colorbar(im, ax=ax[0])
al = np.linspace(-5, 8, 100)
ax[1].plot(al, 1 - al, color="#0969da")
for x, lab in [(0, "no edit"), (1, "erase"), (2, "mirror"), (6, "the paper")]:
ax[1].plot(x, 1 - x, "o", color="#cf222e")
ax[1].annotate(lab, (x, 1 - x), xytext=(6, 4), textcoords="offset points")
ax[1].axhline(0, color="black", lw=0.5)
ax[1].set_xlabel("alpha")
ax[1].set_ylabel("push along u after / before")
ax[1].set_title("What alpha does to the chosen neurons")
fig.tight_layout()
out = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "results", "figures")
os.makedirs(out, exist_ok=True)
fig.savefig(os.path.join(out, "lab0_toy.png"), dpi=130)
print("\nall checks passed; figure in results/figures/lab0_toy.png")