File size: 6,407 Bytes
7420c83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# CUERDAS-021 common: sealed set, varied replacement pool, training loop, evaluation.
import os, sys, json, hashlib, random, re, time
import torch, torch.nn as nn, torch.nn.functional as F
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import run018 as R
INP = os.environ['INP']; OD = os.environ['OD']
raw = open(f'{INP}/sealed021.json').read(); H = hashlib.sha256(raw.encode()).hexdigest()
assert H == 'a86a6a5ee375ae67a3e79f969c9553b0e5548ed3b0e80e5315862e6552ae72bc', H
S = json.loads(raw); NEW = S['new_values']; EXCL = set(S['exclude_token_ids'])
SETS = ['sealed_id', 'sealed_ood', 'sealed_1_new', 'sealed_2_new', 'sealed_4_new']
SMOKE = bool(os.environ.get('SMOKE'))
if SMOKE:  # dry run on dev messages only; sealed sets are never touched
    for s_ in SETS: S[s_] = R.FRESH['ood'][:6]
STEPS = 4 if SMOKE else 3000; EVERY = 2 if SMOKE else 250
t1 = R.t1; tok = R.tok; Emb = R.Emb; D = R.D; FIELDS = R.FIELDS; V = R.V
CAND = [torch.tensor(sorted({t1(w)[0] for w in V[j] + NEW[j]})) for j in range(4)]
assert all(len(CAND[j]) == len(V[j]) + 8 for j in range(4))
def gidx(cand, j, w): return int((cand[j] == t1(w)[0]).nonzero()[0])
# ---- varied pool (training) and held-out validation pool; all disjoint from sealed tokens and from 014/dev/020 values ----
voc = tok.get_vocab(); BAN = EXCL | R.BANNED
def ok_tok(t): return voc[t] not in BAN
WORDS = sorted(t[1:] for t in voc if re.fullmatch(r'Ġ[a-z]{3,14}', t) and ok_tok(t))
NAMES = sorted(t[1:] for t in voc if re.fullmatch(r'Ġ[A-Z][a-z]{2,10}', t) and ok_tok(t))
SUBS = sorted(t for t in voc if re.fullmatch(r'[a-z]{2,5}', t))
g0 = random.Random(2101); g0.shuffle(WORDS); g0.shuffle(NAMES)
VW, VN = WORDS[-300:], NAMES[-300:]; WORDS, NAMES = WORDS[:-300], NAMES[:-300]
def sample_value(g, W_=None, N_=None):
    W_ = W_ or WORDS; N_ = N_ or NAMES; k = g.randrange(5)
    if k == 0: return g.choice(W_)
    if k == 1: return g.choice(N_)
    if k == 2: return str(g.randrange(10000))
    if k == 3: return ' '.join(g.choice(W_) for _ in range(g.choice((2, 3))))
    return g.choice(W_) + g.choice(SUBS)
def sample_msgs(g, bs=12):
    out = []
    for _ in range(bs):
        m = list(g.choice(R.FIT))
        for j in range(4):
            if g.random() < .4: m[j] = sample_value(g)
        out.append(m)
    return out
# restricted transfer validation: 50 held-out values per field (no numbers: first token not unique), options = field vocab + those
gv = random.Random(2102); CANDV = []; VPOOL = []
for j in range(4):
    used = {t1(w)[0] for w in V[j]}; ws = []
    while len(ws) < 50:
        k = gv.randrange(3); w = gv.choice(VW) if k == 0 else gv.choice(VN) if k == 1 else gv.choice(VW) + ' ' + gv.choice(VW)
        if t1(w)[0] not in used: used.add(t1(w)[0]); ws.append(w)
    VPOOL.append(ws); CANDV.append(torch.tensor(sorted(used)))
VAL_TR = [[gv.choice(VPOOL[j]) for j in range(4)] for _ in range(96)]
VALC = R.VAL[:96]
if SMOKE: VALC, VAL_TR = VALC[:6], VAL_TR[:6]
# ---- generic readout: rows = prefix (embeddings) + body tokens ----
def run_h(embs):
    L = max(e.shape[0] for e in embs); B = len(embs); x = torch.zeros(B, L, D); m = torch.zeros(B, L, dtype=torch.long)
    for i, e in enumerate(embs): x[i, :e.shape[0]] = e; m[i, :e.shape[0]] = 1
    h = R.lm.model(inputs_embeds=x, attention_mask=m).last_hidden_state[torch.arange(B), m.sum(1) - 1]
    return h, R.lm.lm_head(h)
def k2_rows(cod, msgs, tpl):
    Sl = cod(torch.stack([torch.stack([R.lex(w) for w in m]) for m in msgs]))
    return [torch.cat([Sl[i], Emb[R.sufids(tpl, j)]]) for i in range(len(msgs)) for j in range(4)]
def text_rows(msgs, tpl, fmt, prefix=None):
    rows = []
    for m in msgs:
        for j in range(4):
            e = Emb[tok.encode(fmt.format(**dict(zip(FIELDS, m))) + tpl.format(field=FIELDS[j]), add_special_tokens=False)]
            rows.append(e if prefix is None else torch.cat([prefix, e]))
    return rows
@torch.no_grad()
def per_ex(rowfn, msgs, cand, bs=24):
    out = []
    for b in range(0, len(msgs), bs):
        mb = msgs[b:b+bs]; _, lg = run_h(rowfn(mb)); good = [True] * len(mb)
        for r_ in range(len(mb) * 4):
            i, j = divmod(r_, 4); good[i] &= int(lg[r_][cand[j]].argmax()) == gidx(cand, j, mb[i][j])
        out += good
    return [float(x) for x in out]
def val_score(rowfn_main):
    return sum(per_ex(rowfn_main, VALC, R.CAND)) / len(VALC) + sum(per_ex(rowfn_main, VAL_TR, CANDV)) / len(VAL_TR)
def train(params, rowfn, teach, seed, tag, log, steps=None, lr=5e-4, every=None, set_mode=lambda t: None):
    """Same loss/schedule/selection as 018 distill2: CE + KL(teacher) + 0.5(1-cos) ; OneCycle; select on validation only."""
    steps = steps or STEPS; every = every or EVERY
    g = random.Random(seed + 2100); opt = torch.optim.AdamW(params, lr=lr, weight_decay=1e-3)
    sch = torch.optim.lr_scheduler.OneCycleLR(opt, max_lr=lr, total_steps=steps, pct_start=.05)
    def snap(): return [p.detach().clone() for p in params]
    set_mode(False); v = val_score(lambda mb: rowfn(mb, R.SUF_MAIN)); set_mode(True); best = (v, snap(), 0); log({'run': tag, 'step': 0, 'val': v})
    t0 = time.time()
    for s_ in range(1, steps + 1):
        msgs = sample_msgs(g); tpl = g.choice(R.SUF_TRAIN)
        with torch.no_grad(): ht, lt = R.readout_h(teach, msgs, tpl)
        hs, ls = run_h(rowfn(msgs, tpl)); tg = torch.tensor([t1(m[j])[0] for m in msgs for j in range(4)])
        loss = F.cross_entropy(ls, tg) + F.kl_div(F.log_softmax(ls, -1), F.log_softmax(lt, -1), log_target=True, reduction='batchmean') + .5 * (1 - F.cosine_similarity(hs, ht, -1).mean())
        opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(params, 1.0); opt.step(); sch.step()
        if s_ % every == 0:
            set_mode(False); v = val_score(lambda mb: rowfn(mb, R.SUF_MAIN)); set_mode(True)
            log({'run': tag, 'step': s_, 'val': v, 'loss': float(loss), 's': round(time.time() - t0)})
            if v > best[0]: best = (v, snap(), s_)
    with torch.no_grad():
        for p, b in zip(params, best[1]): p.copy_(b)
    set_mode(False); return best[2], best[0]
def load_teacher(sd):
    t = R.Binder(sd); t.load_state_dict(torch.load(f'{INP}/binder4_1000_s{sd}.pt', map_location=R.DEV)); return t.eval().requires_grad_(False)
def opener(name):
    f = open(f'{OD}/{name}', 'w')
    def log(d): f.write(json.dumps(d) + '\n'); f.flush(); print(json.dumps(d), flush=True)
    return log