finner / scripts /run_ablations.py
bkalyankrishnareddy
Initial release: FinNER financial NER β€” test F1 0.8388
ba19370
Raw
History Blame Contribute Delete
5.65 kB
"""Run all Phase 3 ablations in sequence and print a comparison table.
Each ablation changes one variable vs the baseline. Results are logged to
results/runs.jsonl automatically by the training loop.
Usage:
python scripts/run_ablations.py # all ablations
python scripts/run_ablations.py --skip-finbert # skip domain-pretrained run
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from rich.console import Console
from rich.table import Table
from finner.config import settings, RESULTS_DIR, CHECKPOINTS_DIR
from finner.model.train import RunConfig, train
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
console = Console()
os.environ.setdefault("PYTORCH_MPS_HIGH_WATERMARK_RATIO", "0.0")
# ── Ablation definitions ────────────────────────────────────────────────────
# Each entry: (run_name, RunConfig kwargs overrides on top of BASE_CFG)
BASE = dict(
model_name="bert-base-uncased",
learning_rate=2e-5,
warmup_ratio=0.06,
num_epochs=3,
batch_size=4,
weight_decay=0.01,
lr_scheduler_type="linear",
disc_lr_factor=0.0,
focal_loss_gamma=0.0,
o_token_weight=1.0,
gradient_accumulation_steps=4,
gradient_checkpointing=True,
)
ABLATIONS: list[tuple[str, dict]] = [
# 1. Discriminative LR: lower encoder layers get 10Γ— smaller LR than head
("disc_lr_0.1", {"disc_lr_factor": 0.1, "notes": "disc_lr: encoder=lr*0.1"}),
# 2. Cosine LR schedule instead of linear
("cosine_sched", {"lr_scheduler_type": "cosine", "notes": "cosine_lr_decay"}),
# 3. Focal loss Ξ³=2 β€” down-weights easy O-token examples
("focal_g2", {"focal_loss_gamma": 2.0, "notes": "focal_loss_gamma=2"}),
# 4. Class-weighted CE: O token gets 0.3Γ— weight to reduce its dominance
("weighted_ce", {"o_token_weight": 0.3, "notes": "o_token_weight=0.3"}),
# 5. Longer warmup (10% vs 6%)
("warmup_10pct", {"warmup_ratio": 0.10, "notes": "warmup_ratio=0.10"}),
# 6. Domain-pretrained: FinBERT (same BERT arch, pretrained on financial text)
("finbert", {"model_name": "ProsusAI/finbert", "notes": "finbert_domain_pretrained"}),
]
def run_ablation(name: str, overrides: dict) -> dict:
cfg_kwargs = {**BASE, **overrides}
cfg = RunConfig(**cfg_kwargs)
console.print(f"\n[bold cyan]β–Ά Running ablation: {name}[/bold cyan]")
console.print(f" Config: {overrides}")
result = train(cfg, run_name=name)
console.print(f" Best val entity-F1: [bold green]{result['best_val_entity_f1']:.4f}[/bold green] (epoch {result['best_epoch']})")
return result
def load_baseline_f1() -> float:
baseline_path = RESULTS_DIR / "baseline.json"
if baseline_path.exists():
with open(baseline_path) as f:
return json.load(f).get("overall_f1", 0.0)
return 0.0
def print_comparison(results: list[dict], baseline_f1: float) -> None:
table = Table(title="Phase 3 Ablation Results", show_header=True)
table.add_column("Run", style="cyan")
table.add_column("Val entity-F1", justify="right")
table.add_column("Ξ” vs baseline", justify="right")
table.add_column("Best epoch", justify="right")
# baseline row
table.add_row("baseline", f"{baseline_f1:.4f}", "β€”", "β€”")
for r in sorted(results, key=lambda x: x["best_val_entity_f1"], reverse=True):
delta = r["best_val_entity_f1"] - baseline_f1
delta_str = f"[green]+{delta:.4f}[/green]" if delta >= 0 else f"[red]{delta:.4f}[/red]"
table.add_row(r["run_name"], f"{r['best_val_entity_f1']:.4f}", delta_str, str(r["best_epoch"]))
console.print(table)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--skip-finbert", action="store_true")
parser.add_argument("--only", default=None, help="Run only this ablation name")
parser.add_argument("--skip", default=None, help="Comma-separated list of ablation names to skip")
args = parser.parse_args()
baseline_f1 = load_baseline_f1()
console.print(f"\n[bold]Baseline val entity-F1: {baseline_f1:.4f}[/bold]")
ablations = ABLATIONS
if args.skip_finbert:
ablations = [(n, c) for n, c in ablations if n != "finbert"]
if args.only:
ablations = [(n, c) for n, c in ablations if n == args.only]
if args.skip:
skip_set = {s.strip() for s in args.skip.split(",")}
ablations = [(n, c) for n, c in ablations if n not in skip_set]
results = []
for name, overrides in ablations:
try:
r = run_ablation(name, overrides)
results.append(r)
except Exception as exc:
console.print(f"[red]Ablation {name} failed: {exc}[/red]")
if results:
print_comparison(results, baseline_f1)
# Save ablation table
ablation_table = {
"baseline_f1": baseline_f1,
"ablations": [
{"run": r["run_name"], "val_f1": r["best_val_entity_f1"],
"delta": r["best_val_entity_f1"] - baseline_f1,
"best_epoch": r["best_epoch"]}
for r in results
]
}
out = RESULTS_DIR / "ablation_table.json"
with open(out, "w") as f:
json.dump(ablation_table, f, indent=2)
console.print(f"\n[bold green]βœ“ Ablation table saved β†’ {out}[/bold green]")
if __name__ == "__main__":
main()