Download parser/model.py from Ericu950/Stoicheia-code: direct link, hf CLI and curl.
- Browser
- Download file 6.26 kB
-
https://huggingface.co/Ericu950/Stoicheia-code/resolve/main/parser/model.py
- Command line
-
hf download hf://Ericu950/Stoicheia-code/parser/model.py
-
curl -L -o model.py https://huggingface.co/Ericu950/Stoicheia-code/resolve/main/parser/model.py
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 | |