Skip to content

Commit dedc0ed

Browse files
andre15silvaTuxTuclaude
authored
feat: weighted loss, AUC, ECE (#17)
* feat(probe): add weighted cross-entropy loss, sweep-to-final pipeline, and vectorized binning - Add --loss {cross_entropy,weighted_cross_entropy} with per-bin adaptive class weights w_pos = λ · n_neg/n_pos to counter class imbalance - Add --pos-weight λ (log-uniform swept in [0.1, 10.0]) as tunable multiplier - Add sweep --then-final to automatically fetch best W&B config and train all layers in one command - Add final --from-sweep to load best HPs from an existing sweep via W&B API - Add fetch_best_sweep_config() to query sweep.best_run() programmatically - Vectorize bin_mask computation: pre-compute bin_ids tensor instead of per-element .item() Python loop (O(N × n_bins) → O(N)) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(readme): document weighted CE loss, sweep→final pipeline, and --from-sweep Add sections for --loss {cross_entropy,weighted_cross_entropy}, --pos-weight, --then-final (one-command sweep→final), and --from-sweep (auto-load best HPs). Update SLURM examples to show new flags. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(probe): add fixed_params to sweep, ECE metric, and extend dashboard exports - create_sweep() now accepts optional fixed_params dict to pin specific HPs while sweeping the remaining ones (e.g., sweep only pos_weight) - sweep subcommand gains --fixed JSON arg for CLI-driven fixed params - Added Expected Calibration Error (ECE) to ProbeResult and training loop - Dashboard exports now include test_auc, val_auc, test_ece, val_ece - Bumped SLURM sweep time limit from 2h to 4h for sweep+final chains Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(probe): save model weights after final training, add ECE to tests - train_probe_layer() now returns (results, weights) tuple — weights dict contains per-bin state_dict and training mean for inference - run_final() saves weights.pt alongside results.pt - run_sweep() discards weights (sweeps don't need them) - Fixed tests for new ProbeResult fields (val_ece, test_ece) and new return type of train_probe_layer() Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(probe): require explicit n_bins=1 for pooled training with n_eval_bins Replace implicit train_n_bins override (1 if n_eval_bins set) with explicit validation: n_eval_bins must equal n_bins or n_bins must be 1. Update CLI help text to reflect the new constraint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Apply suggestion from @andre15silva --------- Co-authored-by: Tux <tux.tu@outlook.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 3d7b934 commit dedc0ed

8 files changed

Lines changed: 231 additions & 49 deletions

File tree

README.md

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ run_build_cache.py concatenate .pt files into per-(layer, probe) tensors
2222
→ cache/<run_id>/<probe>_layer<n>.pt
2323
2424
25-
run_probe.py sweep W&B hyperparameter sweep → pick best lr/wd/batch-size
25+
run_probe.py sweep W&B hyperparameter sweep → pick best lr/wd/batch-size/loss
2626
run_probe.py final train linear probe on best hparams → W&B run with metrics
27+
(or pass --from-sweep to auto-load best HPs from a sweep)
2728
2829
2930
run_figures.py accuracy heatmaps per layer × relative-position bin
@@ -192,29 +193,69 @@ uv run python run_build_cache.py \
192193
--probe will_be_correct
193194
```
194195

195-
### 3. Probe sweep (W&B)
196+
### 3. Probe training
197+
198+
Probe training has three modes: **manual**, **sweep-only**, and **sweep→final** (one-shot).
199+
200+
#### Loss function
201+
202+
The `--loss` flag selects the objective:
203+
204+
| `--loss` | Behavior |
205+
|---|---|
206+
| `cross_entropy` (default) | Standard `CrossEntropyLoss` — no class weighting |
207+
| `weighted_cross_entropy` | `CrossEntropyLoss(weight=[1.0, λ · n_neg/n_pos])` — adaptive per-bin class balancing |
208+
209+
When using `weighted_cross_entropy`, the `--pos-weight` λ parameter (default 1.0) multiplies the inverse class ratio:
210+
- λ = 1.0 → exactly balanced classes per bin
211+
- λ > 1.0 → penalize false negatives more (up-weight positives)
212+
- λ < 1.0 → penalize false positives more (down-weight positives)
213+
214+
#### Mode A: Manual (specify all HPs)
196215

197216
```bash
198-
uv run python run_probe.py sweep \
199-
--run-id my_run \
200-
--probe will_be_correct \
201-
--model-config configs/models/qwen3_8b.yaml
217+
uv run python run_probe.py \
218+
--run-id my_run --probe will_be_correct \
219+
--model-config configs/models/qwen3_8b.yaml \
220+
final \
221+
--lr 1e-3 --weight-decay 1e-4 --batch-size 512 --patience 10 \
222+
--loss weighted_cross_entropy --pos-weight 1.0
202223
```
203224

204-
### 4. Probe final run
225+
#### Mode B: Sweep only
205226

206227
```bash
207-
uv run python run_probe.py final \
208-
--run-id my_run \
209-
--probe will_be_correct \
228+
uv run python run_probe.py \
229+
--run-id my_run --probe will_be_correct \
210230
--model-config configs/models/qwen3_8b.yaml \
211-
--lr 1e-3 \
212-
--weight-decay 1e-4 \
213-
--batch-size 512 \
214-
--patience 10
231+
sweep --count 50
215232
```
216233

217-
### 5. Figures
234+
The sweep explores `lr`, `weight_decay`, `batch_size`, `patience`, `loss` (cross_entropy vs weighted_cross_entropy), and `pos_weight` (log-uniform in [0.1, 10.0]) via Bayesian optimisation. Copy the best HPs from the W&B dashboard.
235+
236+
#### Mode C: Sweep → final (fully automatic)
237+
238+
```bash
239+
uv run python run_probe.py \
240+
--run-id my_run --probe will_be_correct \
241+
--model-config configs/models/qwen3_8b.yaml \
242+
sweep --count 50 --then-final
243+
```
244+
245+
Runs the sweep, waits for completion, fetches the best config from W&B via `fetch_best_sweep_config()`, and trains on all probe layers — no manual copy-paste.
246+
247+
#### Mode D: Final from an existing sweep
248+
249+
```bash
250+
uv run python run_probe.py \
251+
--run-id my_run --probe will_be_correct \
252+
--model-config configs/models/qwen3_8b.yaml \
253+
final --from-sweep abc123xyz
254+
```
255+
256+
Queries `sweep.best_run()` via the W&B API to auto-populate `lr`, `weight_decay`, `batch_size`, `patience`, `loss`, and `pos_weight`.
257+
258+
### 4. Figures
218259

219260
```bash
220261
uv run python run_figures.py \
@@ -223,7 +264,7 @@ uv run python run_figures.py \
223264
--model-config configs/models/qwen3_8b.yaml
224265
```
225266

226-
### 6. Export dashboard
267+
### 5. Export dashboard
227268

228269
```bash
229270
uv run python run_export_dashboard.py \
@@ -257,10 +298,18 @@ sbatch --array=0-29 slurm/probe_sweep.sh \
257298
--run-id my_run --probe will_be_correct \
258299
--model-config configs/models/qwen3_8b.yaml
259300

301+
# Final with manual HPs:
302+
sbatch slurm/probe_final.sh \
303+
--run-id my_run --probe will_be_correct \
304+
--model-config configs/models/qwen3_8b.yaml \
305+
--lr 1e-3 --weight-decay 1e-4 --batch-size 512 --patience 10 \
306+
--loss weighted_cross_entropy --pos-weight 1.0
307+
308+
# Final auto-loaded from sweep:
260309
sbatch slurm/probe_final.sh \
261310
--run-id my_run --probe will_be_correct \
262311
--model-config configs/models/qwen3_8b.yaml \
263-
--lr 1e-3 --weight-decay 1e-4 --batch-size 512 --patience 10
312+
--from-sweep abc123xyz
264313

265314
sbatch slurm/figures.sh --run-id my_run --probe will_be_correct \
266315
--model-config configs/models/qwen3_8b.yaml

run_probe.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import argparse
2+
import json
23
from src.configs import ModelConfig, load_config
34
from src.probe import run_sweep, run_final
45

@@ -28,12 +29,22 @@ def main():
2829
sweep_p = subparsers.add_parser("sweep")
2930
sweep_p.add_argument("--sweep-id", default=None, help="Join an existing W&B sweep instead of creating a new one")
3031
sweep_p.add_argument("--count", type=int, default=None, help="Max number of runs this agent will execute")
32+
sweep_p.add_argument("--then-final", action="store_true",
33+
help="After sweep, automatically train on all layers with best HPs")
34+
sweep_p.add_argument("--fixed", type=json.loads, default=None,
35+
help='JSON dict of fixed HP values, e.g. \'{"lr": 0.001, "loss": "weighted_cross_entropy"}\'')
3136

3237
final_p = subparsers.add_parser("final")
33-
final_p.add_argument("--lr", type=float, required=True)
34-
final_p.add_argument("--weight-decay", type=float, required=True)
35-
final_p.add_argument("--batch-size", type=int, required=True)
36-
final_p.add_argument("--patience", type=int, required=True)
38+
final_p.add_argument("--lr", type=float, default=None)
39+
final_p.add_argument("--weight-decay", type=float, default=None)
40+
final_p.add_argument("--batch-size", type=int, default=None)
41+
final_p.add_argument("--patience", type=int, default=None)
42+
final_p.add_argument("--from-sweep", default=None,
43+
help="Sweep ID to load best hyperparameters from")
44+
final_p.add_argument("--loss", choices=["cross_entropy", "weighted_cross_entropy"],
45+
default="cross_entropy")
46+
final_p.add_argument("--pos-weight", type=float, default=1.0,
47+
help="Positive class weight multiplier (only used with --loss weighted_cross_entropy)")
3748

3849
args = parser.parse_args()
3950
if args.n_eval_bins is not None and args.n_eval_bins != args.n_bins and args.n_bins != 1:
@@ -55,6 +66,9 @@ def main():
5566
probe_arch=args.probe_arch,
5667
n_eval_bins=args.n_eval_bins,
5768
eval_bin_axis=args.eval_bin_axis,
69+
then_final=args.then_final,
70+
results_dir=args.results_dir,
71+
fixed_params=args.fixed,
5872
shuffle_labels=args.shuffle_labels,
5973
)
6074
else:
@@ -67,11 +81,14 @@ def main():
6781
batch_size=args.batch_size,
6882
patience=args.patience,
6983
seed=args.seed,
84+
sweep_id=args.from_sweep,
7085
cache_dir=args.cache_dir,
7186
cache_run_id=cache_run_id,
7287
results_dir=args.results_dir,
7388
n_bins=args.n_bins,
7489
probe_arch=args.probe_arch,
90+
loss=args.loss,
91+
pos_weight=args.pos_weight,
7592
n_eval_bins=args.n_eval_bins,
7693
eval_bin_axis=args.eval_bin_axis,
7794
shuffle_labels=args.shuffle_labels,

slurm/probe_sweep.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#SBATCH -p berzelius-cpu
44
#SBATCH -n 8
55
#SBATCH --mem=150G
6-
#SBATCH -t 02:00:00
6+
#SBATCH -t 04:00:00
77
#SBATCH -o logs/probe_sweep_%j.out
88
#SBATCH -e logs/probe_sweep_%j.err
99

src/export_dashboard.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ def export_dashboard(
146146
probe_results[probe_name][str(layer_idx)][str(bin_idx)] = {
147147
"test_acc": r.test_acc if hasattr(r, "test_acc") else r["test_acc"],
148148
"val_acc": r.val_acc if hasattr(r, "val_acc") else r["val_acc"],
149+
"test_auc": r.test_auc if hasattr(r, "test_auc") else r.get("test_auc", None),
150+
"val_auc": r.val_auc if hasattr(r, "val_auc") else r.get("val_auc", None),
151+
"test_ece": r.test_ece if hasattr(r, "test_ece") else r.get("test_ece", None),
152+
"val_ece": r.val_ece if hasattr(r, "val_ece") else r.get("val_ece", None),
149153
"n_train": r.n_train if hasattr(r, "n_train") else r["n_train"],
150154
"n_val": r.n_val if hasattr(r, "n_val") else r["n_val"],
151155
"n_test": r.n_test if hasattr(r, "n_test") else r["n_test"],

src/export_swebench_dashboard.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,8 @@ def _load_labels_for(f: Path) -> dict:
290290
"val_acc": r.val_acc if hasattr(r, "val_acc") else r["val_acc"],
291291
"test_auc": r.test_auc if hasattr(r, "test_auc") else r.get("test_auc"),
292292
"val_auc": r.val_auc if hasattr(r, "val_auc") else r.get("val_auc"),
293+
"test_ece": r.test_ece if hasattr(r, "test_ece") else r.get("test_ece"),
294+
"val_ece": r.val_ece if hasattr(r, "val_ece") else r.get("val_ece"),
293295
"n_train": r.n_train if hasattr(r, "n_train") else r["n_train"],
294296
"n_val": r.n_val if hasattr(r, "n_val") else r["n_val"],
295297
"n_test": r.n_test if hasattr(r, "n_test") else r["n_test"],

0 commit comments

Comments
 (0)