GOVINDFROM's picture
Automated push from Zaratan
dbbc6f9 verified
Raw History Blame Contribute Delete
6.07 kB
"""One-pass masked diffusion on SIGMORPHON morphological inflection.
Autoregressive seq2seq needs |y| decoding steps, about ten per word. If a
diffusion model can commit the whole form in ONE forward pass at the same
exact-match accuracy, that is an order-of-magnitude inference saving on a task
with published baselines across 52 languages.
Breadth here is measured across typologically diverse languages rather than
across seeds of a single configuration, which is the honest test of whether a
method generalises.
"""
import argparse, json, math, os, time
import numpy as np
import torch
import torch.nn.functional as F
from sigmorphon import load_language
from model import Transformer
import diffusion as dfn
def get_args():
p = argparse.ArgumentParser()
p.add_argument("--lang", default="english")
p.add_argument("--size", default="medium", choices=["low", "medium", "high"])
p.add_argument("--mode", choices=["ar", "mdlm", "matched", "distill", "full", "prog"], default="mdlm",
help="ar = autoregressive baseline; full = ours")
p.add_argument("--seq_len", type=int, default=48)
p.add_argument("--d", type=int, default=256)
p.add_argument("--layers", type=int, default=6)
p.add_argument("--heads", type=int, default=8)
p.add_argument("--pe", default="rope")
p.add_argument("--bs", type=int, default=64)
p.add_argument("--steps", type=int, default=12000)
p.add_argument("--lr", type=float, default=1e-4)
p.add_argument("--warmup", type=int, default=1000)
p.add_argument("--K", type=int, default=4)
p.add_argument("--lam", type=float, default=1.0)
p.add_argument("--seed", type=int, default=0)
p.add_argument("--data_root", default="data/sig")
p.add_argument("--eval_bs", type=int, default=128)
p.add_argument("--out", default="runs/dev")
return p.parse_args()
PASSES = [1, 2, 4, 8, 16, 32]
def exact_match(pred, gold, amask):
ok = 0
for p, g, a in zip(pred.tolist(), gold.tolist(), amask.tolist()):
pa = [p[i] for i in range(len(a)) if a[i]]
ga = [g[i] for i in range(len(a)) if a[i]]
ok += pa == ga
return ok / max(len(gold), 1)
@torch.no_grad()
def ar_decode(model, x0, amask, tok, device):
"""Autoregressive baseline: fill the answer strictly left to right, one
position per forward pass. This is the |y|-step cost the method removes."""
x = torch.where(amask, torch.full_like(x0, tok.mask), x0).clone()
order = amask[0].nonzero().flatten().tolist()
for j in order:
logits = model(x)
x[:, j] = torch.where(amask[:, j], logits[:, j].argmax(-1), x[:, j])
return x
@torch.no_grad()
def evaluate(model, a, tok, Xdv, Adv, device):
model.eval()
out = {}
for steps in PASSES:
accs = []
for i in range(0, len(Xdv), a.eval_bs):
xb = Xdv[i:i + a.eval_bs].to(device)
ab = Adv[i:i + a.eval_bs].to(device)
pred = dfn.cond_decode(model, xb, ab, tok, steps, device)
accs.append(exact_match(pred.cpu(), xb.cpu(), ab.cpu()))
out[str(steps)] = float(np.mean(accs))
# full autoregressive decode: one pass per answer position
accs = []
for i in range(0, len(Xdv), a.eval_bs):
xb = Xdv[i:i + a.eval_bs].to(device)
ab = Adv[i:i + a.eval_bs].to(device)
accs.append(exact_match(ar_decode(model, xb, ab, tok, device).cpu(),
xb.cpu(), ab.cpu()))
out["ar"] = float(np.mean(accs))
model.train()
return out
def main():
a = get_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)
(Xtr, Atr), (Xdv, Adv), tok = load_language(a.lang, a.data_root, a.size, a.seq_len)
Xtr, Atr = torch.from_numpy(Xtr), torch.from_numpy(Atr)
Xdv, Adv = torch.from_numpy(Xdv), torch.from_numpy(Adv)
model = Transformer(len(tok), a.d, a.layers, a.heads, a.pe,
causal=False, max_len=a.seq_len + 8).to(dev)
print(f"[{a.lang}/{a.size}/{a.mode}] params={model.n_params()/1e6:.2f}M "
f"vocab={len(tok)} train={len(Xtr)} dev={len(Xdv)}", flush=True)
opt = torch.optim.AdamW(model.parameters(), lr=a.lr, weight_decay=0.01)
sched = torch.optim.lr_scheduler.LambdaLR(
opt, lambda s: min(1.0, (s + 1) / a.warmup) *
0.5 * (1 + math.cos(math.pi * min(1.0, s / a.steps))))
amp = dict(device_type="cuda", dtype=torch.bfloat16) if dev == "cuda" \
else dict(device_type="cpu", enabled=False)
t0 = time.time()
for step in range(a.steps):
idx = torch.randint(0, len(Xtr), (a.bs,))
xb, ab = Xtr[idx].to(dev), Atr[idx].to(dev)
opt.zero_grad(set_to_none=True)
with torch.autocast(**amp):
if a.mode == "prog":
loss, _, _ = dfn.pmd_progressive(model, xb, ab, tok,
step / max(1, a.steps), 16, a.lam)
elif a.mode == "ar":
# same network, but supervised only on left-to-right prefixes
loss = dfn.cond_mdlm_loss(model, xb, ab, tok, "uniform")
else:
loss = dfn.pmd_loss(model, xb, ab, tok, a.mode, a.K, a.lam)
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} ({time.time()-t0:.0f}s)", flush=True)
acc = evaluate(model, a, tok, Xdv, Adv, dev)
json.dump({"args": vars(a), "accuracy_by_passes": acc,
"minutes": (time.time() - t0) / 60,
"compute": {"params": model.n_params(),
"answer_len_mean": float(Adv.sum(1).float().mean())}},
open(os.path.join(a.out, "result.json"), "w"), indent=2)
torch.save(model.state_dict(), os.path.join(a.out, "model.pt"))
print("FINAL", json.dumps(acc), flush=True)
if __name__ == "__main__":
main()