QLNI's picture
Embeddings Are Overpriced? release: code, data, checkpoints and evaluation
f31f59b
Raw History Blame Contribute Delete
5.06 kB
"""Validation loss split by how often the target word appeared in TRAINING.
Question: does the frozen table win on rare words, where a learned table is starved of examples, even if the average
loss favours the learned table (common words dominate the average)?
python freq_eval.py --arm A --ckpt A/step_07820.pt [--tag A] [--seqs 512]
python freq_eval.py --compare reports/freq_A.json reports/freq_B.json reports/freq_C.json reports/freq_B2.json
Buckets by the target token's count in the 2.05B training tokens (data/unigram_counts.npy):
never seen (0) | 1-9 | 10-99 | 100-999 | 1k-9.9k | 10k-99k | 100k-999k | 1M+
Saved per validation sequence (loss sum and count per bucket), so --compare gives paired 95% ranges (bootstrap over
sequences, 10,000 re-draws) for every arm minus the first file.
"""
import argparse, json, pathlib, time
import numpy as np
import torch
import torch.nn.functional as F
SEQ = 1024
EDGES = [0, 1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, np.inf]
NAMES = ["never seen", "1-9", "10-99", "100-999", "1k-9.9k", "10k-99k", "100k-999k", "1M+"]
def bucket_of(counts):
b = np.searchsorted(np.array(EDGES[1:-1]), counts, side="right") # 0 -> bucket 0 (count 0), 1..9 -> 1, ...
b[counts == 0] = 0
b[(counts > 0) & (b == 0)] = 1
return b
@torch.no_grad()
def run(a):
import train
counts = np.load(pathlib.Path(a.data) / "unigram_counts.npy")
bk = torch.from_numpy(bucket_of(counts)).cuda()
m = train.Model(a.arm, counts).cuda().eval()
st = torch.load(a.ckpt, map_location="cuda", weights_only=False)
m.load_state_dict(st["model"])
val = np.load(pathlib.Path(a.data) / "val.npy", mmap_mode="r")
x = torch.from_numpy(val[:a.seqs * (SEQ + 1)].astype(np.int64).reshape(a.seqs, SEQ + 1))
nb = len(NAMES)
sums = np.zeros((a.seqs, nb)); cnts = np.zeros((a.seqs, nb))
for i in range(a.seqs):
ids = x[i:i + 1].cuda()
with torch.autocast("cuda", dtype=torch.bfloat16):
lg = m.logits(m.hidden(ids[:, :-1])).float()
tgt = ids[0, 1:]
l = F.cross_entropy(lg[0], tgt, reduction="none")
b = bk[tgt]
sums[i] = torch.zeros(nb, device="cuda").scatter_add_(0, b, l).cpu().numpy()
cnts[i] = torch.bincount(b, minlength=nb).float().cpu().numpy()
tag = a.tag or st.get("tag", a.arm)
out = {"tag": tag, "ckpt": a.ckpt, "buckets": NAMES, "sums": sums.tolist(), "counts": cnts.tolist(),
"vocab_per_bucket": np.bincount(bucket_of(counts), minlength=nb).tolist(), "date": time.strftime("%F %H:%M")}
pathlib.Path("reports").mkdir(exist_ok=True)
p = pathlib.Path("reports") / f"freq_{tag}.json"; json.dump(out, open(p, "w"))
print(f"{tag}:", " | ".join(f"{n} {sums[:, j].sum() / max(cnts[:, j].sum(), 1):.3f}" for j, n in enumerate(NAMES)))
print("saved", p)
def compare(files):
rng = np.random.default_rng(0)
R = [json.load(open(f)) for f in files]
ref = R[0]; S0 = np.array(ref["sums"]); C0 = np.array(ref["counts"])
nseq = S0.shape[0]; idx = rng.integers(0, nseq, (10_000, nseq))
L = ["# Validation loss by training frequency of the target word", "",
f"{nseq} held-out sequences x 1,024 tokens. Loss in nats/token, lower is better. Differences vs {ref['tag']}, paired,"
" 95% bootstrap range over sequences.", "",
"| bucket (count in training) | vocab words | share of val tokens | " + " | ".join(r["tag"] for r in R) + " | "
+ " | ".join(f"{r['tag']} - {ref['tag']}" for r in R[1:]) + " |",
"|---|---|---|" + "---|" * (len(R) + len(R) - 1)]
tot = C0.sum()
for j, name in enumerate(ref["buckets"]):
if C0[:, j].sum() == 0:
continue
cells = []
for r in R:
S = np.array(r["sums"]); C = np.array(r["counts"])
cells.append(f"{S[:, j].sum() / C[:, j].sum():.3f}")
diffs = []
for r in R[1:]:
S = np.array(r["sums"]); C = np.array(r["counts"])
d = S[:, j].sum() / C[:, j].sum() - S0[:, j].sum() / C0[:, j].sum()
bs = S[idx, j].sum(1) / np.maximum(C[idx, j].sum(1), 1) - S0[idx, j].sum(1) / np.maximum(C0[idx, j].sum(1), 1)
lo, hi = np.percentile(bs, [2.5, 97.5])
verdict = "tied" if lo <= 0 <= hi else ("better" if d < 0 else "worse")
diffs.append(f"{d:+.3f} ({lo:+.3f} to {hi:+.3f}) {verdict}")
L.append(f"| {name} | {ref['vocab_per_bucket'][j]:,} | {C0[:, j].sum() / tot * 100:.2f}% | " + " | ".join(cells) + " | "
+ " | ".join(diffs) + " |")
open("reports/FREQ_SPLIT.md", "w").write("\n".join(L) + "\n")
print("\n".join(L))
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--arm", choices=["A", "B", "C", "D"])
ap.add_argument("--ckpt"); ap.add_argument("--tag", default="")
ap.add_argument("--data", default="data"); ap.add_argument("--seqs", type=int, default=512)
ap.add_argument("--compare", nargs="+")
a = ap.parse_args()
compare(a.compare) if a.compare else run(a)