Stoicheia-code / parser /joint_model.py
anonymous-stoicheia's picture
Upload folder using huggingface_hub
7ed86c3 verified
Raw
History Blame Contribute Delete
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
@staticmethod
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