OverviewFilesCommitsIssuesDocsReleasesPeers

atomspace-layer-discovery / experiments / level2 / lv.py

103 lines · python ref:
#!/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])