| """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 |
|
|
| |
| 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 |
|
|
| |
| cyc_set = set(cyc) |
| others = [v for v in range(n) if v not in cyc_set] |
| cnode = len(others) |
| remap = {v: i for i, v in enumerate(others)} |
| m = len(others) + 1 |
| S2 = np.full((m, m), -np.inf) |
| |
| in_w = {v: S[heads[v], v] for v in cyc} |
|
|
| |
| for h in others: |
| for d in others: |
| if h != d: |
| S2[remap[h], remap[d]] = S[h, d] |
| |
| back = {} |
| 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 |
| |
| 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) |
|
|
| |
| res = np.full(n, -1, dtype=np.int64) |
| for d in others: |
| if d == 0: |
| continue |
| h_sub = sub[remap[d]] |
| if h_sub == cnode: |
| res[d] = fwd[d] |
| else: |
| res[d] = others[h_sub] |
| |
| h_sub = sub[cnode] |
| real_h = others[h_sub] |
| enter = back[real_h] |
| res[enter] = real_h |
| for v in cyc: |
| if v != enter: |
| res[v] = heads[v] |
| 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 |
| 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: |
| 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 |
| |
| S = np.full((L + 1, L + 1), -1e9) |
| block = sc[b, :L, :L + 1] |
| S[:, 1:] = block.T |
| heads = chu_liu_edmonds(S) |
| out[b, :L] = torch.from_numpy(heads[1:]) |
| 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 = 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 |
|
|