Stoicheia-code / parser /model.py
anonymous-stoicheia's picture
Upload folder using huggingface_hub
7ed86c3 verified
Raw
History Blame Contribute Delete
6.26 kB
"""Arm wrapper: frozen Stoicheia backbone + a learned scalar mix, producing per-sentence
word vectors for the biaffine head. Gold head/deprel alignment lives here too (tokens with no
Greek letters are skipped, exactly like the tagger pipeline skips them).
NOTE: this release drops the LemmaArm (a second arm over a LemmaDiff-grc encoder, fused or
compared against the char arm) and the "lemma"/"fused" SyntaxModel paths that depended on it —
LemmaDiff-grc is a separate, unpublished side-repo and out of scope here. The published joint
model (parser/joint_model.py, trained via parser/joint_train.py) supersedes those ablations
anyway: it beats both the char-only and lemma-only/fused specialists on test LAS. Only the char
arm below, and the "char"-only path in SyntaxModel/train.py/evaluate.py, remain."""
from __future__ import annotations
import torch
import torch.nn as nn
from tagger.dataset import encode_word
from parser.biaffine import ScalarMix, pool_words
from parser.pack import pack_char_rows, batch_char
def encodable_positions(sent):
return [i for i, t in enumerate(sent.tokens) if encode_word(t.form) is not None]
def build_gold(sent, deprel_vocab):
"""-> (n, gold_head[n] (0=root, else 1..n), gold_label[n]) for the encodable tokens."""
enc_pos = encodable_positions(sent)
new_index = {p: i for i, p in enumerate(enc_pos)}
n = len(enc_pos)
heads = [-100] * n
labels = [-100] * n
for i, p in enumerate(enc_pos):
t = sent.tokens[p]
if not t.head.isdigit():
continue
h = int(t.head)
if h == 0:
heads[i] = 0
else:
hp = h - 1
if hp in new_index:
heads[i] = new_index[hp] + 1
else:
continue # gold head was a skipped (non-Greek) token: exclude
labels[i] = deprel_vocab.id(t.deprel)
return n, heads, labels
def gold_tensors(sents, deprel_vocab, device):
infos = [build_gold(s, deprel_vocab) for s in sents]
maxW = max((n for n, _, _ in infos), default=0)
B = len(sents)
heads = torch.full((B, maxW), -100, dtype=torch.long)
labels = torch.full((B, maxW), -100, dtype=torch.long)
mask = torch.zeros((B, maxW), dtype=torch.bool)
for b, (n, h, l) in enumerate(infos):
if n == 0:
continue
heads[b, :n] = torch.tensor(h)
labels[b, :n] = torch.tensor(l)
mask[b, :n] = True
return heads.to(device), labels.to(device), mask.to(device), maxW
def _scatter(pooled, slots, pairs, n_sents, max_words):
"""Place each word vector at its COMPACTED encodable index (0,1,2,… in word order),
matching build_gold's indexing. NOTE: do NOT use the raw token index `ti` here — the
char arm's `ti` is the position in sent.tokens (with skipped non-Greek tokens leaving
gaps), which shifts every vector off its gold head. A per-sentence running counter over
the words in emitted order is the encodable index for both arms (a no-op for the lemma
arm, whose `ti` is already compacted)."""
D = pooled.shape[-1]
out = pooled.new_zeros(n_sents, max_words, D)
counter = {}
for r, rs in enumerate(slots):
for w, (si, ti) in enumerate(rs):
orig = pairs[si][0]
idx = counter.get(orig, 0)
counter[orig] = idx + 1
if idx < max_words:
out[orig, idx] = pooled[r, w]
return out
class CharArm(nn.Module):
def __init__(self, char_model, n_layers, finetune=False):
super().__init__()
self.model = char_model # CharBertWithHidden
self.finetune = finetune
if not finetune: # frozen probe: no backbone grads
for p in self.model.parameters():
p.requires_grad_(False)
self.model.return_layers = True
self.mix = ScalarMix(n_layers)
def out_dim(self):
return self.model.cfg.d_model
def _encode_rows(self, rows, T, W, device):
b, slots = batch_char(rows, T, W, device)
ctx = torch.enable_grad() if self.finetune else torch.no_grad()
with ctx:
out = self.model(b)
return out, b, slots
def forward(self, sents, T, W, device, micro=8, max_words=None):
rows, pairs, _ = pack_char_rows(sents, T, W)
pooled_all, slots_all, rowbase = [], [], 0
for r0 in range(0, len(rows), micro):
chunk = rows[r0:r0 + micro]
out, b, slots = self._encode_rows(chunk, T, W, device)
layers = out["layers"] + [out["hidden"]] # 32 blocks + final norm
mixed = self.mix(layers) # grad flows (mix + backbone if finetune)
pooled = pool_words(mixed, b["word_id"], W, "mean")
pooled_all.append(pooled)
slots_all.extend(slots)
pooled = torch.cat(pooled_all, 0) if pooled_all else \
torch.zeros(0, W, self.out_dim(), device=device)
mw = max_words if max_words is not None else W
return _scatter(pooled, slots_all, pairs, len(sents), mw)
# LemmaArm class removed for this release (depended on the unpublished LemmaDiff-grc
# encoder + ldf.model.lemma_diff — see module docstring above).
class SyntaxModel(nn.Module):
"""Only arm="char" is supported in this release (see module docstring)."""
def __init__(self, arm, char_arm, lemma_arm, head):
super().__init__()
assert arm == "char", 'only arm="char" is supported in this release (lemma/fused dropped)'
self.arm = arm
self.char_arm = char_arm
self.lemma_arm = lemma_arm # always None in this release; kept for state_dict shape parity
self.head = head
def word_vectors(self, sents, T, W, device, max_words):
return self.char_arm(sents, T, W, device, max_words=max_words)
def forward(self, sents, deprel_vocab, T, W, device):
heads, labels, mask, maxW = gold_tensors(sents, deprel_vocab, device)
if maxW == 0:
return None, None, None, None, mask
w = self.word_vectors(sents, T, W, device, maxW)
arc_scores, rel_scores = self.head(w, mask)
return arc_scores, rel_scores, heads, labels, mask