AnkitAI commited on
Commit
92508af
·
verified ·
1 Parent(s): 8dcfca3

Upload eval/train_and_eval.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. eval/train_and_eval.py +144 -0
eval/train_and_eval.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FINSENSE stage-1 trainer — deberta-v3-base on Financial PhraseBank 50agree.
2
+
3
+ Research-verified recipe (FINSENSE.md): generic encoder, no DAPT, our own
4
+ published stratified split (FPB has no official one), macro-F1 primary,
5
+ confusion matrix logged (pos/neutral confusion = the known ceiling).
6
+
7
+ Usage: python3 finsense_train.py [--seed 42] [--smoke] [--base microsoft/deberta-v3-base]
8
+ Runs on MPS (Mac) or CUDA. Smoke = 200 steps sanity.
9
+ """
10
+ import argparse
11
+ import json
12
+ import os
13
+
14
+ import io
15
+ import zipfile
16
+
17
+ import numpy as np
18
+ import torch
19
+ from huggingface_hub import hf_hub_download
20
+ from sklearn.metrics import accuracy_score, confusion_matrix, f1_score
21
+ from transformers import (AutoModelForSequenceClassification, AutoTokenizer,
22
+ Trainer, TrainingArguments)
23
+
24
+ p = argparse.ArgumentParser()
25
+ p.add_argument("--seed", type=int, default=42)
26
+ p.add_argument("--smoke", action="store_true")
27
+ p.add_argument("--base", default="microsoft/deberta-v3-base")
28
+ p.add_argument("--corpus", default=None,
29
+ help="optional finsense_arm_*.jsonl — replaces the FPB train split "
30
+ "as training data (val/test splits stay FPB, untouched)")
31
+ args = p.parse_args()
32
+
33
+ _arm = ("-" + os.path.basename(args.corpus).replace("finsense_arm_", "arm").replace(".jsonl", "")) if args.corpus else ""
34
+ OUT = os.path.expanduser(f"~/finsense_runs/{args.base.split('/')[-1]}-s{args.seed}{_arm}")
35
+ os.makedirs(OUT, exist_ok=True)
36
+ LABELS = ["negative", "neutral", "positive"] # FPB label order 0/1/2
37
+
38
+ # parse the raw FPB zip directly (script-datasets unsupported, no parquet branch)
39
+ zp = hf_hub_download("takala/financial_phrasebank", "data/FinancialPhraseBank-v1.0.zip",
40
+ repo_type="dataset")
41
+ LAB2ID = {"negative": 0, "neutral": 1, "positive": 2}
42
+ rows = []
43
+ with zipfile.ZipFile(zp) as z:
44
+ with z.open("FinancialPhraseBank-v1.0/Sentences_50Agree.txt") as fh:
45
+ for line in io.TextIOWrapper(fh, encoding="latin-1"):
46
+ line = line.strip()
47
+ if not line or "@" not in line:
48
+ continue
49
+ sent, lab = line.rsplit("@", 1)
50
+ rows.append({"sentence": sent.strip(), "label": LAB2ID[lab.strip()]})
51
+ print(f"FPB 50agree rows: {len(rows)}", flush=True)
52
+
53
+ # published split: stratified 80/10/10, split-seed FIXED at 42 regardless of
54
+ # training seed (same rule as Parable corpus splits)
55
+ import random as _rnd
56
+ _rnd.Random(42).shuffle(rows)
57
+ by_label = {i: [] for i in range(3)}
58
+ for r in rows:
59
+ by_label[r["label"]].append(r)
60
+ train, val, test = [], [], []
61
+ for lab, rows in by_label.items():
62
+ n = len(rows)
63
+ test += rows[: int(n * 0.10)]
64
+ val += rows[int(n * 0.10): int(n * 0.20)]
65
+ train += rows[int(n * 0.20):]
66
+ print(f"split: train={len(train)} val={len(val)} test={len(test)}", flush=True)
67
+
68
+ if args.corpus:
69
+ train = [json.loads(l) for l in open(args.corpus)]
70
+ import random as _r2
71
+ _r2.Random(args.seed).shuffle(train)
72
+ print(f"corpus override: {args.corpus} -> train={len(train)}", flush=True)
73
+
74
+ _trc = {"trust_remote_code": True} if "NeoBERT" in args.base else {}
75
+ tok = AutoTokenizer.from_pretrained(args.base, **_trc)
76
+
77
+ def enc(rows):
78
+ from datasets import Dataset
79
+ d = Dataset.from_list(rows)
80
+ return d.map(lambda b: tok(b["sentence"], truncation=True, max_length=128),
81
+ batched=True, remove_columns=["sentence"])
82
+
83
+ torch.manual_seed(args.seed)
84
+ model = AutoModelForSequenceClassification.from_pretrained(
85
+ args.base, num_labels=3, **_trc,
86
+ id2label=dict(enumerate(LABELS)), label2id={l: i for i, l in enumerate(LABELS)})
87
+
88
+ def metrics(pred):
89
+ y, yhat = pred.label_ids, pred.predictions.argmax(-1)
90
+ return {"accuracy": accuracy_score(y, yhat),
91
+ "macro_f1": f1_score(y, yhat, average="macro")}
92
+
93
+ from transformers import TrainerCallback
94
+
95
+ class MPSCacheFlush(TrainerCallback):
96
+ """torch-MPS leaks on deberta attention kernels; flush or the kernel
97
+ OOM-kills the process silently around step ~100 on a 16GB machine."""
98
+ def on_step_end(self, args, state, control, **kw):
99
+ if state.global_step % 25 == 0 and torch.backends.mps.is_available():
100
+ torch.mps.empty_cache()
101
+
102
+ import transformers as _tf
103
+ _tok_kw = {"processing_class": tok} if int(_tf.__version__.split(".")[0]) >= 5 else {"tokenizer": tok}
104
+ trainer = Trainer(
105
+ model=model,
106
+ **_tok_kw,
107
+ callbacks=[MPSCacheFlush()],
108
+ train_dataset=enc(train),
109
+ eval_dataset=enc(val),
110
+ compute_metrics=metrics,
111
+ args=TrainingArguments(
112
+ output_dir=f"{OUT}/ckpt",
113
+ num_train_epochs=1 if args.smoke else 5,
114
+ max_steps=50 if args.smoke else -1,
115
+ per_device_train_batch_size=8, gradient_accumulation_steps=2, per_device_eval_batch_size=16,
116
+ learning_rate=(1e-5 if "deberta" in args.base else 2e-5), warmup_ratio=0.1, weight_decay=0.01,
117
+ eval_strategy="epoch" if not args.smoke else "no",
118
+ save_strategy="epoch" if not args.smoke else "no",
119
+ load_best_model_at_end=not args.smoke,
120
+ metric_for_best_model="macro_f1",
121
+ logging_steps=25, seed=args.seed, report_to=[],
122
+ use_cpu=False,
123
+ # deberta-v3's disentangled attention overflows under fp16 -> class
124
+ # collapse; transformers v5 auto-enables mixed precision on CUDA
125
+ fp16=False, bf16=False,
126
+ ),
127
+ )
128
+ trainer.train()
129
+
130
+ # final: held-out test (never used for selection)
131
+ pred = trainer.predict(enc(test))
132
+ y, yhat = pred.label_ids, pred.predictions.argmax(-1)
133
+ res = {"base": args.base, "seed": args.seed, "smoke": args.smoke,
134
+ "test_accuracy": float(accuracy_score(y, yhat)),
135
+ "test_macro_f1": float(f1_score(y, yhat, average="macro")),
136
+ "confusion_matrix": confusion_matrix(y, yhat).tolist(),
137
+ "labels": LABELS, "split": "stratified 80/10/10, split-seed 42",
138
+ "corpus": args.corpus or "fpb-train",
139
+ "dataset": "financial_phrasebank sentences_50agree"}
140
+ json.dump(res, open(f"{OUT}/result.json", "w"), indent=1)
141
+ print("FINSENSE_RESULT", json.dumps(res), flush=True)
142
+ if not args.smoke:
143
+ model.save_pretrained(f"{OUT}/model")
144
+ tok.save_pretrained(f"{OUT}/model")