Spaces:
Sleeping
Sleeping
| """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() | |