knoxel commited on
Commit
79405d9
·
verified ·
1 Parent(s): 44aff12

Upload run_conformalesm_unified.py

Browse files
Files changed (1) hide show
  1. run_conformalesm_unified.py +672 -0
run_conformalesm_unified.py ADDED
@@ -0,0 +1,672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ConformalESM-Extended: Unified Uncertainty Quantification for Protein Language Models
3
+ =====================================================================================
4
+
5
+ Runs overnight on Hugging Face CPU infrastructure. Pushes results to Hub.
6
+
7
+ Coverage:
8
+ - Task 1: Secondary Structure (Q3: H/E/C) — 8M + 650M
9
+ - Task 2: Disorder Prediction (binary) — 35M + 650M
10
+ - Baselines: Entropy, Max-Margin
11
+ - Methods: Temperature Scaling, Split Conformal, Class-Conditional, Mondrian,
12
+ Size-Stratified, Experiment Prioritization, Cross-Model Transfer
13
+ - All post-hoc, CPU-only, no retraining
14
+
15
+ Auto-pushes results to knoxel/conformalesm-paper-starter
16
+ """
17
+
18
+ import os
19
+ import sys
20
+ import json
21
+ import time
22
+ import numpy as np
23
+ from collections import defaultdict
24
+ from datasets import load_dataset
25
+ from transformers import AutoTokenizer, AutoModelForTokenClassification
26
+ import torch
27
+
28
+ # ======================== CONFIG ========================
29
+
30
+ SEED = 42
31
+ np.random.seed(SEED)
32
+ torch.manual_seed(SEED)
33
+
34
+ MAX_LEN = 1022
35
+ N_CAL = 500
36
+ N_TEST = 500
37
+
38
+ # Models
39
+ SS_MODEL_8M = "AmelieSchreiber/esm2_t6_8M_UR50D-finetuned-secondary-structure"
40
+ SS_MODEL_650M = "gaodrew/esm2_t33_650M_UR50D-finetuned-secondary-structure"
41
+ DIS_MODEL_35M = "CQSB/esm2_35M-LoRA-ID-DisProt7"
42
+ DIS_MODEL_650M = "CQSB/esm2_650M-LoRA-ID-DisProt7"
43
+
44
+ # Datasets
45
+ SS_DATASET = "lamm-mit/protein_secondary_structure_from_PDB"
46
+ DIS_DATASET = "CQSB/SoftDis"
47
+ DIS_CONFIG = "id05"
48
+ DIS_THRESHOLD = 0.5
49
+
50
+ # Label mappings
51
+ SS_ID2LABEL = {0: "C", 1: "H", 2: "E"}
52
+ SS_LABEL2ID = {"C": 0, "H": 1, "E": 2}
53
+ DIS_LABEL2ID = {"Ordered": 0, "Disordered": 1}
54
+
55
+ # ======================== UTILS ========================
56
+
57
+ def log(msg):
58
+ print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)
59
+
60
+ def dssp_to_q3(c):
61
+ if c in "HGI":
62
+ return "H"
63
+ elif c in "EB":
64
+ return "E"
65
+ else:
66
+ return "C"
67
+
68
+ # ======================== DATA ========================
69
+
70
+ def load_ss_data():
71
+ ds = load_dataset(SS_DATASET, split="train")
72
+ ds = ds.filter(lambda x: x["Sequence_length"] <= MAX_LEN - 2)
73
+ ds = ds.shuffle(seed=SEED)
74
+ cal = ds.select(range(min(N_CAL, len(ds))))
75
+ test = ds.select(range(min(N_CAL, len(ds)), min(N_CAL + N_TEST, len(ds))))
76
+ return cal, test
77
+
78
+
79
+ def load_disorder_data():
80
+ ds = load_dataset(DIS_DATASET, DIS_CONFIG)
81
+ train = ds["train"].shuffle(seed=SEED)
82
+ cal = train.select(range(min(N_CAL, len(train))))
83
+ test = ds["test"].shuffle(seed=SEED)
84
+ test = test.select(range(min(N_TEST, len(test))))
85
+ return cal, test
86
+
87
+ # ======================== MODEL LOADING ========================
88
+
89
+ def load_model(model_id, is_650m=False):
90
+ """Load model, handling LoRA adapters if needed."""
91
+ log(f"Loading model: {model_id}")
92
+
93
+ if "LoRA" in model_id or "lora" in model_id.lower():
94
+ from peft import PeftModel
95
+ if "650M" in model_id or "t33" in model_id:
96
+ base_id = "facebook/esm2_t33_650M_UR50D"
97
+ elif "35M" in model_id or "t12" in model_id:
98
+ base_id = "facebook/esm2_t12_35M_UR50D"
99
+ else:
100
+ base_id = "facebook/esm2_t6_8M_UR50D"
101
+
102
+ log(f" Base model: {base_id}")
103
+ base = AutoModelForTokenClassification.from_pretrained(base_id)
104
+ model = PeftModel.from_pretrained(base, model_id)
105
+ else:
106
+ model = AutoModelForTokenClassification.from_pretrained(model_id)
107
+
108
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
109
+ model.eval()
110
+ log(f" Model loaded. Parameters: {sum(p.numel() for p in model.parameters()):,}")
111
+ return model, tokenizer
112
+
113
+ # ======================== INFERENCE ========================
114
+
115
+ def infer_ss(model, tokenizer, dataset, batch_size=2):
116
+ """Secondary structure inference. Returns list of {true, probs, preds}."""
117
+ results = []
118
+ with torch.no_grad():
119
+ for i in range(0, len(dataset), batch_size):
120
+ batch = dataset[i:i + batch_size]
121
+ for j in range(len(batch["Sequence_spaced"])):
122
+ seq = batch["Sequence_spaced"][j].split()
123
+ ss = batch["Secondary_structure"][j][:len(seq)]
124
+ true = np.array([SS_LABEL2ID[dssp_to_q3(c)] for c in ss])
125
+
126
+ spaced = " ".join(seq[:MAX_LEN - 2])
127
+ inputs = tokenizer(spaced, return_tensors="pt", truncation=True, max_length=MAX_LEN)
128
+ logits = model(**inputs).logits.squeeze(0)
129
+ probs = torch.softmax(logits, dim=-1).numpy()
130
+
131
+ # Align to residues (skip special tokens)
132
+ input_ids = inputs["input_ids"].squeeze(0).tolist()
133
+ aligned_probs = []
134
+ residue_idx = 0
135
+ cls_id = tokenizer.cls_token_id
136
+ eos_id = tokenizer.eos_token_id
137
+ pad_id = tokenizer.pad_token_id
138
+
139
+ for tid in input_ids:
140
+ if tid in [cls_id, eos_id, pad_id]:
141
+ continue
142
+ if residue_idx < len(true):
143
+ aligned_probs.append(probs[residue_idx + 1])
144
+ residue_idx += 1
145
+
146
+ aligned_probs = np.array(aligned_probs)
147
+ min_len = min(len(true), len(aligned_probs))
148
+ results.append({
149
+ "true": true[:min_len],
150
+ "probs": aligned_probs[:min_len],
151
+ "preds": np.argmax(aligned_probs[:min_len], axis=-1),
152
+ })
153
+ return results
154
+
155
+
156
+ def infer_disorder(model, tokenizer, dataset, batch_size=2):
157
+ """Disorder inference. Returns list of {true, probs, preds}."""
158
+ results = []
159
+ with torch.no_grad():
160
+ for i in range(0, len(dataset), batch_size):
161
+ batch = dataset[i:i + batch_size]
162
+ for j in range(len(batch["sequence"])):
163
+ seq = batch["sequence"][j]
164
+ freqs = batch["soft_disorder_frequency"][j]
165
+ true = np.array([1 if f >= DIS_THRESHOLD else 0 for f in freqs[:len(seq)]])
166
+
167
+ spaced = " ".join(list(seq)[:MAX_LEN - 2])
168
+ inputs = tokenizer(
169
+ spaced, return_tensors="pt", truncation=True, max_length=MAX_LEN,
170
+ return_special_tokens_mask=True
171
+ )
172
+ special_mask = inputs.pop("special_tokens_mask").squeeze(0).bool().numpy()
173
+
174
+ logits = model(**inputs).logits.squeeze(0)
175
+ probs = torch.softmax(logits, dim=-1).numpy()
176
+
177
+ # Remove special tokens
178
+ aligned_probs = probs[~special_mask]
179
+ min_len = min(len(true), len(aligned_probs))
180
+ results.append({
181
+ "true": true[:min_len],
182
+ "probs": aligned_probs[:min_len],
183
+ "preds": np.argmax(aligned_probs[:min_len], axis=-1),
184
+ })
185
+ return results
186
+
187
+ # ======================== METRICS ========================
188
+
189
+ def compute_accuracy(results):
190
+ correct = sum(np.sum(r["preds"] == r["true"]) for r in results)
191
+ total = sum(len(r["true"]) for r in results)
192
+ return correct / total if total else 0
193
+
194
+
195
+ def compute_ece(results, n_bins=10):
196
+ all_conf, all_correct = [], []
197
+ for r in results:
198
+ conf = np.max(r["probs"], axis=-1)
199
+ correct = (r["preds"] == r["true"]).astype(float)
200
+ all_conf.extend(conf)
201
+ all_correct.extend(correct)
202
+ all_conf = np.array(all_conf)
203
+ all_correct = np.array(all_correct)
204
+
205
+ ece_val = 0.0
206
+ for i in range(n_bins):
207
+ lo, hi = i / n_bins, (i + 1) / n_bins
208
+ mask = (all_conf > lo) & (all_conf <= hi)
209
+ if mask.sum() == 0:
210
+ continue
211
+ ece_val += mask.sum() * abs(all_conf[mask].mean() - all_correct[mask].mean())
212
+ return ece_val / len(all_conf) if len(all_conf) else 0
213
+
214
+
215
+ def compute_brier(results):
216
+ scores = []
217
+ for r in results:
218
+ n = len(r["true"])
219
+ if n == 0:
220
+ continue
221
+ n_cls = r["probs"].shape[1]
222
+ one_hot = np.zeros((n, n_cls))
223
+ one_hot[np.arange(n), r["true"]] = 1
224
+ scores.append(np.mean(np.sum((r["probs"] - one_hot) ** 2, axis=-1)))
225
+ return np.mean(scores) if scores else 0
226
+
227
+ # ======================== TEMPERATURE SCALING ========================
228
+
229
+ def find_temperature(cal_results, grid=None):
230
+ if grid is None:
231
+ grid = np.linspace(0.5, 5.0, 50)
232
+ all_logits, all_labels = [], []
233
+ for r in cal_results:
234
+ probs = np.clip(r["probs"], 1e-10, 1.0)
235
+ all_logits.append(np.log(probs))
236
+ all_labels.append(r["true"])
237
+ all_logits = np.concatenate(all_logits)
238
+ all_labels = np.concatenate(all_labels)
239
+
240
+ best_t, best_nll = 1.0, float("inf")
241
+ for t in grid:
242
+ scaled = all_logits / t
243
+ max_log = np.max(scaled, axis=-1, keepdims=True)
244
+ log_probs = scaled - max_log - np.log(np.sum(np.exp(scaled - max_log), axis=-1, keepdims=True))
245
+ nll = -np.mean(log_probs[np.arange(len(all_labels)), all_labels])
246
+ if nll < best_nll:
247
+ best_nll = nll
248
+ best_t = t
249
+ return best_t
250
+
251
+
252
+ def apply_temperature(results, temp):
253
+ scaled = []
254
+ for r in results:
255
+ probs = np.clip(r["probs"], 1e-10, 1.0)
256
+ logits = np.log(probs) / temp
257
+ max_log = np.max(logits, axis=-1, keepdims=True)
258
+ new_probs = np.exp(logits - max_log) / np.sum(np.exp(logits - max_log), axis=-1, keepdims=True)
259
+ scaled.append({"true": r["true"], "probs": new_probs, "preds": np.argmax(new_probs, axis=-1)})
260
+ return scaled
261
+
262
+ # ======================== CONFORMAL ========================
263
+
264
+ def conformal_qhat(cal_results, alpha=0.1):
265
+ scores = [1.0 - r["probs"][j, label]
266
+ for r in cal_results for j, label in enumerate(r["true"])]
267
+ scores = np.array(scores)
268
+ n = len(scores)
269
+ q = np.ceil((n + 1) * (1 - alpha)) / n
270
+ return np.quantile(scores, q, method="higher")
271
+
272
+
273
+ def conformal_qhat_class_conditional(cal_results, alpha=0.1):
274
+ class_scores = defaultdict(list)
275
+ for r in cal_results:
276
+ for j, label in enumerate(r["true"]):
277
+ class_scores[label].append(1.0 - r["probs"][j, label])
278
+ thresholds = {}
279
+ for label, scores in class_scores.items():
280
+ scores = np.array(scores)
281
+ n = len(scores)
282
+ if n == 0:
283
+ thresholds[label] = 1.0
284
+ continue
285
+ q = np.ceil((n + 1) * (1 - alpha)) / n
286
+ thresholds[label] = np.quantile(scores, q, method="higher")
287
+ return thresholds
288
+
289
+
290
+ def evaluate_conformal(results, q_hat, n_classes, per_class_thresholds=None):
291
+ coverage_count, total = 0, 0
292
+ set_sizes = []
293
+ class_cov = defaultdict(int)
294
+ class_tot = defaultdict(int)
295
+ class_set = defaultdict(list)
296
+ size_strat = defaultdict(lambda: {"correct": 0, "total": 0})
297
+
298
+ for r in results:
299
+ for j, label in enumerate(r["true"]):
300
+ total += 1
301
+ threshold = per_class_thresholds.get(label, q_hat) if per_class_thresholds else q_hat
302
+ pred_set = [y for y in range(n_classes) if (1.0 - r["probs"][j, y]) <= threshold]
303
+ set_size = len(pred_set)
304
+ set_sizes.append(set_size)
305
+ size_strat[set_size]["total"] += 1
306
+
307
+ if label in pred_set:
308
+ coverage_count += 1
309
+ class_cov[label] += 1
310
+ size_strat[set_size]["correct"] += 1
311
+ class_tot[label] += 1
312
+ class_set[label].append(set_size)
313
+
314
+ coverage = coverage_count / total if total else 0
315
+ avg_size = np.mean(set_sizes) if set_sizes else 0
316
+
317
+ per_class = {}
318
+ for k in sorted(class_tot.keys()):
319
+ per_class[k] = {
320
+ "coverage": class_cov[k] / class_tot[k] if class_tot[k] else 0,
321
+ "avg_set_size": np.mean(class_set[k]) if class_set[k] else 0,
322
+ }
323
+
324
+ size_strat_out = {}
325
+ for size in sorted(size_strat.keys()):
326
+ d = size_strat[size]
327
+ size_strat_out[size] = {
328
+ "coverage": d["correct"] / d["total"] if d["total"] else 0,
329
+ "n": d["total"],
330
+ }
331
+
332
+ return coverage, avg_size, per_class, size_strat_out
333
+
334
+
335
+ def evaluate_mondrian(cal_results, test_results, alpha, n_classes):
336
+ class_cal = defaultdict(list)
337
+ for r in cal_results:
338
+ for j, label in enumerate(r["true"]):
339
+ class_cal[label].append(1.0 - r["probs"][j, label])
340
+
341
+ thresholds = {}
342
+ for label, scores in class_cal.items():
343
+ scores = np.array(scores)
344
+ n = len(scores)
345
+ if n == 0:
346
+ thresholds[label] = 1.0
347
+ continue
348
+ q = np.ceil((n + 1) * (1 - alpha)) / n
349
+ thresholds[label] = np.quantile(scores, q, method="higher")
350
+
351
+ class_cov = defaultdict(lambda: {"correct": 0, "total": 0})
352
+ class_set = defaultdict(list)
353
+
354
+ for r in test_results:
355
+ for j, label in enumerate(r["true"]):
356
+ threshold = thresholds.get(label, 1.0)
357
+ pred_set = [y for y in range(n_classes) if (1.0 - r["probs"][j, y]) <= threshold]
358
+ set_size = len(pred_set)
359
+ class_cov[label]["total"] += 1
360
+ class_set[label].append(set_size)
361
+ if label in pred_set:
362
+ class_cov[label]["correct"] += 1
363
+
364
+ mondrian = {}
365
+ for k in sorted(class_cov.keys()):
366
+ d = class_cov[k]
367
+ mondrian[k] = {
368
+ "coverage": d["correct"] / d["total"] if d["total"] else 0,
369
+ "avg_set_size": np.mean(class_set[k]) if class_set[k] else 0,
370
+ "n": d["total"],
371
+ }
372
+ return mondrian
373
+
374
+ # ======================== BASELINES ========================
375
+
376
+ def entropy_baseline(results, alpha, n_classes):
377
+ coverage_count, total = 0, 0
378
+ set_sizes = []
379
+ for r in results:
380
+ for j, label in enumerate(r["true"]):
381
+ total += 1
382
+ probs = r["probs"][j]
383
+ sorted_idx = np.argsort(-probs)
384
+ cumsum = np.cumsum(probs[sorted_idx])
385
+ n_include = np.searchsorted(cumsum, 1 - alpha) + 1
386
+ pred_set = sorted_idx[:n_include].tolist()
387
+ set_sizes.append(len(pred_set))
388
+ if label in pred_set:
389
+ coverage_count += 1
390
+ return coverage_count / total if total else 0, np.mean(set_sizes) if set_sizes else 0
391
+
392
+
393
+ def maxmargin_baseline(results, alpha, n_classes):
394
+ all_margins = []
395
+ for r in results:
396
+ for j in range(len(r["true"])):
397
+ probs = r["probs"][j]
398
+ sp = np.sort(probs)[::-1]
399
+ all_margins.append(sp[0] - sp[1] if len(sp) > 1 else 1.0)
400
+ all_margins = np.array(all_margins)
401
+
402
+ n = len(all_margins)
403
+ q = np.ceil((n + 1) * (1 - alpha)) / n
404
+ margin_thresh = np.quantile(all_margins, q, method="higher")
405
+
406
+ coverage_count, total = 0, 0
407
+ set_sizes = []
408
+ for r in results:
409
+ for j, label in enumerate(r["true"]):
410
+ total += 1
411
+ probs = r["probs"][j]
412
+ sorted_idx = np.argsort(-probs)
413
+ sp = np.sort(probs)[::-1]
414
+ margin = sp[0] - sp[1] if len(sp) > 1 else 1.0
415
+ if margin >= margin_thresh:
416
+ pred_set = [sorted_idx[0]]
417
+ else:
418
+ pred_set = sorted_idx[:min(2, n_classes)].tolist()
419
+ set_sizes.append(len(pred_set))
420
+ if label in pred_set:
421
+ coverage_count += 1
422
+ return coverage_count / total if total else 0, np.mean(set_sizes) if set_sizes else 0
423
+
424
+ # ======================== PRIORITIZATION ========================
425
+
426
+ def experiment_prioritization(results, budgets):
427
+ all_unc, all_errors = [], []
428
+ for r in results:
429
+ max_probs = np.max(r["probs"], axis=-1)
430
+ uncertainties = 1 - max_probs
431
+ errors = (r["preds"] != r["true"]).astype(float)
432
+ all_unc.extend(uncertainties)
433
+ all_errors.extend(errors)
434
+
435
+ all_unc = np.array(all_unc)
436
+ all_errors = np.array(all_errors)
437
+ n_total = len(all_unc)
438
+
439
+ out = {}
440
+ for budget in budgets:
441
+ b = min(budget, n_total)
442
+ random_idx = np.random.choice(n_total, size=b, replace=False)
443
+ random_rate = all_errors[random_idx].mean()
444
+
445
+ sorted_idx = np.argsort(-all_unc)
446
+ top_idx = sorted_idx[:b]
447
+ unc_rate = all_errors[top_idx].mean()
448
+
449
+ catch = unc_rate / random_rate if random_rate > 0 else float('inf')
450
+ out[budget] = {
451
+ "random_error_rate": float(random_rate),
452
+ "uncertainty_error_rate": float(unc_rate),
453
+ "catch_rate": float(catch),
454
+ }
455
+ return out
456
+
457
+ # ======================== CROSS-MODEL ========================
458
+
459
+ def cross_model_transfer(cal_results_small, test_results_large, alpha, n_classes):
460
+ q = conformal_qhat(cal_results_small, alpha)
461
+ cov, size, _, size_strat = evaluate_conformal(test_results_large, q, n_classes)
462
+ return {
463
+ "q_hat": float(q),
464
+ "coverage": float(cov),
465
+ "avg_set_size": float(size),
466
+ "size_stratified": {str(k): v for k, v in size_strat.items()},
467
+ }
468
+
469
+ # ======================== PIPELINE ========================
470
+
471
+ def run_pipeline(model_id, dataset_loader, infer_fn, task_name, n_classes, label_map,
472
+ budgets=[100, 500, 1000, 5000]):
473
+ """Run full conformal pipeline for one model+task."""
474
+ log(f"\n{'='*60}")
475
+ log(f"TASK: {task_name}")
476
+ log(f"MODEL: {model_id}")
477
+ log(f"{'='*60}")
478
+
479
+ model, tokenizer = load_model(model_id)
480
+ cal_ds, test_ds = dataset_loader()
481
+ log(f" Calibration: {len(cal_ds)} seqs, Test: {len(test_ds)} seqs")
482
+
483
+ log(" Running inference (calibration)...")
484
+ cal_results = infer_fn(model, tokenizer, cal_ds)
485
+ log(f" Calibration residues: {sum(len(r['true']) for r in cal_results):,}")
486
+
487
+ log(" Running inference (test)...")
488
+ test_results = infer_fn(model, tokenizer, test_ds)
489
+ log(f" Test residues: {sum(len(r['true']) for r in test_results):,}")
490
+
491
+ # Free model memory
492
+ del model
493
+ if torch.cuda.is_available():
494
+ torch.cuda.empty_cache()
495
+
496
+ # Baseline
497
+ base_acc = compute_accuracy(test_results)
498
+ base_ece = compute_ece(test_results)
499
+ base_brier = compute_brier(test_results)
500
+ log(f" Baseline: Acc={base_acc:.4f}, ECE={base_ece:.4f}, Brier={base_brier:.4f}")
501
+
502
+ # Temperature scaling
503
+ best_t = find_temperature(cal_results)
504
+ scaled_cal = apply_temperature(cal_results, best_t)
505
+ scaled_test = apply_temperature(test_results, best_t)
506
+ ts_acc = compute_accuracy(scaled_test)
507
+ ts_ece = compute_ece(scaled_test)
508
+ ts_brier = compute_brier(scaled_test)
509
+ ece_red = (base_ece - ts_ece) / base_ece * 100 if base_ece else 0
510
+ log(f" Temperature T={best_t:.2f}: Acc={ts_acc:.4f}, ECE={ts_ece:.4f} ({ece_red:+.0f}%), Brier={ts_brier:.4f}")
511
+
512
+ # Conformal (raw)
513
+ log(" Conformal prediction...")
514
+ conformal = {}
515
+ for alpha in [0.05, 0.10, 0.20]:
516
+ q = conformal_qhat(cal_results, alpha)
517
+ cov, size, pclass, sstrat = evaluate_conformal(test_results, q, n_classes)
518
+ log(f" Raw alpha={alpha:.2f}: cov={cov:.4f}, set={size:.2f}")
519
+
520
+ q_s = conformal_qhat(scaled_cal, alpha)
521
+ cov_s, size_s, pclass_s, sstrat_s = evaluate_conformal(scaled_test, q_s, n_classes)
522
+ log(f" T-scaled alpha={alpha:.2f}: cov={cov_s:.4f}, set={size_s:.2f}")
523
+
524
+ conformal[f"alpha_{alpha}"] = {
525
+ "raw": {"coverage": float(cov), "avg_set_size": float(size),
526
+ "per_class": {label_map.get(k, str(k)): v for k, v in pclass.items()},
527
+ "size_stratified": {str(kk): vv for kk, vv in sstrat.items()}},
528
+ "temperature_scaled": {"coverage": float(cov_s), "avg_set_size": float(size_s),
529
+ "per_class": {label_map.get(k, str(k)): v for k, v in pclass_s.items()},
530
+ "size_stratified": {str(kk): vv for kk, vv in sstrat_s.items()}},
531
+ }
532
+
533
+ # Class-conditional
534
+ log(" Class-conditional conformal...")
535
+ cc = {}
536
+ for alpha in [0.05, 0.10, 0.20]:
537
+ th = conformal_qhat_class_conditional(cal_results, alpha)
538
+ cov, size, pclass, _ = evaluate_conformal(test_results, 0, n_classes, th)
539
+ log(f" alpha={alpha:.2f}: cov={cov:.4f}, set={size:.2f}")
540
+ cc[f"alpha_{alpha}"] = {
541
+ "coverage": float(cov), "avg_set_size": float(size),
542
+ "per_class": {label_map.get(k, str(k)): v for k, v in pclass.items()},
543
+ }
544
+
545
+ # Mondrian
546
+ log(" Mondrian conformal...")
547
+ mondrian = {}
548
+ for alpha in [0.05, 0.10, 0.20]:
549
+ mon = evaluate_mondrian(cal_results, test_results, alpha, n_classes)
550
+ log(f" alpha={alpha:.2f}")
551
+ for k, v in mon.items():
552
+ log(f" {label_map.get(k, str(k))}: cov={v['coverage']:.4f}, set={v['avg_set_size']:.2f}, n={v['n']}")
553
+ mondrian[f"alpha_{alpha}"] = {label_map.get(k, str(k)): v for k, v in mon.items()}
554
+
555
+ # Baselines
556
+ log(" Baselines...")
557
+ ent = {}
558
+ mm = {}
559
+ for alpha in [0.05, 0.10, 0.20]:
560
+ ec, es = entropy_baseline(test_results, alpha, n_classes)
561
+ mc, ms = maxmargin_baseline(test_results, alpha, n_classes)
562
+ log(f" alpha={alpha:.2f}: Entropy cov={ec:.4f} set={es:.2f}, MaxMargin cov={mc:.4f} set={ms:.2f}")
563
+ ent[f"alpha_{alpha}"] = {"coverage": float(ec), "avg_set_size": float(es)}
564
+ mm[f"alpha_{alpha}"] = {"coverage": float(mc), "avg_set_size": float(ms)}
565
+
566
+ # Prioritization
567
+ log(" Experiment prioritization...")
568
+ prio = experiment_prioritization(test_results, budgets)
569
+ for b, d in prio.items():
570
+ log(f" Budget={b}: random={d['random_error_rate']:.3f}, unc={d['uncertainty_error_rate']:.3f}, catch={d['catch_rate']:.2f}x")
571
+
572
+ return {
573
+ "task": task_name,
574
+ "model": model_id,
575
+ "baseline": {"accuracy": float(base_acc), "ece": float(base_ece), "brier": float(base_brier)},
576
+ "temperature_scaling": {"temperature": float(best_t), "accuracy": float(ts_acc),
577
+ "ece": float(ts_ece), "brier": float(ts_brier),
578
+ "ece_reduction_pct": float(ece_red)},
579
+ "conformal": conformal,
580
+ "class_conditional": cc,
581
+ "mondrian": mondrian,
582
+ "entropy_baseline": ent,
583
+ "maxmargin_baseline": mm,
584
+ "experiment_prioritization": prio,
585
+ "_cal_raw": cal_results,
586
+ "_cal_scaled": scaled_cal,
587
+ "_test_raw": test_results,
588
+ "_test_scaled": scaled_test,
589
+ }
590
+
591
+ # ======================== MAIN ========================
592
+
593
+ def main():
594
+ log("=" * 60)
595
+ log("ConformalESM-Extended: Unified Pipeline")
596
+ log("All post-hoc, CPU-only, no retraining")
597
+ log("=" * 60)
598
+
599
+ all_results = {}
600
+
601
+ # Task 1: Secondary Structure - 8M
602
+ ss8m = run_pipeline(SS_MODEL_8M, load_ss_data, infer_ss,
603
+ "Secondary Structure (Q3) - ESM-2-8M", 3, SS_ID2LABEL)
604
+ all_results["ss_8m"] = {k: v for k, v in ss8m.items() if not k.startswith("_")}
605
+
606
+ # Task 1: Secondary Structure - 650M
607
+ ss650m = run_pipeline(SS_MODEL_650M, load_ss_data, infer_ss,
608
+ "Secondary Structure (Q3) - ESM-2-650M", 3, SS_ID2LABEL)
609
+ all_results["ss_650m"] = {k: v for k, v in ss650m.items() if not k.startswith("_")}
610
+
611
+ # Task 2: Disorder - 35M
612
+ dis35m = run_pipeline(DIS_MODEL_35M, load_disorder_data, infer_disorder,
613
+ "Disorder Prediction - ESM-2-35M", 2, {0: "Ordered", 1: "Disordered"})
614
+ all_results["disorder_35m"] = {k: v for k, v in dis35m.items() if not k.startswith("_")}
615
+
616
+ # Task 2: Disorder - 650M
617
+ dis650m = run_pipeline(DIS_MODEL_650M, load_disorder_data, infer_disorder,
618
+ "Disorder Prediction - ESM-2-650M", 2, {0: "Ordered", 1: "Disordered"})
619
+ all_results["disorder_650m"] = {k: v for k, v in dis650m.items() if not k.startswith("_")}
620
+
621
+ # Cross-model transfer
622
+ log(f"\n{'='*60}")
623
+ log("Cross-Model Calibration Transfer")
624
+ log(f"{'='*60}")
625
+
626
+ log(" SS: 8M calibrate -> 650M test...")
627
+ t1 = cross_model_transfer(ss8m["_cal_raw"], ss650m["_test_raw"], 0.10, 3)
628
+ log(f" Coverage: {t1['coverage']:.4f}, Avg set: {t1['avg_set_size']:.2f}")
629
+ all_results["transfer_ss_8m_to_650m"] = t1
630
+
631
+ log(" SS: 8M calibrate (T-scaled) -> 650M test...")
632
+ t2 = cross_model_transfer(ss8m["_cal_scaled"], ss650m["_test_scaled"], 0.10, 3)
633
+ log(f" Coverage: {t2['coverage']:.4f}, Avg set: {t2['avg_set_size']:.2f}")
634
+ all_results["transfer_ss_8m_to_650m_temperature_scaled"] = t2
635
+
636
+ log(" Disorder: 35M calibrate -> 650M test...")
637
+ t3 = cross_model_transfer(dis35m["_cal_raw"], dis650m["_test_raw"], 0.10, 2)
638
+ log(f" Coverage: {t3['coverage']:.4f}, Avg set: {t3['avg_set_size']:.2f}")
639
+ all_results["transfer_dis_35m_to_650m"] = t3
640
+
641
+ # Save
642
+ log(f"\n{'='*60}")
643
+ log("Saving Results")
644
+ log(f"{'='*60}")
645
+
646
+ with open("conformalesm_unified_results.json", "w") as f:
647
+ json.dump(all_results, f, indent=2)
648
+ log(" Saved: conformalesm_unified_results.json")
649
+
650
+ # Push to hub
651
+ log(" Pushing to knoxel/conformalesm-paper-starter...")
652
+ try:
653
+ from huggingface_hub import HfApi
654
+ api = HfApi()
655
+ api.upload_file(
656
+ path_or_fileobj="conformalesm_unified_results.json",
657
+ path_in_repo="conformalesm_unified_results.json",
658
+ repo_id="knoxel/conformalesm-paper-starter",
659
+ repo_type="model",
660
+ )
661
+ log(" Successfully pushed to Hub!")
662
+ except Exception as e:
663
+ log(f" Could not push to Hub: {e}")
664
+ log(" Results saved locally.")
665
+
666
+ log(f"\n{'='*60}")
667
+ log("ALL DONE")
668
+ log(f"{'='*60}")
669
+
670
+
671
+ if __name__ == "__main__":
672
+ main()