Download parser/mst.py from Ericu950/Stoicheia-code: direct link, hf CLI and curl.
- Browser
- Download file 5.64 kB
-
https://huggingface.co/Ericu950/Stoicheia-code/resolve/main/parser/mst.py
- Command line
-
hf download hf://Ericu950/Stoicheia-code/parser/mst.py
-
curl -L -o mst.py https://huggingface.co/Ericu950/Stoicheia-code/resolve/main/parser/mst.py
5.64 kB
| """Chu-Liu-Edmonds maximum-spanning-arborescence decoding for non-projective dependency trees. | |
| The biaffine head's greedy per-token argmax can produce non-trees (cycles, multiple roots). | |
| CLE returns the maximum-scoring valid tree rooted at the pseudo-root, which typically lifts | |
| LAS by a few tenths of a point over greedy on non-projective treebanks like AGDT. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| def chu_liu_edmonds(score): | |
| """score: (n, n) float, score[h, d] = weight of arc h->d; node 0 is the root (no incoming | |
| arc). Returns heads: int array length n with heads[d] the selected head of d, heads[0] = -1. | |
| Maximization. O(n^3) with cycle contraction — fine for sentence-length n.""" | |
| n = score.shape[0] | |
| S = score.astype(np.float64).copy() | |
| np.fill_diagonal(S, -np.inf) | |
| S[:, 0] = -np.inf # root has no incoming arc | |
| # greedily pick the best head for each non-root node | |
| heads = np.full(n, -1, dtype=np.int64) | |
| for d in range(1, n): | |
| heads[d] = int(np.argmax(S[:, d])) | |
| cyc = _find_cycle(heads, n) | |
| if cyc is None: | |
| return heads | |
| # contract the cycle into a single node and recurse | |
| cyc_set = set(cyc) | |
| others = [v for v in range(n) if v not in cyc_set] | |
| cnode = len(others) # index of the contracted node in the subproblem | |
| remap = {v: i for i, v in enumerate(others)} | |
| m = len(others) + 1 | |
| S2 = np.full((m, m), -np.inf) | |
| # cycle-internal arc weight into each cycle node (for the "swap" bookkeeping) | |
| in_w = {v: S[heads[v], v] for v in cyc} | |
| # arcs among non-cycle nodes: copy through | |
| for h in others: | |
| for d in others: | |
| if h != d: | |
| S2[remap[h], remap[d]] = S[h, d] | |
| # arcs entering the cycle: best over cycle targets, discounted by the broken internal arc | |
| back = {} # (real_head) -> (real cycle node it best enters) | |
| for h in others: | |
| best, bestv = -np.inf, None | |
| for v in cyc: | |
| w = S[h, v] - in_w[v] | |
| if w > best: | |
| best, bestv = w, v | |
| S2[remap[h], cnode] = best | |
| back[h] = bestv | |
| # arcs leaving the cycle: best over cycle sources (root can't be a dependent) | |
| fwd = {} | |
| for d in others: | |
| if d == 0: | |
| continue | |
| best, bestv = -np.inf, None | |
| for v in cyc: | |
| if S[v, d] > best: | |
| best, bestv = S[v, d], v | |
| S2[cnode, remap[d]] = best | |
| fwd[d] = bestv | |
| sub = chu_liu_edmonds(S2) | |
| # expand the contracted solution back to the original nodes | |
| res = np.full(n, -1, dtype=np.int64) | |
| for d in others: | |
| if d == 0: | |
| continue # root keeps head -1 | |
| h_sub = sub[remap[d]] | |
| if h_sub == cnode: | |
| res[d] = fwd[d] # arc came out of the cycle | |
| else: | |
| res[d] = others[h_sub] | |
| # the contracted node's incoming arc: keep the whole cycle except the one broken edge | |
| h_sub = sub[cnode] | |
| real_h = others[h_sub] | |
| enter = back[real_h] # cycle node that the external arc enters | |
| res[enter] = real_h | |
| for v in cyc: | |
| if v != enter: | |
| res[v] = heads[v] # keep original internal cycle arc | |
| return res | |
| def _find_cycle(heads, n): | |
| """Return one cycle (list of nodes) in the head-pointer graph, or None if it's a forest.""" | |
| state = [0] * n # 0=unvisited,1=on-stack,2=done | |
| for start in range(1, n): | |
| if state[start] != 0: | |
| continue | |
| path, v = [], start | |
| while v > 0 and state[v] == 0: | |
| state[v] = 1; path.append(v); v = heads[v] | |
| if v > 0 and state[v] == 1: # hit a node on the current stack -> cycle | |
| cyc = path[path.index(v):] | |
| for u in path: | |
| state[u] = 2 | |
| return cyc | |
| for u in path: | |
| state[u] = 2 | |
| return None | |
| def decode_mst(arc_scores, mask): | |
| """arc_scores: (B, W, W+1) [col 0 = root]. mask: (B, W) bool. Returns heads (B, W) long | |
| tensor on cpu with values in [0..W] (0 = root), padded positions = 0 — matching the greedy | |
| decode's output convention so downstream LAS counting is identical.""" | |
| import torch | |
| B, W, _ = arc_scores.shape | |
| out = torch.zeros(B, W, dtype=torch.long) | |
| sc = arc_scores.detach().float().cpu().numpy() | |
| lengths = mask.sum(1).cpu().numpy() | |
| for b in range(B): | |
| L = int(lengths[b]) | |
| if L == 0: | |
| continue | |
| # build (L+1)x(L+1): node 0 = root, nodes 1..L = words; score[h, d] | |
| S = np.full((L + 1, L + 1), -1e9) | |
| block = sc[b, :L, :L + 1] # (L, L+1): [dep, head_col] | |
| S[:, 1:] = block.T # score[h, d] = arc_scores[dep=d-1, head=h] | |
| heads = chu_liu_edmonds(S) # heads[d] in [0..L] | |
| out[b, :L] = torch.from_numpy(heads[1:]) # word d (1..L) -> column index (0=root) | |
| return out | |
| def mst_heads_labels(arc_scores, rel_scores, mask): | |
| """MST-decoded heads + the label argmax at each chosen head. Mirrors BiaffineHead.decode's | |
| (heads_cpu, labels_cpu) return so LAS/UAS counting is unchanged.""" | |
| import torch | |
| pred_heads = decode_mst(arc_scores, mask) # (B,W) cpu, 0=root | |
| B, W = pred_heads.shape | |
| dev = rel_scores.device | |
| bi = torch.arange(B, device=dev)[:, None].expand(B, W) | |
| wi = torch.arange(W, device=dev)[None, :].expand(B, W) | |
| pred_labels = rel_scores[bi, wi, pred_heads.to(dev)].argmax(-1).cpu() | |
| return pred_heads, pred_labels | |