Download parser/joint_model.py from Ericu950/Stoicheia-code: direct link, hf CLI and curl.
- Browser
- Download file 4.34 kB
-
https://huggingface.co/Ericu950/Stoicheia-code/resolve/main/parser/joint_model.py
- Command line
-
hf download hf://Ericu950/Stoicheia-code/parser/joint_model.py
-
curl -L -o joint_model.py https://huggingface.co/Ericu950/Stoicheia-code/resolve/main/parser/joint_model.py
4.34 kB
| """JointModel: one fine-tuned Stoicheia backbone -> word vectors -> ALL CoNLL-U columns. | |
| Shares the tagger's pooled + scalar-mixed word representation between: | |
| * the tagger heads (factored XPOS / lemma edit-script / UPOS / flat-tag) — row-level, and | |
| * a Dozat-Manning biaffine head (HEAD + DEPREL) — regrouped per sentence. | |
| The tagger packs several sentences per row (block-diagonal attention) and never splits a | |
| sentence across rows, so each word slot's (sentence_index, token_index) in `batch['slots']` | |
| lets us gather a sentence's word vectors back into a contiguous (n_sent, max_w, D) tensor for | |
| the arc/label scorer. Word order within a sentence is token order over *encodable* tokens — | |
| exactly parser.model.build_gold's indexing, so gold heads/labels line up slot-for-slot. | |
| """ | |
| from __future__ import annotations | |
| import torch | |
| import torch.nn as nn | |
| from tagger.model import TaggerModel, pool_words | |
| from parser.biaffine import BiaffineHead | |
| class JointModel(nn.Module): | |
| def __init__(self, encoder, vocab, tcfg, pcfg, W=384): | |
| super().__init__() | |
| self.tagger = TaggerModel(encoder, vocab, tcfg, W=W) # heads + scalar mix + LM-head freeze | |
| self.biaffine = BiaffineHead(encoder.cfg.d_model, pcfg) | |
| self.W = W | |
| # ---- shared word representation (mirrors TaggerModel.forward's pooling) -------------- | |
| def _word_vectors(self, batch): | |
| enc = self.tagger.encoder | |
| out = enc(batch) | |
| if self.tagger.tcfg.scalar_mix: | |
| pooled = torch.stack( | |
| [pool_words(h, batch["word_id"], self.W, self.tagger.tcfg.pool) | |
| for h in [*out["layers"], out["hidden"]]]) # (L+1,B,W,D) | |
| mix = torch.softmax(self.tagger.mix_w, 0) | |
| w = torch.einsum("l,lbwd->bwd", mix.to(pooled.dtype), pooled) | |
| else: | |
| w = pool_words(out["hidden"], batch["word_id"], self.W, self.tagger.tcfg.pool) | |
| return w | |
| def _tag_heads(self, w): | |
| tg = self.tagger | |
| wt = tg.dropout(w) | |
| r = dict(xpos=[hd(wt) for hd in tg.xpos_heads], | |
| script=tg.head_script(wt), | |
| upos=tg.head_upos(wt)) | |
| if tg.head_flat is not None: | |
| r["flat"] = tg.head_flat(wt) | |
| return r | |
| def _regroup(w, slots): | |
| """(B,W,D) row-packed word vectors -> (n_sent, max_w, D) per-sentence, using slots. | |
| Returns (w_sent, word_mask, sent_ids) where sent_ids[local] is the global sentence | |
| index and word position = running per-sentence counter (== build_gold order).""" | |
| B, W, D = w.shape | |
| sent_ids, sid_to_local, pos_counter = [], {}, {} | |
| b_idx, k_idx, l_idx, p_idx = [], [], [], [] | |
| for b, rs in enumerate(slots): | |
| for k, (si, _ti) in enumerate(rs): | |
| if si not in sid_to_local: | |
| sid_to_local[si] = len(sent_ids); sent_ids.append(si) | |
| local = sid_to_local[si] | |
| pos = pos_counter.get(local, 0); pos_counter[local] = pos + 1 | |
| b_idx.append(b); k_idx.append(k); l_idx.append(local); p_idx.append(pos) | |
| n_sent = len(sent_ids) | |
| max_w = max(pos_counter.values(), default=0) | |
| w_sent = w.new_zeros(n_sent, max_w, D) | |
| mask = torch.zeros(n_sent, max_w, dtype=torch.bool, device=w.device) | |
| if b_idx: | |
| dev = w.device | |
| bs = torch.tensor(b_idx, device=dev); ks = torch.tensor(k_idx, device=dev) | |
| ls = torch.tensor(l_idx, device=dev); ps = torch.tensor(p_idx, device=dev) | |
| w_sent[ls, ps] = w[bs, ks] # differentiable advanced-index gather | |
| mask[ls, ps] = True | |
| return w_sent, mask, sent_ids | |
| def forward(self, batch): | |
| """-> (tag_out, arc_scores, rel_scores, word_mask, sent_ids). Loss/gold in the trainer | |
| (it holds the Sentence objects); all trainable submodules run inside this one forward | |
| so DDP sees the whole graph each step.""" | |
| w = self._word_vectors(batch) | |
| tag_out = self._tag_heads(w) | |
| w_sent, word_mask, sent_ids = self._regroup(w, batch["slots"]) | |
| if w_sent.shape[1] == 0: | |
| return tag_out, None, None, word_mask, sent_ids | |
| arc_scores, rel_scores = self.biaffine(w_sent, word_mask) | |
| return tag_out, arc_scores, rel_scores, word_mask, sent_ids | |