File size: 5,649 Bytes
ba19370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""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()