sphragis-leaderboard / scripts /publish_atomic_results.py
Urdatorn's picture
Land the post-remake refresh pieces held back by the quota
a9c1cfe
Raw
History Blame Contribute Delete
40.1 kB
#!/usr/bin/env python3
"""Validate rebuilt atomic-model predictions and generate leaderboard data."""
from __future__ import annotations
import argparse
import json
from collections import Counter, defaultdict
from pathlib import Path
import pyarrow.parquet as pq
MAIN_TASKS = ("sentence_1", "sentence_5", "sentence_10", "sentence_50")
SOURCE_TASKS = (
"source_held_out_1",
"source_held_out_5",
"source_held_out_10",
"source_held_out_50",
)
VERSE_TASKS = ("verse_1", "verse_5", "verse_10", "verse_50")
def read_json(path: Path) -> dict:
return json.loads(path.read_text())
def read_predictions(path: Path) -> list[dict[str, str]]:
rows = [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
if not rows:
raise ValueError(f"empty predictions: {path}")
return rows
def macro_f1(gold: list[str], predicted: list[str]) -> float:
scores = []
for label in sorted(set(gold)):
tp = sum(g == label and p == label for g, p in zip(gold, predicted))
fp = sum(g != label and p == label for g, p in zip(gold, predicted))
fn = sum(g == label and p != label for g, p in zip(gold, predicted))
denominator = 2 * tp + fp + fn
scores.append(2 * tp / denominator if denominator else 0.0)
return sum(scores) / len(scores)
def official_test(dataset_root: Path, task: str) -> dict[str, str]:
files = sorted((dataset_root / task).glob("test-*.parquet"))
if not files:
raise FileNotFoundError(f"missing official test parquet for {task}")
rows = pq.read_table(files, columns=["id", "author"]).to_pylist()
return {str(row["id"]): str(row["author"]) for row in rows}
def diagnostics(
predictions_path: Path,
dataset_root: Path,
task: str,
expected_macro_f1: float,
) -> tuple[float, dict[str, float], dict[str, dict[str, int]]]:
predictions = read_predictions(predictions_path)
official = official_test(dataset_root, task)
ids = [str(row["id"]) for row in predictions]
if len(ids) != len(set(ids)):
raise ValueError(f"duplicate prediction IDs for {task}: {predictions_path}")
if set(ids) != set(official):
missing = len(set(official) - set(ids))
extra = len(set(ids) - set(official))
raise ValueError(f"prediction ID mismatch for {task}: missing={missing}, extra={extra}")
for row in predictions:
if str(row["gold"]) != official[str(row["id"])]:
raise ValueError(f"gold-label mismatch for {task}, row {row['id']}")
gold = [str(row["gold"]) for row in predictions]
predicted = [str(row["prediction"]) for row in predictions]
score = macro_f1(gold, predicted)
if abs(score - expected_macro_f1) > 1e-10:
raise ValueError(
f"macro-F1 mismatch for {task}: recomputed={score}, saved={expected_macro_f1}"
)
correct: Counter[str] = Counter()
support: Counter[str] = Counter(gold)
confusion: defaultdict[str, Counter[str]] = defaultdict(Counter)
for actual, prediction in zip(gold, predicted):
correct[actual] += actual == prediction
confusion[actual][prediction] += 1
accuracy = {label: 100 * correct[label] / support[label] for label in sorted(support)}
sparse_confusion = {
label: dict(sorted(confusion[label].items())) for label in sorted(confusion)
}
return 100 * score, accuracy, sparse_confusion
def merge_author_accuracy(
target: dict[str, dict[str, float]], task: str, values: dict[str, float]
) -> None:
for author, value in values.items():
target.setdefault(author, {})[task] = value
def ts(value: object) -> str:
return json.dumps(value, ensure_ascii=False, indent=2)
def feature_count(metrics: dict) -> int:
return int(metrics["feature_metadata"]["train_shape"][1])
def build_row(
*,
model: str,
url: str,
dimensionality: str,
classifier: str,
status: str,
task_specs: list[tuple[str, Path, Path, float]],
dataset_roots: dict[str, Path],
) -> dict:
scores: dict[str, float] = {}
author_accuracy: dict[str, dict[str, float]] = {}
confusions: dict[str, dict[str, dict[str, int]]] = {}
for task, predictions_path, _metrics_path, saved_macro_f1 in task_specs:
dataset_root = dataset_roots["verse" if task.startswith("verse_") else "sphragis"]
score, accuracy, confusion = diagnostics(
predictions_path, dataset_root, task, saved_macro_f1
)
scores[task] = score
merge_author_accuracy(author_accuracy, task, accuracy)
confusions[task] = confusion
return {
"model": model,
"url": url,
"dimensionality": dimensionality,
"classifier": classifier,
"status": status,
"scores": scores,
"authorAccuracy": author_accuracy,
"confusions": confusions,
}
def finetune_specs(root: Path, family: str, tasks: tuple[str, ...],
encoder: str = "greberta"):
family_root = root / f"{encoder}-finetune" / family
metrics_path = family_root / "metrics.json"
metrics = read_json(metrics_path)
return [
(task, family_root / task / "predictions.jsonl", metrics_path,
float(metrics["test"][task]["macro_f1"]))
for task in tasks
]
def siamese_specs(root: Path, family: str, tasks: tuple[str, ...]):
"""Specs for the siamese contender, whose finalizer wrote a separate tree.
The training run never reads test; ``siamese-test`` is written by
``sphragis_models.siamese_finalize``, which reloads the validation-selected
checkpoint and rule and evaluates test once. The flag is checked here so a
run that never reached the finalizer cannot reach the leaderboard.
"""
family_root = root / "siamese-test" / family / "supcon_ce"
metrics_path = family_root / "metrics.json"
metrics = read_json(metrics_path)
if metrics.get("test_loaded") is not True:
raise ValueError(f"siamese test not finalized: {metrics_path}")
if metrics.get("selection_split") != "validation":
raise ValueError(f"siamese selection was not made on validation: {metrics_path}")
return [
(task, family_root / task / "predictions.jsonl", metrics_path,
float(metrics["test"][task]["macro_f1"]))
for task in tasks
], str(metrics["selected_rule"]), int(metrics["selected_epoch"])
def mosteller_wallace_specs(root: Path, family: str, model: str, tasks: tuple[str, ...]):
"""Specs for one Mosteller-Wallace count model, plus its selected settings.
The grid job never reads test; ``mosteller-wallace-test`` is written by
``sphragis_models.mosteller_wallace_finalize``, which refits the selected
settings on train plus validation and evaluates test once.
"""
family_root = root / "mosteller-wallace-test" / family / model
metrics_path = family_root / "metrics.json"
metrics = read_json(metrics_path)
if metrics.get("test_loaded") is not True:
raise ValueError(f"Mosteller-Wallace test not finalized: {metrics_path}")
if metrics.get("selection_split") != "validation":
raise ValueError(f"Mosteller-Wallace selection was not made on validation: {metrics_path}")
specs = [
(task, family_root / task / "predictions.jsonl", metrics_path,
float(metrics["test"][task]["macro_f1"]))
for task in tasks
]
chosen = [metrics["selected"][task] for task in tasks]
return specs, chosen
def ordinary_specs(root: Path, base: str, family: str, tasks: tuple[str, ...]):
specs = []
for task in tasks:
task_root = root / base / family / "models" / task
metrics_path = task_root / "metrics.json"
metrics = read_json(metrics_path)
specs.append((task, task_root / "predictions.jsonl", metrics_path,
float(metrics["test_metrics"]["macro_f1"])))
return specs
def burrows_specs(root: Path, family: str, tasks: tuple[str, ...]):
specs = []
for task in tasks:
task_root = root / "burrows-logreg" / family / task
metrics_path = task_root / "metrics.json"
metrics = read_json(metrics_path)
specs.append((task, task_root / "predictions.jsonl", metrics_path,
float(metrics["test_metrics"]["macro_f1"])))
return specs
def syntax_specs(root: Path, family: str, tasks: tuple[str, ...]):
specs, dimensions, counts = [], [], []
for task in tasks:
selection_path = root / "syntax-logreg" / family / "selected" / task / "selection.json"
selection = read_json(selection_path)
if selection.get("test_loaded") is not True:
raise ValueError(f"syntax test not finalized: {selection_path}")
dimension = int(selection["selected"]["max_dimensions"])
task_root = root / "syntax-logreg" / family / f"d{dimension}-all-train" / "models" / task
metrics_path = task_root / "metrics.json"
metrics = read_json(metrics_path)
dimensions.append(dimension)
counts.append(feature_count(metrics))
specs.append((task, task_root / "predictions.jsonl", metrics_path,
float(metrics["test_metrics"]["macro_f1"])))
return specs, dimensions, counts
def pair_specs(root: Path, pair: str, family: str, tasks: tuple[str, ...]):
"""Specs for a double-combination model, one selected scaling per task.
``sphragis_models.pair_select`` chooses each task's block scaling on
validation and only then reads test, so the flag is checked here: a task
whose scaling was never finalized cannot reach the leaderboard.
"""
specs, scalings, counts = [], [], []
for task in tasks:
selection_path = root / "pair-logreg" / pair / family / "selected" / task / "selection.json"
selection = read_json(selection_path)
if selection.get("test_loaded") is not True:
raise ValueError(f"pair test not finalized: {selection_path}")
metrics_path = Path(selection["metrics_path"])
metrics = read_json(metrics_path)
if metrics["feature_metadata"]["representation"] != "arm_pair":
raise ValueError(f"not a pair model: {metrics_path}")
scalings.append(str(selection["selected"]["scaling"]))
counts.append(feature_count(metrics))
specs.append((task, metrics_path.parent / "predictions.jsonl", metrics_path,
float(metrics["test_metrics"]["macro_f1"])))
return specs, scalings, counts
def method_specs(root: Path, family: str, tasks: tuple[str, ...]) -> dict[str, tuple]:
"""Specs for every combination whose method was settled on validation.
``scripts/run_method_search.py`` scores concatenation and five posterior
combiners on validation, keeps the best, and reads test once for it. Each
cell records the winning method and where that method's predictions live:
a pooling win writes its own, a concatenation win points back into the pair
grid. Returns {combination: (specs, methods)} keyed by the arms joined with
a hyphen, in the search's sorted order.
"""
out: dict[str, tuple] = {}
for combo_dir in sorted((root / "combination-method").glob("*")):
family_dir = combo_dir / family
if not family_dir.is_dir():
continue
specs, methods = [], []
for task in tasks:
selection_path = family_dir / task / "selection.json"
if not selection_path.is_file():
continue
selection = read_json(selection_path)
if selection.get("test_loaded") is not True or not selection.get("predictions_path"):
raise ValueError(f"combination not finalized: {selection_path}")
methods.append(str(selection["selected"]["method"]))
predictions = Path(selection["predictions_path"])
if not predictions.is_absolute():
# The search was run from the models repository root with a
# relative output root; resolve against that root.
predictions = root.parent.parent / predictions
specs.append((task, predictions, selection_path,
float(selection["test_metrics"]["macro_f1"])))
if specs:
out[combo_dir.name] = (specs, methods)
return out
def alm_specs(root: Path, family: str, tasks: tuple[str, ...]):
"""Specs for the remade authorial ensemble, or None until it exists.
``scripts/alm_attribution_rows.py`` writes one predictions/metrics pair per
task from the remade models' scores. The directory is absent until the
remake has landed, and a leaderboard build before then simply carries no
ensemble row rather than a stale one.
"""
family_root = root.parent / "alm-remake-20260902" / "leaderboard" / family
if not family_root.is_dir():
return None
specs, models = [], set()
for task in tasks:
metrics_path = family_root / task / "metrics.json"
if not metrics_path.is_file():
return None
metrics = read_json(metrics_path)
models.update(metrics["models"])
specs.append((task, family_root / task / "predictions.jsonl", metrics_path,
float(metrics["test_metrics"]["macro_f1"])))
return specs, len(models)
def dimensions_for_specs(specs: list[tuple[str, Path, Path, float]]) -> list[int]:
return [feature_count(read_json(metrics_path)) for _, _, metrics_path, _ in specs]
def make_rows(results_root: Path, sphragis_data: Path, metre_data: Path):
roots = {"sphragis": sphragis_data, "verse": metre_data}
main_and_source = MAIN_TASKS + SOURCE_TASKS
ft_sphragis = finetune_specs(results_root, "sphragis-main", MAIN_TASKS)
ft_sphragis += finetune_specs(results_root, "sphragis-source-held-out", SOURCE_TASKS)
ft_metre = finetune_specs(results_root, "sphragis-metre", VERSE_TASKS)
kaino_ft_sphragis = finetune_specs(
results_root, "sphragis-main", MAIN_TASKS, encoder="kainobert"
) + finetune_specs(
results_root, "sphragis-source-held-out", SOURCE_TASKS, encoder="kainobert"
)
kaino_ft_metre = finetune_specs(
results_root, "sphragis-metre", VERSE_TASKS, encoder="kainobert"
)
roberta_ft_sphragis = finetune_specs(
results_root, "sphragis-main", MAIN_TASKS, encoder="kainoberta"
) + finetune_specs(
results_root, "sphragis-source-held-out", SOURCE_TASKS, encoder="kainoberta"
)
roberta_ft_metre = finetune_specs(
results_root, "sphragis-metre", VERSE_TASKS, encoder="kainoberta"
)
greberta_sphragis = ordinary_specs(
results_root, "greberta-logreg", "sphragis-main", MAIN_TASKS
) + ordinary_specs(
results_root, "greberta-logreg", "sphragis-source-held-out", SOURCE_TASKS
)
greberta_metre = ordinary_specs(
results_root, "greberta-logreg", "sphragis-metre", VERSE_TASKS
)
kainobert_sphragis = ordinary_specs(
results_root, "kainobert-logreg", "sphragis-main", MAIN_TASKS
) + ordinary_specs(
results_root, "kainobert-logreg", "sphragis-source-held-out", SOURCE_TASKS
)
kainobert_metre = ordinary_specs(
results_root, "kainobert-logreg", "sphragis-metre", VERSE_TASKS
)
tfidf_sphragis = ordinary_specs(
results_root, "tfidf-logreg", "sphragis-main", MAIN_TASKS
) + ordinary_specs(
results_root, "tfidf-logreg", "sphragis-source-held-out", SOURCE_TASKS
)
tfidf_metre = ordinary_specs(
results_root, "tfidf-logreg", "sphragis-metre", VERSE_TASKS
)
burrows_sphragis = burrows_specs(
results_root, "sphragis-main", MAIN_TASKS
) + burrows_specs(results_root, "sphragis-source-held-out", SOURCE_TASKS)
burrows_metre = burrows_specs(results_root, "sphragis-metre", VERSE_TASKS)
syntax_sphragis, syntax_sphragis_d, syntax_sphragis_counts = syntax_specs(
results_root, "sphragis-main", MAIN_TASKS
)
source_specs, source_d, source_counts = syntax_specs(
results_root, "sphragis-source-held-out", SOURCE_TASKS
)
syntax_sphragis += source_specs
syntax_sphragis_d += source_d
syntax_sphragis_counts += source_counts
syntax_metre, syntax_metre_d, syntax_metre_counts = syntax_specs(
results_root, "sphragis-metre", VERSE_TASKS
)
metre_specs = ordinary_specs(results_root, "metre-logreg", "", VERSE_TASKS)
pairs = (
"greberta-alm", "greberta-tfidf", "greberta-char", "greberta-burrows",
"char-alm", "tfidf-alm", "burrows-alm", "char-tfidf",
"burrows-char", "burrows-tfidf", "greberta-syntax", "burrows-syntax",
"syntax-tfidf", "syntax-char", "syntax-alm",
# Cube cells, admitted only where all three constituent pairs were
# positive in the square grid.
"greberta-char-tfidf", "greberta-tfidf-alm", "char-tfidf-alm",
"greberta-burrows-alm", "greberta-burrows-char", "burrows-char-alm",
"greberta-char-alm",
# The same cells with KainoBERT in place of GreBERTa. KainoBERT was
# pretrained on a corpus that provably excludes the test split, so
# each twin says what the combination is worth without the exposure
# GreBERTa's pretraining corpus cannot rule out.
"kainobert-alm", "kainobert-tfidf", "kainobert-char", "kainobert-burrows",
"kainobert-syntax", "kainobert-char-tfidf", "kainobert-tfidf-alm",
"kainobert-burrows-alm", "kainobert-burrows-char", "kainobert-char-alm",
)
alm_main = alm_specs(results_root, "sphragis-main", MAIN_TASKS)
alm_source = alm_specs(results_root, "sphragis-source-held-out", SOURCE_TASKS)
alm_metre = alm_specs(results_root, "sphragis-metre", VERSE_TASKS)
alm_status = (
"One OLMo-1B model further pretrained per author on the current release with "
"validation-loss early stopping; attribution is the lowest mean per-token surprisal; "
"no base-model or epoch search; test evaluated once"
)
alm_rows = []
if alm_main and alm_source:
alm_rows.append(build_row(
model="OLMo-1B authorial language models",
url="https://github.com/Urdatorn/sphragis_models",
dimensionality=f"1.18B parameters per author; {alm_main[1]} and {alm_source[1]} models",
classifier="Lowest mean per-token surprisal", status=alm_status,
task_specs=alm_main[0] + alm_source[0], dataset_roots=roots))
alm_metre_rows = []
if alm_metre:
alm_metre_rows.append(build_row(
model="OLMo-1B authorial language models",
url="https://github.com/Urdatorn/sphragis_models",
dimensionality=f"1.18B parameters per author; {alm_metre[1]} models",
classifier="Lowest mean per-token surprisal", status=alm_status,
task_specs=alm_metre[0], dataset_roots=roots))
method_main = method_specs(results_root, "sphragis-main", MAIN_TASKS)
method_source = method_specs(results_root, "sphragis-source-held-out", SOURCE_TASKS)
method_sphragis: dict[str, tuple] = {}
for combo in sorted(set(method_main) | set(method_source)):
a = method_main.get(combo, ([], [])); b = method_source.get(combo, ([], []))
method_sphragis[combo] = (a[0] + b[0], a[1] + b[1])
method_metre = method_specs(results_root, "sphragis-metre", VERSE_TASKS)
settled = {frozenset(c.split("-")) for c in list(method_sphragis) + list(method_metre)}
# A concatenation-only row stays only where the method search never ran,
# which today means the KainoBERT twins.
pairs = tuple(p for p in pairs if frozenset(p.split("-")) not in settled)
pair_data: dict[str, tuple] = {}
for pair in pairs:
main = pair_specs(results_root, pair, "sphragis-main", MAIN_TASKS)
source = pair_specs(results_root, pair, "sphragis-source-held-out", SOURCE_TASKS)
pair_data[pair] = tuple(a + b for a, b in zip(main, source))
metre_pairs = (
"greberta-tfidf", "greberta-char", "greberta-burrows", "greberta-metre",
"char-tfidf", "burrows-char", "burrows-tfidf", "burrows-metre",
"char-metre", "tfidf-metre", "greberta-alm", "char-alm", "tfidf-alm",
"burrows-alm", "metre-alm",
"greberta-syntax", "syntax-char", "syntax-metre", "syntax-alm",
"burrows-greberta-metre", "burrows-metre-tfidf", "greberta-metre-tfidf",
"alm-greberta-tfidf", "char-greberta-tfidf", "burrows-char-metre",
"alm-char-tfidf", "burrows-greberta-tfidf", "alm-char-greberta",
"char-metre-tfidf", "burrows-char-greberta", "char-greberta-metre",
"burrows-char-tfidf",
)
metre_pair_data: dict[str, tuple] = {}
metre_pairs = tuple(p for p in metre_pairs if frozenset(p.split("-")) not in settled)
for pair in metre_pairs:
try:
metre_pair_data[pair] = pair_specs(results_root, pair, "sphragis-metre", VERSE_TASKS)
except (FileNotFoundError, KeyError, ValueError):
continue
mw_specs: dict[tuple[str, str], tuple] = {}
for model in ("dirichlet_multinomial", "multinomial_nb"):
mw_specs[(model, "sphragis")] = tuple(
a + b
for a, b in zip(
mosteller_wallace_specs(results_root, "sphragis-main", model, MAIN_TASKS),
mosteller_wallace_specs(
results_root, "sphragis-source-held-out", model, SOURCE_TASKS
),
)
)
mw_specs[(model, "verse")] = mosteller_wallace_specs(
results_root, "sphragis-metre", model, VERSE_TASKS
)
ARM_NAMES = {
"greberta": "GreBERTa",
"kainobert": "KainoBERT",
"tfidf": "lemma TF-IDF",
"char": "Char n-gram",
"syntax": "syntax rates",
"burrows": "Burrows z-scores",
"alm": "ALM",
"metre": "metrical line features",
"greberta_e2e": "GreBERTa (end-to-end)",
"siamese": "Siamese GreBERTa",
}
METHOD_NAMES = {
"concatenation": "concatenation", "vote_hard": "hard vote", "vote_soft": "soft vote",
"logit_mean": "geometric mean", "rank_mean": "mean rank",
"vote_weighted": "validation-weighted vote",
}
def method_classifier(methods: list[str]) -> str:
counts = Counter(METHOD_NAMES[m] for m in methods)
return "Validation-selected per task: " + ", ".join(
f"{name} \u00d7{n}" if n > 1 else name for name, n in counts.most_common())
def method_status(combo: str) -> str:
arms = combo.split("-")
note = ""
if "alm" in arms:
note = "; the authorial language models are being remade and this row will be refreshed"
return (
f"{len(arms)} atomic models combined; the combination method is a hyperparameter "
"chosen per task on validation macro-F1 among concatenation of prepared feature "
"blocks and five training-free posterior combiners (hard vote, soft vote, geometric "
"mean, mean rank, validation-weighted vote); test evaluated once for the chosen "
"method" + note
)
def pair_model_name(pair: str) -> str:
return " + ".join(ARM_NAMES[arm] for arm in pair.split("-"))
def pair_dimensionality(pair: str, counts: list[int]) -> str:
members = [ARM_NAMES[arm] for arm in pair.split("-")]
joined = ", ".join(members[:-1]) + " and " + members[-1]
return f"{joined} columns stacked: " + slash(counts)
def pair_status(scalings: list[str]) -> str:
return (
"Two prepared single-channel feature blocks concatenated, each arm at the "
"variant its own standalone model selected on validation; block scaling ("
+ "/".join(scalings)
+ ") selected per task on validation macro-F1, where balanced divides each "
"block by its mean training row norm and native keeps the standalone scales; "
+ common_status
)
def mw_dimensionality(chosen: list[dict]) -> str:
return "Validation-selected n=" + "/".join(str(entry["n"]) for entry in chosen)
def mw_status(chosen: list[dict], bursty: bool) -> str:
alphas = "/".join(str(entry["alpha"]) for entry in chosen)
base = (
"Character n-gram counts with a uniform author prior fixed a priori; "
f"validation-selected n and Dirichlet smoothing alpha={alphas}; "
"vocabulary from the atomic training split only; train+validation refit; "
"test evaluated once"
)
if not bursty:
return base
scales = "/".join(
"raw posterior" if entry["scale"] is None else str(entry["scale"])
for entry in chosen
)
return (
base
+ f"; validation-selected Dirichlet concentration={scales}, where the raw "
"posterior is the non-bursty limit that reproduces naive Bayes"
)
siamese_main, siamese_main_rule, siamese_main_epoch = siamese_specs(
results_root, "sphragis-main", MAIN_TASKS
)
siamese_source, siamese_source_rule, siamese_source_epoch = siamese_specs(
results_root, "sphragis-source-held-out", SOURCE_TASKS
)
siamese_sphragis = siamese_main + siamese_source
siamese_metre, siamese_metre_rule, siamese_metre_epoch = siamese_specs(
results_root, "sphragis-metre", VERSE_TASKS
)
def siamese_status(rules: list[str], epochs: list[int]) -> str:
return (
"Shared GreBERTa encoder trained with supervised contrastive plus cross-entropy "
"on author-balanced batches; epoch and decision rule ("
+ "/".join(rules)
+ " at epoch "
+ "/".join(map(str, epochs))
+ ") both selected on the full validation split; larger-task scores are means "
"over exact constituents; test evaluated once"
)
def slash(values: list[int]) -> str:
return " / ".join(f"{value:,}" for value in values)
def selected_n(specs):
return [int(read_json(path)["selected"]["n"]) for _, _, path, _ in specs]
common_status = (
"Five-point learning-rate search on validation macro-F1; validation-loss "
"early stopping; train+validation refit; test evaluated once"
)
sphragis_rows = [
build_row(
model="GreBERTa (end-to-end)", url="https://huggingface.co/bowphs/GreBerta",
dimensionality="~126M trainable parameters per benchmark track",
classifier="Linear softmax head",
status=("Fine-tuned on the atomic task with validation-loss early stopping; "
"larger-task logits are means over exact constituents; test evaluated once"),
task_specs=ft_sphragis, dataset_roots=roots,
),
build_row(
model="KainoBERT (end-to-end)", url="https://huggingface.co/Urdatorn/KainoBERT-sphragis",
dimensionality="~136M trainable parameters per benchmark track",
classifier="Linear softmax head",
status=("Fine-tuned on the atomic task with validation-loss early stopping; "
"larger-task logits are means over exact constituents; test evaluated once"),
task_specs=kaino_ft_sphragis, dataset_roots=roots,
),
build_row(
model="KainoBERTa (end-to-end)", url="https://huggingface.co/Urdatorn/KainoBERTa-sphragis",
dimensionality="~112M trainable parameters per benchmark track",
classifier="Linear softmax head",
status=("Fine-tuned on the atomic task with validation-loss early stopping; larger-task logits are means over exact constituents; test evaluated once. Architecture control for KainoBERT: same pretraining corpus, blocks, tokenizer, masking and schedule, stopped by the same validation plateau rule"),
task_specs=roberta_ft_sphragis, dataset_roots=roots,
),
build_row(
model="GreBERTa", url="https://huggingface.co/bowphs/GreBerta",
dimensionality="768 frozen features",
classifier="PyTorch multinomial logistic regression", status=common_status,
task_specs=greberta_sphragis, dataset_roots=roots,
),
build_row(
model="KainoBERT", url="https://huggingface.co/Urdatorn/KainoBERT-sphragis",
dimensionality="768 frozen features",
classifier="PyTorch multinomial logistic regression", status=common_status,
task_specs=kainobert_sphragis, dataset_roots=roots,
),
build_row(
model="Lemma TF-IDF", url="https://github.com/Urdatorn/sphragis_models",
dimensionality=slash(dimensions_for_specs(tfidf_sphragis)),
classifier="PyTorch multinomial logistic regression", status=common_status,
task_specs=tfidf_sphragis, dataset_roots=roots,
),
build_row(
model="Burrows lemma z-scores", url="https://github.com/Urdatorn/sphragis_models",
dimensionality="Validation-selected MFW n=" + "/".join(map(str, selected_n(burrows_sphragis))),
classifier="PyTorch multinomial logistic regression", status=common_status,
task_specs=burrows_sphragis, dataset_roots=roots,
),
build_row(
model="Syntax feature rates", url="https://github.com/Urdatorn/sphragis_models",
dimensionality=("Validation-selected d=" + "/".join(map(str, syntax_sphragis_d))
+ "; features=" + slash(syntax_sphragis_counts)),
classifier="PyTorch multinomial logistic regression",
status=("Combination dimension 1-4 selected independently per task on validation "
"macro-F1; " + common_status),
task_specs=syntax_sphragis, dataset_roots=roots,
),
build_row(
model="Siamese GreBERTa (SupCon + CE)",
url="https://github.com/Urdatorn/sphragis_models",
dimensionality="~127M trainable parameters; 256-d L2-normalized embedding",
classifier="Nearest author centroid and linear softmax head",
status=siamese_status(
[siamese_main_rule, siamese_source_rule],
[siamese_main_epoch, siamese_source_epoch],
),
task_specs=siamese_sphragis, dataset_roots=roots,
),
build_row(
model="Char n-gram Dirichlet-multinomial",
url="https://github.com/Urdatorn/sphragis_models",
dimensionality=mw_dimensionality(mw_specs[("dirichlet_multinomial", "sphragis")][1]),
classifier="Dirichlet compound multinomial posterior predictive",
status=mw_status(mw_specs[("dirichlet_multinomial", "sphragis")][1], bursty=True),
task_specs=list(mw_specs[("dirichlet_multinomial", "sphragis")][0]),
dataset_roots=roots,
),
build_row(
model="Char n-gram naive Bayes",
url="https://github.com/Urdatorn/sphragis_models",
dimensionality=mw_dimensionality(mw_specs[("multinomial_nb", "sphragis")][1]),
classifier="Multinomial naive Bayes",
status=mw_status(mw_specs[("multinomial_nb", "sphragis")][1], bursty=False),
task_specs=list(mw_specs[("multinomial_nb", "sphragis")][0]),
dataset_roots=roots,
),
*(
build_row(
model=pair_model_name(pair),
url="https://github.com/Urdatorn/sphragis_models",
dimensionality=pair_dimensionality(pair, pair_data[pair][2]),
classifier="PyTorch multinomial logistic regression",
status=pair_status(pair_data[pair][1]),
task_specs=list(pair_data[pair][0]),
dataset_roots=roots,
)
for pair in pairs
),
*alm_rows,
*(
build_row(
model=pair_model_name(combo),
url="https://github.com/Urdatorn/sphragis_models",
dimensionality=f"{len(combo.split('-'))} arms",
classifier=method_classifier(method_sphragis[combo][1]),
status=method_status(combo),
task_specs=list(method_sphragis[combo][0]),
dataset_roots=roots,
)
for combo in method_sphragis
),
]
metre_rows = [
build_row(
model="GreBERTa (end-to-end)", url="https://huggingface.co/bowphs/GreBerta",
dimensionality="~126M trainable parameters",
classifier="Linear softmax head",
status=("Fine-tuned on verse_1 with validation-loss early stopping; larger-task "
"logits are means over exact constituents; test evaluated once"),
task_specs=ft_metre, dataset_roots=roots,
),
build_row(
model="KainoBERT (end-to-end)", url="https://huggingface.co/Urdatorn/KainoBERT-sphragis",
dimensionality="~136M trainable parameters",
classifier="Linear softmax head",
status=("Fine-tuned on verse_1 with validation-loss early stopping; larger-task "
"logits are means over exact constituents; test evaluated once"),
task_specs=kaino_ft_metre, dataset_roots=roots,
),
build_row(
model="KainoBERTa (end-to-end)", url="https://huggingface.co/Urdatorn/KainoBERTa-sphragis",
dimensionality="~112M trainable parameters",
classifier="Linear softmax head",
status=("Fine-tuned on verse_1 with validation-loss early stopping; larger-task logits are means over exact constituents; test evaluated once. Architecture control for KainoBERT: same pretraining corpus, blocks, tokenizer, masking and schedule, stopped by the same validation plateau rule"),
task_specs=roberta_ft_metre, dataset_roots=roots,
),
build_row(
model="GreBERTa", url="https://huggingface.co/bowphs/GreBerta",
dimensionality="768 frozen features",
classifier="PyTorch multinomial logistic regression", status=common_status,
task_specs=greberta_metre, dataset_roots=roots,
),
build_row(
model="KainoBERT", url="https://huggingface.co/Urdatorn/KainoBERT-sphragis",
dimensionality="768 frozen features",
classifier="PyTorch multinomial logistic regression", status=common_status,
task_specs=kainobert_metre, dataset_roots=roots,
),
build_row(
model="Lemma TF-IDF", url="https://github.com/Urdatorn/sphragis_models",
dimensionality=slash(dimensions_for_specs(tfidf_metre)),
classifier="PyTorch multinomial logistic regression", status=common_status,
task_specs=tfidf_metre, dataset_roots=roots,
),
build_row(
model="Burrows lemma z-scores", url="https://github.com/Urdatorn/sphragis_models",
dimensionality="Validation-selected MFW n=" + "/".join(map(str, selected_n(burrows_metre))),
classifier="PyTorch multinomial logistic regression", status=common_status,
task_specs=burrows_metre, dataset_roots=roots,
),
build_row(
model="Syntax feature rates", url="https://github.com/Urdatorn/sphragis_models",
dimensionality=("Validation-selected d=" + "/".join(map(str, syntax_metre_d))
+ "; features=" + slash(syntax_metre_counts)),
classifier="PyTorch multinomial logistic regression",
status=("Combination dimension 1-4 selected independently per task on validation "
"macro-F1; " + common_status),
task_specs=syntax_metre, dataset_roots=roots,
),
build_row(
model="Metrical line features", url="https://github.com/Urdatorn/sphragis_models",
dimensionality=slash(dimensions_for_specs(metre_specs)),
classifier="PyTorch multinomial logistic regression",
status=("All metrical features observed in atomic training lines; " + common_status),
task_specs=metre_specs, dataset_roots=roots,
),
build_row(
model="Siamese GreBERTa (SupCon + CE)",
url="https://github.com/Urdatorn/sphragis_models",
dimensionality="~127M trainable parameters; 256-d L2-normalized embedding",
classifier="Nearest author centroid and linear softmax head",
status=siamese_status([siamese_metre_rule], [siamese_metre_epoch]),
task_specs=siamese_metre, dataset_roots=roots,
),
build_row(
model="Char n-gram Dirichlet-multinomial",
url="https://github.com/Urdatorn/sphragis_models",
dimensionality=mw_dimensionality(mw_specs[("dirichlet_multinomial", "verse")][1]),
classifier="Dirichlet compound multinomial posterior predictive",
status=mw_status(mw_specs[("dirichlet_multinomial", "verse")][1], bursty=True),
task_specs=list(mw_specs[("dirichlet_multinomial", "verse")][0]),
dataset_roots=roots,
),
build_row(
model="Char n-gram naive Bayes",
url="https://github.com/Urdatorn/sphragis_models",
dimensionality=mw_dimensionality(mw_specs[("multinomial_nb", "verse")][1]),
classifier="Multinomial naive Bayes",
status=mw_status(mw_specs[("multinomial_nb", "verse")][1], bursty=False),
task_specs=list(mw_specs[("multinomial_nb", "verse")][0]),
dataset_roots=roots,
),
*(
build_row(
model=pair_model_name(pair),
url="https://github.com/Urdatorn/sphragis_models",
dimensionality=pair_dimensionality(pair, metre_pair_data[pair][2]),
classifier="PyTorch multinomial logistic regression",
status=pair_status(metre_pair_data[pair][1]),
task_specs=list(metre_pair_data[pair][0]),
dataset_roots=roots,
)
for pair in metre_pairs if pair in metre_pair_data
),
*alm_metre_rows,
*(
build_row(
model=pair_model_name(combo),
url="https://github.com/Urdatorn/sphragis_models",
dimensionality=f"{len(combo.split('-'))} arms",
classifier=method_classifier(method_metre[combo][1]),
status=method_status(combo),
task_specs=list(method_metre[combo][0]),
dataset_roots=roots,
)
for combo in method_metre
),
]
return sphragis_rows, metre_rows
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--results-root", type=Path, required=True)
parser.add_argument("--sphragis-data", type=Path, required=True)
parser.add_argument("--metre-data", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
sphragis_rows, metre_rows = make_rows(
args.results_root, args.sphragis_data, args.metre_data
)
content = (
"// Generated by scripts/publish_atomic_results.py; do not edit manually.\n"
"import type { ResultRow } from './data';\n\n"
f"export const sphragisAtomicResults: ResultRow[] = {ts(sphragis_rows)};\n\n"
f"export const metreAtomicResults: ResultRow[] = {ts(metre_rows)};\n"
)
args.output.write_text(content)
print(
json.dumps({
"output": str(args.output),
"sphragis_rows": len(sphragis_rows),
"metre_rows": len(metre_rows),
"validated_tasks": sum(len(row["scores"]) for row in sphragis_rows + metre_rows),
})
)
if __name__ == "__main__":
main()