legacy_numpy.py · 可读全文
这里保留原文的中文、公式标记、代码和链接。可直接交给 Agent 读取,也可选取下方原文复制。此入口返回 HTML;原始 .md / .py 文件仍保留,完整候选包同时包含 UTF-8 原件。
#!/usr/bin/env python3
"""Lecture 05 R2.1: small, offline, NumPy-only experiments A--G.
The saved results distinguish constructed computations, optimization smoke tests,
and a small trained LM comparison. No pretrained model or cloud notebook is used.
Run from the repository root: python labs/r21_numpy.py
"""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import itertools
import json
import math
import os
import platform
import re
import sys
import time
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
# Keep the small CPU timing exercise reproducible; do not overwrite user settings.
for _name in ("OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS", "MKL_NUM_THREADS"):
os.environ.setdefault(_name, "1")
import numpy as np
VERSION = "l5-r21-numpy-1.0"
BASE_SEED = 20261013
ATOL = 1e-10
_CLASSROOM = None
def classroom_parameters():
"""Use the teaching source; the generated sidecar keeps downloaded labs runnable."""
global _CLASSROOM
if _CLASSROOM is not None:
return _CLASSROOM
source = Path(__file__).resolve().parents[1] / "content" / "r21" / "attention_transformer.py"
snapshot = Path(__file__).with_name("classroom-parameters.json")
if source.exists():
spec = importlib.util.spec_from_file_location("l5_classroom_numbers", source)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
_CLASSROOM = {"source_path": "content/r21/attention_transformer.py",
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
"READ": module.READ, "BLOCK": module.BLOCK}
snapshot.write_text(json.dumps(_CLASSROOM, ensure_ascii=False, indent=2) + "\n")
else:
_CLASSROOM = json.loads(snapshot.read_text())
return _CLASSROOM
def softmax(x):
z = x - np.max(x, axis=-1, keepdims=True)
z = np.exp(z)
return z / z.sum(axis=-1, keepdims=True)
def sigmoid(x):
return 1 / (1 + np.exp(-np.clip(x, -40, 40)))
def norm(grads):
return float(np.sqrt(sum(np.sum(g * g) for g in grads.values())))
def clip(grads, threshold):
before = norm(grads)
scale = min(1.0, threshold / max(before, 1e-30))
return {k: v * scale for k, v in grads.items()}, before, before * scale
class Adam:
def __init__(self, params, lr=0.01):
self.lr, self.t = lr, 0
self.m = {k: np.zeros_like(v) for k, v in params.items()}
self.v = {k: np.zeros_like(v) for k, v in params.items()}
def step(self, params, grads):
self.t += 1
for k in params:
self.m[k] = .9 * self.m[k] + .1 * grads[k]
self.v[k] = .999 * self.v[k] + .001 * grads[k] ** 2
m = self.m[k] / (1 - .9 ** self.t)
v = self.v[k] / (1 - .999 ** self.t)
params[k] -= self.lr * m / (np.sqrt(v) + 1e-8)
def parameter_count(params):
return sum(v.size for v in params.values())
def digest_rows(rows):
return hashlib.sha256(json.dumps(rows, separators=(",", ":")).encode()).hexdigest()
class Recurrent:
"""One-layer RNN/LSTM with embeddings and a shared output head; manual BPTT."""
def __init__(self, inputs, outputs, embed=8, hidden=24, kind="rnn", seed=0):
rng = np.random.default_rng(seed)
self.hidden, self.kind = hidden, kind
gate_width = hidden * (4 if kind == "lstm" else 1)
self.p = {
"E": rng.normal(0, .15, (inputs, embed)),
"Wx": rng.normal(0, 1 / math.sqrt(embed), (embed, gate_width)),
"Wh": rng.normal(0, .45 / math.sqrt(hidden), (hidden, gate_width)),
"b": np.zeros(gate_width),
"Wy": rng.normal(0, 1 / math.sqrt(hidden), (hidden, outputs)),
"by": np.zeros(outputs),
}
if kind == "lstm":
self.p["b"][hidden:2 * hidden] = 1.0 # explicit forget-gate bias
def forward(self, x):
p = self.p
b, steps = x.shape
h = np.zeros((b, self.hidden))
c = np.zeros_like(h)
states, cache = [], []
for t in range(steps):
e, hp, cp = p["E"][x[:, t]], h, c
a = e @ p["Wx"] + hp @ p["Wh"] + p["b"]
if self.kind == "rnn":
h = np.tanh(a)
cache.append((e, hp, h))
else:
ai, af, ag, ao = np.split(a, 4, axis=-1)
i, f, g, o = sigmoid(ai), sigmoid(af), np.tanh(ag), sigmoid(ao)
c = f * cp + i * g
h = o * np.tanh(c)
cache.append((e, hp, cp, i, f, g, o, c))
states.append(h)
hs = np.stack(states, axis=1)
return hs @ p["Wy"] + p["by"], hs, cache
def loss_grad(self, x, targets, mask=None):
logits, hs, cache = self.forward(x)
probs = softmax(logits)
if mask is None:
mask = np.ones(targets.shape)
denom = mask.sum()
picked = np.take_along_axis(probs, targets[..., None], axis=-1)[..., 0]
loss = float(-(np.log(np.maximum(picked, 1e-300)) * mask).sum() / denom)
dl = probs.copy()
bidx, tidx = np.indices(targets.shape)
dl[bidx, tidx, targets] -= 1
dl *= mask[..., None] / denom
p = self.p
grads = {k: np.zeros_like(v) for k, v in p.items()}
grads["Wy"] = hs.reshape(-1, self.hidden).T @ dl.reshape(-1, dl.shape[-1])
grads["by"] = dl.sum(axis=(0, 1))
direct = dl @ p["Wy"].T
dh_next = np.zeros_like(hs[:, 0])
dc_next = np.zeros_like(dh_next)
path = np.zeros(x.shape[1])
for t in reversed(range(x.shape[1])):
dh = direct[:, t] + dh_next
path[t] = np.linalg.norm(dh)
if self.kind == "rnn":
e, hp, h = cache[t]
da = dh * (1 - h * h)
else:
e, hp, cp, i, f, g, o, c = cache[t]
tc = np.tanh(c)
dc = dc_next + dh * o * (1 - tc * tc)
di, df, dg, do = dc * g, dc * cp, dc * i, dh * tc
da = np.concatenate((di * i * (1 - i), df * f * (1 - f),
dg * (1 - g * g), do * o * (1 - o)), axis=-1)
dc_next = dc * f
grads["Wx"] += e.T @ da
grads["Wh"] += hp.T @ da
grads["b"] += da.sum(axis=0)
np.add.at(grads["E"], x[:, t], da @ p["Wx"].T)
dh_next = da @ p["Wh"].T
return loss, grads, {"hidden_gradient_l2_by_position": path, "probs": probs}
class FixedWindow:
def __init__(self, inputs, outputs, window=2, embed=8, hidden=24, seed=0):
rng = np.random.default_rng(seed)
self.window = window
self.p = {"E": rng.normal(0, .15, (inputs, embed)),
"W": rng.normal(0, 1 / math.sqrt(window * embed), (window * embed, hidden)),
"b": np.zeros(hidden), "U": rng.normal(0, 1 / math.sqrt(hidden), (hidden, outputs)),
"c": np.zeros(outputs)}
def forward(self, contexts):
z = self.p["E"][contexts].reshape(len(contexts), -1)
h = np.tanh(z @ self.p["W"] + self.p["b"])
return h @ self.p["U"] + self.p["c"], (z, h)
def loss_grad(self, contexts, targets):
logits, (z, h) = self.forward(contexts)
probs = softmax(logits)
loss = float(-np.log(probs[np.arange(len(targets)), targets]).mean())
dl = probs.copy()
dl[np.arange(len(targets)), targets] -= 1
dl /= len(targets)
da = (dl @ self.p["U"].T) * (1 - h * h)
grads = {"U": h.T @ dl, "c": dl.sum(0), "W": z.T @ da, "b": da.sum(0),
"E": np.zeros_like(self.p["E"])}
dz = (da @ self.p["W"].T).reshape(len(targets), self.window, -1)
np.add.at(grads["E"], contexts.reshape(-1), dz.reshape(-1, dz.shape[-1]))
return loss, grads
def route_a():
sentences = [
"she deposited money at the bank", "she sat on the bank of the river",
"the bank accepted cash and money", "the river covered the grassy bank",
"cash arrived at the bank today", "birds rested on the river bank",
"she read a book near the river", "she paid cash for the book",
]
docs = [re.findall(r"[a-z]+", s.lower()) for s in sentences]
vocab = sorted(set(itertools.chain.from_iterable(docs)))
ids = {w: i for i, w in enumerate(vocab)}
reports, embeddings = {}, {}
for window in (1, 3):
counts = np.zeros((len(vocab), len(vocab)))
pairs = []
for doc in docs:
for i, word in enumerate(doc):
for j in range(max(0, i - window), min(len(doc), i + window + 1)):
if i != j:
counts[ids[word], ids[doc[j]]] += 1
pairs.append([word, doc[j]])
total = counts.sum()
ratio = counts * total / np.maximum(counts.sum(1)[:, None] * counts.sum(0)[None, :], 1)
ppmi = np.maximum(np.log(np.maximum(ratio, 1e-300)), 0)
u, s, _ = np.linalg.svd(ppmi, full_matrices=False)
emb = u[:, :4] * np.sqrt(s[:4])
embeddings[window] = emb
reports[str(window)] = {"training_pair_count": len(pairs), "count_shape": counts.shape,
"bank_cooccurrence": {w: int(counts[ids['bank'], j]) for j, w in enumerate(vocab) if counts[ids['bank'], j]},
"first_12_pairs": pairs[:12]}
emb = embeddings[3]
contexts = []
reps = []
for doc in docs[:2]:
i = doc.index("bank")
neighbors = [doc[j] for j in range(max(0, i - 3), min(len(doc), i + 4)) if j != i]
contexts.append(neighbors)
# This is an explicit, untrained contextual composition, not a language model.
reps.append(np.concatenate((emb[ids["bank"]], emb[[ids[w] for w in neighbors]].mean(0))))
fixed_bank_vectors = [emb[ids[doc[doc.index("bank")]]].copy() for doc in docs[:2]]
return {"status": "constructed_count_SVD_and_untrained_context_composition", "seed": None,
"sentences": sentences, "tokenization": "lowercase regex [a-z]+; punctuation discarded",
"vocabulary": vocab, "documents": len(docs), "tokens": sum(map(len, docs)),
"windows": reports, "svd_dimensions": 4, "fixed_bank_vectors": fixed_bank_vectors,
"fixed_bank_max_abs_difference": float(np.max(np.abs(fixed_bank_vectors[0] - fixed_bank_vectors[1]))),
"context_windows": contexts, "context_vectors": reps,
"context_vector_l2_difference": float(np.linalg.norm(reps[0] - reps[1])),
"interpretation": "Different constructed vectors establish context dependence, not learned word-sense accuracy."}
def lm_dataset():
sentences = [[s, v, "a", obj, "to", r, "near", "the", place, "<EOS>"]
for s, v, obj, r, place in itertools.product(
["amy", "ben", "cao", "dia"], ["gave", "sent", "showed", "lent"],
["book", "key", "cup", "map"], ["eli", "fay", "gus", "han"],
["river", "school", "office", "park"])]
rng = np.random.default_rng(BASE_SEED)
order = rng.permutation(len(sentences))
# Vocab is fixed by the published synthetic grammar, not inferred from test labels.
vocab = sorted(set(itertools.chain.from_iterable(sentences)))
ids = {w: i for i, w in enumerate(vocab)}
encoded = np.array([[ids[w] for w in s] for s in sentences], dtype=np.int64)
splits = {"train": encoded[order[:600]], "validation": encoded[order[600:700]], "test": encoded[order[700:800]]}
return splits, vocab
def fixed_contexts(targets, bos, k=2):
pad = np.pad(targets, ((0, 0), (k, 0)), constant_values=bos)
return np.stack([pad[:, t:t+k] for t in range(targets.shape[1])], axis=1).reshape(-1, k)
def route_b():
splits, vocab = lm_dataset()
v, bos, k = len(vocab), len(vocab), 2
counts = defaultdict(lambda: np.zeros(v))
for context, y in zip(fixed_contexts(splits["train"], bos, k), splits["train"].reshape(-1)):
counts[tuple(context)][y] += 1
alpha = .1
def ngram_prob(context):
c = counts.get(tuple(context), np.zeros(v))
return (c + alpha) / (c.sum() + alpha * v)
fixed = FixedWindow(v + 1, v, seed=BASE_SEED + 1)
rnn = Recurrent(v + 1, v, seed=BASE_SEED + 1)
models = {"fixed_window": fixed, "rnn": rnn}
metrics, curves = {}, {}
def model_loss(name, model, y):
if name == "fixed_window":
return model.loss_grad(fixed_contexts(y, bos), y.reshape(-1))[0]
x = np.concatenate((np.full((len(y), 1), bos), y[:, :-1]), axis=1)
return model.loss_grad(x, y)[0]
for name, model in models.items():
opt = Adam(model.p, .015)
rng = np.random.default_rng(BASE_SEED + 2)
curve = [{"epoch": 0, "train_loss": model_loss(name, model, splits["train"]),
"validation_loss": model_loss(name, model, splits["validation"])}]
started = time.perf_counter()
grad_max = 0.0
for epoch in range(1, 13):
order = rng.permutation(len(splits["train"]))
for start in range(0, len(order), 32):
y = splits["train"][order[start:start + 32]]
if name == "fixed_window":
_, grads = model.loss_grad(fixed_contexts(y, bos), y.reshape(-1))
else:
x = np.concatenate((np.full((len(y), 1), bos), y[:, :-1]), axis=1)
_, grads, _ = model.loss_grad(x, y)
grads, pre, _ = clip(grads, 1.0)
grad_max = max(grad_max, pre)
opt.step(model.p, grads)
if epoch in (1, 4, 8, 12):
curve.append({"epoch": epoch, "train_loss": model_loss(name, model, splits["train"]),
"validation_loss": model_loss(name, model, splits["validation"])})
elapsed = time.perf_counter() - started
loss = model_loss(name, model, splits["test"])
metrics[name] = {"test_nll_nats_per_token": loss, "test_perplexity": math.exp(loss),
"parameter_count": parameter_count(model.p), "updates": opt.t,
"epochs": 12, "wall_seconds_including_curve_evaluation": elapsed,
"max_unclipped_gradient_l2": grad_max}
curves[name] = curve
ng_losses = []
for context, target in zip(fixed_contexts(splits["test"], bos), splits["test"].reshape(-1)):
ng_losses.append(-math.log(ngram_prob(context)[target]))
ng_loss = float(np.mean(ng_losses))
metrics["trigram_add_0.1"] = {"test_nll_nats_per_token": ng_loss, "test_perplexity": math.exp(ng_loss),
"observed_contexts": len(counts), "stored_count_cells": len(counts) * v,
"updates": "one counting pass"}
def generate(name, temperature, seed):
rng = np.random.default_rng(seed)
prefix, output = [], []
for _ in range(20):
if name == "trigram_add_0.1":
dist = ngram_prob(([bos] * k + prefix)[-k:])
elif name == "fixed_window":
context = np.array([([bos] * k + prefix)[-k:]])
dist = softmax(fixed.forward(context)[0])[0]
else:
dist = softmax(rnn.forward(np.array([[bos] + prefix]))[0])[0, -1]
nxt = int(np.argmax(dist)) if temperature == 0 else int(rng.choice(v, p=softmax(np.log(dist) / temperature)))
output.append(vocab[nxt])
if vocab[nxt] == "<EOS>":
break
prefix.append(nxt)
return {"tokens": output, "terminated_with_eos": output[-1] == "<EOS>"}
generation = {name: {"greedy": generate(name, 0, BASE_SEED + 3),
"temperature_0.8": generate(name, .8, BASE_SEED + 3)} for name in metrics}
return {"status": "actual_small_synthetic_LM_training_single_seed", "seed_data": BASE_SEED,
"seed_initialization": BASE_SEED + 1, "seed_minibatches": BASE_SEED + 2,
"seed_sampling": BASE_SEED + 3, "vocabulary": vocab, "bos_input_only_id": bos,
"tokenization": "published grammar tokens; no subword model; EOS counted, BOS not a target",
"grammar_total_unique_sentences": 1024, "splits": {name: {"sentences": len(y), "predicted_tokens": y.size,
"sha256": digest_rows(y.tolist())} for name, y in splits.items()},
"window": 2, "embed": 8, "hidden": 24, "batch_sentences": 32,
"optimizer": "Adam lr=0.015 beta=(0.9,0.999) eps=1e-8; global gradient clipping at 1.0",
"metrics": metrics, "learning_curves": curves, "generation": generation,
"interpretation": "One grammar, one split, one initialization; no architecture ranking or natural-language claim."}
def delayed_bit_data(noise_length, template_counts=(96, 32, 32), seed=0, evidence_position=0):
"""Split unique distractor templates BEFORE making bit-0/bit-1 paired examples."""
rng, templates = np.random.default_rng(seed), set()
need = sum(template_counts)
if not 0 <= evidence_position <= noise_length:
raise ValueError("Evidence must be inserted before the QUERY token")
if 8 ** noise_length < need:
raise ValueError("Not enough unique distractor templates for disjoint splits")
ordered = []
while len(ordered) < need:
suffix = tuple(rng.integers(2, 10, noise_length).tolist())
if suffix not in templates:
templates.add(suffix)
ordered.append(suffix)
result, start = {}, 0
for name, n in zip(("train", "validation", "test"), template_counts):
group = ordered[start:start+n]
x = np.array([list(s[:evidence_position]) + [bit] + list(s[evidence_position:]) + [10]
for s in group for bit in (0, 1)])
y = np.array([bit for _ in group for bit in (0, 1)])
result[name] = (x, y, group)
start += n
return result
def classify_loss(model, x, y):
targets = np.zeros_like(x)
targets[:, -1] = y
mask = np.zeros_like(x, dtype=float)
mask[:, -1] = 1
loss, grads, aux = model.loss_grad(x, targets, mask)
pred = aux["probs"][:, -1].argmax(-1)
return loss, grads, {**aux, "accuracy": float((pred == y).mean()),
"both_correct_pair_fraction": float(np.all((pred == y).reshape(-1, 2), axis=1).mean())}
def gradient_check(model, x, y, seed=0, samples_per_tensor=6):
_, analytical, _ = classify_loss(model, x, y)
rng, checks = np.random.default_rng(seed), []
epsilon = 1e-5
for name, p in model.p.items():
for flat in rng.choice(p.size, min(samples_per_tensor, p.size), replace=False):
index = np.unravel_index(flat, p.shape)
original = p[index]
p[index] = original + epsilon
plus = classify_loss(model, x, y)[0]
p[index] = original - epsilon
minus = classify_loss(model, x, y)[0]
p[index] = original
numerical = (plus - minus) / (2 * epsilon)
exact = analytical[name][index]
checks.append({"parameter": name, "index": index, "analytic": exact, "numerical": numerical,
"absolute_error": abs(exact - numerical),
"scaled_error": abs(exact - numerical) / max(1, abs(exact), abs(numerical))})
maximum = max(c["absolute_error"] for c in checks)
assert maximum < 1e-7, (model.kind, maximum)
return {"epsilon": epsilon, "absolute_tolerance": 1e-7, "checked_elements": len(checks),
"max_absolute_error": maximum, "samples": checks}
def route_c():
seed = BASE_SEED + 4
data = delayed_bit_data(8, seed=seed)
split_sets = {n: set(t) for n, (_, _, t) in data.items()}
assert not (split_sets["train"] & split_sets["test"])
assert not (split_sets["validation"] & split_sets["test"])
assert not (split_sets["train"] & split_sets["validation"])
checks = {}
for gap in (3, 8, 32, 64):
d = delayed_bit_data(gap, (96, 32, 32), seed)
x, y, _ = d["test"]
suffix_groups = defaultdict(list)
for row, label in zip(x[:, -min(4, gap+1):], y):
suffix_groups[tuple(row)].append(int(label))
best_suffix_accuracy = sum(max(Counter(labels).values()) for labels in suffix_groups.values()) / len(y)
assert best_suffix_accuracy == .5
checks[str(gap)] = {"sequence_length": x.shape[1], "query_minus_relevant_position": gap+1,
"same_suffix_opposite_label_pairs": len(y)//2,
"best_deterministic_suffix_only_accuracy": best_suffix_accuracy}
small = delayed_bit_data(3, (2, 1, 1), seed)
gx, gy, _ = small["train"]
results = {}
for kind in ("rnn", "lstm"):
gcmodel = Recurrent(11, 2, embed=3, hidden=5, kind=kind, seed=seed)
gc = gradient_check(gcmodel, gx, gy, seed)
model = Recurrent(11, 2, embed=4, hidden=8, kind=kind, seed=seed)
x, y, _ = data["train"]
opt = Adam(model.p, .01)
initial, _, aux0 = classify_loss(model, x, y)
curve, grad_norms = [], []
started = time.perf_counter()
# Only a smoke run: 12 full-batch steps, NOT a capability comparison.
for step in range(12):
loss, grads, aux = classify_loss(model, x, y)
grads, before, after = clip(grads, .1)
grad_norms.append({"step": step, "before": before, "after": after})
opt.step(model.p, grads)
curve.append(loss)
final, _, aux1 = classify_loss(model, x, y)
tx, ty, _ = data["test"]
tloss, _, taux = classify_loss(model, tx, ty)
results[kind] = {"parameter_count": parameter_count(model.p), "gradient_check": gc,
"smoke_updates": 12, "initial_train_loss": initial, "final_train_loss": final,
"train_loss_before_each_update": curve, "test_loss": tloss,
"test_accuracy_smoke_only": taux["accuracy"],
"test_both_correct_pair_fraction_smoke_only": taux["both_correct_pair_fraction"],
"initial_hidden_gradient_l2_by_position": aux0["hidden_gradient_l2_by_position"],
"final_hidden_gradient_l2_by_position": aux1["hidden_gradient_l2_by_position"],
"gradient_clipping": grad_norms, "wall_seconds": time.perf_counter() - started}
return {"status": "generator_leakage_checks_BPTT_gradient_checks_and_short_optimization_smoke",
"seed": seed, "input_ids": {"bit_0": 0, "bit_1": 1, "independent_noise": "2..9", "QUERY": 10},
"smoke_noise_length": 8, "loss_locations": "QUERY only; no label inside the input",
"splits": {n: {"paired_templates": len(t), "examples": len(y), "label_mean": float(y.mean()),
"sha256": digest_rows(x.tolist())} for n, (x, y, t) in data.items()},
"suffix_checks": checks, "models": results,
"interpretation": "Finite gradients and optimization plumbing only. These 12 updates do not test RNN/LSTM long-memory superiority."}
TREE = {(): {"a": .6, "b": .4}, ("a",): {"x": .55, "<EOS>": .45},
("b",): {"y": .99, "<EOS>": .01}, ("a", "x"): {"<EOS>": 1.0},
("b", "y"): {"<EOS>": 1.0}}
def beam_search(tree, width=2, max_steps=6, length_alpha=0.0):
"""Keep finished hypotheses, never expand EOS; explicit max-step failure."""
beams, trace = [((), 0.0)], []
def score(item):
tokens, logp = item
return logp / (((5 + len(tokens)) / 6) ** length_alpha)
for step in range(max_steps):
candidates = []
for tokens, logp in beams:
if tokens and tokens[-1] == "<EOS>":
candidates.append((tokens, logp))
continue
probs = tree[tokens]
assert abs(sum(probs.values()) - 1) < 1e-12
candidates.extend((tokens + (token,), logp + math.log(prob)) for token, prob in probs.items() if prob > 0)
beams = sorted(candidates, key=lambda item: (-score(item), item[0]))[:width]
trace.append([{"tokens": s, "log_probability": l, "probability": math.exp(l), "score": score((s, l))} for s, l in beams])
if all(s[-1] == "<EOS>" for s, _ in beams):
break
finished = [item for item in beams if item[0][-1] == "<EOS>"]
if not finished:
raise RuntimeError("No EOS-completed beam; increase max_steps and report truncation")
best = max(finished, key=score)
return {"tokens": best[0], "probability": math.exp(best[1]), "log_probability": best[1],
"score": score(best), "width": width, "length_alpha": length_alpha, "trace": trace}
def exhaustive_paths(tree, prefix=(), probability=1.0):
if prefix and prefix[-1] == "<EOS>":
return [{"tokens": prefix, "probability": probability}]
return list(itertools.chain.from_iterable(exhaustive_paths(tree, prefix + (tok,), probability * p)
for tok, p in tree[prefix].items()))
def route_d():
greedy, beam = beam_search(TREE, 1), beam_search(TREE, 2)
all_paths = exhaustive_paths(TREE)
assert greedy["tokens"] == ("a", "x", "<EOS>")
assert beam["tokens"] == ("b", "y", "<EOS>")
assert abs(beam["probability"] - max(x["probability"] for x in all_paths)) < ATOL
fail_tree = {(): {"a": .4, "b": .35, "c": .25}, ("a",): {"<EOS>": .5, "x": .5},
("b",): {"<EOS>": .5, "x": .5}, ("c",): {"<EOS>": 1.0},
("a", "x"): {"<EOS>": 1.0}, ("b", "x"): {"<EOS>": 1.0}}
failed_beam = beam_search(fail_tree, 2)
fail_best = max(exhaustive_paths(fail_tree), key=lambda r: r["probability"])
assert failed_beam["probability"] < fail_best["probability"]
source = ["a", "c", "b"]
target = source[::-1] + ["<EOS>"]
decoder_input = ["<BOS>"] + target[:-1]
return {"status": "constructed_probability_tree_search_and_teacher_forcing_alignment",
"seed": None, "tree": {" ".join(k) or "<BOS>": v for k, v in TREE.items()},
"greedy": greedy, "beam_2": beam, "exhaustive_paths": all_paths,
"length_penalty_alpha_0.6": beam_search(TREE, 2, length_alpha=.6),
"beam_failure_counterexample": {"tree": {" ".join(k) or "<BOS>": v for k, v in fail_tree.items()},
"beam_2": failed_beam, "exhaustive_best": fail_best},
"seq2seq_example": {"source": source, "target": target, "decoder_input": decoder_input,
"prediction_rows": [{"t": t, "input": i, "gold_target": y} for t, (i, y) in enumerate(zip(decoder_input, target))]},
"interpretation": "Search is exact on this finite tree only when checked against enumeration; seq2seq training remains a design."}
def attention(q, k, v, allowed=None, scale=None):
scores = q @ k.T / (math.sqrt(q.shape[-1]) if scale is None else scale)
if allowed is not None:
if not np.all(allowed.any(-1)):
raise ValueError("Each query must have at least one allowed key")
scores = np.where(allowed, scores, -np.inf)
weights = softmax(scores)
return weights @ v, weights
def attention_reference(q, k, v, allowed=None, scale=None):
rows = []
for i in range(len(q)):
valid = list(range(len(k))) if allowed is None else np.flatnonzero(allowed[i]).tolist()
divisor = math.sqrt(q.shape[-1]) if scale is None else scale
scores = [sum(float(a) * float(b) for a, b in zip(q[i], k[j])) / divisor for j in valid]
exps = [math.exp(s - max(scores)) for s in scores]
denominator = sum(exps)
rows.append([sum(exps[a] / denominator * float(v[j, f]) for a, j in enumerate(valid)) for f in range(v.shape[-1])])
return np.array(rows)
def position_encoding(length, width, offset=0):
positions = np.arange(offset, offset + length)[:, None]
inv = 10000.0 ** (-np.arange(0, width, 2) / width)
out = np.zeros((length, width))
out[:, 0::2] = np.sin(positions * inv)
out[:, 1::2] = np.cos(positions * inv)
return out
def route_e():
classroom = classroom_parameters()
read_example = classroom["READ"]
q = np.array([read_example["q"], read_example["q2"], [1., 1.]])
k = np.array(read_example["K"], dtype=float)
v = np.array(read_example["V"], dtype=float)
y, a = attention(q, k, v, scale=1)
qc = q.copy()
qc[0] = read_example["q2"]
yq, aq = attention(qc, k, v, scale=1)
vc = np.array(read_example["V_changed"], dtype=float)
yv, av = attention(q, k, vc, scale=1)
causal = np.tri(3, dtype=bool)
ym, am = attention(q, k, v, causal, scale=1)
vf = v.copy()
vf[1:] += 1000
ymf, _ = attention(q, k, vf, causal, scale=1)
reference_error = np.max(np.abs(ym - attention_reference(q, k, v, causal, scale=1)))
classroom_errors = {name: float(np.max(np.abs(actual[0] - np.array(read_example[name]["output"])[0])))
for name, actual in (("base", y), ("query_changed", yq), ("value_changed", yv))}
scaled_y, scaled_a = attention(q, k, v, causal)
scaled_error = float(np.max(np.abs(scaled_y - attention_reference(q, k, v, causal))))
assert max(classroom_errors.values()) < ATOL and scaled_error < ATOL
assert reference_error < ATOL
assert np.max(np.abs(a - av)) == 0
assert np.max(np.abs((yv - y) - a[:, 1, None] * (vc[1] - v[1]))) < ATOL
assert np.max(np.abs(ymf[0] - ym[0])) == 0
rng = np.random.default_rng(BASE_SEED + 5)
x = rng.normal(size=(4, 4))
wq, wk, wv = [rng.normal(size=(4, 3)) / 2 for _ in range(3)]
p = np.array([2, 0, 3, 1])
read = lambda z, mask=None: attention(z @ wq, z @ wk, z @ wv, mask)[0]
error = float(np.max(np.abs(read(x[p]) - read(x)[p])))
pe = position_encoding(4, 4)
position_delta = float(np.max(np.abs(read(x[p] + pe) - read(x + pe)[p])))
causal_delta = float(np.max(np.abs(read(x[p], np.tri(4, dtype=bool)) - read(x, np.tri(4, dtype=bool))[p])))
assert error < ATOL
assert position_delta > 1e-5 and causal_delta > 1e-5
return {"status": "shared_classroom_READ_forward_and_independent_scalar_reference", "seed_permutation": BASE_SEED + 5,
"classroom_source": classroom["source_path"], "classroom_source_sha256": classroom["source_sha256"],
"basic_READ_score_divisor": 1.0, "classroom_reference_max_abs_errors": classroom_errors,
"Q": q, "K": k, "V": v, "softmax_axis": "keys / last axis", "weights": a, "output": y,
"query_changed": qc, "query_changed_weights": aq, "query_changed_output": yq,
"value_changed": vc, "value_changed_weights_max_abs_delta": float(np.max(np.abs(a-av))), "value_changed_output": yv,
"causal_allowed": causal, "causal_weights": am, "causal_output": ym,
"scaled_Transformer_variant": {"score_divisor": math.sqrt(2), "weights": scaled_a, "output": scaled_y,
"scalar_reference_max_abs_error": scaled_error},
"scalar_reference_max_abs_error": float(reference_error),
"masked_future_value_perturbation_query_0_max_abs_delta": float(np.max(np.abs(ymf[0]-ym[0]))),
"permutation": p, "no_position_full_attention_equivariance_max_abs_error": error,
"fixed_absolute_position_permutation_max_abs_delta": position_delta,
"fixed_causal_graph_permutation_max_abs_delta": causal_delta, "absolute_tolerance": ATOL,
"interpretation": "Full unpositioned self-attention is equivariant; fixed positions or a fixed causal graph change the experiment."}
def layer_norm(x, gamma, beta, eps=1e-5):
return (x - x.mean(-1, keepdims=True)) / np.sqrt(x.var(-1, keepdims=True) + eps) * gamma + beta
def row_norm_reference(row, gamma, beta, eps=1e-5):
mean = sum(float(v) for v in row) / len(row)
variance = sum((float(v) - mean) ** 2 for v in row) / len(row)
return np.array([(float(v) - mean) / math.sqrt(variance + eps) * float(g) + float(b) for v, g, b in zip(row, gamma, beta)])
def make_block(rng, width=8, ff=16):
p = {key: rng.normal(0, 1/math.sqrt(width), (width, width)) for key in ("Wq", "Wk", "Wv", "Wo")}
p.update({"W1": rng.normal(0, 1/math.sqrt(width), (width, ff)), "b1": np.zeros(ff),
"W2": rng.normal(0, 1/math.sqrt(ff), (ff, width)), "b2": np.zeros(width),
"g1": np.ones(width), "bN1": np.zeros(width), "g2": np.ones(width), "bN2": np.zeros(width)})
return p
def multihead(x, p, heads=2, causal=False):
steps, width = x.shape
dk = width // heads
q, k, v = [(x @ p[key]).reshape(steps, heads, dk).transpose(1, 0, 2) for key in ("Wq", "Wk", "Wv")]
scores = (q @ k.transpose(0, 2, 1)) / math.sqrt(dk)
if causal:
scores = np.where(np.tri(steps, dtype=bool)[None], scores, -np.inf)
a = softmax(scores)
per_head = a @ v
concat = per_head.transpose(1, 0, 2).reshape(steps, width)
return concat @ p["Wo"], {"Q": q, "K": k, "V": v, "weights": a,
"head_outputs": per_head, "concat": concat}
def route_f():
classroom = classroom_parameters()
block = classroom["BLOCK"]
x = np.array(block["X"], dtype=float)
p = {"Wq": np.concatenate([block["WQ1"], block["WQ2"]], axis=1),
"Wk": np.concatenate([block["WK1"], block["WK2"]], axis=1),
"Wv": np.concatenate([block["WV1"], block["WV2"]], axis=1),
"Wo": np.array(block["WO"], dtype=float), "W1": np.array(block["W1"], dtype=float),
"W2": np.array(block["W2"], dtype=float), "b1": np.zeros(6), "b2": np.zeros(4),
"g1": np.ones(4), "bN1": np.zeros(4), "g2": np.ones(4), "bN2": np.zeros(4)}
att, trace = multihead(x, p)
residual_1 = x + att
n1 = layer_norm(residual_1, p["g1"], p["bN1"])
ff_hidden = np.maximum(n1 @ p["W1"] + p["b1"], 0)
ff = ff_hidden @ p["W2"] + p["b2"]
residual_2 = n1 + ff
y = layer_norm(residual_2, p["g2"], p["bN2"])
# Independent head and per-position paths, not a comparison against the same call.
refs = [attention_reference(trace["Q"][h], trace["K"][h], trace["V"][h]) for h in range(2)]
att_ref = np.concatenate(refs, axis=-1) @ p["Wo"]
n1_ref = np.array([row_norm_reference(row, p["g1"], p["bN1"]) for row in x + att_ref])
ff_ref = np.array([np.maximum(row @ p["W1"] + p["b1"], 0) @ p["W2"] + p["b2"] for row in n1_ref])
y_ref = np.array([row_norm_reference(row, p["g2"], p["bN2"]) for row in n1_ref + ff_ref])
err = float(np.max(np.abs(y - y_ref)))
assert err < ATOL
classroom_error = float(np.max(np.abs(y[0] - np.array(block["z"]))))
assert classroom_error < ATOL
z = n1.copy()
z[0] += .5
ffz = np.maximum(z @ p["W1"] + p["b1"], 0) @ p["W2"] + p["b2"]
assert np.max(np.abs(ffz[1:] - ff[1:])) == 0
shifted = layer_norm(residual_1 + np.arange(1, len(x)+1)[:, None], p["g1"], p["bN1"])
return {"status": "shared_classroom_BLOCK_untrained_post_norm_forward", "seed": None,
"classroom_source": classroom["source_path"], "classroom_source_sha256": classroom["source_sha256"],
"classroom_token_0_reference_max_abs_error": classroom_error,
"config": {"tokens": 3, "d_model": 4, "heads": 2, "d_k": 2, "d_v": 2, "d_ff": 6,
"batch_axis": "omitted for a single sequence", "dropout": 0, "LayerNorm_epsilon": 1e-5},
"parameter_shapes": {key: v.shape for key, v in p.items()}, "parameter_count": parameter_count(p),
"activation_shapes": {key: a.shape for key, a in {"input": x, **trace, "attention_output": att,
"residual_1": residual_1, "norm_1": n1, "ff_hidden": ff_hidden,
"ff_output": ff, "residual_2": residual_2, "block_output": y}.items()},
"tracked_token_index": 0,
"token_0_trace": {"input": x[0], "head_0": trace["head_outputs"][0, 0], "head_1": trace["head_outputs"][1, 0],
"concat": trace["concat"][0], "attention_output": att[0], "residual_1": residual_1[0],
"norm_1": n1[0], "ff_hidden": ff_hidden[0], "ff_output": ff[0],
"residual_2": residual_2[0], "output": y[0]},
"output": y, "independent_reference_max_abs_error": err,
"LayerNorm_output_feature_means": y.mean(-1), "LayerNorm_output_feature_variances": y.var(-1),
"LayerNorm_per_row_constant_shift_max_abs_error": float(np.max(np.abs(n1-shifted))),
"FFN_other_positions_after_one_position_change_max_abs_delta": float(np.max(np.abs(ffz[1:]-ff[1:]))),
"absolute_tolerance": ATOL, "interpretation": "Traceable computation and shape checks, not a trained Transformer or translation score."}
class TinyDecoder:
"""Two pre-norm causal blocks; fixed random weights, no training/dropout.
full() computes every prefix position with batched matrix operations.
incremental() independently computes only the new position, loops over heads,
and normalizes a row with a scalar reference implementation.
"""
def __init__(self, seed=0, width=8, heads=2, layers=2, vocab=17):
rng = np.random.default_rng(seed)
self.width, self.heads, self.vocab = width, heads, vocab
self.embedding = rng.normal(0, .5, (vocab, width))
self.blocks = [make_block(rng, width, 2 * width) for _ in range(layers)]
self.final_g = rng.uniform(.8, 1.2, width)
self.final_b = rng.normal(0, .03, width)
self.output = rng.normal(0, 1/math.sqrt(width), (width, vocab))
def full(self, tokens, position_offset=0):
x = self.embedding[np.asarray(tokens)] + position_encoding(len(tokens), self.width, position_offset)
caches = []
for p in self.blocks:
n = layer_norm(x, p["g1"], p["bN1"])
a, tr = multihead(n, p, self.heads, causal=True)
caches.append({"K": tr["K"].copy(), "V": tr["V"].copy()})
x = x + a
n = layer_norm(x, p["g2"], p["bN2"])
x = x + np.maximum(n @ p["W1"] + p["b1"], 0) @ p["W2"] + p["b2"]
return layer_norm(x, self.final_g, self.final_b) @ self.output, caches
def incremental(self, token, caches, position):
d = self.width // self.heads
# Separate scalar absolute-position calculation from the vectorized full path.
pe = np.array([math.sin(position / 10000 ** (j / self.width)) if j % 2 == 0
else math.cos(position / 10000 ** ((j-1) / self.width)) for j in range(self.width)])
x = self.embedding[token] + pe
new_caches = []
for p, cache in zip(self.blocks, caches):
n = row_norm_reference(x, p["g1"], p["bN1"])
q, kn, vn = [(n @ p[key]).reshape(self.heads, d) for key in ("Wq", "Wk", "Wv")]
ks = np.concatenate((cache["K"], kn[:, None]), axis=1)
vs = np.concatenate((cache["V"], vn[:, None]), axis=1)
# The new row may attend to all cached positions and itself; no square mask.
heads = [attention_reference(q[h:h+1], ks[h], vs[h])[0] for h in range(self.heads)]
x = x + np.concatenate(heads) @ p["Wo"]
n = row_norm_reference(x, p["g2"], p["bN2"])
x = x + np.maximum(n @ p["W1"] + p["b1"], 0) @ p["W2"] + p["b2"]
new_caches.append({"K": ks, "V": vs})
return row_norm_reference(x, self.final_g, self.final_b) @ self.output, new_caches
def empty_cache(self):
shape = (self.heads, 0, self.width // self.heads)
return [{"K": np.empty(shape), "V": np.empty(shape)} for _ in self.blocks]
def timing_summary(samples):
return {"median_ms": float(np.median(samples) * 1000), "min_ms": min(samples) * 1000,
"max_ms": max(samples) * 1000, "repeats": len(samples)}
def route_g():
seed = BASE_SEED + 7
model = TinyDecoder(seed)
tokens = np.random.default_rng(seed + 1).integers(0, model.vocab, 48).tolist()
full_logits, _ = model.full(tokens)
cases = []
maximum = 0.0
# Include no-prefill, several prefill sizes, and a nonzero absolute position offset.
for prefill, offset in ((0, 0), (1, 0), (7, 0), (16, 0), (7, 11)):
if prefill:
_, caches = model.full(tokens[:prefill], offset)
else:
caches = model.empty_cache()
errors = []
for t in range(prefill, len(tokens)):
incremental_logits, caches = model.incremental(tokens[t], caches, t + offset)
fresh_logits, _ = model.full(tokens[:t+1], offset)
error = float(np.max(np.abs(incremental_logits - fresh_logits[-1])))
errors.append(error)
maximum = max(maximum, max(errors))
cases.append({"prefill_tokens": prefill, "absolute_position_offset": offset,
"decode_steps": len(errors), "max_absolute_logits_error": max(errors),
"errors_by_step": errors})
assert maximum < ATOL
prefix_logits, prefix_cache = model.full(tokens[:16])
prefix_invariance = float(np.max(np.abs(full_logits[:16] - prefix_logits)))
assert prefix_invariance < ATOL
correct, _ = model.incremental(tokens[16], prefix_cache, 16)
wrong_position, _ = model.incremental(tokens[16], prefix_cache, 0)
position_failure = float(np.max(np.abs(correct - wrong_position)))
changed = tokens[:17].copy()
changed[0] = (changed[0] + 1) % model.vocab
changed_logits, _ = model.full(changed)
stale_failure = float(np.max(np.abs(correct - changed_logits[-1])))
assert position_failure > 1e-5 and stale_failure > 1e-5
# Actual autoregressive choices: start with the same prefix, choose independently.
prompt = tokens[:7]
slow_tokens, fast_tokens = prompt.copy(), prompt.copy()
fast_logits, cache = model.full(prompt)
last_fast = fast_logits[-1]
for _ in range(8):
slow_logits, _ = model.full(slow_tokens)
slow_tokens.append(int(np.argmax(slow_logits[-1])))
fast_token = int(np.argmax(last_fast))
fast_tokens.append(fast_token)
last_fast, cache = model.incremental(fast_token, cache, len(fast_tokens)-1)
assert slow_tokens == fast_tokens
# Timed work uses predetermined tokens, so both branches process identical input.
prefill_samples, cached_samples, uncached_samples = [], [], []
for repeat in range(6):
started = time.perf_counter()
_, caches = model.full(tokens[:16])
prefill_time = time.perf_counter() - started
started = time.perf_counter()
for t in range(16, 48):
_, caches = model.incremental(tokens[t], caches, t)
cached_time = time.perf_counter() - started
started = time.perf_counter()
for t in range(16, 48):
model.full(tokens[:t+1])
uncached_time = time.perf_counter() - started
if repeat: # one warm-up, then five measurements
prefill_samples.append(prefill_time)
cached_samples.append(cached_time)
uncached_samples.append(uncached_time)
payload_bytes = sum(a.nbytes for c in caches for a in c.values())
context_task_checks = []
for length in (32, 128, 512):
for fraction in (.1, .5, .9):
position = int((length - 2) * fraction)
tasks = delayed_bit_data(length-2, (16, 8, 8), seed+2, evidence_position=position)
x, labels, _ = tasks["test"]
assert np.array_equal(x[0::2, -4:], x[1::2, -4:])
assert np.all(labels[0::2] != labels[1::2])
assert np.all(x[:, position] == labels)
context_task_checks.append({"sequence_length": length, "evidence_position_zero_based": position,
"distance_to_query": length-1-position, "test_paired_examples": len(labels),
"identical_last_4_tokens_opposite_labels": True,
"test_sha256": digest_rows(x.tolist())})
return {"status": "untrained_decoder_equivalence_and_CPU_microtiming_not_language_ability",
"implementation_version": VERSION, "seed_weights": seed, "seed_tokens": seed+1,
"config": {"layers": 2, "d_model": 8, "heads": 2, "d_k": 4, "d_ff": 16, "vocab": 17,
"architecture": "pre-norm causal decoder; separate final LayerNorm and vocab projection",
"position": "absolute sinusoidal, float64", "dtype": "float64", "eval": True, "dropout": 0.0,
"padding": False, "batch_size": 1, "prefix_immutable": True},
"input_token_ids": tokens, "cases": cases, "max_absolute_logits_error": maximum,
"absolute_tolerance": ATOL, "relative_tolerance": 0.0,
"old_position_logits_after_future_append_max_abs_error": prefix_invariance,
"wrong_position_negative_control_max_abs_delta": position_failure,
"changed_prefix_stale_cache_negative_control_max_abs_delta": stale_failure,
"greedy_generated_ids_after_prompt": fast_tokens[len(prompt):], "greedy_paths_equal": slow_tokens == fast_tokens,
"timing": {"clock": "time.perf_counter", "warmups": 1, "prefill_tokens": 16, "decode_tokens": 32,
"prefill": timing_summary(prefill_samples), "cached_decode_total": timing_summary(cached_samples),
"noncached_decode_total": timing_summary(uncached_samples),
"note": "Python scalar cached reference versus vectorized NumPy full-prefix; not an optimized cache speed benchmark."},
"cache_payload_bytes_at_48_tokens": payload_bytes,
"cache_payload_formula_bytes": 2 * 2 * 48 * 8 * 8,
"decode_projected_token_layers": {"cached": 32 * 2, "noncached": sum(range(17, 49)) * 2},
"decode_attention_score_elements": {"cached": sum(range(17, 49)) * 2 * 2,
"noncached": sum(n*n for n in range(17, 49)) * 2 * 2},
"effective_context_generator_checks": context_task_checks,
"effective_context_ability_test": "GENERATOR CHECKED; MODEL TRAINING/EVALUATION PENDING: evaluate a trained retrieval model on held-out balanced paired templates by evidence position; this decoder is random and untrained."}
def json_default(obj):
if isinstance(obj, np.ndarray):
return obj.tolist()
if isinstance(obj, np.generic):
return obj.item()
raise TypeError(type(obj).__name__)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--routes", default="ABCDEFG", help="Subset of A B C D E F G, default all")
parser.add_argument("--output", type=Path, default=Path(__file__).parent / "results" / "r21-results.json")
args = parser.parse_args()
route_funcs = {"A": route_a, "B": route_b, "C": route_c, "D": route_d, "E": route_e, "F": route_f, "G": route_g}
chosen = list(dict.fromkeys(args.routes.upper().replace(",", "").replace(" ", "")))
if any(name not in route_funcs for name in chosen):
parser.error("--routes must contain only A..G")
started = time.perf_counter()
cpu = "unknown"
cpuinfo = Path("/proc/cpuinfo")
if cpuinfo.exists():
matches = re.findall(r"model name\s*:\s*(.+)", cpuinfo.read_text())
if matches:
cpu = matches[0]
result = {"metadata": {"implementation_version": VERSION, "generated_at_utc": datetime.now(timezone.utc).isoformat(),
"python": sys.version, "numpy": np.__version__, "platform": platform.platform(), "cpu": cpu,
"dtype": "float64", "torch_installed_or_used": False,
"thread_environment": {key: os.environ.get(key) for key in ("OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS", "MKL_NUM_THREADS")},
"source_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
"run_scope": "local CPU only; no Deepnote API calls, no cloud runs, no publication"}, "routes": {}}
for name in chosen:
start = time.perf_counter()
result["routes"][name] = route_funcs[name]()
result["routes"][name]["route_wall_seconds"] = time.perf_counter() - start
print(f"{name}: {result['routes'][name]['status']} ({result['routes'][name]['route_wall_seconds']:.3f}s)", flush=True)
result["metadata"]["total_wall_seconds"] = time.perf_counter() - started
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2, default=json_default, allow_nan=False) + "\n")
print(f"Saved {args.output}")
if __name__ == "__main__":
main()