cotlab 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cotlab/__init__.py +3 -0
- cotlab/analyse_experiments.py +392 -0
- cotlab/analysis/__init__.py +11 -0
- cotlab/analysis/cot_parser.py +243 -0
- cotlab/analysis/faithfulness_metrics.py +192 -0
- cotlab/backends/__init__.py +16 -0
- cotlab/backends/base.py +78 -0
- cotlab/backends/transformers_backend.py +335 -0
- cotlab/backends/vllm_backend.py +227 -0
- cotlab/cli.py +83 -0
- cotlab/core/__init__.py +34 -0
- cotlab/core/base.py +749 -0
- cotlab/core/config.py +90 -0
- cotlab/core/registry.py +68 -0
- cotlab/datasets/__init__.py +45 -0
- cotlab/datasets/loaders.py +1889 -0
- cotlab/experiment/__init__.py +315 -0
- cotlab/experiments/__init__.py +43 -0
- cotlab/experiments/activation_compare.py +290 -0
- cotlab/experiments/activation_patching.py +1050 -0
- cotlab/experiments/attention_analysis.py +885 -0
- cotlab/experiments/classification.py +235 -0
- cotlab/experiments/composite_shift_detector.py +524 -0
- cotlab/experiments/cot_ablation.py +277 -0
- cotlab/experiments/cot_faithfulness.py +187 -0
- cotlab/experiments/cot_heads.py +208 -0
- cotlab/experiments/full_layer_cot.py +232 -0
- cotlab/experiments/full_layer_patching.py +225 -0
- cotlab/experiments/h_neuron_analysis.py +712 -0
- cotlab/experiments/logit_lens.py +439 -0
- cotlab/experiments/multi_head_cot.py +220 -0
- cotlab/experiments/multi_head_patching.py +229 -0
- cotlab/experiments/probing_classifier.py +402 -0
- cotlab/experiments/residual_norm_ood.py +413 -0
- cotlab/experiments/sae_feature_analysis.py +673 -0
- cotlab/experiments/steering_vectors.py +223 -0
- cotlab/experiments/sycophancy_heads.py +224 -0
- cotlab/logging/__init__.py +5 -0
- cotlab/logging/json_logger.py +161 -0
- cotlab/main.py +317 -0
- cotlab/patching/__init__.py +24 -0
- cotlab/patching/cache.py +141 -0
- cotlab/patching/hooks.py +558 -0
- cotlab/patching/interventions.py +86 -0
- cotlab/patching/patcher.py +439 -0
- cotlab/patching/sae.py +181 -0
- cotlab/prompts/__init__.py +43 -0
- cotlab/prompts/cardiology.py +378 -0
- cotlab/prompts/histopathology.py +265 -0
- cotlab/prompts/length_matched_strategies.py +157 -0
- cotlab/prompts/mcq.py +193 -0
- cotlab/prompts/neurology.py +353 -0
- cotlab/prompts/oncology.py +367 -0
- cotlab/prompts/plab.py +162 -0
- cotlab/prompts/pubhealthbench.py +82 -0
- cotlab/prompts/pubmedqa.py +173 -0
- cotlab/prompts/radiology.py +414 -0
- cotlab/prompts/strategies.py +939 -0
- cotlab/prompts/tcga.py +168 -0
- cotlab/runner.py +204 -0
- cotlab-0.8.0.dist-info/METADATA +166 -0
- cotlab-0.8.0.dist-info/RECORD +65 -0
- cotlab-0.8.0.dist-info/WHEEL +4 -0
- cotlab-0.8.0.dist-info/entry_points.txt +3 -0
- cotlab-0.8.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Residual Norm OOD Detection Experiment.
|
|
2
|
+
|
|
3
|
+
Tests whether the L2 norm of the residual stream at a target layer (default:
|
|
4
|
+
last transformer block) correlates with per-sample answer accuracy, and
|
|
5
|
+
whether a threshold on that norm can serve as a single-forward-pass OOD flag.
|
|
6
|
+
|
|
7
|
+
Two metrics are computed from the same single forward pass:
|
|
8
|
+
|
|
9
|
+
1. Residual norm ||h_L[-1]||₂
|
|
10
|
+
The L2 norm of the last-token residual vector at layer L.
|
|
11
|
+
Hypothesis: higher norm → model has formed a strong, confident
|
|
12
|
+
representation → more likely correct.
|
|
13
|
+
|
|
14
|
+
2. Logit entropy -Σ p(letter) · log p(letter) [MCQ mode]
|
|
15
|
+
Entropy over the answer-letter (A/B/C/D/E) logit distribution.
|
|
16
|
+
This is a single-pass approximation of Semantic Entropy for MCQ tasks.
|
|
17
|
+
Hypothesis: lower entropy → model is more peaked on one answer → more
|
|
18
|
+
likely correct. AUROC is computed with (-entropy) so that higher
|
|
19
|
+
values still indicate correctness.
|
|
20
|
+
|
|
21
|
+
Both AUROCs are reported so the norm can be directly compared to the logit
|
|
22
|
+
entropy baseline and benchmarked against published multi-pass Semantic Entropy
|
|
23
|
+
numbers (Kuhn et al. 2023, AUROC ~0.65–0.75 on medical QA).
|
|
24
|
+
|
|
25
|
+
Threshold analysis
|
|
26
|
+
------------------
|
|
27
|
+
A threshold τ* is found on the collected norm scores that maximises balanced
|
|
28
|
+
accuracy. This τ can be applied at inference time as a zero-extra-cost OOD
|
|
29
|
+
flag. Cross-dataset generalisation is evaluated by running the experiment on
|
|
30
|
+
multiple datasets and comparing AUROCs — a threshold trained on MedQA that
|
|
31
|
+
transfers to PubHealthBench would indicate the norm captures model-internal
|
|
32
|
+
state rather than dataset-specific surface features.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import math
|
|
36
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
import torch
|
|
40
|
+
from sklearn.metrics import balanced_accuracy_score, roc_auc_score
|
|
41
|
+
from tqdm import tqdm
|
|
42
|
+
|
|
43
|
+
from ..backends.base import InferenceBackend
|
|
44
|
+
from ..core.base import BaseExperiment, ExperimentResult
|
|
45
|
+
from ..core.registry import Registry
|
|
46
|
+
from ..datasets.loaders import BaseDataset
|
|
47
|
+
from ..logging import ExperimentLogger
|
|
48
|
+
|
|
49
|
+
# MCQ answer letters and their common tokenisation variants.
|
|
50
|
+
_MCQ_LETTERS = list("ABCDEFGHIJ")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@Registry.register_experiment("residual_norm_ood")
|
|
54
|
+
class ResidualNormOODExperiment(BaseExperiment):
|
|
55
|
+
"""
|
|
56
|
+
Single-pass residual norm OOD detection with logit entropy comparison.
|
|
57
|
+
|
|
58
|
+
Hooks the residual stream at ``target_layer`` (default: last transformer
|
|
59
|
+
block), extracts the last-token hidden state, computes its L2 norm, and
|
|
60
|
+
collects the logit-entropy over answer letters — all from one forward pass.
|
|
61
|
+
|
|
62
|
+
Reports AUROC for both metrics against per-sample correctness, plus a
|
|
63
|
+
threshold search for the norm that maximises balanced accuracy.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
name: str = "residual_norm_ood",
|
|
69
|
+
description: str = "L2 residual norm OOD flag vs logit entropy baseline",
|
|
70
|
+
target_layer: Optional[int] = None, # null = last transformer block
|
|
71
|
+
num_samples: Optional[int] = None, # null = full dataset
|
|
72
|
+
seed: int = 42,
|
|
73
|
+
max_input_tokens: int = 1024,
|
|
74
|
+
answer_cue: str = "\n\nAnswer:",
|
|
75
|
+
mcq_letters: Optional[List[str]] = None,
|
|
76
|
+
threshold_percentile_step: int = 5, # granularity of τ search
|
|
77
|
+
**kwargs,
|
|
78
|
+
):
|
|
79
|
+
self._name = name
|
|
80
|
+
self.description = description
|
|
81
|
+
self._target_layer_config = target_layer
|
|
82
|
+
self.num_samples = num_samples
|
|
83
|
+
self.seed = seed
|
|
84
|
+
self.max_input_tokens = max_input_tokens
|
|
85
|
+
self.answer_cue = answer_cue
|
|
86
|
+
self.mcq_letters = mcq_letters or _MCQ_LETTERS
|
|
87
|
+
self.threshold_percentile_step = threshold_percentile_step
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def name(self) -> str:
|
|
91
|
+
return self._name
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
# Helpers
|
|
95
|
+
# ------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def _resolve_layer(self, backend: InferenceBackend) -> int:
|
|
98
|
+
if self._target_layer_config is not None:
|
|
99
|
+
return int(self._target_layer_config)
|
|
100
|
+
# Default: last transformer block (index = num_layers - 1).
|
|
101
|
+
return backend.hook_manager.num_layers - 1
|
|
102
|
+
|
|
103
|
+
def _tokenize(self, tokenizer, text: str, device: str) -> Dict[str, torch.Tensor]:
|
|
104
|
+
return tokenizer(
|
|
105
|
+
text,
|
|
106
|
+
return_tensors="pt",
|
|
107
|
+
truncation=True,
|
|
108
|
+
max_length=self.max_input_tokens,
|
|
109
|
+
).to(device)
|
|
110
|
+
|
|
111
|
+
def _answer_letter_token_ids(self, tokenizer) -> List[int]:
|
|
112
|
+
"""Collect all plausible token ids for MCQ answer letters."""
|
|
113
|
+
ids = set()
|
|
114
|
+
for letter in self.mcq_letters:
|
|
115
|
+
for prefix in (" ", "", "\n"):
|
|
116
|
+
encoded = tokenizer.encode(prefix + letter, add_special_tokens=False)
|
|
117
|
+
if encoded:
|
|
118
|
+
ids.add(encoded[-1])
|
|
119
|
+
return sorted(ids)
|
|
120
|
+
|
|
121
|
+
def _answer_token_id(self, tokenizer, label) -> Optional[int]:
|
|
122
|
+
"""Return the first token id of the label string."""
|
|
123
|
+
if label is None:
|
|
124
|
+
return None
|
|
125
|
+
label_str = str(label).strip()
|
|
126
|
+
for prefix in (" ", ""):
|
|
127
|
+
ids = tokenizer.encode(prefix + label_str, add_special_tokens=False)
|
|
128
|
+
if ids:
|
|
129
|
+
return ids[0]
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
def _forward(
|
|
133
|
+
self,
|
|
134
|
+
backend: InferenceBackend,
|
|
135
|
+
tokens: Dict[str, torch.Tensor],
|
|
136
|
+
target_layer: int,
|
|
137
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
138
|
+
"""Single forward pass.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
last_logits : float32 CPU tensor [vocab_size]
|
|
142
|
+
last_hidden : float32 CPU tensor [d_model] at target_layer
|
|
143
|
+
"""
|
|
144
|
+
last_hidden_store: Dict[str, torch.Tensor] = {}
|
|
145
|
+
|
|
146
|
+
def hook(module, inp, output):
|
|
147
|
+
tensor = output[0] if isinstance(output, tuple) else output
|
|
148
|
+
with torch.no_grad():
|
|
149
|
+
last_hidden_store["h"] = tensor[0, -1].detach().float().cpu()
|
|
150
|
+
|
|
151
|
+
mod = backend.hook_manager.get_residual_module(target_layer)
|
|
152
|
+
handle = mod.register_forward_hook(hook)
|
|
153
|
+
try:
|
|
154
|
+
with torch.no_grad():
|
|
155
|
+
out = backend._model(**tokens)
|
|
156
|
+
finally:
|
|
157
|
+
handle.remove()
|
|
158
|
+
|
|
159
|
+
last_logits = out.logits[0, -1].detach().float().cpu()
|
|
160
|
+
return last_logits, last_hidden_store["h"]
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------------------------------
|
|
163
|
+
# Per-sample metric computation
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
def _compute_norm(self, hidden: torch.Tensor) -> float:
|
|
167
|
+
return float(hidden.norm(p=2).item())
|
|
168
|
+
|
|
169
|
+
def _compute_logit_entropy(self, logits: torch.Tensor, letter_ids: List[int]) -> float:
|
|
170
|
+
"""Entropy over answer-letter logits (MCQ single-pass SE proxy).
|
|
171
|
+
|
|
172
|
+
Softmax is applied only over the letter token ids so the distribution
|
|
173
|
+
sums to 1 over the MCQ choice set.
|
|
174
|
+
"""
|
|
175
|
+
if not letter_ids:
|
|
176
|
+
return float("nan")
|
|
177
|
+
letter_logits = logits[letter_ids]
|
|
178
|
+
probs = torch.softmax(letter_logits, dim=0)
|
|
179
|
+
entropy = -float((probs * (probs + 1e-10).log()).sum().item())
|
|
180
|
+
return entropy
|
|
181
|
+
|
|
182
|
+
# ------------------------------------------------------------------
|
|
183
|
+
# Threshold search
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def _find_threshold(self, norms: List[float], labels: List[bool]) -> Tuple[float, float]:
|
|
187
|
+
"""Find τ* that maximises balanced accuracy on (norms > τ) → correct.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
(tau_star, balanced_acc_at_tau)
|
|
191
|
+
"""
|
|
192
|
+
arr = np.array(norms)
|
|
193
|
+
lbl = np.array(labels, dtype=int)
|
|
194
|
+
percentiles = np.arange(
|
|
195
|
+
self.threshold_percentile_step,
|
|
196
|
+
100,
|
|
197
|
+
self.threshold_percentile_step,
|
|
198
|
+
)
|
|
199
|
+
best_tau, best_ba = float(arr.mean()), 0.0
|
|
200
|
+
unique_labels = set(lbl.tolist())
|
|
201
|
+
if len(unique_labels) < 2:
|
|
202
|
+
return best_tau, best_ba
|
|
203
|
+
for pct in percentiles:
|
|
204
|
+
tau = float(np.percentile(arr, pct))
|
|
205
|
+
preds = (arr >= tau).astype(int)
|
|
206
|
+
if preds.sum() == 0 or preds.sum() == len(preds):
|
|
207
|
+
continue
|
|
208
|
+
ba = balanced_accuracy_score(lbl, preds)
|
|
209
|
+
if ba > best_ba:
|
|
210
|
+
best_ba, best_tau = ba, tau
|
|
211
|
+
return best_tau, best_ba
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# Summary printing
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def _print_summary(
|
|
218
|
+
self,
|
|
219
|
+
dataset_name: str,
|
|
220
|
+
target_layer: int,
|
|
221
|
+
n: int,
|
|
222
|
+
accuracy: float,
|
|
223
|
+
auroc_norm: float,
|
|
224
|
+
auroc_entropy: float,
|
|
225
|
+
tau: float,
|
|
226
|
+
balanced_acc: float,
|
|
227
|
+
mean_norm_correct: float,
|
|
228
|
+
mean_norm_incorrect: float,
|
|
229
|
+
mean_entropy_correct: float,
|
|
230
|
+
mean_entropy_incorrect: float,
|
|
231
|
+
) -> None:
|
|
232
|
+
print("\n" + "=" * 66)
|
|
233
|
+
print(f"RESIDUAL NORM OOD — {dataset_name} (L{target_layer})")
|
|
234
|
+
print("=" * 66)
|
|
235
|
+
print(f" Samples : {n}")
|
|
236
|
+
print(f" Accuracy : {accuracy:.4f}")
|
|
237
|
+
print()
|
|
238
|
+
print(f" {'Metric':<22} {'AUROC':>7} {'mean(corr)':>11} {'mean(incorr)':>12}")
|
|
239
|
+
print(" " + "-" * 56)
|
|
240
|
+
auc_n = f"{auroc_norm:.4f}" if auroc_norm is not None else " n/a "
|
|
241
|
+
auc_e = f"{auroc_entropy:.4f}" if auroc_entropy is not None else " n/a "
|
|
242
|
+
print(
|
|
243
|
+
f" {'L2 norm':.<22} {auc_n:>7} "
|
|
244
|
+
f"{mean_norm_correct:>11.2f} {mean_norm_incorrect:>12.2f}"
|
|
245
|
+
)
|
|
246
|
+
print(
|
|
247
|
+
f" {'Logit entropy (neg)':.<22} {auc_e:>7} "
|
|
248
|
+
f"{mean_entropy_correct:>11.4f} {mean_entropy_incorrect:>12.4f}"
|
|
249
|
+
)
|
|
250
|
+
print()
|
|
251
|
+
print(f" Norm threshold τ* : {tau:.2f}")
|
|
252
|
+
print(f" Balanced acc @ τ* : {balanced_acc:.4f}")
|
|
253
|
+
print("=" * 66)
|
|
254
|
+
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
# Main entry point
|
|
257
|
+
# ------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
def run(
|
|
260
|
+
self,
|
|
261
|
+
backend: InferenceBackend,
|
|
262
|
+
dataset: BaseDataset,
|
|
263
|
+
prompt_strategy: Any,
|
|
264
|
+
logger: Optional[ExperimentLogger] = None,
|
|
265
|
+
**kwargs,
|
|
266
|
+
) -> ExperimentResult:
|
|
267
|
+
"""Run residual norm OOD experiment."""
|
|
268
|
+
|
|
269
|
+
target_layer = self._resolve_layer(backend)
|
|
270
|
+
tokenizer = backend._tokenizer
|
|
271
|
+
device = backend.device
|
|
272
|
+
letter_ids = self._answer_letter_token_ids(tokenizer)
|
|
273
|
+
|
|
274
|
+
samples = (
|
|
275
|
+
dataset.sample(self.num_samples, seed=self.seed) if self.num_samples else list(dataset)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
print(f"Model : {backend.model_name}")
|
|
279
|
+
print(f"Dataset : {dataset.name}")
|
|
280
|
+
print(f"Target layer : L{target_layer}")
|
|
281
|
+
print(f"Samples : {len(samples)}")
|
|
282
|
+
print(f"MCQ letter ids: {len(letter_ids)} token ids")
|
|
283
|
+
|
|
284
|
+
per_sample: List[Dict] = []
|
|
285
|
+
norms: List[float] = []
|
|
286
|
+
entropies: List[float] = []
|
|
287
|
+
labels: List[bool] = []
|
|
288
|
+
|
|
289
|
+
for sample in tqdm(samples, desc="Residual norm"):
|
|
290
|
+
answer_tok_id = self._answer_token_id(tokenizer, sample.label)
|
|
291
|
+
|
|
292
|
+
prompt_str = (
|
|
293
|
+
prompt_strategy.build_prompt(
|
|
294
|
+
{
|
|
295
|
+
"text": sample.text,
|
|
296
|
+
"question": sample.text,
|
|
297
|
+
"metadata": sample.metadata or {},
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
+ self.answer_cue
|
|
301
|
+
)
|
|
302
|
+
tokens = self._tokenize(tokenizer, prompt_str, device)
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
logits, hidden = self._forward(backend, tokens, target_layer)
|
|
306
|
+
except Exception as exc:
|
|
307
|
+
tqdm.write(f" [skip] sample {sample.idx}: {exc}")
|
|
308
|
+
torch.cuda.empty_cache() if torch.cuda.is_available() else None
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
norm = self._compute_norm(hidden)
|
|
312
|
+
entropy = self._compute_logit_entropy(logits, letter_ids)
|
|
313
|
+
|
|
314
|
+
if answer_tok_id is not None and letter_ids:
|
|
315
|
+
best_letter_tok = max(letter_ids, key=lambda t: logits[t].item())
|
|
316
|
+
is_correct = best_letter_tok == answer_tok_id
|
|
317
|
+
else:
|
|
318
|
+
is_correct = False
|
|
319
|
+
|
|
320
|
+
norms.append(norm)
|
|
321
|
+
entropies.append(entropy)
|
|
322
|
+
labels.append(is_correct)
|
|
323
|
+
|
|
324
|
+
per_sample.append(
|
|
325
|
+
{
|
|
326
|
+
"sample_idx": sample.idx,
|
|
327
|
+
"is_correct": is_correct,
|
|
328
|
+
"l2_norm": round(norm, 4),
|
|
329
|
+
"logit_entropy": round(entropy, 6) if not math.isnan(entropy) else None,
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
del hidden, logits
|
|
334
|
+
torch.cuda.empty_cache() if torch.cuda.is_available() else None
|
|
335
|
+
|
|
336
|
+
# ── Aggregate ──────────────────────────────────────────────────
|
|
337
|
+
n = len(labels)
|
|
338
|
+
accuracy = sum(labels) / n if n else 0.0
|
|
339
|
+
|
|
340
|
+
lbl_arr = np.array(labels, dtype=int)
|
|
341
|
+
|
|
342
|
+
# AUROC — handle edge case where all labels are the same.
|
|
343
|
+
def _auroc(scores: List[float], lbl: np.ndarray) -> Optional[float]:
|
|
344
|
+
if lbl.sum() == 0 or lbl.sum() == len(lbl) or len(scores) < 2:
|
|
345
|
+
return None
|
|
346
|
+
return float(roc_auc_score(lbl, scores))
|
|
347
|
+
|
|
348
|
+
auroc_norm = _auroc(norms, lbl_arr)
|
|
349
|
+
# Negate entropy: higher entropy → less likely correct.
|
|
350
|
+
neg_ent = [-e for e in entropies if not math.isnan(e)]
|
|
351
|
+
lbl_ent = lbl_arr[[not math.isnan(e) for e in entropies]]
|
|
352
|
+
auroc_entropy = _auroc(neg_ent, lbl_ent)
|
|
353
|
+
|
|
354
|
+
# Threshold search on norm.
|
|
355
|
+
tau, balanced_acc = self._find_threshold(norms, labels)
|
|
356
|
+
|
|
357
|
+
# Mean metrics by correctness.
|
|
358
|
+
correct_norms = [n for n, c in zip(norms, labels) if c]
|
|
359
|
+
incorrect_norms = [n for n, c in zip(norms, labels) if not c]
|
|
360
|
+
correct_ent = [e for e, c in zip(entropies, labels) if c and not math.isnan(e)]
|
|
361
|
+
incorrect_ent = [e for e, c in zip(entropies, labels) if not c and not math.isnan(e)]
|
|
362
|
+
|
|
363
|
+
mean_norm_corr = sum(correct_norms) / len(correct_norms) if correct_norms else 0.0
|
|
364
|
+
mean_norm_incorr = sum(incorrect_norms) / len(incorrect_norms) if incorrect_norms else 0.0
|
|
365
|
+
mean_ent_corr = sum(correct_ent) / len(correct_ent) if correct_ent else 0.0
|
|
366
|
+
mean_ent_incorr = sum(incorrect_ent) / len(incorrect_ent) if incorrect_ent else 0.0
|
|
367
|
+
|
|
368
|
+
self._print_summary(
|
|
369
|
+
dataset_name=dataset.name,
|
|
370
|
+
target_layer=target_layer,
|
|
371
|
+
n=n,
|
|
372
|
+
accuracy=accuracy,
|
|
373
|
+
auroc_norm=auroc_norm or 0.0,
|
|
374
|
+
auroc_entropy=auroc_entropy or 0.0,
|
|
375
|
+
tau=tau,
|
|
376
|
+
balanced_acc=balanced_acc,
|
|
377
|
+
mean_norm_correct=mean_norm_corr,
|
|
378
|
+
mean_norm_incorrect=mean_norm_incorr,
|
|
379
|
+
mean_entropy_correct=mean_ent_corr,
|
|
380
|
+
mean_entropy_incorrect=mean_ent_incorr,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return ExperimentResult(
|
|
384
|
+
experiment_name=self.name,
|
|
385
|
+
model_name=backend.model_name,
|
|
386
|
+
prompt_strategy=(
|
|
387
|
+
prompt_strategy.name if hasattr(prompt_strategy, "name") else "custom"
|
|
388
|
+
),
|
|
389
|
+
metrics={
|
|
390
|
+
"dataset": dataset.name,
|
|
391
|
+
"target_layer": target_layer,
|
|
392
|
+
"num_samples": n,
|
|
393
|
+
"accuracy": round(accuracy, 4),
|
|
394
|
+
"auroc_l2_norm": round(auroc_norm, 4) if auroc_norm is not None else None,
|
|
395
|
+
"auroc_logit_entropy": round(auroc_entropy, 4)
|
|
396
|
+
if auroc_entropy is not None
|
|
397
|
+
else None,
|
|
398
|
+
"norm_threshold_tau": round(tau, 4),
|
|
399
|
+
"balanced_acc_at_tau": round(balanced_acc, 4),
|
|
400
|
+
"mean_norm_correct": round(mean_norm_corr, 4),
|
|
401
|
+
"mean_norm_incorrect": round(mean_norm_incorr, 4),
|
|
402
|
+
"mean_entropy_correct": round(mean_ent_corr, 4),
|
|
403
|
+
"mean_entropy_incorrect": round(mean_ent_incorr, 4),
|
|
404
|
+
},
|
|
405
|
+
raw_outputs={"per_sample": per_sample},
|
|
406
|
+
metadata={
|
|
407
|
+
"target_layer": target_layer,
|
|
408
|
+
"num_samples": n,
|
|
409
|
+
"seed": self.seed,
|
|
410
|
+
"answer_cue": self.answer_cue,
|
|
411
|
+
"se_literature_baseline_auroc": "0.65-0.75 (Kuhn et al. 2023)",
|
|
412
|
+
},
|
|
413
|
+
)
|