GOVINDFROM's picture
Automated push from Zaratan
dbbc6f9 verified
Raw History Blame Contribute Delete
3.93 kB
"""Autoregressive evaluator for generative perplexity on text8.
Generative perplexity - the likelihood an independent language model assigns to
our samples - is the standard quality metric for diffusion language models,
because the diffusion model's own ELBO says nothing about the quality of what it
actually generates at a low number of function evaluations.
The evaluator is trained here from scratch on the same corpus rather than using
a pretrained GPT-2, for two reasons. The project's constraint is that everything
is trained from scratch, and a character-level evaluator trained on text8 itself
is the correct judge for text8 samples - a word-piece model trained on web text
would score character-level artifacts through a tokenizer that never sees them.
The evaluator is trained ONCE and shared by every diffusion run, so all systems
are judged by exactly the same yardstick.
"""
import argparse, json, math, os, time
import numpy as np
import torch
import torch.nn.functional as F
from model import Transformer
from text8 import load_text8, batches
p = argparse.ArgumentParser()
p.add_argument("--root", default="data/text8")
p.add_argument("--seq_len", type=int, default=256)
p.add_argument("--d", type=int, default=512)
p.add_argument("--layers", type=int, default=8)
p.add_argument("--heads", type=int, default=8)
p.add_argument("--bs", type=int, default=128)
p.add_argument("--steps", type=int, default=60000)
p.add_argument("--lr", type=float, default=3e-4)
p.add_argument("--warmup", type=int, default=2000)
p.add_argument("--seed", type=int, default=0)
p.add_argument("--out", default="runs/evallm")
a = p.parse_args()
torch.manual_seed(a.seed)
np.random.seed(a.seed)
dev = "cuda" if torch.cuda.is_available() else "cpu"
os.makedirs(a.out, exist_ok=True)
tr, va, te, tok = load_text8(a.root, a.seq_len)
print(f"text8: train {tr.shape}, valid {va.shape}, test {te.shape}, vocab {len(tok)}",
flush=True)
model = Transformer(len(tok), a.d, a.layers, a.heads, "rope",
causal=True, max_len=a.seq_len + 8).to(dev)
print(f"evaluator params: {sum(p.numel() for p in model.parameters())/1e6:.1f}M", flush=True)
opt = torch.optim.AdamW(model.parameters(), lr=a.lr, weight_decay=0.01, betas=(0.9, 0.95))
sched = torch.optim.lr_scheduler.LambdaLR(
opt, lambda s: min((s + 1) / a.warmup, max(0.0, (a.steps - s) / max(1, a.steps - a.warmup))))
it = batches(tr, a.bs, seed=a.seed)
t0 = time.time()
for step in range(a.steps):
x = torch.from_numpy(next(it)).to(dev)
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=(dev == "cuda")):
logits = model(x)
# next-character prediction: position i predicts position i+1
loss = F.cross_entropy(logits[:, :-1].reshape(-1, len(tok)),
x[:, 1:].reshape(-1))
opt.zero_grad(set_to_none=True)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step(); sched.step()
if step % 2000 == 0:
print(f"step {step:6d} loss {loss.item():.4f} "
f"bpc {loss.item()/math.log(2):.3f} {time.time()-t0:.0f}s", flush=True)
@torch.no_grad()
def test_bpc(arr, n=200):
model.eval()
tot, cnt = 0.0, 0
for i in range(0, min(n * a.bs, len(arr)), a.bs):
x = torch.from_numpy(arr[i:i + a.bs]).to(dev)
if len(x) < 2:
continue
logits = model(x)
ce = F.cross_entropy(logits[:, :-1].reshape(-1, len(tok)),
x[:, 1:].reshape(-1), reduction="sum")
tot += float(ce); cnt += x[:, 1:].numel()
model.train()
return tot / cnt / math.log(2)
bpc = test_bpc(te)
print(f"\nevaluator test BPC: {bpc:.4f}", flush=True)
torch.save(model.state_dict(), os.path.join(a.out, "model.pt"))
json.dump({"args": vars(a), "test_bpc": bpc, "vocab": len(tok)},
open(os.path.join(a.out, "result.json"), "w"), indent=2)
print(f"saved to {a.out}", flush=True)