Upload verify_v2.py
Browse files- verify_v2.py +155 -57
verify_v2.py
CHANGED
|
@@ -1,18 +1,20 @@
|
|
| 1 |
"""Independent verification of v2 ACO specialist models.
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
|
| 6 |
-
Usage:
|
| 7 |
-
|
| 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 |
-
""
|
| 108 |
-
print(f"
|
| 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 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
| 146 |
# Predict
|
| 147 |
all_probs = []
|
| 148 |
all_labels = []
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
logits = model(**inputs).logits
|
| 155 |
probs = torch.softmax(logits, dim=-1).cpu().numpy()
|
| 156 |
-
|
| 157 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
else:
|
| 181 |
-
|
|
|
|
| 182 |
print(f"\n Classification Report:")
|
| 183 |
-
print(f" {classification_report(labels, preds_default, zero_division=0)}")
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
def main():
|
| 187 |
results = {}
|
| 188 |
-
|
| 189 |
-
for task_name in
|
| 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 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 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
|
| 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()
|