Horizon-Labs's picture
v1.0: multilingual punctuation + truecasing (base)
b11a6e7 verified
Raw History Blame Contribute Delete
5.3 kB
"""Comma -> capital boundary errors of a joint truecase model, and a boundary-consistent decoding.
The mark of word i and the case of word i+1 are separate labels, so the model can write "hello, How are you". Where the argmax gives
(',' after word i, Cap on word i+1), compare three options with the marginal probabilities: keep (',' , Cap) * beta, ('.'/'?' , Cap),
(',' , lower). beta = 1 is the plain argmax. beta is chosen on held-out VALIDATION windows (val.parquet rows after the 6,000 used for
checkpoint selection), then the test sets are scored with every beta for the record.
python truecase/boundary_check.py OUT.json DATA_DIR EVAL_DIR MODEL [MODEL ...]"""
import json, os, sys
import numpy as np, pandas as pd, torch
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))); sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "punct"))
from common import LABELS as PL, NOSPACE, lower_keep_len
from common_tc import unit_cases
from tc_metrics import chunks, score
from transformers import AutoModelForTokenClassification, AutoTokenizer
out_f, data, ev = sys.argv[1:4]; models = sys.argv[4:]
BETAS = [1.0, 0.8, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]
COMMA, PER, Q = PL.index(","), PL.index("."), PL.index("?")
va = pd.read_parquet(f"{data}/val.parquet").sample(frac=1.0, random_state=0).iloc[6000:]
val_items = [dict(set="val", lang=l, text=t, ends=e.tolist(), gp=[PL[x] for x in lb], gc=unit_cases(t, e.tolist(), l in NOSPACE), nos=l in NOSPACE)
for t, e, lb, l in zip(va.text, va.ends, va.labs, va.lang)]
test_items = [it for it in chunks(ev) if it["set"] in ("europarl", "ted", "flores")]
print("val", len(val_items), "test", len(test_items), flush=True)
def probs(tok, model, items):
texts = [lower_keep_len(it["text"]) for it in items]; order = np.argsort([len(t) for t in texts]); res = [None] * len(items)
for s in range(0, len(order), 64):
idx = order[s:s + 64]
enc = tok([texts[i] for i in idx], truncation=True, max_length=512, padding=True, return_offsets_mapping=True, return_tensors="pt")
offs = enc.pop("offset_mapping").tolist()
with torch.no_grad():
p = torch.softmax(model(**{k: v.cuda() for k, v in enc.items()}).logits.float(), -1).cpu().numpy()
for j, i in enumerate(idx):
at = {e: k for k, (b, e) in enumerate(offs[j]) if e > b}
P = np.zeros((len(items[i]["ends"]), 18), np.float32); P[:, 0] = 1.0
for u, e in enumerate(items[i]["ends"]):
if e in at:
P[u] = p[j, at[e]]
res[i] = P
return res
def decode(P, beta):
j = P.argmax(1); pm, pc = (j // 3).tolist(), (j % 3).tolist()
if beta < 1:
M, C = P.reshape(-1, 6, 3).sum(2), P.reshape(-1, 6, 3).sum(1)
for i in range(len(pm) - 1):
if pm[i] == COMMA and pc[i + 1] == 1:
b = PER if M[i, PER] >= M[i, Q] else Q
z, x, y = M[i, COMMA] * C[i + 1, 1] * beta, M[i, b] * C[i + 1, 1], M[i, COMMA] * C[i + 1, 0]
if x >= z and x >= y:
pm[i] = b
elif y > z:
pc[i + 1] = 0
return [PL[m] for m in pm], pc
def events(items, PP):
"""raw argmax (',' , Cap) boundaries by gold: sentence boundary, comma + capital (name/I), comma/none + lower (wrong capital)."""
n = dict(total=0, gold_sentence_break=0, gold_comma_cap=0, gold_lower=0, other=0, words=0)
for it, P in zip(items, PP):
pm, pc = decode(P, 1.0); n["words"] += len(pm)
for i in range(len(pm) - 1):
if pm[i] == "," and pc[i + 1] == 1:
n["total"] += 1; g, gc = it["gp"][i], it["gc"][i + 1]
k = "gold_sentence_break" if g in (".", "?") and gc == 1 else "gold_comma_cap" if gc == 1 else "gold_lower" if gc == 0 else "other"
n[k] += 1
return n
res = json.load(open(out_f)) if os.path.exists(out_f) else {}
for m in models:
tok = AutoTokenizer.from_pretrained(m); model = AutoModelForTokenClassification.from_pretrained(m, dtype=torch.bfloat16).cuda().eval()
PV, PT = probs(tok, model, val_items), probs(tok, model, test_items)
r = dict(events_val=events(val_items, PV), events_test={s: events([it for it in test_items if it["set"] == s], [P for it, P in zip(test_items, PT) if it["set"] == s])
for s in ("europarl", "ted", "flores")}, val={}, test={})
for b in BETAS:
dv = [decode(P, b) for P in PV]; dt = [decode(P, b) for P in PT]
r["val"][b] = score(val_items, [d[0] for d in dv], [d[1] for d in dv])["summary"]
r["test"][b] = score(test_items, [d[0] for d in dt], [d[1] for d in dt])["summary"]
v = r["val"][b]["val"]; print(m[-50:], "beta", b, "val", round(v["punct_f1"], 4), round(v["case_f1"], 4),
{s: (round(x["punct_f1"], 4), round(x["case_f1"], 4)) for s, x in r["test"][b].items()}, flush=True)
best = max(BETAS, key=lambda b: (r["val"][b]["val"]["punct_f1"] + r["val"][b]["val"]["case_f1"]) / 2); r["best_beta_val"] = best
print(m[-50:], "events", r["events_val"], r["events_test"], "best beta (val)", best, flush=True)
res[m] = r; json.dump(res, open(out_f, "w"), indent=1)