CharlesCNorton
Add artifact provenance contract, shared evaluation protocol, and consistency tests
938f692
Raw
History Blame Contribute Delete
4.06 kB
"""Single-head ablation sweep over the 144 (block, head) pairs.
For each head, zero the columns of its block's attention output projection,
score the calibration pool with the Stage 0 classifier, and record the F1 delta
and the L2 deviation of the 40 classifier-relevant output dims. Heads are then
ranked by ascending F1 drop, so the head whose removal helps most sorts first,
and the cumulative curve prunes that prefix.
Writes head_importance.json and pruning_curve.json.
"""
import argparse
import json
import sys
import time
from pathlib import Path
import torch
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # repo root, for `common`
from common import (BACKBONE, N_BLOCKS, N_HEADS, device, f1_at, heads_masked, # noqa: E402
load_pool, score_pool, write_artifact)
from common.models import load_backbone # noqa: E402
from common.pools import CALIB1000, by_name # noqa: E402
HERE = Path(__file__).resolve().parent
CLASSIFIER = HERE.parent / 'stage_0' / 'classifier.json'
CURVE_K = [1, 5, 10, 15, 20, 30, 40, 50, 60, 80, 100, 120, 144]
RANKING = 'ascending F1_drop; the smallest drop is the most prunable head'
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--backbone', default=BACKBONE)
ap.add_argument('--pool', default=CALIB1000.name)
args = ap.parse_args()
dev = device()
c = json.loads(CLASSIFIER.read_text())
pos = torch.tensor(c['pos_dims'], dtype=torch.long, device=dev)
neg = torch.tensor(c['neg_dims'], dtype=torch.long, device=dev)
target_dims = torch.cat([pos, neg]).unique()
thr = float(c['threshold'])
print(f'[init] |target_dims|={len(target_dims)} threshold={thr:.3f}', flush=True)
backbone = load_backbone(args.backbone).to(dev)
pool = by_name(args.pool)
print(f'[pool] {pool.name}, preloading', flush=True)
loaded = load_pool(pool, dev, preload=True)
print(f' person rate {loaded.positive_rate:.3f}', flush=True)
base_scores, base_targets = score_pool(backbone, loaded, pos, neg, target_dims)
base = f1_at(base_scores, loaded.labels, thr)
print(f'[baseline] F1={base.f1:.4f} P={base.precision:.4f} R={base.recall:.4f}',
flush=True)
results = []
t0 = time.time()
for b in range(N_BLOCKS):
for h in range(N_HEADS):
with heads_masked(backbone, [(b, h)]):
scores, targets = score_pool(backbone, loaded, pos, neg, target_dims)
m = f1_at(scores, loaded.labels, thr)
l2 = (targets - base_targets).pow(2).sum(dim=1).sqrt().mean().item()
results.append({'block': b, 'head': h, 'F1': m.f1, 'precision': m.precision,
'recall': m.recall, 'F1_drop': base.f1 - m.f1, 'target_L2': l2})
print(f' B{b:>2}H{h:>2} F1={m.f1:.4f} drop={base.f1 - m.f1:+.4f} '
f'L2={l2:.3f} {time.time() - t0:.1f}s', flush=True)
ranked = sorted(results, key=lambda r: r['F1_drop'])
curve = []
for k in CURVE_K:
with heads_masked(backbone, [(r['block'], r['head']) for r in ranked[:k]]):
scores, _ = score_pool(backbone, loaded, pos, neg)
m = f1_at(scores, loaded.labels, thr)
curve.append({'heads_pruned': k, 'F1': m.f1, 'F1_drop': base.f1 - m.f1,
'precision': m.precision, 'recall': m.recall})
print(f' K={k:>3} F1={m.f1:.4f} drop={base.f1 - m.f1:+.4f}', flush=True)
stamp = dict(generator='stage_2/ablate.py', classifier=CLASSIFIER,
pool_info=loaded.provenance())
write_artifact(HERE / 'head_importance.json', {
'ranking': RANKING,
'baseline': base.asdict(),
'per_head': results,
'ranked_most_prunable_first': [(r['block'], r['head'], r['F1_drop'])
for r in ranked],
}, **stamp)
write_artifact(HERE / 'pruning_curve.json',
{'baseline': base.asdict(), 'curve': curve}, **stamp)
print('[done]', flush=True)
if __name__ == '__main__':
main()