narcolepticchicken commited on
Commit
46d62fa
Β·
verified Β·
1 Parent(s): 6be3751

Upload verify_v2.py

Browse files
Files changed (1) hide show
  1. verify_v2.py +155 -57
verify_v2.py CHANGED
@@ -1,18 +1,20 @@
1
  """Independent verification of v2 ACO specialist models.
2
 
3
- Run after training completes. Recreates test splits with same seed,
4
- loads models, computes metrics, compares to v1 baseline.
5
 
6
- Usage:
7
- uv run --with transformers,torch,datasets,scikit-learn,huggingface_hub verify_v2.py
8
-
9
- Or via hf_jobs:
10
- hf_jobs run --script verify_v2.py --deps transformers,torch,datasets,scikit-learn --hardware a10g-large --timeout 2h
11
  """
12
- import torch, numpy as np, json, os
13
  from datasets import Dataset, load_dataset
14
  from transformers import AutoTokenizer, AutoModelForSequenceClassification
15
- from sklearn.metrics import accuracy_score, f1_score, classification_report
 
 
 
 
 
16
 
17
  V1_MODELS = {
18
  "tier_router": "narcolepticchicken/aco-specialists-tier-router",
@@ -25,8 +27,12 @@ V2_MODELS = {
25
  "verifier_gater": "narcolepticchicken/aco-specialists-verifier-gater-v2",
26
  }
27
  NUM_LABELS_MAP = {"tier_router": 3, "tool_gater": 2, "verifier_gater": 2}
 
 
 
 
 
28
 
29
- # --- Dataset loaders (SAME as training script) ---
30
  def load_tool_gater():
31
  import re
32
  ds = load_dataset("lockon/ToolACE", split="train")
@@ -103,21 +109,46 @@ def load_verifier_gater():
103
 
104
  LOADERS = {"tool_gater": load_tool_gater, "tier_router": load_tier_router, "verifier_gater": load_verifier_gater}
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  def evaluate_model(model_name, task_name, num_labels):
107
- """Load model, tokenize test set, compute metrics."""
108
- print(f"\n{'='*60}\nEVALUATING: {model_name} [{task_name}]\n{'='*60}")
109
-
 
110
  # Load data
111
  ds = LOADERS[task_name]()
112
  test_ds = ds["test"]
113
  print(f" Test samples: {len(test_ds)}")
114
-
115
  # Class distribution
116
  lc = {}
117
  for lb in test_ds["labels"]:
118
  lc[lb] = lc.get(lb, 0) + 1
119
  print(f" Class dist: {lc}")
120
-
121
  # Load model
122
  try:
123
  tokenizer = AutoTokenizer.from_pretrained(model_name)
@@ -131,85 +162,152 @@ def evaluate_model(model_name, task_name, num_labels):
131
  print(f" Model loaded on {device}")
132
  except Exception as e:
133
  print(f" FAILED to load model: {e}")
 
134
  return None
135
-
136
  # Extract threshold from config
137
  threshold = getattr(model.config, "threshold", 0.5)
138
  print(f" Threshold from config: {threshold}")
139
-
140
- # Tokenize
141
- def tokenize(ex):
142
- return tokenizer(ex["text"], truncation=True, max_length=2048, padding=False)
143
- test_tok = test_ds.map(tokenize, batched=True)
144
- test_tok.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
145
-
 
146
  # Predict
147
  all_probs = []
148
  all_labels = []
149
- bs = 32
150
- for i in range(0, len(test_tok), bs):
151
- batch = test_tok[i:i+bs]
152
- inputs = {k: v.to(device) for k, v in batch.items() if k != "labels"}
153
- with torch.no_grad():
154
- logits = model(**inputs).logits
155
  probs = torch.softmax(logits, dim=-1).cpu().numpy()
156
- all_probs.append(probs)
157
- all_labels.extend(batch["labels"].cpu().numpy().tolist())
158
-
159
  probs = np.vstack(all_probs)
160
  labels = np.array(all_labels)
161
-
162
  # Default predictions
163
  preds_default = np.argmax(probs, axis=-1)
164
  acc_default = accuracy_score(labels, preds_default)
165
  f1_default = f1_score(labels, preds_default, average="macro", zero_division=0)
166
-
167
- # Calibrated predictions (binary tasks)
 
168
  if num_labels == 2:
 
169
  preds_cal = (probs[:, 1] >= threshold).astype(int)
170
  acc_cal = accuracy_score(labels, preds_cal)
171
  f1_cal = f1_score(labels, preds_cal, average="macro", zero_division=0)
172
-
173
- print(f" Default: acc={acc_default:.4f}, f1_macro={f1_default:.4f}")
 
 
174
  print(f" Calibrated (t={threshold:.3f}): acc={acc_cal:.4f}, f1_macro={f1_cal:.4f}")
 
 
 
 
 
 
 
175
  print(f"\n Classification Report (calibrated):")
176
- print(f" {classification_report(labels, preds_cal, target_names=['neg','pos'], zero_division=0)}")
177
-
178
- return {"accuracy": acc_cal, "f1_macro": f1_cal, "threshold": threshold,
179
- "accuracy_default": acc_default, "f1_default": f1_default}
 
 
 
 
 
 
 
 
 
 
180
  else:
181
- print(f" Accuracy: {acc_default:.4f}, F1 macro: {f1_default:.4f}")
 
182
  print(f"\n Classification Report:")
183
- print(f" {classification_report(labels, preds_default, zero_division=0)}")
184
- return {"accuracy": acc_default, "f1_macro": f1_default, "threshold": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
  def main():
187
  results = {}
188
-
189
- for task_name in ["tier_router", "tool_gater", "verifier_gater"]:
190
  num_labels = NUM_LABELS_MAP[task_name]
191
-
192
  # Evaluate v2
 
 
 
193
  v2_res = evaluate_model(V2_MODELS[task_name], task_name, num_labels)
194
-
195
  # Evaluate v1
 
 
 
196
  v1_res = evaluate_model(V1_MODELS[task_name], task_name, num_labels)
197
-
198
  if v2_res and v1_res:
199
- delta = v2_res["f1_macro"] - v1_res["f1_macro"]
200
- print(f"\n >>> v1->v2 F1 delta: {v1_res['f1_macro']:.4f} -> {v2_res['f1_macro']:.4f} = {delta:+.4f}")
201
-
202
- results[task_name] = {"v1": v1_res, "v2": v2_res}
203
-
 
 
 
 
204
  print(f"\n{'='*60}")
205
  print("FINAL COMPARISON")
206
  print(f"{'='*60}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  print(json.dumps(results, indent=2, default=str))
208
-
209
  # Save results
210
  with open("/tmp/v2_verification_results.json", "w") as f:
211
  json.dump(results, f, indent=2, default=str)
212
-
213
  # Push results
214
  from huggingface_hub import HfApi
215
  api = HfApi()
@@ -219,7 +317,7 @@ def main():
219
  repo_id="narcolepticchicken/agent-cost-optimizer",
220
  repo_type="model",
221
  )
222
- print("\nResults pushed to agent-cost-optimizer repo")
223
 
224
  if __name__ == "__main__":
225
  main()
 
1
  """Independent verification of v2 ACO specialist models.
2
 
3
+ Recreates test splits with same seed, loads v1 and v2 models,
4
+ computes metrics, compares delta. Robust to dataset format quirks.
5
 
6
+ Usage via hf_jobs:
7
+ hf_jobs run --script verify_v2.py --deps transformers,torch,datasets,scikit-learn,huggingface_hub --hardware a10g-large --timeout 2h
 
 
 
8
  """
9
+ import torch, numpy as np, json, os, sys
10
  from datasets import Dataset, load_dataset
11
  from transformers import AutoTokenizer, AutoModelForSequenceClassification
12
+ from sklearn.metrics import accuracy_score, f1_score, classification_report, precision_recall_fscore_support
13
+ from torch.utils.data import DataLoader
14
+
15
+ # ═══════════════════════════════════════════
16
+ # Constants
17
+ # ═══════════════════════════════════════════
18
 
19
  V1_MODELS = {
20
  "tier_router": "narcolepticchicken/aco-specialists-tier-router",
 
27
  "verifier_gater": "narcolepticchicken/aco-specialists-verifier-gater-v2",
28
  }
29
  NUM_LABELS_MAP = {"tier_router": 3, "tool_gater": 2, "verifier_gater": 2}
30
+ TASK_NAMES = ["tier_router", "tool_gater", "verifier_gater"]
31
+
32
+ # ═══════════════════════════════════════════
33
+ # Dataset loaders (SAME as training script)
34
+ # ═══════════════════════════════════════════
35
 
 
36
  def load_tool_gater():
37
  import re
38
  ds = load_dataset("lockon/ToolACE", split="train")
 
109
 
110
  LOADERS = {"tool_gater": load_tool_gater, "tier_router": load_tier_router, "verifier_gater": load_verifier_gater}
111
 
112
+ # ═══════════════════════════════════════════
113
+ # Simple tensor dataset wrapper
114
+ # ═══════════════════════════════════════════
115
+
116
+ class TensorDataset(torch.utils.data.Dataset):
117
+ def __init__(self, encodings, labels):
118
+ self.input_ids = encodings["input_ids"]
119
+ self.attention_mask = encodings["attention_mask"]
120
+ self.labels = labels
121
+
122
+ def __len__(self):
123
+ return len(self.labels)
124
+
125
+ def __getitem__(self, idx):
126
+ return {
127
+ "input_ids": torch.tensor(self.input_ids[idx]),
128
+ "attention_mask": torch.tensor(self.attention_mask[idx]),
129
+ "label": torch.tensor(self.labels[idx]),
130
+ }
131
+
132
+ # ═══════════════════════════════════════════
133
+ # Evaluation
134
+ # ═══════════════════════════════════════════
135
+
136
  def evaluate_model(model_name, task_name, num_labels):
137
+ print(f"\n{'='*60}")
138
+ print(f"EVALUATING: {model_name} [{task_name}]")
139
+ print(f"{'='*60}")
140
+
141
  # Load data
142
  ds = LOADERS[task_name]()
143
  test_ds = ds["test"]
144
  print(f" Test samples: {len(test_ds)}")
145
+
146
  # Class distribution
147
  lc = {}
148
  for lb in test_ds["labels"]:
149
  lc[lb] = lc.get(lb, 0) + 1
150
  print(f" Class dist: {lc}")
151
+
152
  # Load model
153
  try:
154
  tokenizer = AutoTokenizer.from_pretrained(model_name)
 
162
  print(f" Model loaded on {device}")
163
  except Exception as e:
164
  print(f" FAILED to load model: {e}")
165
+ import traceback; traceback.print_exc()
166
  return None
167
+
168
  # Extract threshold from config
169
  threshold = getattr(model.config, "threshold", 0.5)
170
  print(f" Threshold from config: {threshold}")
171
+
172
+ # Tokenize into plain lists (avoids set_format/slicing bugs)
173
+ texts = test_ds["text"]
174
+ labels_list = test_ds["labels"]
175
+ encodings = tokenizer(texts, truncation=True, max_length=2048, padding=True)
176
+ tensor_ds = TensorDataset(encodings, labels_list)
177
+ loader = DataLoader(tensor_ds, batch_size=32, shuffle=False)
178
+
179
  # Predict
180
  all_probs = []
181
  all_labels = []
182
+ with torch.no_grad():
183
+ for batch in loader:
184
+ input_ids = batch["input_ids"].to(device)
185
+ attention_mask = batch["attention_mask"].to(device)
186
+ logits = model(input_ids=input_ids, attention_mask=attention_mask).logits
 
187
  probs = torch.softmax(logits, dim=-1).cpu().numpy()
188
+ all_probs.append(probs)
189
+ all_labels.extend(batch["label"].cpu().numpy().tolist())
190
+
191
  probs = np.vstack(all_probs)
192
  labels = np.array(all_labels)
193
+
194
  # Default predictions
195
  preds_default = np.argmax(probs, axis=-1)
196
  acc_default = accuracy_score(labels, preds_default)
197
  f1_default = f1_score(labels, preds_default, average="macro", zero_division=0)
198
+
199
+ print(f" Default: acc={acc_default:.4f}, f1_macro={f1_default:.4f}")
200
+
201
  if num_labels == 2:
202
+ # Calibrated predictions
203
  preds_cal = (probs[:, 1] >= threshold).astype(int)
204
  acc_cal = accuracy_score(labels, preds_cal)
205
  f1_cal = f1_score(labels, preds_cal, average="macro", zero_division=0)
206
+
207
+ # Per-class precision/recall
208
+ p, r, f1, support = precision_recall_fscore_support(labels, preds_cal, zero_division=0)
209
+
210
  print(f" Calibrated (t={threshold:.3f}): acc={acc_cal:.4f}, f1_macro={f1_cal:.4f}")
211
+
212
+ # DETECT COLLAPSE: if all predictions are same class
213
+ unique_preds = np.unique(preds_cal)
214
+ if len(unique_preds) == 1:
215
+ print(f" ⚠️ MAJORITY-CLASS COLLAPSE DETECTED: model predicts only class {unique_preds[0]}")
216
+ print(f" Accuracy = base rate of class {unique_preds[0]} = {max(pct=(labels==unique_preds[0]).mean()):.1%}")
217
+
218
  print(f"\n Classification Report (calibrated):")
219
+ print(f" {classification_report(labels, preds_cal, target_names=['neg','pos'], zero_division=0, digits=4)}")
220
+ print(f" Per-class: neg P={p[0]:.4f} R={r[0]:.4f} F1={f1[0]:.4f} | pos P={p[1]:.4f} R={r[1]:.4f} F1={f1[1]:.4f}")
221
+
222
+ return {
223
+ "accuracy": acc_cal, "f1_macro": f1_cal,
224
+ "accuracy_default": acc_default, "f1_default": f1_default,
225
+ "threshold": threshold,
226
+ "per_class": {
227
+ "neg": {"precision": float(p[0]), "recall": float(r[0]), "f1": float(f1[0]), "support": int(support[0])},
228
+ "pos": {"precision": float(p[1]), "recall": float(r[1]), "f1": float(f1[1]), "support": int(support[1])},
229
+ },
230
+ "collapsed": len(unique_preds) == 1,
231
+ "class_dist": lc,
232
+ }
233
  else:
234
+ # Multi-class
235
+ p, r, f1_per, support_per = precision_recall_fscore_support(labels, preds_default, zero_division=0)
236
  print(f"\n Classification Report:")
237
+ print(f" {classification_report(labels, preds_default, zero_division=0, digits=4)}")
238
+
239
+ per_class = {}
240
+ for i in range(num_labels):
241
+ per_class[str(i)] = {"precision": float(p[i]), "recall": float(r[i]), "f1": float(f1_per[i]), "support": int(support_per[i])}
242
+
243
+ return {
244
+ "accuracy": acc_default, "f1_macro": f1_default,
245
+ "threshold": None,
246
+ "per_class": per_class,
247
+ "collapsed": np.unique(preds_default).size == 1,
248
+ "class_dist": lc,
249
+ }
250
+
251
+ # ═══════════════════════════════════════════
252
+ # Main
253
+ # ═══════════════════════════════════════════
254
 
255
  def main():
256
  results = {}
257
+
258
+ for task_name in TASK_NAMES:
259
  num_labels = NUM_LABELS_MAP[task_name]
260
+
261
  # Evaluate v2
262
+ print(f"\n{'#'*60}")
263
+ print(f"### V2 MODEL: {task_name}")
264
+ print(f"{'#'*60}")
265
  v2_res = evaluate_model(V2_MODELS[task_name], task_name, num_labels)
266
+
267
  # Evaluate v1
268
+ print(f"\n{'#'*60}")
269
+ print(f"### V1 MODEL: {task_name} (baseline)")
270
+ print(f"{'#'*60}")
271
  v1_res = evaluate_model(V1_MODELS[task_name], task_name, num_labels)
272
+
273
  if v2_res and v1_res:
274
+ delta_f1 = v2_res["f1_macro"] - v1_res["f1_macro"]
275
+ delta_acc = v2_res["accuracy"] - v1_res["accuracy"]
276
+ print(f"\n >>> v1 β†’ v2 delta: F1 {v1_res['f1_macro']:.4f} β†’ {v2_res['f1_macro']:.4f} = {delta_f1:+.4f}")
277
+ print(f" >>> v1 β†’ v2 delta: Acc {v1_res['accuracy']:.4f} β†’ {v2_res['accuracy']:.4f} = {delta_acc:+.4f}")
278
+ results[task_name] = {"v1": v1_res, "v2": v2_res, "delta_f1": delta_f1, "delta_acc": delta_acc}
279
+ else:
280
+ results[task_name] = {"v1": v1_res, "v2": v2_res, "error": "One or both models failed"}
281
+
282
+ # Final summary
283
  print(f"\n{'='*60}")
284
  print("FINAL COMPARISON")
285
  print(f"{'='*60}")
286
+
287
+ for tn in TASK_NAMES:
288
+ r = results.get(tn, {})
289
+ v1ok = r.get("v1") and not r["v1"].get("collapsed") if r.get("v1") else False
290
+ v2ok = r.get("v2") and not r["v2"].get("collapsed") if r.get("v2") else False
291
+ v1collapsed = r.get("v1", {}).get("collapsed", False)
292
+ v2collapsed = r.get("v2", {}).get("collapsed", False)
293
+ delta = r.get("delta_f1", float("nan"))
294
+
295
+ status = "OK"
296
+ if v2collapsed:
297
+ status = "⚠️ V2 COLLAPSED"
298
+ elif v1collapsed:
299
+ status = "⚠️ V1 COLLAPSED"
300
+
301
+ print(f" {tn:<20} v1_f1={r.get('v1',{}).get('f1_macro',0):.4f} "
302
+ f"v2_f1={r.get('v2',{}).get('f1_macro',0):.4f} "
303
+ f"delta={delta:+.4f} {status}")
304
+
305
  print(json.dumps(results, indent=2, default=str))
306
+
307
  # Save results
308
  with open("/tmp/v2_verification_results.json", "w") as f:
309
  json.dump(results, f, indent=2, default=str)
310
+
311
  # Push results
312
  from huggingface_hub import HfApi
313
  api = HfApi()
 
317
  repo_id="narcolepticchicken/agent-cost-optimizer",
318
  repo_type="model",
319
  )
320
+ print("\nResults pushed to narcolepticchicken/agent-cost-optimizer")
321
 
322
  if __name__ == "__main__":
323
  main()