#!/usr/bin/env python3
# PREREG-LV.md: Linas's clusterer (ladder/port/cluster.c) as layer 1.
# lv.py agg <sections-tr.tsv> <out.tsv> Sections summed over tokens, cluster.c format
# lv.py label <sections-tr.tsv> <sections-ho.tsv> <state.tsv> <out-prefix>
# label writes <out-prefix>-{tr,ho}.lab (one int per token) and prints one Q row of held-out measures:
# Q <vmeasure> <m1> <nlabels> <class-share> <sense> <sense-coll>
import collections, itertools, math, sys
def read_sections (path):
toks = [] # (token index, germ, gold class, [(word, dir)])
for line in open(path):
idx, g, gold, cs = line.rstrip('\n').split('\t')
conns = []
for part in cs.split('(Connector (WordNode "')[1:]:
conns.append((part[:part.index('"')], part[part.index('(SexNode "') + 10]))
toks.append((int(idx), g, int(gold), conns))
return toks
def agg (src, out):
c = collections.Counter()
for _, g, _, conns in read_sections(src):
if conns: c[(g, ' '.join(f'(Connector (WordNode "{w}") (SexNode "{d}"))' for w, d in conns))] += 1
with open(out, 'w') as f:
for (g, cs), n in sorted(c.items()): f.write(f'(Section (WordNode "{g}") (ConnectorSeq {cs}))\t0\t0 0 {n}\n')
def vmeasure (lab, gold):
N = len(lab); ct = collections.Counter(zip(lab, gold)); rl = collections.Counter(lab); rg = collections.Counter(gold)
Hc = -sum(v / N * math.log(v / N) for v in rg.values()); Hk = -sum(v / N * math.log(v / N) for v in rl.values())
Hck = -sum(v / N * math.log(v / rl[l]) for (l, g), v in ct.items()); Hkc = -sum(v / N * math.log(v / rg[g]) for (l, g), v in ct.items())
h = 1 - Hck / Hc if Hc > 0 else 1; c = 1 - Hkc / Hk if Hk > 0 else 1
return 2 * h * c / (h + c) if h + c > 0 else 0
def m1_map (lab, gold):
ct = collections.Counter(zip(lab, gold)); best = {}
for (l, g), v in ct.items():
if v > best.get(l, (0, None))[0]: best[l] = (v, g)
return {l: g for l, (v, g) in best.items()}
def label (tr_path, ho_path, state, prefix):
name, is_cls, parents, sect = {}, {}, collections.defaultdict(list), {}
for line in open(state):
p = line.rstrip('\n').split('\t')
if p[0] == 'A': is_cls[int(p[1])] = p[2] == '1'; name[p[3]] = int(p[1]) if p[2] == '0' else name.get(p[3])
elif p[0] == 'M': parents[int(p[1])].append((int(p[2]), float(p[3])))
elif p[0] == 'S':
key = tuple((int(x[:-1]), x[-1]) for x in p[3].split())
sect[(int(p[1]), key)] = float(p[2])
word_id = {n: i for n, i in name.items() if i is not None and not is_cls[i]}
anc_cache = {}
def ancestors (a): # all classes a belongs to, transitively
if a not in anc_cache:
out, todo = [], [a]
while todo:
x = todo.pop()
for c, _ in parents.get(x, []):
if c not in out: out.append(c); todo.append(c)
anc_cache[a] = out
return anc_cache[a]
extra = {}
def unseen (w): # held-out word never seen by the clusterer: its own new label
if w not in extra: extra[w] = len(is_cls) + len(extra)
return extra[w]
def assign (g, conns):
a = word_id.get(g)
if a is None: return unseen(g), False
rows = ancestors(a)
if rows and conns:
opts = [[word_id.get(w, -1)] + ancestors(word_id[w]) if w in word_id else [-1] for w, _ in conns]
best, bn = None, 0
for combo in itertools.islice(itertools.product(*opts), 4096):
key = tuple(zip(combo, (d for _, d in conns)))
for r in rows:
n = sect.get((r, key), 0)
if n > bn: best, bn = r, n
if best is not None: return best, True
if parents.get(a): return max(parents[a], key=lambda x: x[1])[0], True
return a, False
out = {}
for split, path in (('tr', tr_path), ('ho', ho_path)):
toks = sorted(read_sections(path))
labs = [assign(g, conns) for _, g, _, conns in toks]
with open(f'{prefix}-{split}.lab', 'w') as f: f.write('\n'.join(str(l) for l, _ in labs) + '\n')
out[split] = (toks, labs)
tr_toks, _ = out['tr']
senses = collections.defaultdict(set)
for _, g, gold, _ in tr_toks: senses[g].add(gold)
toks, labs = out['ho']
lab = [l for l, _ in labs]; gold = [t[2] for t in toks]; words = [t[1] for t in toks]
mp = m1_map(lab, gold)
m1 = sum(mp[l] == g for l, g in zip(lab, gold)) / len(lab)
coll_of = {}
for (w, l), _ in collections.Counter(zip(words, lab)).most_common():
coll_of.setdefault(w, l)
coll = [coll_of[w] for w in words]
mpc = m1_map(coll, gold)
poly = [len(senses.get(w, ())) >= 2 for w in words]
def sense (ls, m): n = sum(poly); return sum(m[l] == g for l, g, p in zip(ls, gold, poly) if p) / n if n else 0
print('Q\t%.5f\t%.5f\t%d\t%.5f\t%.5f\t%.5f' % (vmeasure(lab, gold), m1, len(set(lab)), sum(c for _, c in labs) / len(labs), sense(lab, mp), sense(coll, mpc)))
if __name__ == '__main__':
if sys.argv[1] == 'agg': agg(sys.argv[2], sys.argv[3])
else: label(*sys.argv[2:6])