Johnyquest7 commited on
Commit
45af8e1
·
verified ·
1 Parent(s): 1e10883

Add full reproducible thyroid ResNet-18 experiment: weights, scripts, configs, calibration, locked threshold, test eval w/ CIs, figures, data exploration, README, LOG

Browse files
Files changed (44) hide show
  1. .gitattributes +6 -0
  2. LOG.md +185 -0
  3. README.md +198 -1
  4. configs/calibration.json +13 -0
  5. configs/env_info.json +19 -0
  6. configs/final_config.yaml +45 -0
  7. configs/preprocess.json +17 -0
  8. configs/threshold.json +14 -0
  9. configs/winning_run_command_line.txt +1 -0
  10. configs/winning_run_resolved_config.json +49 -0
  11. data_exploration_report.md +101 -0
  12. evaluate.py +123 -0
  13. evaluate_external.py +217 -0
  14. explore_data.py +313 -0
  15. final_model.pt +3 -0
  16. finalize.py +277 -0
  17. requirements.txt +15 -0
  18. results/figures/class_distribution.png +0 -0
  19. results/figures/grid_Test_Benign.png +3 -0
  20. results/figures/grid_Test_Malignant.png +3 -0
  21. results/figures/grid_Train_Benign.png +3 -0
  22. results/figures/grid_Train_Malignant.png +3 -0
  23. results/figures/grid_Valid_Benign.png +3 -0
  24. results/figures/grid_Valid_Malignant.png +3 -0
  25. results/figures/intensity_distribution.png +0 -0
  26. results/figures/test_calibration.png +0 -0
  27. results/figures/test_confusion_counts.png +0 -0
  28. results/figures/test_confusion_normalized.png +0 -0
  29. results/figures/test_pr.png +0 -0
  30. results/figures/test_roc.png +0 -0
  31. results/figures/valid_calibration.png +0 -0
  32. results/final_results.json +115 -0
  33. results/tables/class_distribution.csv +5 -0
  34. results/tables/data_exploration_summary.json +60 -0
  35. results/tables/sweep_leaderboard.json +311 -0
  36. results/tables/sweep_results.json +311 -0
  37. results/tables/test_metrics_with_ci.csv +10 -0
  38. results/tables/test_metrics_with_ci.md +20 -0
  39. results/test_predictions.csv +1001 -0
  40. results/valid_predictions.csv +501 -0
  41. results/winning_run_history.json +212 -0
  42. sweep.py +122 -0
  43. thyroid_lib.py +539 -0
  44. train.py +295 -0
.gitattributes CHANGED
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ results/figures/grid_Test_Benign.png filter=lfs diff=lfs merge=lfs -text
37
+ results/figures/grid_Test_Malignant.png filter=lfs diff=lfs merge=lfs -text
38
+ results/figures/grid_Train_Benign.png filter=lfs diff=lfs merge=lfs -text
39
+ results/figures/grid_Train_Malignant.png filter=lfs diff=lfs merge=lfs -text
40
+ results/figures/grid_Valid_Benign.png filter=lfs diff=lfs merge=lfs -text
41
+ results/figures/grid_Valid_Malignant.png filter=lfs diff=lfs merge=lfs -text
LOG.md ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Experiment Log — Agentic Thyroid ResNet-18
2
+
3
+ Chronological, decision-by-decision record for reproducibility and journal review.
4
+
5
+ ## Provenance
6
+
7
+ - **Experiment date (UTC):** 2026-06-05
8
+ - **Dataset:** `Johnyquest7/TN5000-thyroid-nodule-classification`
9
+ - Commit SHA: `73d6c0713a89c8c07125fe6ffc5956be60e9853d`
10
+ - Loaded **directly from the Train/Valid/Test folder structure** (NOT the
11
+ datasets-viewer flattened `train` config, which merges all 5,000 rows),
12
+ so the predefined splits are respected.
13
+ - **Compute:** Hugging Face GPU sandbox, NVIDIA A10G (24 GB), CUDA 13.0, cuDNN 9.2.
14
+ - **Key packages:** torch 2.12.0+cu130, torchvision 0.27.0+cu130, timm 1.0.27,
15
+ scikit-learn 1.9.0, numpy 2.4.6, trackio 0.26.0 (see `configs/env_info.json`).
16
+ - **Global seed:** 42. Strict determinism: `torch.use_deterministic_algorithms(True)`,
17
+ cuDNN deterministic, `CUBLAS_WORKSPACE_CONFIG=:4096:8`, seeded DataLoader workers.
18
+ - **Positive class:** Malignant (label 1).
19
+ - **Experiment tracking:** Trackio project `agentic_thyroid_resnet18`, dashboard
20
+ Space `Johnyquest7/Trakio_agentic_thyroid`, storage dataset
21
+ `Johnyquest7/Trakio_agentic_thyroid_dataset`.
22
+
23
+ ## Exact split usage
24
+
25
+ | Split | n | Use |
26
+ |-------|--:|-----|
27
+ | Train | 3,500 | **Training only.** |
28
+ | Valid | 500 | **Model selection (val AUROC), calibration, threshold selection.** |
29
+ | Test | 1,000 | **Final locked evaluation, exactly once**, after model+calibration+threshold were frozen. |
30
+
31
+ ## Class distribution
32
+
33
+ | Split | Benign | Malignant | Malignant % | Ratio (M:B) |
34
+ |-------|-------:|----------:|------------:|------------:|
35
+ | Train | 1,032 | 2,468 | 70.5% | 2.39 : 1 |
36
+ | Valid | 125 | 375 | 75.0% | 3.00 : 1 |
37
+ | Test | 269 | 731 | 73.1% | 2.72 : 1 |
38
+
39
+ Data audit (`data_exploration_report.md`): **0 corrupt images**, all 224×224 RGB
40
+ PNG; **0 cross-split exact-pixel duplicates**, **0 filename-ID overlaps**, **0
41
+ label conflicts**; per-split mean intensity ≈ 81.4 (std ≈ 19.4) — no distribution
42
+ shift. Conclusion: **no detectable leakage; splits are clean and separate.**
43
+
44
+ ## Literature-informed augmentation rationale
45
+
46
+ Augmentations were restricted to medically plausible B-mode ultrasound transforms
47
+ (MediAug arXiv:2504.18983 + thyroid-US practice):
48
+ - **Kept:** horizontal flip (thyroid is bilaterally symmetric), small rotation
49
+ (≤10°), mild affine translate (5%) / scale (0.9–1.1), mild brightness/contrast
50
+ (±15%, simulates gain/TGC), light Gaussian blur, and (in the `medical_strong`
51
+ ablation) narrow random-resized-crop (0.8–1.0) + mild speckle noise.
52
+ - **Explicitly avoided:** vertical flip (US depth axis is physically meaningful),
53
+ large rotation/shear (distorts taller-than-wide / margin morphology — TI-RADS
54
+ malignancy cues), aggressive crop (<0.8, can remove the nodule), and any
55
+ color/HSV jitter (images are grayscale).
56
+
57
+ Ablation result (val AUROC): `medical_default` **0.9712–0.9756** > `flip_only`
58
+ **0.9637** > `medical_strong` **0.9609**. The literature-default policy won.
59
+
60
+ ## Model variants tried
61
+
62
+ - `torchvision` ResNet-18 (ImageNet1K_V1, bilinear/256→224 preprocessing).
63
+ - `timm:resnet18.a1_in1k` (A1 recipe, bicubic, crop_pct 0.95) — **selected**.
64
+ - `timm:resnet18.a2_in1k` (A2 recipe).
65
+ - Fine-tune depth: full fine-tune vs freeze stem+layer1 (`freeze_stage=1`).
66
+
67
+ ## Hyperparameter sweep (14 trials, one-factor-at-a-time around a literature-informed center)
68
+
69
+ Center: timm a1, lr 2e-4, wd 1e-4, bs 32, `medical_default`, `pos_weight`,
70
+ full fine-tune, BCE, AdamW, cosine, ≤40 epochs, early-stop(8). All runs logged to
71
+ Trackio. **Selection metric: validation AUROC.** All 14 trials completed (rc=0).
72
+
73
+ | Rank | Run | Change vs center | Val AUROC | Best epoch |
74
+ |-----:|-----|------------------|----------:|-----------:|
75
+ | 1 | **c12_loss_focal** | **focal γ=1.0, imbalance=none** | **0.9756** | 6 |
76
+ | 2 | c09_imb_none | imbalance=none (BCE) | 0.9739 | 6 |
77
+ | 3 | c01_backbone_torchvision | torchvision backbone | 0.9731 | 7 |
78
+ | 4 | c03_lr_1e-4 | lr 1e-4 | 0.9721 | 11 |
79
+ | 5 | c06_bs_64 | batch size 64 | 0.9717 | 8 |
80
+ | 6 | c05_wd_1e-3 | weight decay 1e-3 | 0.9712 | 9 |
81
+ | 6 | c00_center_a1 | center config | 0.9712 | 6 |
82
+ | 8 | c10_imb_sampler | weighted sampler | 0.9697 | 13 |
83
+ | 9 | c02_backbone_a2 | a2 backbone | 0.9693 | 6 |
84
+ | 10 | c04_lr_5e-4 | lr 5e-4 | 0.9675 | 9 |
85
+ | 11 | c11_freeze1 | freeze stem+layer1 | 0.9672 | 6 |
86
+ | 12 | c13_lr1e-4_wd1e-3_drop | lr1e-4+wd1e-3+dropout0.2 | 0.9657 | 11 |
87
+ | 13 | c07_aug_flip_only | flip-only aug | 0.9637 | 6 |
88
+ | 14 | c08_aug_strong | strong aug | 0.9609 | 8 |
89
+
90
+ Findings: (1) For this mild (~70/30) imbalance, **focal loss (γ=1.0) and no extra
91
+ reweighting beat class-weighted BCE and weighted sampling** — heavy reweighting
92
+ slightly hurt, consistent with the literature. (2) `medical_default` augmentation
93
+ is the sweet spot. (3) **Full fine-tune > freezing.** (4) Backbones were close
94
+ (a1 ≈ torchvision ≈ a2). Full per-run details: `results/tables/sweep_leaderboard.json`.
95
+
96
+ No excessive trial count was used (14 one-factor trials) to avoid overfitting the
97
+ 500-image validation set.
98
+
99
+ ## Selected run
100
+
101
+ **c12_loss_focal** — `timm:resnet18.a1_in1k`, focal loss (γ=1.0, α=0.5),
102
+ imbalance=none, AdamW lr 2e-4 / wd 1e-4, batch 32, `medical_default` aug, full
103
+ fine-tune, cosine schedule, best epoch 6, **validation AUROC 0.9756**. Selected
104
+ **purely on validation AUROC**, before any test access. Config:
105
+ `configs/final_config.yaml`; weights: `final_model.pt`.
106
+
107
+ ## Calibration decision
108
+
109
+ Assessed on validation. **Temperature scaling** (single parameter, LBFGS on NLL)
110
+ gave **T = 0.5646**. Validation ECE 0.0833 → **0.0308**, Brier 0.0592 → 0.0525,
111
+ AUROC unchanged (0.9756; temperature scaling is monotonic ⇒ discrimination
112
+ preserved). **Decision: use calibrated probabilities** for thresholding and test
113
+ reporting. Parameters + before/after metrics: `configs/calibration.json`.
114
+ Reliability diagrams: `results/figures/{valid,test}_calibration.png`.
115
+
116
+ ## Threshold selection decision
117
+
118
+ On the validation set, using calibrated probabilities, the primary threshold was
119
+ the **highest-specificity threshold achieving sensitivity ≥ 0.95** (sensitivity-
120
+ prioritized, clinically motivated). Target was achievable.
121
+
122
+ - **Locked threshold = 0.7113** → validation sensitivity **0.952**, specificity **0.896**.
123
+ - Secondary reference (Youden's J): coincided at 0.7113 here.
124
+
125
+ Threshold **locked before** the test set was evaluated. Config: `configs/threshold.json`.
126
+
127
+ ## Final locked threshold
128
+
129
+ **0.7113139** (on calibrated malignancy probability).
130
+
131
+ ## Final test results with 95% CIs
132
+
133
+ Test split (n=1000), calibrated probabilities + locked threshold. CIs: stratified
134
+ bootstrap, 2000 resamples, seed=42.
135
+
136
+ | Metric | Point | 95% CI |
137
+ |--------|------:|:------:|
138
+ | AUROC | 0.9371 | [0.9202, 0.9528] |
139
+ | Sensitivity | 0.9042 | [0.8824, 0.9248] |
140
+ | Specificity | 0.7955 | [0.7435, 0.8439] |
141
+ | PPV | 0.9232 | [0.9054, 0.9401] |
142
+ | NPV | 0.7535 | [0.7123, 0.7979] |
143
+ | Accuracy | 0.8750 | [0.8540, 0.8950] |
144
+ | F1 | 0.9136 | [0.8991, 0.9278] |
145
+ | Brier | 0.0823 | — |
146
+ | ECE | 0.0314 | — |
147
+
148
+ Confusion matrix (Test): TN=214, FP=55, FN=70, TP=661.
149
+ Tables: `results/tables/test_metrics_with_ci.{md,csv}`; per-image predictions:
150
+ `results/{valid,test}_predictions.csv`.
151
+
152
+ ## Failed / weaker runs
153
+
154
+ No run errored (all rc=0). Weakest configurations: `medical_strong` augmentation
155
+ (0.9609) and `flip_only` (0.9637) — both under the `medical_default` baseline,
156
+ confirming the augmentation policy choice. Earlier trackio smoke runs
157
+ (`smoke_trackio_test*`, `connectivity_check`, `dataset_pin_check`) are
158
+ infrastructure-validation runs, not experiments. Early Trackio Space creation
159
+ produced transient 401 `/volumes` warnings until a persistent `dataset_id`
160
+ (`Johnyquest7/Trakio_agentic_thyroid_dataset`) was pinned; resolved thereafter.
161
+
162
+ ## Limitations
163
+
164
+ - Single-source dataset; cropped-ROI inputs; mild class imbalance.
165
+ - The ≥0.95-sensitivity operating point set on validation yielded **0.904**
166
+ sensitivity on test — the operating point does not transfer perfectly; ~10% of
167
+ malignant nodules are missed at the locked threshold. Local threshold
168
+ re-calibration is advisable before any use.
169
+ - Leakage checks (exact-pixel hash + filename-ID overlap) are exhaustive for the
170
+ available signal but cannot exclude same-patient/near-duplicate leakage if such
171
+ structure exists upstream in TN5000.
172
+
173
+ ## External validation — NOT yet performed
174
+
175
+ No external/independent dataset has been evaluated. `evaluate_external.py` is
176
+ provided to run the locked model (same preprocessing, calibration T, and locked
177
+ threshold) on a future external set (folder or CSV format). External, ideally
178
+ prospective and multi-site, validation is **required** before any clinical use.
179
+
180
+ ## Test-set integrity statement
181
+
182
+ > The Test split was evaluated **exactly once**, and **only after** the model was
183
+ > selected (by validation AUROC), calibrated (temperature scaling on validation),
184
+ > and the decision threshold was locked (on validation). No hyperparameter,
185
+ > calibration, or threshold decision used the test set.
README.md CHANGED
@@ -1,3 +1,200 @@
1
  ---
2
- license: mit
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ license: cc-by-nc-4.0
3
+ tags:
4
+ - medical-imaging
5
+ - thyroid
6
+ - ultrasound
7
+ - image-classification
8
+ - resnet18
9
+ - calibration
10
+ pipeline_tag: image-classification
11
  ---
12
+
13
+ # Agentic Thyroid ResNet-18 — Ultrasound Nodule Malignancy Classifier
14
+
15
+ > ⚠️ **RESEARCH USE ONLY — NOT FOR CLINICAL USE.** This model is a research
16
+ > artifact trained on a single retrospective dataset. It has **not** been
17
+ > externally validated and **must not** be used for diagnosis, screening, or any
18
+ > clinical decision-making. External, prospective validation is required before
19
+ > any clinical consideration.
20
+
21
+ A ResNet-18 binary classifier that predicts the probability that a cropped
22
+ thyroid ultrasound nodule image is **malignant** (positive class) vs **benign**.
23
+ Built for a reproducible, publication-oriented experiment with proper
24
+ calibration and a sensitivity-prioritized, validation-locked decision threshold.
25
+
26
+ - **Backbone:** `timm` ResNet-18, A1 ImageNet-1k recipe (`resnet18.a1_in1k`), full fine-tune
27
+ - **Selected by:** validation AUROC (14-trial sweep; winner val AUROC **0.9756**)
28
+ - **Calibration:** temperature scaling (T = 0.5646), fit on validation
29
+ - **Locked threshold:** **0.7113** (highest-specificity threshold with validation sensitivity ≥ 0.95)
30
+
31
+ ## Intended use
32
+
33
+ - **Intended:** methodological research, benchmarking, and as a baseline for
34
+ thyroid ultrasound malignancy classification studies.
35
+ - **Out of scope:** any clinical, diagnostic, triage, or screening use; use on
36
+ images acquired/preprocessed differently from the training data without
37
+ re-validation; use on non-thyroid or non-ultrasound images.
38
+
39
+ ## Dataset
40
+
41
+ - **Source:** [`Johnyquest7/TN5000-thyroid-nodule-classification`](https://huggingface.co/datasets/Johnyquest7/TN5000-thyroid-nodule-classification)
42
+ (derived from TN5000; nodule ROI cropped to 224×224 RGB PNG).
43
+ - **Splits (kept strictly separate):** Train 3,500 · Valid 500 · Test 1,000.
44
+ - **Labels:** `0 = Benign`, `1 = Malignant` (positive class = Malignant).
45
+ - **Class balance:** ~70–75% malignant in every split (mild imbalance). See
46
+ [`data_exploration_report.md`](data_exploration_report.md): **0 corrupt images,
47
+ 0 cross-split pixel duplicates, 0 filename-ID overlaps** → no detectable leakage.
48
+
49
+ ## Label definitions
50
+
51
+ | Label | Class | Meaning |
52
+ |------:|-------|---------|
53
+ | 0 | Benign | Non-malignant thyroid nodule |
54
+ | 1 | Malignant | Malignant thyroid nodule (positive class) |
55
+
56
+ ## Preprocessing (locked — `configs/preprocess.json`)
57
+
58
+ Deterministic eval/inference path (no augmentation):
59
+ 1. Resize to **224×224** (bicubic; the timm A1 data config).
60
+ 2. `ToTensor()`.
61
+ 3. Normalize with ImageNet mean `[0.485, 0.456, 0.406]`, std `[0.229, 0.224, 0.225]`.
62
+
63
+ Grayscale ultrasound images are loaded as 3-channel RGB.
64
+
65
+ ## Model architecture
66
+
67
+ ResNet-18 with a single-logit binary head (`num_classes=1`); sigmoid → probability
68
+ of malignancy. ~11.2M parameters. Trained from ImageNet-1k weights (full fine-tune).
69
+
70
+ ## Training procedure (final / winning config)
71
+
72
+ | Setting | Value |
73
+ |---|---|
74
+ | Backbone | `timm:resnet18.a1_in1k`, full fine-tune |
75
+ | Loss | Focal loss (γ=1.0, α=0.5) |
76
+ | Class-imbalance handling | none beyond focal (mild imbalance; preserves calibration) |
77
+ | Augmentation | `medical_default`: hflip, mild affine (rot ≤10°, translate 5%, scale 0.9–1.1), mild brightness/contrast (±15%), light Gaussian blur (p=0.2) |
78
+ | Optimizer | AdamW, lr 2e-4, weight decay 1e-4 |
79
+ | Scheduler | Cosine annealing, 2-epoch warmup |
80
+ | Batch size | 32 |
81
+ | Epochs | ≤40, early stopping on val AUROC (patience 8) |
82
+ | Mixed precision | yes (fp16 autocast) |
83
+ | Seed | 42 (strict determinism; `CUBLAS_WORKSPACE_CONFIG=:4096:8`, cuDNN deterministic) |
84
+ | Best epoch | 6 |
85
+
86
+ Augmentations were chosen to be medically plausible for B-mode ultrasound
87
+ (no vertical flip, no large rotation/shear, no aggressive crop, no color/HSV
88
+ jitter), informed by MediAug (arXiv:2504.18983) and thyroid-US best practice.
89
+ The augmentation ablation confirmed `medical_default` (0.9712–0.9756 val AUROC)
90
+ outperforms both `flip_only` (0.9637) and `medical_strong` (0.9609).
91
+
92
+ Environment: torch 2.12.0+cu130, torchvision 0.27.0+cu130, timm 1.0.27,
93
+ scikit-learn 1.9.0, numpy 2.4.6, NVIDIA A10G (CUDA 13.0, cuDNN 9.2).
94
+ Full versions in `configs/env_info.json`.
95
+
96
+ ## Validation threshold strategy
97
+
98
+ After selecting the model by validation AUROC and calibrating on validation, the
99
+ decision threshold was chosen on the **validation set** as the **highest-specificity
100
+ threshold achieving sensitivity ≥ 0.95** (clinically sensitivity-prioritized).
101
+ Youden's J is reported as a secondary reference. The threshold (**0.7113**) was
102
+ **locked before** the test set was touched.
103
+
104
+ - Validation @ locked threshold: sensitivity **0.952**, specificity **0.896**.
105
+
106
+ ## Test performance (locked model + locked threshold, n=1000)
107
+
108
+ CI method: stratified bootstrap, 2000 resamples, seed=42. Probabilities calibrated.
109
+
110
+ | Metric | Point estimate | 95% CI |
111
+ |--------|---------------:|:------:|
112
+ | AUROC | **0.9371** | [0.9202, 0.9528] |
113
+ | Sensitivity | 0.9042 | [0.8824, 0.9248] |
114
+ | Specificity | 0.7955 | [0.7435, 0.8439] |
115
+ | PPV | 0.9232 | [0.9054, 0.9401] |
116
+ | NPV | 0.7535 | [0.7123, 0.7979] |
117
+ | Accuracy | 0.8750 | [0.8540, 0.8950] |
118
+ | F1 | 0.9136 | [0.8991, 0.9278] |
119
+ | Brier | 0.0823 | — |
120
+ | ECE | 0.0314 | — |
121
+
122
+ ## Calibration results
123
+
124
+ Temperature scaling (T=0.5646) on validation reduced **validation ECE from 0.0833
125
+ → 0.0308** and Brier from 0.0592 → 0.0525 with AUROC unchanged (monotonic). On
126
+ the test set, calibrated ECE is **0.0314** (well calibrated). Reliability diagrams:
127
+ `results/figures/valid_calibration.png`, `results/figures/test_calibration.png`.
128
+
129
+ ## Confusion matrix (Test)
130
+
131
+ TN = 214 · FP = 55 · FN = 70 · TP = 661
132
+
133
+ ![Test confusion matrix](results/figures/test_confusion_counts.png)
134
+
135
+ Additional figures: `results/figures/test_roc.png`,
136
+ `results/figures/test_pr.png`, `results/figures/test_confusion_normalized.png`.
137
+
138
+ ## Files in this repo
139
+
140
+ ```
141
+ final_model.pt # locked weights + backbone/preprocess metadata
142
+ configs/final_config.yaml # single-command training config
143
+ configs/preprocess.json # locked preprocessing
144
+ configs/calibration.json # temperature scaling parameter + before/after metrics
145
+ configs/threshold.json # locked decision threshold + selection method
146
+ configs/env_info.json # exact package/hardware/CUDA versions
147
+ train.py evaluate.py evaluate_external.py explore_data.py sweep.py finalize.py
148
+ thyroid_lib.py # shared preprocessing/model/calibration/metrics
149
+ requirements.txt
150
+ data_exploration_report.md # full data audit (counts, leakage, intensity, grids)
151
+ LOG.md # chronological experiment log
152
+ results/ # figures, tables (incl. CIs + sweep leaderboard), per-image CSVs
153
+ ```
154
+
155
+ ## Reproduce
156
+
157
+ ```bash
158
+ pip install -r requirements.txt
159
+ python explore_data.py --dataset_id Johnyquest7/TN5000-thyroid-nodule-classification
160
+ python train.py --config configs/final_config.yaml
161
+ python evaluate.py --split test --config configs/final_config.yaml
162
+ ```
163
+
164
+ Evaluate a future external dataset with the same preprocessing/calibration/threshold:
165
+
166
+ ```bash
167
+ python evaluate_external.py \
168
+ --model_repo Johnyquest7/agentic_thyroid_model \
169
+ --data_dir /path/to/external_dataset \
170
+ --output_dir external_results
171
+ # external dataset = folder with Benign/ Malignant/ subfolders, OR add --csv labels.csv
172
+ ```
173
+
174
+ ## Limitations, bias, and leakage concerns
175
+
176
+ - **Single-source dataset; no external validation.** Performance on data from
177
+ other scanners, institutions, or populations is unknown and likely lower.
178
+ - **Cropped-ROI inputs.** The model expects nodule-cropped images like TN5000;
179
+ whole-frame ultrasound will be out of distribution.
180
+ - **Sensitivity gap at deployment threshold.** The threshold targets ≥0.95
181
+ sensitivity on validation; on the test set sensitivity was 0.904 — i.e. the
182
+ operating point does not perfectly transfer, and ~10% of malignant nodules
183
+ were missed at this threshold. Threshold re-calibration on local data is
184
+ advisable before any use.
185
+ - **Label/selection bias.** Labels and cohort composition reflect the source
186
+ dataset's referral and pathology-confirmation process.
187
+ - **Leakage checks were exhaustive within the available signal** (exact pixel
188
+ hashing + filename-ID overlap, all zero) but cannot rule out near-duplicate or
189
+ same-patient-different-image leakage if such structure exists in the source.
190
+
191
+ ## ⚠️ External validation required before clinical use
192
+
193
+ This model **requires independent, ideally prospective, multi-site external
194
+ validation** before any clinical consideration. It is released for research
195
+ reproducibility only.
196
+
197
+ ## Citation
198
+
199
+ Dataset: TN5000 (Yu et al., *Scientific Data*, 2025).
200
+ Augmentation guidance: MediAug, arXiv:2504.18983.
configs/calibration.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "method": "temperature_scaling",
3
+ "temperature": 0.5645509958267212,
4
+ "valid": {
5
+ "auroc_uncal": 0.975616,
6
+ "auroc_cal": 0.975616,
7
+ "ece_uncal": 0.08334361362457275,
8
+ "ece_cal": 0.03078642554581164,
9
+ "brier_uncal": 0.05917946249246597,
10
+ "brier_cal": 0.05246718227863312
11
+ },
12
+ "use_calibrated": true
13
+ }
configs/env_info.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "torch": "2.12.0+cu130",
3
+ "cuda_available": true,
4
+ "cuda_version": "13.0",
5
+ "cudnn_version": 92000,
6
+ "cudnn_deterministic": true,
7
+ "cudnn_benchmark": false,
8
+ "gpu_name": "NVIDIA A10G",
9
+ "gpu_count": 1,
10
+ "gpu_total_mem_gb": 23.95,
11
+ "torchvision": "0.27.0+cu130",
12
+ "timm": "1.0.27",
13
+ "sklearn": "1.9.0",
14
+ "numpy": "2.4.6",
15
+ "PIL": "12.2.0",
16
+ "trackio": "0.26.0",
17
+ "cublas_workspace_config": ":4096:8",
18
+ "pythonhashseed": "42"
19
+ }
configs/final_config.yaml ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Final, locked training configuration for the published thyroid ResNet-18 model.
2
+ # Selected by VALIDATION AUROC from a 14-trial sweep (winner: c12_loss_focal,
3
+ # val AUROC = 0.9756). Reproduce training with:
4
+ # python train.py --config configs/final_config.yaml
5
+ # Then evaluate the locked model on the test split with:
6
+ # python evaluate.py --split test --config configs/final_config.yaml
7
+
8
+ dataset_id: Johnyquest7/TN5000-thyroid-nodule-classification
9
+ # data_dir: /path/to/local/TN5000 # optional; if unset, dataset is downloaded from the Hub
10
+ output_dir: run_final
11
+
12
+ # --- model ---
13
+ backbone: timm:resnet18.a1_in1k # timm ResNet-18, A1 ImageNet-1k recipe weights
14
+ freeze_stage: 0 # full fine-tune
15
+ dropout: 0.0
16
+
17
+ # --- augmentation (medically plausible ultrasound augmentations only) ---
18
+ aug_policy: medical_default # hflip + mild affine(rot<=10,trans5%,scale0.9-1.1) + mild brightness/contrast + light gaussian blur
19
+
20
+ # --- loss / class imbalance ---
21
+ loss: focal # focal loss won the sweep for this mild imbalance
22
+ focal_gamma: 1.0
23
+ focal_alpha: 0.5
24
+ imbalance: none # no extra reweighting/sampling (focal handles it; preserves calibration)
25
+
26
+ # --- optimization ---
27
+ optimizer: adamw
28
+ lr: 0.0002
29
+ weight_decay: 0.0001
30
+ batch_size: 32
31
+ epochs: 40
32
+ scheduler: cosine
33
+ warmup_epochs: 2
34
+ early_stop_patience: 8 # early stopping on validation AUROC
35
+ amp: true # mixed precision
36
+
37
+ # --- reproducibility ---
38
+ seed: 42
39
+ strict_determinism: true
40
+
41
+ # --- experiment tracking ---
42
+ trackio_project: agentic_thyroid_resnet18
43
+ trackio_space_id: Johnyquest7/Trakio_agentic_thyroid
44
+ trackio_dataset_id: Johnyquest7/Trakio_agentic_thyroid_dataset
45
+ run_name: final_selected_model
configs/preprocess.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "image_size": 224,
3
+ "mean": [
4
+ 0.485,
5
+ 0.456,
6
+ 0.406
7
+ ],
8
+ "std": [
9
+ 0.229,
10
+ 0.224,
11
+ 0.225
12
+ ],
13
+ "interpolation": "bicubic",
14
+ "positive_class": "Malignant",
15
+ "positive_index": 1,
16
+ "note": "Deterministic eval/inference preprocessing. No augmentation."
17
+ }
configs/threshold.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "primary_method": "highest-specificity threshold with sensitivity >= 0.95 on validation (calibrated probabilities)",
3
+ "locked_threshold": 0.7113139033317566,
4
+ "valid_sensitivity_at_threshold": 0.952,
5
+ "valid_specificity_at_threshold": 0.896,
6
+ "target_sensitivity": 0.95,
7
+ "target_achievable": true,
8
+ "secondary_youden": {
9
+ "threshold": 0.7113139033317566,
10
+ "sensitivity": 0.952,
11
+ "specificity": 0.896
12
+ },
13
+ "probabilities_used": "calibrated"
14
+ }
configs/winning_run_command_line.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python train.py --data_dir /app/TN5000 --output_dir /app/sweep_runs/c12_loss_focal --run_name c12_loss_focal --seed 42 --trackio_project agentic_thyroid_resnet18 --trackio_space_id Johnyquest7/Trakio_agentic_thyroid --trackio_dataset_id Johnyquest7/Trakio_agentic_thyroid_dataset --num_workers 4 --backbone timm:resnet18.a1_in1k --lr 0.0002 --weight_decay 0.0001 --batch_size 32 --aug_policy medical_default --imbalance none --freeze_stage 0 --loss focal --optimizer adamw --scheduler cosine --epochs 40 --early_stop_patience 8 --dropout 0.0 --focal_gamma 1.0
configs/winning_run_resolved_config.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "config": null,
3
+ "dataset_id": "Johnyquest7/TN5000-thyroid-nodule-classification",
4
+ "data_dir": "/app/TN5000",
5
+ "output_dir": "/app/sweep_runs/c12_loss_focal",
6
+ "backbone": "timm:resnet18.a1_in1k",
7
+ "freeze_stage": 0,
8
+ "dropout": 0.0,
9
+ "aug_policy": "medical_default",
10
+ "loss": "focal",
11
+ "focal_gamma": 1.0,
12
+ "focal_alpha": 0.5,
13
+ "imbalance": "none",
14
+ "optimizer": "adamw",
15
+ "lr": 0.0002,
16
+ "weight_decay": 0.0001,
17
+ "batch_size": 32,
18
+ "epochs": 40,
19
+ "scheduler": "cosine",
20
+ "warmup_epochs": 2,
21
+ "early_stop_patience": 8,
22
+ "amp": true,
23
+ "num_workers": 4,
24
+ "seed": 42,
25
+ "strict_determinism": true,
26
+ "trackio_project": "agentic_thyroid_resnet18",
27
+ "trackio_space_id": "Johnyquest7/Trakio_agentic_thyroid",
28
+ "trackio_dataset_id": "Johnyquest7/Trakio_agentic_thyroid_dataset",
29
+ "run_name": "c12_loss_focal",
30
+ "no_trackio": false,
31
+ "pos_weight": null,
32
+ "n_train_neg": 1032,
33
+ "n_train_pos": 2468,
34
+ "preprocess": {
35
+ "image_size": 224,
36
+ "mean": [
37
+ 0.485,
38
+ 0.456,
39
+ 0.406
40
+ ],
41
+ "std": [
42
+ 0.229,
43
+ 0.224,
44
+ 0.225
45
+ ],
46
+ "interpolation": "bicubic"
47
+ },
48
+ "device": "cuda"
49
+ }
data_exploration_report.md ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data Exploration Report — TN5000 Thyroid Nodule Classification
2
+
3
+ - **Generated (UTC):** 2026-06-05T03:24:51.156145+00:00
4
+ - **Dataset:** `Johnyquest7/TN5000-thyroid-nodule-classification`
5
+ - **Source:** TN5000 (Yu et al., *Scientific Data*, 2025), cropped to nodule ROI, 224×224 PNG.
6
+ - **Task:** Binary classification — 0 = Benign, 1 = Malignant. Positive class = Malignant.
7
+
8
+ ## 1. Number of images per split and class
9
+
10
+ | Split | Benign (0) | Malignant (1) | Total | Malignant % | Malignant:Benign ratio |
11
+ |-------|-----------:|--------------:|------:|------------:|------------------------:|
12
+ | Train | 1032 | 2468 | 3500 | 70.5% | 2.39 : 1 |
13
+ | Valid | 125 | 375 | 500 | 75.0% | 3.00 : 1 |
14
+ | Test | 269 | 731 | 1000 | 73.1% | 2.72 : 1 |
15
+ | **Total** | **1426** | **3574** | **5000** | **71.5%** | **2.51 : 1** |
16
+
17
+ ![Class distribution](results/figures/class_distribution.png)
18
+
19
+ ## 2. Class imbalance
20
+
21
+ All three splits are **malignant-majority** (~70–75% malignant), i.e. mild imbalance (malignant:benign roughly 2.4–3.0 : 1), consistent across splits.
22
+
23
+ - **Mitigation evaluated in training:** class-weighted `BCEWithLogitsLoss` (`pos_weight = N_benign/N_malignant`), focal loss, and a weighted sampler were all compared in the sweep; the final model uses focal loss (γ=1.0). Because imbalance is mild and calibration matters, heavy reweighting was avoided.
24
+
25
+ ## 3. Image dimensions, channels, file format
26
+
27
+ - **File format:** PNG (lossless) for all 5000 images.
28
+ - **Dimensions observed:** [(224, 224)] (expected single value (224, 224)).
29
+ - **PIL modes observed:** ['RGB'] (RGB; grayscale replicated across 3 channels).
30
+
31
+ | Split | Unique dimensions | Modes |
32
+ |-------|-------------------|-------|
33
+ | Train | {(224, 224): 3500} | {'RGB': 3500} |
34
+ | Valid | {(224, 224): 500} | {'RGB': 500} |
35
+ | Test | {(224, 224): 1000} | {'RGB': 1000} |
36
+
37
+ ## 4. Missing / corrupt image check
38
+
39
+ - ✅ **No corrupt or unreadable images.** All images opened and decoded via PIL `verify()` + reload.
40
+
41
+ ## 5. Duplicate image check (exact pixel-content MD5)
42
+
43
+ - Duplicate groups **within a single split:** 0
44
+ - Duplicate groups **spanning multiple splits (potential LEAKAGE):** 0
45
+ - Duplicate groups with **conflicting labels:** 0
46
+
47
+ - ✅ No cross-split pixel duplicates and no label conflicts detected.
48
+
49
+ ## 6. Data leakage analysis
50
+
51
+ TN5000 assigns each image a **globally unique numeric ID**, preserved as the PNG filename. Overlap of filename IDs across splits would indicate the same source image in two splits.
52
+
53
+ | Pair | Shared filename IDs |
54
+ |------|--------------------:|
55
+ | Train ∩ Valid | 0 |
56
+ | Train ∩ Test | 0 |
57
+ | Valid ∩ Test | 0 |
58
+
59
+ - ✅ **No filename-ID overlap across splits.** Combined with the exact-pixel duplicate check above, there is no detectable leakage between Train, Valid, and Test.
60
+
61
+ ## 7. Pixel-intensity distribution
62
+
63
+ | Split | Mean | Std | Min | Max |
64
+ |-------|-----:|----:|----:|----:|
65
+ | Train | 81.5 | 19.6 | 27.1 | 163.5 |
66
+ | Valid | 81.3 | 19.4 | 35.0 | 171.5 |
67
+ | Test | 81.4 | 19.1 | 33.4 | 150.6 |
68
+
69
+ ![Intensity distribution](results/figures/intensity_distribution.png)
70
+
71
+ Mean per-image grayscale intensity distributions are **closely matched across splits**, indicating consistent acquisition/preprocessing and no obvious distribution shift.
72
+
73
+ ## 8. Representative image grids
74
+
75
+ **Train / Benign**
76
+
77
+ ![Train Benign](results/figures/grid_Train_Benign.png)
78
+
79
+ **Train / Malignant**
80
+
81
+ ![Train Malignant](results/figures/grid_Train_Malignant.png)
82
+
83
+ **Valid / Benign**
84
+
85
+ ![Valid Benign](results/figures/grid_Valid_Benign.png)
86
+
87
+ **Valid / Malignant**
88
+
89
+ ![Valid Malignant](results/figures/grid_Valid_Malignant.png)
90
+
91
+ **Test / Benign**
92
+
93
+ ![Test Benign](results/figures/grid_Test_Benign.png)
94
+
95
+ **Test / Malignant**
96
+
97
+ ![Test Malignant](results/figures/grid_Test_Malignant.png)
98
+
99
+ ## 9. Train/Valid/Test separation statement
100
+
101
+ > The Train, Valid, and Test folders provided in the dataset repository were kept **strictly separate** throughout this experiment. The model was trained on **Train only**; the **Valid** split was used for model selection, calibration, and threshold selection; and the **Test** split was used **exactly once** for final locked evaluation after the model, calibration, and decision threshold were frozen. The exact-pixel duplicate check and filename-ID overlap check above confirm there is no detectable leakage between the three splits.
evaluate.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Final evaluation script for the validation/test splits of TN5000.
4
+
5
+ Uses the LOCKED final model weights, LOCKED preprocessing, LOCKED calibration
6
+ (temperature scaling) and LOCKED decision threshold stored in this repo. The
7
+ test split is intended to be evaluated only once, after model/calibration/
8
+ threshold were frozen.
9
+
10
+ Usage:
11
+ python evaluate.py --split test --config configs/final_config.yaml
12
+ python evaluate.py --split valid
13
+
14
+ Reads (relative to --repo_dir, default '.'):
15
+ final_model.pt (or weights pointed to by final_config.yaml)
16
+ configs/preprocess.json
17
+ configs/calibration.json
18
+ configs/threshold.json
19
+
20
+ Writes per-image predictions + a metrics table (with bootstrap 95% CI) to
21
+ --output_dir (default results/eval_<split>/).
22
+ """
23
+ import argparse
24
+ import csv
25
+ import json
26
+ from pathlib import Path
27
+
28
+ import numpy as np
29
+ import yaml
30
+
31
+ import thyroid_lib as L
32
+
33
+
34
+ def main():
35
+ ap = argparse.ArgumentParser()
36
+ ap.add_argument("--split", choices=["valid", "test"], required=True)
37
+ ap.add_argument("--config", default=None, help="optional final_config.yaml (for data_dir/dataset_id)")
38
+ ap.add_argument("--repo_dir", default=".")
39
+ ap.add_argument("--data_dir", default=None, help="local TN5000 dir; else downloaded from Hub")
40
+ ap.add_argument("--dataset_id", default="Johnyquest7/TN5000-thyroid-nodule-classification")
41
+ ap.add_argument("--weights", default="final_model.pt")
42
+ ap.add_argument("--output_dir", default=None)
43
+ ap.add_argument("--n_boot", type=int, default=2000)
44
+ ap.add_argument("--boot_seed", type=int, default=42)
45
+ args = ap.parse_args()
46
+
47
+ if args.config and Path(args.config).exists():
48
+ cfg = yaml.safe_load(open(args.config)) or {}
49
+ if not args.data_dir and cfg.get("data_dir"):
50
+ args.data_dir = cfg["data_dir"]
51
+ if cfg.get("dataset_id"):
52
+ args.dataset_id = cfg["dataset_id"]
53
+
54
+ import torch
55
+ from torch.utils.data import DataLoader
56
+ device = "cuda" if torch.cuda.is_available() else "cpu"
57
+ L.set_determinism(args.boot_seed, strict=True)
58
+
59
+ repo = Path(args.repo_dir)
60
+ split_name = {"valid": "Valid", "test": "Test"}[args.split]
61
+ out_dir = Path(args.output_dir) if args.output_dir else repo / "results" / f"eval_{args.split}"
62
+ out_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ # data
65
+ if args.data_dir:
66
+ data_dir = Path(args.data_dir)
67
+ else:
68
+ from huggingface_hub import snapshot_download
69
+ data_dir = Path(snapshot_download(repo_id=args.dataset_id, repo_type="dataset",
70
+ local_dir=str(out_dir / "_data"),
71
+ allow_patterns=[f"{split_name}/**"]))
72
+
73
+ # locked artifacts
74
+ pp = L.PreprocessConfig.from_dict(json.load(open(repo / "configs" / "preprocess.json")))
75
+ calib = json.load(open(repo / "configs" / "calibration.json"))
76
+ thr_cfg = json.load(open(repo / "configs" / "threshold.json"))
77
+ T = calib["temperature"]; use_cal = calib.get("use_calibrated", True)
78
+ thr = thr_cfg["locked_threshold"]
79
+
80
+ # model
81
+ ck = torch.load(repo / args.weights, map_location="cpu", weights_only=False)
82
+ model, _ = L.build_model(ck["backbone"], freeze_stage=ck.get("freeze_stage", 0),
83
+ dropout=ck.get("dropout", 0.0))
84
+ model.load_state_dict(ck["model_state"]); model.to(device).eval()
85
+
86
+ ds = L.ThyroidImageFolder(data_dir / split_name, L.build_eval_transform(pp))
87
+ loader = DataLoader(ds, batch_size=64, shuffle=False, num_workers=4,
88
+ pin_memory=(device == "cuda"))
89
+ logits, y, ids = L.collect_logits(model, loader, device, amp=False)
90
+ probs = L.apply_temperature(logits, T) if use_cal else L.sigmoid(logits)
91
+
92
+ metrics = L.point_metrics(y, probs, thr)
93
+ ci = L.bootstrap_ci(y, probs, thr, n_boot=args.n_boot, seed=args.boot_seed)
94
+
95
+ # per-image csv
96
+ pred = (probs >= thr).astype(int)
97
+ with open(out_dir / f"{args.split}_predictions.csv", "w", newline="") as f:
98
+ w = csv.writer(f)
99
+ w.writerow(["image_id", "true_label", "true_class", "probability_malignant",
100
+ "predicted_label", "predicted_class"])
101
+ for i, yy, pr, pd in zip(ids, y, probs, pred):
102
+ w.writerow([i, int(yy), L.IDX_TO_CLASS[int(yy)], f"{pr:.6f}",
103
+ int(pd), L.IDX_TO_CLASS[int(pd)]])
104
+
105
+ ci_keys = ["auroc", "sensitivity", "specificity", "ppv", "npv", "accuracy", "f1"]
106
+ out = {"split": args.split, "n": metrics["n"], "n_pos": metrics["n_pos"],
107
+ "n_neg": metrics["n_neg"], "threshold": thr,
108
+ "calibration": "temperature(T=%.4f)" % T if use_cal else "none",
109
+ "metrics": metrics, "metrics_95ci": {k: list(ci[k]) for k in ci_keys},
110
+ "ci_method": f"stratified bootstrap, {args.n_boot} resamples, seed={args.boot_seed}"}
111
+ json.dump(out, open(out_dir / f"{args.split}_metrics.json", "w"), indent=2)
112
+
113
+ print(f"=== {args.split.upper()} (n={metrics['n']}, thr={thr:.4f}) ===")
114
+ for k in ci_keys:
115
+ print(f" {k:12s} {metrics[k]:.4f} CI [{ci[k][0]:.4f}, {ci[k][1]:.4f}]")
116
+ print(f" brier {metrics['brier']:.4f}")
117
+ print(f" ece {metrics['ece']:.4f}")
118
+ print(f" confusion TN={metrics['tn']} FP={metrics['fp']} FN={metrics['fn']} TP={metrics['tp']}")
119
+ print("Saved to", out_dir)
120
+
121
+
122
+ if __name__ == "__main__":
123
+ main()
evaluate_external.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Evaluate the LOCKED thyroid ResNet-18 model on an EXTERNAL dataset, using the
4
+ exact same preprocessing, calibration (temperature scaling) and locked decision
5
+ threshold from the final model repo.
6
+
7
+ Usage:
8
+ python evaluate_external.py \
9
+ --model_repo Johnyquest7/agentic_thyroid_model \
10
+ --data_dir /path/to/external_dataset \
11
+ --output_dir external_results
12
+
13
+ The external dataset may be provided in either of two formats:
14
+
15
+ (A) Folder format with class subfolders:
16
+ <data_dir>/Benign/*.png|jpg|...
17
+ <data_dir>/Malignant/*.png|jpg|...
18
+ (case-insensitive; also accepts 0/1 or benign/malignant)
19
+
20
+ (B) CSV with image paths and labels:
21
+ --csv /path/to/labels.csv
22
+ with columns:
23
+ image_path (absolute, or relative to --data_dir or to the CSV's folder)
24
+ label (0/1, or benign/malignant, case-insensitive)
25
+
26
+ If labels are present, full metrics + bootstrap 95% CIs are computed. If labels
27
+ are absent/unknown, only per-image probabilities and predictions are written.
28
+
29
+ The model weights and locked configs are downloaded from --model_repo (or read
30
+ from --local_repo_dir if provided).
31
+ """
32
+ import argparse
33
+ import csv
34
+ import json
35
+ from pathlib import Path
36
+
37
+ import numpy as np
38
+
39
+ import thyroid_lib as L
40
+
41
+ IMG_EXT = {".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp"}
42
+ BENIGN_ALIASES = {"benign", "0", "b", "neg", "negative"}
43
+ MALIGNANT_ALIASES = {"malignant", "1", "m", "pos", "positive", "cancer"}
44
+
45
+
46
+ def parse_label(v):
47
+ s = str(v).strip().lower()
48
+ if s in BENIGN_ALIASES:
49
+ return 0
50
+ if s in MALIGNANT_ALIASES:
51
+ return 1
52
+ return None # unknown
53
+
54
+
55
+ def gather_folder(data_dir):
56
+ data_dir = Path(data_dir)
57
+ items = [] # (path, label_or_None, id)
58
+ # find class subfolders case-insensitively
59
+ subdirs = {p.name.lower(): p for p in data_dir.iterdir() if p.is_dir()}
60
+ cls_map = {}
61
+ for name, p in subdirs.items():
62
+ lab = parse_label(name)
63
+ if lab is not None:
64
+ cls_map[p] = lab
65
+ if cls_map:
66
+ for p, lab in cls_map.items():
67
+ for f in sorted(p.rglob("*")):
68
+ if f.suffix.lower() in IMG_EXT:
69
+ items.append((f, lab, f.stem))
70
+ else:
71
+ # flat folder, no labels
72
+ for f in sorted(data_dir.rglob("*")):
73
+ if f.suffix.lower() in IMG_EXT:
74
+ items.append((f, None, f.stem))
75
+ return items
76
+
77
+
78
+ def gather_csv(csv_path, data_dir):
79
+ csv_path = Path(csv_path)
80
+ base = Path(data_dir) if data_dir else csv_path.parent
81
+ items = []
82
+ with open(csv_path) as f:
83
+ reader = csv.DictReader(f)
84
+ cols = {c.lower(): c for c in reader.fieldnames}
85
+ pcol = cols.get("image_path") or cols.get("path") or cols.get("image") or cols.get("filepath")
86
+ lcol = cols.get("label") or cols.get("class") or cols.get("target")
87
+ if pcol is None:
88
+ raise ValueError("CSV must have an image path column (image_path/path/image).")
89
+ for row in reader:
90
+ raw = row[pcol]
91
+ p = Path(raw)
92
+ if not p.is_absolute():
93
+ cand = base / raw
94
+ p = cand if cand.exists() else (csv_path.parent / raw)
95
+ lab = parse_label(row[lcol]) if lcol else None
96
+ items.append((p, lab, p.stem))
97
+ return items
98
+
99
+
100
+ class ListDataset:
101
+ def __init__(self, items, transform):
102
+ from PIL import Image
103
+ self.Image = Image
104
+ self.items = items
105
+ self.transform = transform
106
+
107
+ def __len__(self):
108
+ return len(self.items)
109
+
110
+ def __getitem__(self, i):
111
+ path, lab, iid = self.items[i]
112
+ with self.Image.open(path) as im:
113
+ x = self.transform(im.convert("RGB"))
114
+ return x, (-1 if lab is None else lab), iid
115
+
116
+
117
+ def main():
118
+ ap = argparse.ArgumentParser()
119
+ ap.add_argument("--model_repo", default="Johnyquest7/agentic_thyroid_model")
120
+ ap.add_argument("--local_repo_dir", default=None,
121
+ help="use a local copy of the model repo instead of downloading")
122
+ ap.add_argument("--data_dir", default=None, help="external image folder (format A) or CSV base")
123
+ ap.add_argument("--csv", default=None, help="CSV with image_path,label (format B)")
124
+ ap.add_argument("--weights", default="final_model.pt")
125
+ ap.add_argument("--output_dir", default="external_results")
126
+ ap.add_argument("--n_boot", type=int, default=2000)
127
+ ap.add_argument("--boot_seed", type=int, default=42)
128
+ args = ap.parse_args()
129
+
130
+ if not args.data_dir and not args.csv:
131
+ raise SystemExit("Provide --data_dir (folder format) or --csv (CSV format).")
132
+
133
+ import torch
134
+ from torch.utils.data import DataLoader
135
+ device = "cuda" if torch.cuda.is_available() else "cpu"
136
+ L.set_determinism(args.boot_seed, strict=True)
137
+
138
+ out_dir = Path(args.output_dir); out_dir.mkdir(parents=True, exist_ok=True)
139
+
140
+ # ---- fetch locked repo artifacts ----
141
+ if args.local_repo_dir:
142
+ repo = Path(args.local_repo_dir)
143
+ else:
144
+ from huggingface_hub import snapshot_download
145
+ repo = Path(snapshot_download(repo_id=args.model_repo, repo_type="model",
146
+ local_dir=str(out_dir / "_model_repo"),
147
+ allow_patterns=[args.weights, "configs/*", "thyroid_lib.py"]))
148
+
149
+ pp = L.PreprocessConfig.from_dict(json.load(open(repo / "configs" / "preprocess.json")))
150
+ calib = json.load(open(repo / "configs" / "calibration.json"))
151
+ thr_cfg = json.load(open(repo / "configs" / "threshold.json"))
152
+ T = calib["temperature"]; use_cal = calib.get("use_calibrated", True)
153
+ thr = thr_cfg["locked_threshold"]
154
+
155
+ ck = torch.load(repo / args.weights, map_location="cpu", weights_only=False)
156
+ model, _ = L.build_model(ck["backbone"], freeze_stage=ck.get("freeze_stage", 0),
157
+ dropout=ck.get("dropout", 0.0))
158
+ model.load_state_dict(ck["model_state"]); model.to(device).eval()
159
+
160
+ # ---- gather external images ----
161
+ items = gather_csv(args.csv, args.data_dir) if args.csv else gather_folder(args.data_dir)
162
+ if not items:
163
+ raise SystemExit("No images found in external dataset.")
164
+ has_labels = all(it[1] is not None for it in items)
165
+ print(f"Found {len(items)} external images; labels available: {has_labels}")
166
+
167
+ ds = ListDataset(items, L.build_eval_transform(pp))
168
+ loader = DataLoader(ds, batch_size=64, shuffle=False, num_workers=4,
169
+ pin_memory=(device == "cuda"))
170
+
171
+ logits_all, labels_all, ids_all = [], [], []
172
+ with torch.no_grad():
173
+ for x, y, iid in loader:
174
+ x = x.to(device)
175
+ out = model(x).view(-1)
176
+ logits_all.append(out.float().cpu().numpy())
177
+ labels_all.append(np.asarray(y))
178
+ ids_all.extend(list(iid))
179
+ logits = np.concatenate(logits_all); labels = np.concatenate(labels_all).astype(int)
180
+ probs = L.apply_temperature(logits, T) if use_cal else L.sigmoid(logits)
181
+ pred = (probs >= thr).astype(int)
182
+
183
+ # ---- per-image CSV ----
184
+ with open(out_dir / "external_predictions.csv", "w", newline="") as f:
185
+ w = csv.writer(f)
186
+ w.writerow(["image_id", "true_label", "probability_malignant",
187
+ "predicted_label", "predicted_class"])
188
+ for i, yy, pr, pd in zip(ids_all, labels, probs, pred):
189
+ w.writerow([i, ("" if yy < 0 else int(yy)), f"{pr:.6f}",
190
+ int(pd), L.IDX_TO_CLASS[int(pd)]])
191
+
192
+ result = {"n": len(items), "threshold": thr,
193
+ "calibration": "temperature(T=%.4f)" % T if use_cal else "none",
194
+ "labels_available": bool(has_labels)}
195
+
196
+ if has_labels:
197
+ metrics = L.point_metrics(labels, probs, thr)
198
+ ci = L.bootstrap_ci(labels, probs, thr, n_boot=args.n_boot, seed=args.boot_seed)
199
+ ci_keys = ["auroc", "sensitivity", "specificity", "ppv", "npv", "accuracy", "f1"]
200
+ result["metrics"] = metrics
201
+ result["metrics_95ci"] = {k: list(ci[k]) for k in ci_keys}
202
+ result["ci_method"] = f"stratified bootstrap, {args.n_boot} resamples, seed={args.boot_seed}"
203
+ print("=== EXTERNAL METRICS ===")
204
+ for k in ci_keys:
205
+ print(f" {k:12s} {metrics[k]:.4f} CI [{ci[k][0]:.4f}, {ci[k][1]:.4f}]")
206
+ print(f" brier {metrics['brier']:.4f}")
207
+ print(f" ece {metrics['ece']:.4f}")
208
+ print(f" confusion TN={metrics['tn']} FP={metrics['fp']} FN={metrics['fn']} TP={metrics['tp']}")
209
+ else:
210
+ print("No labels provided — wrote probabilities and predictions only.")
211
+
212
+ json.dump(result, open(out_dir / "external_metrics.json", "w"), indent=2)
213
+ print("Saved to", out_dir)
214
+
215
+
216
+ if __name__ == "__main__":
217
+ main()
explore_data.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Data exploration for the TN5000 thyroid-nodule classification dataset.
4
+
5
+ Generates a publication-ready `data_exploration_report.md` plus figures in
6
+ `results/figures/` covering: split/class counts, imbalance ratios, image
7
+ dimension/channel/format summary, corrupt-image check, duplicate-image check
8
+ (exact pixel hash, within and across splits), pixel-intensity distribution,
9
+ representative benign/malignant image grids per split, and leakage analysis.
10
+
11
+ Usage:
12
+ python explore_data.py --dataset_id Johnyquest7/TN5000-thyroid-nodule-classification \
13
+ --output_dir . [--data_dir /path/to/already/downloaded/TN5000]
14
+
15
+ If --data_dir is not given, the dataset is downloaded from the Hub.
16
+ The expected layout is <data_dir>/<Split>/<Class>/<id>.png with
17
+ Split in {Train, Valid, Test} and Class in {Benign, Malignant}.
18
+ """
19
+ import argparse
20
+ import hashlib
21
+ import json
22
+ import os
23
+ from collections import Counter
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+ from PIL import Image
28
+
29
+ import matplotlib
30
+ matplotlib.use("Agg")
31
+ import matplotlib.pyplot as plt
32
+
33
+ SPLITS = ["Train", "Valid", "Test"]
34
+ CLASSES = ["Benign", "Malignant"] # index 0 = Benign, 1 = Malignant
35
+
36
+
37
+ def get_data_dir(args):
38
+ if args.data_dir:
39
+ return Path(args.data_dir)
40
+ from huggingface_hub import snapshot_download
41
+ p = snapshot_download(
42
+ repo_id=args.dataset_id, repo_type="dataset",
43
+ local_dir=os.path.join(args.output_dir, "_tn5000_data"),
44
+ allow_patterns=["Train/**", "Valid/**", "Test/**"],
45
+ )
46
+ return Path(p)
47
+
48
+
49
+ def list_images(data_dir):
50
+ out = {s: {c: [] for c in CLASSES} for s in SPLITS}
51
+ for s in SPLITS:
52
+ for c in CLASSES:
53
+ d = data_dir / s / c
54
+ if d.is_dir():
55
+ out[s][c] = sorted(d.glob("*.png"))
56
+ return out
57
+
58
+
59
+ def md5_of_pixels(path):
60
+ with Image.open(path) as im:
61
+ arr = np.asarray(im.convert("RGB"))
62
+ return hashlib.md5(arr.tobytes()).hexdigest()
63
+
64
+
65
+ def main():
66
+ ap = argparse.ArgumentParser()
67
+ ap.add_argument("--dataset_id", default="Johnyquest7/TN5000-thyroid-nodule-classification")
68
+ ap.add_argument("--data_dir", default=None)
69
+ ap.add_argument("--output_dir", default=".")
70
+ args = ap.parse_args()
71
+
72
+ out_dir = Path(args.output_dir)
73
+ fig_dir = out_dir / "results" / "figures"
74
+ tab_dir = out_dir / "results" / "tables"
75
+ fig_dir.mkdir(parents=True, exist_ok=True)
76
+ tab_dir.mkdir(parents=True, exist_ok=True)
77
+
78
+ data_dir = get_data_dir(args)
79
+ print("Data dir:", data_dir)
80
+ images = list_images(data_dir)
81
+
82
+ counts = {s: {c: len(images[s][c]) for c in CLASSES} for s in SPLITS}
83
+ rows = []
84
+ for s in SPLITS:
85
+ b, m = counts[s]["Benign"], counts[s]["Malignant"]
86
+ tot = b + m
87
+ mal_pct = 100.0 * m / tot if tot else 0.0
88
+ imb = m / b if b else float("inf")
89
+ rows.append((s, b, m, tot, mal_pct, imb))
90
+ tot_b = sum(counts[s]["Benign"] for s in SPLITS)
91
+ tot_m = sum(counts[s]["Malignant"] for s in SPLITS)
92
+ tot_all = tot_b + tot_m
93
+
94
+ corrupt = []
95
+ intensity = {s: [] for s in SPLITS}
96
+ all_dims = []
97
+ dim_summary, mode_summary = {}, {}
98
+ for s in SPLITS:
99
+ dims, modes = Counter(), Counter()
100
+ for c in CLASSES:
101
+ for p in images[s][c]:
102
+ try:
103
+ with Image.open(p) as im:
104
+ im.verify()
105
+ with Image.open(p) as im:
106
+ w, h = im.size
107
+ modes[im.mode] += 1
108
+ dims[(w, h)] += 1
109
+ all_dims.append((w, h))
110
+ g = np.asarray(im.convert("L"), dtype=np.float32)
111
+ intensity[s].append(float(g.mean()))
112
+ except Exception as e:
113
+ corrupt.append((s, c, str(p), repr(e)))
114
+ dim_summary[s] = dims
115
+ mode_summary[s] = modes
116
+
117
+ pixel_hash = {}
118
+ for s in SPLITS:
119
+ for c in CLASSES:
120
+ for p in images[s][c]:
121
+ try:
122
+ hh = md5_of_pixels(p)
123
+ except Exception:
124
+ continue
125
+ pixel_hash.setdefault(hh, []).append((s, c, p.name))
126
+ dup_within, dup_across, dup_labelconf = [], [], []
127
+ for hh, locs in pixel_hash.items():
128
+ if len(locs) > 1:
129
+ splits_involved = set(l[0] for l in locs)
130
+ classes_involved = set(l[1] for l in locs)
131
+ (dup_across if len(splits_involved) > 1 else dup_within).append((hh, locs))
132
+ if len(classes_involved) > 1:
133
+ dup_labelconf.append((hh, locs))
134
+
135
+ ids = {s: set() for s in SPLITS}
136
+ for s in SPLITS:
137
+ for c in CLASSES:
138
+ for p in images[s][c]:
139
+ ids[s].add(p.stem)
140
+ id_tr_va = ids["Train"] & ids["Valid"]
141
+ id_tr_te = ids["Train"] & ids["Test"]
142
+ id_va_te = ids["Valid"] & ids["Test"]
143
+
144
+ # figures
145
+ fig, ax = plt.subplots(figsize=(7, 4.2))
146
+ x = np.arange(len(SPLITS)); w = 0.38
147
+ ben = [counts[s]["Benign"] for s in SPLITS]; mal = [counts[s]["Malignant"] for s in SPLITS]
148
+ ax.bar(x - w / 2, ben, w, label="Benign (0)", color="#4C72B0")
149
+ ax.bar(x + w / 2, mal, w, label="Malignant (1)", color="#C44E52")
150
+ for i, (bb, mm) in enumerate(zip(ben, mal)):
151
+ ax.text(i - w / 2, bb + 5, str(bb), ha="center", va="bottom", fontsize=9)
152
+ ax.text(i + w / 2, mm + 5, str(mm), ha="center", va="bottom", fontsize=9)
153
+ ax.set_xticks(x); ax.set_xticklabels(SPLITS); ax.set_ylabel("Number of images")
154
+ ax.set_title("Class distribution by split"); ax.legend()
155
+ fig.tight_layout(); fig.savefig(fig_dir / "class_distribution.png", dpi=150); plt.close(fig)
156
+
157
+ fig, ax = plt.subplots(figsize=(7, 4.2))
158
+ for s in SPLITS:
159
+ ax.hist(intensity[s], bins=50, alpha=0.5, label=f"{s} (n={len(intensity[s])})", density=True)
160
+ ax.set_xlabel("Mean grayscale intensity (0-255)"); ax.set_ylabel("Density")
161
+ ax.set_title("Per-image mean pixel-intensity distribution"); ax.legend()
162
+ fig.tight_layout(); fig.savefig(fig_dir / "intensity_distribution.png", dpi=150); plt.close(fig)
163
+
164
+ rng = np.random.default_rng(42)
165
+ for s in SPLITS:
166
+ for c in CLASSES:
167
+ paths = images[s][c]
168
+ if not paths:
169
+ continue
170
+ sel = rng.choice(len(paths), size=min(8, len(paths)), replace=False)
171
+ ncol = 4; nrow = int(np.ceil(len(sel) / ncol))
172
+ fig, axes = plt.subplots(nrow, ncol, figsize=(2.2 * ncol, 2.2 * nrow))
173
+ axes = np.array(axes).reshape(-1)
174
+ for ax in axes:
175
+ ax.axis("off")
176
+ for ax, idx in zip(axes, sel):
177
+ with Image.open(paths[idx]) as im:
178
+ ax.imshow(np.asarray(im.convert("RGB")))
179
+ ax.set_title(paths[idx].name, fontsize=7); ax.axis("off")
180
+ fig.suptitle(f"{s} / {c} (representative)", fontsize=11)
181
+ fig.tight_layout(); fig.savefig(fig_dir / f"grid_{s}_{c}.png", dpi=130); plt.close(fig)
182
+
183
+ import csv
184
+ with open(tab_dir / "class_distribution.csv", "w", newline="") as f:
185
+ wri = csv.writer(f)
186
+ wri.writerow(["split", "benign", "malignant", "total", "malignant_pct", "malignant_to_benign_ratio"])
187
+ for (s, b, m, tot, mal_pct, imb) in rows:
188
+ wri.writerow([s, b, m, tot, f"{mal_pct:.2f}", f"{imb:.3f}"])
189
+ wri.writerow(["Total", tot_b, tot_m, tot_all, f"{100.0*tot_m/tot_all:.2f}", f"{tot_m/tot_b:.3f}"])
190
+
191
+ intensity_stats = {s: (float(np.mean(intensity[s])), float(np.std(intensity[s])),
192
+ float(np.min(intensity[s])), float(np.max(intensity[s])))
193
+ for s in SPLITS}
194
+ all_dims_set = set(all_dims)
195
+ all_modes = set()
196
+ for s in SPLITS:
197
+ all_modes |= set(mode_summary[s].keys())
198
+
199
+ import datetime
200
+ now = datetime.datetime.now(datetime.timezone.utc).isoformat()
201
+ L = []
202
+ L.append("# Data Exploration Report — TN5000 Thyroid Nodule Classification\n")
203
+ L.append(f"- **Generated (UTC):** {now}")
204
+ L.append(f"- **Dataset:** `{args.dataset_id}`")
205
+ L.append("- **Source:** TN5000 (Yu et al., *Scientific Data*, 2025), cropped to nodule ROI, 224×224 PNG.")
206
+ L.append("- **Task:** Binary classification — 0 = Benign, 1 = Malignant. Positive class = Malignant.\n")
207
+
208
+ L.append("## 1. Number of images per split and class\n")
209
+ L.append("| Split | Benign (0) | Malignant (1) | Total | Malignant % | Malignant:Benign ratio |")
210
+ L.append("|-------|-----------:|--------------:|------:|------------:|------------------------:|")
211
+ for (s, b, m, tot, mal_pct, imb) in rows:
212
+ L.append(f"| {s} | {b} | {m} | {tot} | {mal_pct:.1f}% | {imb:.2f} : 1 |")
213
+ L.append(f"| **Total** | **{tot_b}** | **{tot_m}** | **{tot_all}** | **{100.0*tot_m/tot_all:.1f}%** | **{tot_m/tot_b:.2f} : 1** |\n")
214
+ L.append("![Class distribution](results/figures/class_distribution.png)\n")
215
+
216
+ L.append("## 2. Class imbalance\n")
217
+ L.append("All three splits are **malignant-majority** (~70–75% malignant), i.e. mild imbalance "
218
+ "(malignant:benign roughly 2.4–3.0 : 1), consistent across splits.\n")
219
+ L.append("- **Mitigation evaluated in training:** class-weighted `BCEWithLogitsLoss` "
220
+ "(`pos_weight = N_benign/N_malignant`), focal loss, and a weighted sampler "
221
+ "were all compared in the sweep; the final model uses focal loss (γ=1.0). "
222
+ "Because imbalance is mild and calibration matters, heavy reweighting was avoided.\n")
223
+
224
+ L.append("## 3. Image dimensions, channels, file format\n")
225
+ L.append(f"- **File format:** PNG (lossless) for all {tot_all} images.")
226
+ L.append(f"- **Dimensions observed:** {sorted(all_dims_set)} (expected single value (224, 224)).")
227
+ L.append(f"- **PIL modes observed:** {sorted(all_modes)} (RGB; grayscale replicated across 3 channels).")
228
+ L.append("\n| Split | Unique dimensions | Modes |")
229
+ L.append("|-------|-------------------|-------|")
230
+ for s in SPLITS:
231
+ L.append(f"| {s} | {dict(dim_summary[s])} | {dict(mode_summary[s])} |")
232
+ L.append("")
233
+
234
+ L.append("## 4. Missing / corrupt image check\n")
235
+ if corrupt:
236
+ L.append(f"- **{len(corrupt)} corrupt/unreadable images found:**")
237
+ for (s, c, p, e) in corrupt[:50]:
238
+ L.append(f" - `{s}/{c}/{Path(p).name}` — {e}")
239
+ else:
240
+ L.append("- ✅ **No corrupt or unreadable images.** All images opened and decoded via PIL `verify()` + reload.")
241
+ L.append("")
242
+
243
+ L.append("## 5. Duplicate image check (exact pixel-content MD5)\n")
244
+ L.append(f"- Duplicate groups **within a single split:** {len(dup_within)}")
245
+ L.append(f"- Duplicate groups **spanning multiple splits (potential LEAKAGE):** {len(dup_across)}")
246
+ L.append(f"- Duplicate groups with **conflicting labels:** {len(dup_labelconf)}")
247
+ if dup_across:
248
+ L.append("\n **Cross-split duplicate groups (first 50):**")
249
+ for hh, locs in dup_across[:50]:
250
+ L.append(" - " + ", ".join(f"{s}/{c}/{n}" for (s, c, n) in locs))
251
+ if not dup_across and not dup_labelconf:
252
+ L.append("\n- ✅ No cross-split pixel duplicates and no label conflicts detected.")
253
+ L.append("")
254
+
255
+ L.append("## 6. Data leakage analysis\n")
256
+ L.append("TN5000 assigns each image a **globally unique numeric ID**, preserved as the PNG filename. "
257
+ "Overlap of filename IDs across splits would indicate the same source image in two splits.\n")
258
+ L.append("| Pair | Shared filename IDs |")
259
+ L.append("|------|--------------------:|")
260
+ L.append(f"| Train ∩ Valid | {len(id_tr_va)} |")
261
+ L.append(f"| Train ∩ Test | {len(id_tr_te)} |")
262
+ L.append(f"| Valid ∩ Test | {len(id_va_te)} |")
263
+ if id_tr_va or id_tr_te or id_va_te:
264
+ L.append("\n- ⚠️ **Filename overlap detected** — review listed IDs.")
265
+ else:
266
+ L.append("\n- ✅ **No filename-ID overlap across splits.** Combined with the exact-pixel duplicate "
267
+ "check above, there is no detectable leakage between Train, Valid, and Test.")
268
+ L.append("")
269
+
270
+ L.append("## 7. Pixel-intensity distribution\n")
271
+ L.append("| Split | Mean | Std | Min | Max |")
272
+ L.append("|-------|-----:|----:|----:|----:|")
273
+ for s in SPLITS:
274
+ mu, sd, mn, mx = intensity_stats[s]
275
+ L.append(f"| {s} | {mu:.1f} | {sd:.1f} | {mn:.1f} | {mx:.1f} |")
276
+ L.append("\n![Intensity distribution](results/figures/intensity_distribution.png)\n")
277
+ L.append("Mean per-image grayscale intensity distributions are **closely matched across splits**, "
278
+ "indicating consistent acquisition/preprocessing and no obvious distribution shift.\n")
279
+
280
+ L.append("## 8. Representative image grids\n")
281
+ for s in SPLITS:
282
+ for c in CLASSES:
283
+ L.append(f"**{s} / {c}**\n")
284
+ L.append(f"![{s} {c}](results/figures/grid_{s}_{c}.png)\n")
285
+
286
+ L.append("## 9. Train/Valid/Test separation statement\n")
287
+ L.append("> The Train, Valid, and Test folders provided in the dataset repository were kept "
288
+ "**strictly separate** throughout this experiment. The model was trained on **Train only**; "
289
+ "the **Valid** split was used for model selection, calibration, and threshold selection; "
290
+ "and the **Test** split was used **exactly once** for final locked evaluation after the model, "
291
+ "calibration, and decision threshold were frozen. The exact-pixel duplicate check and "
292
+ "filename-ID overlap check above confirm there is no detectable leakage between the three splits.\n")
293
+
294
+ (out_dir / "data_exploration_report.md").write_text("\n".join(L))
295
+
296
+ summary = {"generated_utc": now, "counts": counts,
297
+ "totals": {"benign": tot_b, "malignant": tot_m, "all": tot_all},
298
+ "corrupt_count": len(corrupt), "dup_within_groups": len(dup_within),
299
+ "dup_across_groups": len(dup_across), "dup_labelconflict_groups": len(dup_labelconf),
300
+ "filename_overlap": {"train_valid": len(id_tr_va), "train_test": len(id_tr_te),
301
+ "valid_test": len(id_va_te)},
302
+ "dims_observed": sorted([list(d) for d in all_dims_set]),
303
+ "modes_observed": sorted(list(all_modes)),
304
+ "intensity_stats": {s: {"mean": intensity_stats[s][0], "std": intensity_stats[s][1],
305
+ "min": intensity_stats[s][2], "max": intensity_stats[s][3]}
306
+ for s in SPLITS}}
307
+ (tab_dir / "data_exploration_summary.json").write_text(json.dumps(summary, indent=2))
308
+ print(json.dumps(summary, indent=2))
309
+ print("Report written to", out_dir / "data_exploration_report.md")
310
+
311
+
312
+ if __name__ == "__main__":
313
+ main()
final_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a419778170c029203994df546d9ba69bfe20046cd4b3fcdcced5d4e56dc2338c
3
+ size 44789067
finalize.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Finalization pipeline (run ONCE after the sweep selects the best model by
4
+ validation AUROC):
5
+
6
+ 1. Load the best checkpoint (selected purely on val AUROC).
7
+ 2. Recompute Valid + Test logits with deterministic eval preprocessing.
8
+ 3. Calibrate on VALIDATION via temperature scaling; compare uncalibrated vs
9
+ calibrated (ECE, Brier, AUROC). Keep calibration only if it does not harm
10
+ discrimination (AUROC unchanged — temperature scaling is monotonic) and
11
+ improves/maintains calibration.
12
+ 4. Select a LOCKED threshold on VALIDATION: highest-specificity threshold with
13
+ sensitivity >= 0.95 (primary); Youden's J reported as secondary.
14
+ 5. Evaluate ONCE on TEST with calibrated probs + locked threshold.
15
+ 6. Bootstrap 95% CIs (stratified, 2000 resamples, seed=42).
16
+ 7. Save all figures (ROC, PR, calibration/reliability, confusion matrices),
17
+ tables (markdown + CSV), per-image prediction CSVs (valid + test),
18
+ calibration config, threshold config, preprocessing config.
19
+
20
+ Outputs go to --output_dir (default model_repo/).
21
+ """
22
+ import argparse
23
+ import csv
24
+ import json
25
+ from pathlib import Path
26
+
27
+ import numpy as np
28
+ import matplotlib
29
+ matplotlib.use("Agg")
30
+ import matplotlib.pyplot as plt
31
+
32
+ import thyroid_lib as L
33
+
34
+ TARGET_SENS = 0.95
35
+ N_BOOT = 2000
36
+ BOOT_SEED = 42
37
+
38
+
39
+ def load_model(ckpt_path, device):
40
+ import torch
41
+ ck = torch.load(ckpt_path, map_location="cpu", weights_only=False)
42
+ model, pp = L.build_model(ck["backbone"], freeze_stage=ck.get("freeze_stage", 0),
43
+ dropout=ck.get("dropout", 0.0))
44
+ model.load_state_dict(ck["model_state"])
45
+ model.to(device).eval()
46
+ pp = L.PreprocessConfig.from_dict(ck["preprocess"])
47
+ return model, pp, ck
48
+
49
+
50
+ def get_logits(model, data_dir, split, pp, device):
51
+ import torch
52
+ from torch.utils.data import DataLoader
53
+ ds = L.ThyroidImageFolder(Path(data_dir) / split, L.build_eval_transform(pp))
54
+ loader = DataLoader(ds, batch_size=64, shuffle=False, num_workers=4,
55
+ pin_memory=(device == "cuda"))
56
+ logits, labels, ids = L.collect_logits(model, loader, device, amp=False)
57
+ return logits, labels, ids
58
+
59
+
60
+ def save_per_image_csv(path, ids, labels, probs, thr):
61
+ pred = (np.asarray(probs) >= thr).astype(int)
62
+ with open(path, "w", newline="") as f:
63
+ w = csv.writer(f)
64
+ w.writerow(["image_id", "true_label", "true_class", "probability_malignant",
65
+ "predicted_label", "predicted_class"])
66
+ for i, y, p, pr in zip(ids, labels, probs, pred):
67
+ w.writerow([i, int(y), L.IDX_TO_CLASS[int(y)], f"{p:.6f}",
68
+ int(pr), L.IDX_TO_CLASS[int(pr)]])
69
+
70
+
71
+ def plot_roc(y, p, path, title):
72
+ from sklearn.metrics import roc_curve, roc_auc_score
73
+ fpr, tpr, _ = roc_curve(y, p)
74
+ auc = roc_auc_score(y, p)
75
+ fig, ax = plt.subplots(figsize=(5, 5))
76
+ ax.plot(fpr, tpr, label=f"AUROC = {auc:.3f}", color="#C44E52")
77
+ ax.plot([0, 1], [0, 1], "--", color="gray")
78
+ ax.set_xlabel("1 - Specificity (FPR)"); ax.set_ylabel("Sensitivity (TPR)")
79
+ ax.set_title(title); ax.legend(loc="lower right")
80
+ fig.tight_layout(); fig.savefig(path, dpi=150); plt.close(fig)
81
+
82
+
83
+ def plot_pr(y, p, path, title):
84
+ from sklearn.metrics import precision_recall_curve, average_precision_score
85
+ prec, rec, _ = precision_recall_curve(y, p)
86
+ ap = average_precision_score(y, p)
87
+ fig, ax = plt.subplots(figsize=(5, 5))
88
+ ax.plot(rec, prec, label=f"AP = {ap:.3f}", color="#4C72B0")
89
+ ax.set_xlabel("Recall (Sensitivity)"); ax.set_ylabel("Precision (PPV)")
90
+ ax.set_title(title); ax.legend(loc="lower left")
91
+ fig.tight_layout(); fig.savefig(path, dpi=150); plt.close(fig)
92
+
93
+
94
+ def plot_reliability(y, p_uncal, p_cal, path, title):
95
+ from sklearn.calibration import calibration_curve
96
+ fig, ax = plt.subplots(figsize=(5.5, 5.5))
97
+ ax.plot([0, 1], [0, 1], "--", color="gray", label="Perfect calibration")
98
+ for p, lab, col in [(p_uncal, "Uncalibrated", "#888888"), (p_cal, "Temperature-scaled", "#C44E52")]:
99
+ fpos, mpred = calibration_curve(y, p, n_bins=10, strategy="uniform")
100
+ ece = L.expected_calibration_error(y, p)
101
+ br = L.brier(y, p)
102
+ ax.plot(mpred, fpos, "o-", color=col, label=f"{lab} (ECE={ece:.3f}, Brier={br:.3f})")
103
+ ax.set_xlabel("Mean predicted probability"); ax.set_ylabel("Observed frequency")
104
+ ax.set_title(title); ax.legend(loc="upper left", fontsize=8)
105
+ fig.tight_layout(); fig.savefig(path, dpi=150); plt.close(fig)
106
+
107
+
108
+ def plot_confusion(cm, path, title, normalize=False):
109
+ cm = np.asarray(cm, dtype=float)
110
+ disp = cm.copy()
111
+ if normalize:
112
+ disp = cm / cm.sum(axis=1, keepdims=True).clip(min=1e-9)
113
+ fig, ax = plt.subplots(figsize=(4.8, 4.4))
114
+ im = ax.imshow(disp, cmap="Blues", vmin=0, vmax=disp.max())
115
+ ax.set_xticks([0, 1]); ax.set_yticks([0, 1])
116
+ ax.set_xticklabels(["Predicted benign", "Predicted malignant"])
117
+ ax.set_yticklabels(["True benign", "True malignant"])
118
+ for i in range(2):
119
+ for j in range(2):
120
+ txt = f"{int(cm[i,j])}" + (f"\n({disp[i,j]*100:.1f}%)" if normalize else "")
121
+ ax.text(j, i, txt, ha="center", va="center",
122
+ color="white" if disp[i, j] > disp.max() / 2 else "black", fontsize=11)
123
+ ax.set_title(title)
124
+ fig.tight_layout(); fig.savefig(path, dpi=150); plt.close(fig)
125
+
126
+
127
+ def main():
128
+ ap = argparse.ArgumentParser()
129
+ ap.add_argument("--ckpt", required=True)
130
+ ap.add_argument("--data_dir", default="/app/TN5000")
131
+ ap.add_argument("--output_dir", default="/app/model_repo")
132
+ ap.add_argument("--best_run_name", default="")
133
+ ap.add_argument("--best_val_auroc", default="")
134
+ args = ap.parse_args()
135
+
136
+ import torch
137
+ device = "cuda" if torch.cuda.is_available() else "cpu"
138
+ L.set_determinism(42, strict=True)
139
+
140
+ out = Path(args.output_dir)
141
+ res = out / "results"; figs = res / "figures"; tabs = res / "tables"
142
+ for d in [out / "configs", figs, tabs]:
143
+ d.mkdir(parents=True, exist_ok=True)
144
+
145
+ model, pp, ck = load_model(args.ckpt, device)
146
+
147
+ # ---- logits ----
148
+ val_logits, val_y, val_ids = get_logits(model, args.data_dir, "Valid", pp, device)
149
+ test_logits, test_y, test_ids = get_logits(model, args.data_dir, "Test", pp, device)
150
+
151
+ val_p_uncal = L.sigmoid(val_logits)
152
+ test_p_uncal = L.sigmoid(test_logits)
153
+
154
+ # ---- calibration: temperature scaling on VALIDATION ----
155
+ T = L.fit_temperature(val_logits, val_y)
156
+ val_p_cal = L.apply_temperature(val_logits, T)
157
+ test_p_cal = L.apply_temperature(test_logits, T)
158
+
159
+ from sklearn.metrics import roc_auc_score
160
+ cal_report = {
161
+ "method": "temperature_scaling",
162
+ "temperature": T,
163
+ "valid": {
164
+ "auroc_uncal": float(roc_auc_score(val_y, val_p_uncal)),
165
+ "auroc_cal": float(roc_auc_score(val_y, val_p_cal)),
166
+ "ece_uncal": L.expected_calibration_error(val_y, val_p_uncal),
167
+ "ece_cal": L.expected_calibration_error(val_y, val_p_cal),
168
+ "brier_uncal": L.brier(val_y, val_p_uncal),
169
+ "brier_cal": L.brier(val_y, val_p_cal),
170
+ },
171
+ }
172
+ # Decision: temperature scaling is monotonic -> AUROC unchanged. Use calibrated
173
+ # if ECE improves or is within tolerance; else fall back to uncalibrated.
174
+ use_calibrated = cal_report["valid"]["ece_cal"] <= cal_report["valid"]["ece_uncal"] + 1e-6
175
+ cal_report["use_calibrated"] = bool(use_calibrated)
176
+ val_p = val_p_cal if use_calibrated else val_p_uncal
177
+ test_p = test_p_cal if use_calibrated else test_p_uncal
178
+ L.save_json(cal_report, out / "configs" / "calibration.json")
179
+
180
+ # ---- threshold selection on VALIDATION (calibrated probs) ----
181
+ thr, sens_v, spec_v, achievable = L.threshold_for_sensitivity(val_y, val_p, TARGET_SENS)
182
+ yj_thr, yj_sens, yj_spec = L.youden_threshold(val_y, val_p)
183
+ thr_report = {
184
+ "primary_method": f"highest-specificity threshold with sensitivity >= {TARGET_SENS} on validation (calibrated probabilities)",
185
+ "locked_threshold": thr,
186
+ "valid_sensitivity_at_threshold": sens_v,
187
+ "valid_specificity_at_threshold": spec_v,
188
+ "target_sensitivity": TARGET_SENS,
189
+ "target_achievable": bool(achievable),
190
+ "secondary_youden": {"threshold": yj_thr, "sensitivity": yj_sens, "specificity": yj_spec},
191
+ "probabilities_used": "calibrated" if use_calibrated else "uncalibrated",
192
+ }
193
+ L.save_json(thr_report, out / "configs" / "threshold.json")
194
+
195
+ # ---- preprocessing config (locked) ----
196
+ L.save_json({**pp.to_dict(), "positive_class": "Malignant", "positive_index": 1,
197
+ "note": "Deterministic eval/inference preprocessing. No augmentation."},
198
+ out / "configs" / "preprocess.json")
199
+
200
+ # ---- VALIDATION metrics at locked threshold ----
201
+ val_metrics = L.point_metrics(val_y, val_p, thr)
202
+
203
+ # ---- TEST metrics (locked) ----
204
+ test_metrics = L.point_metrics(test_y, test_p, thr)
205
+ test_ci = L.bootstrap_ci(test_y, test_p, thr, n_boot=N_BOOT, seed=BOOT_SEED)
206
+
207
+ # ---- per-image CSVs ----
208
+ save_per_image_csv(res / "valid_predictions.csv", val_ids, val_y, val_p, thr)
209
+ save_per_image_csv(res / "test_predictions.csv", test_ids, test_y, test_p, thr)
210
+
211
+ # ---- figures ----
212
+ plot_roc(test_y, test_p, figs / "test_roc.png", "ROC — Test set")
213
+ plot_pr(test_y, test_p, figs / "test_pr.png", "Precision-Recall — Test set")
214
+ plot_reliability(val_y, val_p_uncal, val_p_cal, figs / "valid_calibration.png",
215
+ "Reliability diagram — Validation")
216
+ plot_reliability(test_y, test_p_uncal, test_p_cal, figs / "test_calibration.png",
217
+ "Reliability diagram — Test")
218
+ cm = np.array([[test_metrics["tn"], test_metrics["fp"]],
219
+ [test_metrics["fn"], test_metrics["tp"]]])
220
+ plot_confusion(cm, figs / "test_confusion_counts.png", "Confusion matrix (counts) — Test")
221
+ plot_confusion(cm, figs / "test_confusion_normalized.png",
222
+ "Confusion matrix (row-normalized) — Test", normalize=True)
223
+
224
+ # ---- metrics table (markdown + csv) with CIs ----
225
+ ci_keys = ["auroc", "sensitivity", "specificity", "ppv", "npv", "accuracy", "f1"]
226
+ md = ["# Final Test Metrics (locked model + locked threshold)\n",
227
+ f"- Selected run: {args.best_run_name} | selection val AUROC: {args.best_val_auroc}",
228
+ f"- Backbone: {ck['backbone']} | Calibration: temperature scaling (T={T:.4f}, "
229
+ f"{'used' if use_calibrated else 'not used'})",
230
+ f"- Locked threshold (val sens>={TARGET_SENS}): {thr:.4f} | probabilities: "
231
+ f"{'calibrated' if use_calibrated else 'uncalibrated'}",
232
+ f"- CI method: stratified bootstrap, {N_BOOT} resamples, seed={BOOT_SEED}\n",
233
+ "| Metric | Point estimate | 95% CI |",
234
+ "|--------|---------------:|:------:|"]
235
+ rows_csv = [["metric", "point_estimate", "ci_low", "ci_high"]]
236
+ for k in ci_keys:
237
+ pe = test_metrics[k]; lo, hi = test_ci[k]
238
+ md.append(f"| {k.upper()} | {pe:.4f} | [{lo:.4f}, {hi:.4f}] |")
239
+ rows_csv.append([k, f"{pe:.6f}", f"{lo:.6f}", f"{hi:.6f}"])
240
+ for k in ["brier", "ece"]:
241
+ md.append(f"| {k.upper()} | {test_metrics[k]:.4f} | — |")
242
+ rows_csv.append([k, f"{test_metrics[k]:.6f}", "", ""])
243
+ md.append(f"\n**Confusion matrix (Test):** TN={test_metrics['tn']}, FP={test_metrics['fp']}, "
244
+ f"FN={test_metrics['fn']}, TP={test_metrics['tp']}\n")
245
+ (tabs / "test_metrics_with_ci.md").write_text("\n".join(md))
246
+ with open(tabs / "test_metrics_with_ci.csv", "w", newline="") as f:
247
+ csv.writer(f).writerows(rows_csv)
248
+
249
+ # ---- consolidated results json ----
250
+ final = {
251
+ "selected_run": args.best_run_name,
252
+ "selection_val_auroc": args.best_val_auroc,
253
+ "backbone": ck["backbone"],
254
+ "preprocess": pp.to_dict(),
255
+ "calibration": cal_report,
256
+ "threshold": thr_report,
257
+ "valid_metrics_at_locked_threshold": val_metrics,
258
+ "test_metrics_at_locked_threshold": test_metrics,
259
+ "test_metrics_95ci": {k: list(test_ci[k]) for k in ci_keys},
260
+ "ci_method": f"stratified bootstrap, {N_BOOT} resamples, seed={BOOT_SEED}",
261
+ }
262
+ L.save_json(final, res / "final_results.json")
263
+
264
+ print("=== CALIBRATION ===")
265
+ print(json.dumps(cal_report, indent=2))
266
+ print("=== THRESHOLD ===")
267
+ print(json.dumps(thr_report, indent=2))
268
+ print("=== TEST METRICS ===")
269
+ for k in ci_keys:
270
+ print(f" {k:12s} {test_metrics[k]:.4f} CI [{test_ci[k][0]:.4f}, {test_ci[k][1]:.4f}]")
271
+ print(f" brier {test_metrics['brier']:.4f}")
272
+ print(f" ece {test_metrics['ece']:.4f}")
273
+ print("Saved to", out)
274
+
275
+
276
+ if __name__ == "__main__":
277
+ main()
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pinned to the exact versions used to produce the published results
2
+ # (NVIDIA A10G, CUDA 13.0, Python 3.12). torch/torchvision built with +cu130;
3
+ # install the CUDA build matching your system from https://pytorch.org if needed.
4
+ torch==2.12.0
5
+ torchvision==0.27.0
6
+ timm==1.0.27
7
+ scikit-learn==1.9.0
8
+ numpy==2.4.6
9
+ pillow==12.2.0
10
+ matplotlib==3.10.9
11
+ pyyaml==6.0.3
12
+ trackio==0.26.0
13
+ huggingface_hub==1.17.0
14
+ # optional: only needed if you enable the 'clahe' augmentation ablation
15
+ opencv-python-headless>=4.8
results/figures/class_distribution.png ADDED
results/figures/grid_Test_Benign.png ADDED

Git LFS Details

  • SHA256: cb64005cb5f115f6fac8be8bd22bcee0eecffa7043c2c7f0c8666e7b7c846d79
  • Pointer size: 131 Bytes
  • Size of remote file: 249 kB
results/figures/grid_Test_Malignant.png ADDED

Git LFS Details

  • SHA256: 4957154927bc5b60f5a2617acc0c159e362b6f77e1548ed49a476fc5263b39c7
  • Pointer size: 131 Bytes
  • Size of remote file: 320 kB
results/figures/grid_Train_Benign.png ADDED

Git LFS Details

  • SHA256: 093ceb4dd4af87cdc0a4452b0dbf4ab168c1acf6d4e44e4276b81d75b7efd894
  • Pointer size: 131 Bytes
  • Size of remote file: 266 kB
results/figures/grid_Train_Malignant.png ADDED

Git LFS Details

  • SHA256: 43b9dc532da999254dc6f6bbf75a810fc908482d6ff5101f0a32361ddca69d25
  • Pointer size: 131 Bytes
  • Size of remote file: 302 kB
results/figures/grid_Valid_Benign.png ADDED

Git LFS Details

  • SHA256: 2169c83e1586f415053bd9bb101754394132e7cd2a9c7eba318b7b5834201f34
  • Pointer size: 131 Bytes
  • Size of remote file: 312 kB
results/figures/grid_Valid_Malignant.png ADDED

Git LFS Details

  • SHA256: 0fe2c0e790500c401039e238f0f231d6d0730a82fbed554e6d9d1a5cfcb0a81f
  • Pointer size: 131 Bytes
  • Size of remote file: 282 kB
results/figures/intensity_distribution.png ADDED
results/figures/test_calibration.png ADDED
results/figures/test_confusion_counts.png ADDED
results/figures/test_confusion_normalized.png ADDED
results/figures/test_pr.png ADDED
results/figures/test_roc.png ADDED
results/figures/valid_calibration.png ADDED
results/final_results.json ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "selected_run": "c12_loss_focal",
3
+ "selection_val_auroc": "0.9756",
4
+ "backbone": "timm:resnet18.a1_in1k",
5
+ "preprocess": {
6
+ "image_size": 224,
7
+ "mean": [
8
+ 0.485,
9
+ 0.456,
10
+ 0.406
11
+ ],
12
+ "std": [
13
+ 0.229,
14
+ 0.224,
15
+ 0.225
16
+ ],
17
+ "interpolation": "bicubic"
18
+ },
19
+ "calibration": {
20
+ "method": "temperature_scaling",
21
+ "temperature": 0.5645509958267212,
22
+ "valid": {
23
+ "auroc_uncal": 0.975616,
24
+ "auroc_cal": 0.975616,
25
+ "ece_uncal": 0.08334361362457275,
26
+ "ece_cal": 0.03078642554581164,
27
+ "brier_uncal": 0.05917946249246597,
28
+ "brier_cal": 0.05246718227863312
29
+ },
30
+ "use_calibrated": true
31
+ },
32
+ "threshold": {
33
+ "primary_method": "highest-specificity threshold with sensitivity >= 0.95 on validation (calibrated probabilities)",
34
+ "locked_threshold": 0.7113139033317566,
35
+ "valid_sensitivity_at_threshold": 0.952,
36
+ "valid_specificity_at_threshold": 0.896,
37
+ "target_sensitivity": 0.95,
38
+ "target_achievable": true,
39
+ "secondary_youden": {
40
+ "threshold": 0.7113139033317566,
41
+ "sensitivity": 0.952,
42
+ "specificity": 0.896
43
+ },
44
+ "probabilities_used": "calibrated"
45
+ },
46
+ "valid_metrics_at_locked_threshold": {
47
+ "auroc": 0.975616,
48
+ "accuracy": 0.938,
49
+ "sensitivity": 0.952,
50
+ "specificity": 0.896,
51
+ "ppv": 0.9648648648648649,
52
+ "npv": 0.8615384615384616,
53
+ "f1": 0.9583892617449664,
54
+ "brier": 0.05246718227863312,
55
+ "ece": 0.03078642554581164,
56
+ "tp": 357,
57
+ "tn": 112,
58
+ "fp": 13,
59
+ "fn": 18,
60
+ "threshold": 0.7113139033317566,
61
+ "n": 500,
62
+ "n_pos": 375,
63
+ "n_neg": 125
64
+ },
65
+ "test_metrics_at_locked_threshold": {
66
+ "auroc": 0.9371436998764233,
67
+ "accuracy": 0.875,
68
+ "sensitivity": 0.9042407660738714,
69
+ "specificity": 0.7955390334572491,
70
+ "ppv": 0.9231843575418994,
71
+ "npv": 0.7535211267605634,
72
+ "f1": 0.9136143745680718,
73
+ "brier": 0.08228471130132675,
74
+ "ece": 0.03143832388520238,
75
+ "tp": 661,
76
+ "tn": 214,
77
+ "fp": 55,
78
+ "fn": 70,
79
+ "threshold": 0.7113139033317566,
80
+ "n": 1000,
81
+ "n_pos": 731,
82
+ "n_neg": 269
83
+ },
84
+ "test_metrics_95ci": {
85
+ "auroc": [
86
+ 0.9202044101119309,
87
+ 0.9528172183544465
88
+ ],
89
+ "sensitivity": [
90
+ 0.8823529411764706,
91
+ 0.9247606019151847
92
+ ],
93
+ "specificity": [
94
+ 0.7434944237918215,
95
+ 0.8438661710037175
96
+ ],
97
+ "ppv": [
98
+ 0.9054223401271771,
99
+ 0.9400883200831714
100
+ ],
101
+ "npv": [
102
+ 0.7122702943800179,
103
+ 0.797906592520954
104
+ ],
105
+ "accuracy": [
106
+ 0.854,
107
+ 0.895
108
+ ],
109
+ "f1": [
110
+ 0.8990953375086986,
111
+ 0.9277781562216167
112
+ ]
113
+ },
114
+ "ci_method": "stratified bootstrap, 2000 resamples, seed=42"
115
+ }
results/tables/class_distribution.csv ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ split,benign,malignant,total,malignant_pct,malignant_to_benign_ratio
2
+ Train,1032,2468,3500,70.51,2.391
3
+ Valid,125,375,500,75.00,3.000
4
+ Test,269,731,1000,73.10,2.717
5
+ Total,1426,3574,5000,71.48,2.506
results/tables/data_exploration_summary.json ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "generated_utc": "2026-06-05T03:24:51.156145+00:00",
3
+ "counts": {
4
+ "Train": {
5
+ "Benign": 1032,
6
+ "Malignant": 2468
7
+ },
8
+ "Valid": {
9
+ "Benign": 125,
10
+ "Malignant": 375
11
+ },
12
+ "Test": {
13
+ "Benign": 269,
14
+ "Malignant": 731
15
+ }
16
+ },
17
+ "totals": {
18
+ "benign": 1426,
19
+ "malignant": 3574,
20
+ "all": 5000
21
+ },
22
+ "corrupt_count": 0,
23
+ "dup_within_groups": 0,
24
+ "dup_across_groups": 0,
25
+ "dup_labelconflict_groups": 0,
26
+ "filename_overlap": {
27
+ "train_valid": 0,
28
+ "train_test": 0,
29
+ "valid_test": 0
30
+ },
31
+ "dims_observed": [
32
+ [
33
+ 224,
34
+ 224
35
+ ]
36
+ ],
37
+ "modes_observed": [
38
+ "RGB"
39
+ ],
40
+ "intensity_stats": {
41
+ "Train": {
42
+ "mean": 81.51711741311209,
43
+ "std": 19.637604872156206,
44
+ "min": 27.07162857055664,
45
+ "max": 163.4593963623047
46
+ },
47
+ "Valid": {
48
+ "mean": 81.32348302459717,
49
+ "std": 19.410170086309197,
50
+ "min": 35.048988342285156,
51
+ "max": 171.485107421875
52
+ },
53
+ "Test": {
54
+ "mean": 81.36260342407226,
55
+ "std": 19.13009627746057,
56
+ "min": 33.38518524169922,
57
+ "max": 150.60565185546875
58
+ }
59
+ }
60
+ }
results/tables/sweep_leaderboard.json ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "c12_loss_focal",
4
+ "config": {
5
+ "backbone": "timm:resnet18.a1_in1k",
6
+ "lr": 0.0002,
7
+ "weight_decay": 0.0001,
8
+ "batch_size": 32,
9
+ "aug_policy": "medical_default",
10
+ "imbalance": "none",
11
+ "freeze_stage": 0,
12
+ "loss": "focal",
13
+ "optimizer": "adamw",
14
+ "scheduler": "cosine",
15
+ "epochs": 40,
16
+ "early_stop_patience": 8,
17
+ "dropout": 0.0,
18
+ "focal_gamma": 1.0
19
+ },
20
+ "best_val_auroc": 0.9756053333333333,
21
+ "best_epoch": 6,
22
+ "out_dir": "/app/sweep_runs/c12_loss_focal",
23
+ "returncode": 0
24
+ },
25
+ {
26
+ "name": "c09_imb_none",
27
+ "config": {
28
+ "backbone": "timm:resnet18.a1_in1k",
29
+ "lr": 0.0002,
30
+ "weight_decay": 0.0001,
31
+ "batch_size": 32,
32
+ "aug_policy": "medical_default",
33
+ "imbalance": "none",
34
+ "freeze_stage": 0,
35
+ "loss": "bce",
36
+ "optimizer": "adamw",
37
+ "scheduler": "cosine",
38
+ "epochs": 40,
39
+ "early_stop_patience": 8,
40
+ "dropout": 0.0
41
+ },
42
+ "best_val_auroc": 0.9739093333333334,
43
+ "best_epoch": 6,
44
+ "out_dir": "/app/sweep_runs/c09_imb_none",
45
+ "returncode": 0
46
+ },
47
+ {
48
+ "name": "c01_backbone_torchvision",
49
+ "config": {
50
+ "backbone": "torchvision",
51
+ "lr": 0.0002,
52
+ "weight_decay": 0.0001,
53
+ "batch_size": 32,
54
+ "aug_policy": "medical_default",
55
+ "imbalance": "pos_weight",
56
+ "freeze_stage": 0,
57
+ "loss": "bce",
58
+ "optimizer": "adamw",
59
+ "scheduler": "cosine",
60
+ "epochs": 40,
61
+ "early_stop_patience": 8,
62
+ "dropout": 0.0
63
+ },
64
+ "best_val_auroc": 0.9730986666666667,
65
+ "best_epoch": 7,
66
+ "out_dir": "/app/sweep_runs/c01_backbone_torchvision",
67
+ "returncode": 0
68
+ },
69
+ {
70
+ "name": "c03_lr_1e-4",
71
+ "config": {
72
+ "backbone": "timm:resnet18.a1_in1k",
73
+ "lr": 0.0001,
74
+ "weight_decay": 0.0001,
75
+ "batch_size": 32,
76
+ "aug_policy": "medical_default",
77
+ "imbalance": "pos_weight",
78
+ "freeze_stage": 0,
79
+ "loss": "bce",
80
+ "optimizer": "adamw",
81
+ "scheduler": "cosine",
82
+ "epochs": 40,
83
+ "early_stop_patience": 8,
84
+ "dropout": 0.0
85
+ },
86
+ "best_val_auroc": 0.9720533333333334,
87
+ "best_epoch": 11,
88
+ "out_dir": "/app/sweep_runs/c03_lr_1e-4",
89
+ "returncode": 0
90
+ },
91
+ {
92
+ "name": "c06_bs_64",
93
+ "config": {
94
+ "backbone": "timm:resnet18.a1_in1k",
95
+ "lr": 0.0002,
96
+ "weight_decay": 0.0001,
97
+ "batch_size": 64,
98
+ "aug_policy": "medical_default",
99
+ "imbalance": "pos_weight",
100
+ "freeze_stage": 0,
101
+ "loss": "bce",
102
+ "optimizer": "adamw",
103
+ "scheduler": "cosine",
104
+ "epochs": 40,
105
+ "early_stop_patience": 8,
106
+ "dropout": 0.0
107
+ },
108
+ "best_val_auroc": 0.9717439999999999,
109
+ "best_epoch": 8,
110
+ "out_dir": "/app/sweep_runs/c06_bs_64",
111
+ "returncode": 0
112
+ },
113
+ {
114
+ "name": "c05_wd_1e-3",
115
+ "config": {
116
+ "backbone": "timm:resnet18.a1_in1k",
117
+ "lr": 0.0002,
118
+ "weight_decay": 0.001,
119
+ "batch_size": 32,
120
+ "aug_policy": "medical_default",
121
+ "imbalance": "pos_weight",
122
+ "freeze_stage": 0,
123
+ "loss": "bce",
124
+ "optimizer": "adamw",
125
+ "scheduler": "cosine",
126
+ "epochs": 40,
127
+ "early_stop_patience": 8,
128
+ "dropout": 0.0
129
+ },
130
+ "best_val_auroc": 0.9712106666666667,
131
+ "best_epoch": 9,
132
+ "out_dir": "/app/sweep_runs/c05_wd_1e-3",
133
+ "returncode": 0
134
+ },
135
+ {
136
+ "name": "c00_center_a1",
137
+ "config": {
138
+ "backbone": "timm:resnet18.a1_in1k",
139
+ "lr": 0.0002,
140
+ "weight_decay": 0.0001,
141
+ "batch_size": 32,
142
+ "aug_policy": "medical_default",
143
+ "imbalance": "pos_weight",
144
+ "freeze_stage": 0,
145
+ "loss": "bce",
146
+ "optimizer": "adamw",
147
+ "scheduler": "cosine",
148
+ "epochs": 40,
149
+ "early_stop_patience": 8,
150
+ "dropout": 0.0
151
+ },
152
+ "best_val_auroc": 0.9711786666666667,
153
+ "best_epoch": 6,
154
+ "out_dir": "/app/sweep_runs/c00_center_a1",
155
+ "returncode": 0
156
+ },
157
+ {
158
+ "name": "c10_imb_sampler",
159
+ "config": {
160
+ "backbone": "timm:resnet18.a1_in1k",
161
+ "lr": 0.0002,
162
+ "weight_decay": 0.0001,
163
+ "batch_size": 32,
164
+ "aug_policy": "medical_default",
165
+ "imbalance": "sampler",
166
+ "freeze_stage": 0,
167
+ "loss": "bce",
168
+ "optimizer": "adamw",
169
+ "scheduler": "cosine",
170
+ "epochs": 40,
171
+ "early_stop_patience": 8,
172
+ "dropout": 0.0
173
+ },
174
+ "best_val_auroc": 0.9696640000000001,
175
+ "best_epoch": 13,
176
+ "out_dir": "/app/sweep_runs/c10_imb_sampler",
177
+ "returncode": 0
178
+ },
179
+ {
180
+ "name": "c02_backbone_a2",
181
+ "config": {
182
+ "backbone": "timm:resnet18.a2_in1k",
183
+ "lr": 0.0002,
184
+ "weight_decay": 0.0001,
185
+ "batch_size": 32,
186
+ "aug_policy": "medical_default",
187
+ "imbalance": "pos_weight",
188
+ "freeze_stage": 0,
189
+ "loss": "bce",
190
+ "optimizer": "adamw",
191
+ "scheduler": "cosine",
192
+ "epochs": 40,
193
+ "early_stop_patience": 8,
194
+ "dropout": 0.0
195
+ },
196
+ "best_val_auroc": 0.9693440000000001,
197
+ "best_epoch": 6,
198
+ "out_dir": "/app/sweep_runs/c02_backbone_a2",
199
+ "returncode": 0
200
+ },
201
+ {
202
+ "name": "c04_lr_5e-4",
203
+ "config": {
204
+ "backbone": "timm:resnet18.a1_in1k",
205
+ "lr": 0.0005,
206
+ "weight_decay": 0.0001,
207
+ "batch_size": 32,
208
+ "aug_policy": "medical_default",
209
+ "imbalance": "pos_weight",
210
+ "freeze_stage": 0,
211
+ "loss": "bce",
212
+ "optimizer": "adamw",
213
+ "scheduler": "cosine",
214
+ "epochs": 40,
215
+ "early_stop_patience": 8,
216
+ "dropout": 0.0
217
+ },
218
+ "best_val_auroc": 0.9675306666666666,
219
+ "best_epoch": 9,
220
+ "out_dir": "/app/sweep_runs/c04_lr_5e-4",
221
+ "returncode": 0
222
+ },
223
+ {
224
+ "name": "c11_freeze1",
225
+ "config": {
226
+ "backbone": "timm:resnet18.a1_in1k",
227
+ "lr": 0.0002,
228
+ "weight_decay": 0.0001,
229
+ "batch_size": 32,
230
+ "aug_policy": "medical_default",
231
+ "imbalance": "pos_weight",
232
+ "freeze_stage": 1,
233
+ "loss": "bce",
234
+ "optimizer": "adamw",
235
+ "scheduler": "cosine",
236
+ "epochs": 40,
237
+ "early_stop_patience": 8,
238
+ "dropout": 0.0
239
+ },
240
+ "best_val_auroc": 0.9671786666666665,
241
+ "best_epoch": 6,
242
+ "out_dir": "/app/sweep_runs/c11_freeze1",
243
+ "returncode": 0
244
+ },
245
+ {
246
+ "name": "c13_lr1e-4_wd1e-3_drop",
247
+ "config": {
248
+ "backbone": "timm:resnet18.a1_in1k",
249
+ "lr": 0.0001,
250
+ "weight_decay": 0.001,
251
+ "batch_size": 32,
252
+ "aug_policy": "medical_default",
253
+ "imbalance": "pos_weight",
254
+ "freeze_stage": 0,
255
+ "loss": "bce",
256
+ "optimizer": "adamw",
257
+ "scheduler": "cosine",
258
+ "epochs": 40,
259
+ "early_stop_patience": 8,
260
+ "dropout": 0.2
261
+ },
262
+ "best_val_auroc": 0.965696,
263
+ "best_epoch": 11,
264
+ "out_dir": "/app/sweep_runs/c13_lr1e-4_wd1e-3_drop",
265
+ "returncode": 0
266
+ },
267
+ {
268
+ "name": "c07_aug_flip_only",
269
+ "config": {
270
+ "backbone": "timm:resnet18.a1_in1k",
271
+ "lr": 0.0002,
272
+ "weight_decay": 0.0001,
273
+ "batch_size": 32,
274
+ "aug_policy": "flip_only",
275
+ "imbalance": "pos_weight",
276
+ "freeze_stage": 0,
277
+ "loss": "bce",
278
+ "optimizer": "adamw",
279
+ "scheduler": "cosine",
280
+ "epochs": 40,
281
+ "early_stop_patience": 8,
282
+ "dropout": 0.0
283
+ },
284
+ "best_val_auroc": 0.9637226666666667,
285
+ "best_epoch": 6,
286
+ "out_dir": "/app/sweep_runs/c07_aug_flip_only",
287
+ "returncode": 0
288
+ },
289
+ {
290
+ "name": "c08_aug_strong",
291
+ "config": {
292
+ "backbone": "timm:resnet18.a1_in1k",
293
+ "lr": 0.0002,
294
+ "weight_decay": 0.0001,
295
+ "batch_size": 32,
296
+ "aug_policy": "medical_strong",
297
+ "imbalance": "pos_weight",
298
+ "freeze_stage": 0,
299
+ "loss": "bce",
300
+ "optimizer": "adamw",
301
+ "scheduler": "cosine",
302
+ "epochs": 40,
303
+ "early_stop_patience": 8,
304
+ "dropout": 0.0
305
+ },
306
+ "best_val_auroc": 0.9609386666666667,
307
+ "best_epoch": 8,
308
+ "out_dir": "/app/sweep_runs/c08_aug_strong",
309
+ "returncode": 0
310
+ }
311
+ ]
results/tables/sweep_results.json ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "name": "c00_center_a1",
4
+ "config": {
5
+ "backbone": "timm:resnet18.a1_in1k",
6
+ "lr": 0.0002,
7
+ "weight_decay": 0.0001,
8
+ "batch_size": 32,
9
+ "aug_policy": "medical_default",
10
+ "imbalance": "pos_weight",
11
+ "freeze_stage": 0,
12
+ "loss": "bce",
13
+ "optimizer": "adamw",
14
+ "scheduler": "cosine",
15
+ "epochs": 40,
16
+ "early_stop_patience": 8,
17
+ "dropout": 0.0
18
+ },
19
+ "best_val_auroc": 0.9711786666666667,
20
+ "best_epoch": 6,
21
+ "out_dir": "/app/sweep_runs/c00_center_a1",
22
+ "returncode": 0
23
+ },
24
+ {
25
+ "name": "c01_backbone_torchvision",
26
+ "config": {
27
+ "backbone": "torchvision",
28
+ "lr": 0.0002,
29
+ "weight_decay": 0.0001,
30
+ "batch_size": 32,
31
+ "aug_policy": "medical_default",
32
+ "imbalance": "pos_weight",
33
+ "freeze_stage": 0,
34
+ "loss": "bce",
35
+ "optimizer": "adamw",
36
+ "scheduler": "cosine",
37
+ "epochs": 40,
38
+ "early_stop_patience": 8,
39
+ "dropout": 0.0
40
+ },
41
+ "best_val_auroc": 0.9730986666666667,
42
+ "best_epoch": 7,
43
+ "out_dir": "/app/sweep_runs/c01_backbone_torchvision",
44
+ "returncode": 0
45
+ },
46
+ {
47
+ "name": "c02_backbone_a2",
48
+ "config": {
49
+ "backbone": "timm:resnet18.a2_in1k",
50
+ "lr": 0.0002,
51
+ "weight_decay": 0.0001,
52
+ "batch_size": 32,
53
+ "aug_policy": "medical_default",
54
+ "imbalance": "pos_weight",
55
+ "freeze_stage": 0,
56
+ "loss": "bce",
57
+ "optimizer": "adamw",
58
+ "scheduler": "cosine",
59
+ "epochs": 40,
60
+ "early_stop_patience": 8,
61
+ "dropout": 0.0
62
+ },
63
+ "best_val_auroc": 0.9693440000000001,
64
+ "best_epoch": 6,
65
+ "out_dir": "/app/sweep_runs/c02_backbone_a2",
66
+ "returncode": 0
67
+ },
68
+ {
69
+ "name": "c03_lr_1e-4",
70
+ "config": {
71
+ "backbone": "timm:resnet18.a1_in1k",
72
+ "lr": 0.0001,
73
+ "weight_decay": 0.0001,
74
+ "batch_size": 32,
75
+ "aug_policy": "medical_default",
76
+ "imbalance": "pos_weight",
77
+ "freeze_stage": 0,
78
+ "loss": "bce",
79
+ "optimizer": "adamw",
80
+ "scheduler": "cosine",
81
+ "epochs": 40,
82
+ "early_stop_patience": 8,
83
+ "dropout": 0.0
84
+ },
85
+ "best_val_auroc": 0.9720533333333334,
86
+ "best_epoch": 11,
87
+ "out_dir": "/app/sweep_runs/c03_lr_1e-4",
88
+ "returncode": 0
89
+ },
90
+ {
91
+ "name": "c04_lr_5e-4",
92
+ "config": {
93
+ "backbone": "timm:resnet18.a1_in1k",
94
+ "lr": 0.0005,
95
+ "weight_decay": 0.0001,
96
+ "batch_size": 32,
97
+ "aug_policy": "medical_default",
98
+ "imbalance": "pos_weight",
99
+ "freeze_stage": 0,
100
+ "loss": "bce",
101
+ "optimizer": "adamw",
102
+ "scheduler": "cosine",
103
+ "epochs": 40,
104
+ "early_stop_patience": 8,
105
+ "dropout": 0.0
106
+ },
107
+ "best_val_auroc": 0.9675306666666666,
108
+ "best_epoch": 9,
109
+ "out_dir": "/app/sweep_runs/c04_lr_5e-4",
110
+ "returncode": 0
111
+ },
112
+ {
113
+ "name": "c05_wd_1e-3",
114
+ "config": {
115
+ "backbone": "timm:resnet18.a1_in1k",
116
+ "lr": 0.0002,
117
+ "weight_decay": 0.001,
118
+ "batch_size": 32,
119
+ "aug_policy": "medical_default",
120
+ "imbalance": "pos_weight",
121
+ "freeze_stage": 0,
122
+ "loss": "bce",
123
+ "optimizer": "adamw",
124
+ "scheduler": "cosine",
125
+ "epochs": 40,
126
+ "early_stop_patience": 8,
127
+ "dropout": 0.0
128
+ },
129
+ "best_val_auroc": 0.9712106666666667,
130
+ "best_epoch": 9,
131
+ "out_dir": "/app/sweep_runs/c05_wd_1e-3",
132
+ "returncode": 0
133
+ },
134
+ {
135
+ "name": "c06_bs_64",
136
+ "config": {
137
+ "backbone": "timm:resnet18.a1_in1k",
138
+ "lr": 0.0002,
139
+ "weight_decay": 0.0001,
140
+ "batch_size": 64,
141
+ "aug_policy": "medical_default",
142
+ "imbalance": "pos_weight",
143
+ "freeze_stage": 0,
144
+ "loss": "bce",
145
+ "optimizer": "adamw",
146
+ "scheduler": "cosine",
147
+ "epochs": 40,
148
+ "early_stop_patience": 8,
149
+ "dropout": 0.0
150
+ },
151
+ "best_val_auroc": 0.9717439999999999,
152
+ "best_epoch": 8,
153
+ "out_dir": "/app/sweep_runs/c06_bs_64",
154
+ "returncode": 0
155
+ },
156
+ {
157
+ "name": "c07_aug_flip_only",
158
+ "config": {
159
+ "backbone": "timm:resnet18.a1_in1k",
160
+ "lr": 0.0002,
161
+ "weight_decay": 0.0001,
162
+ "batch_size": 32,
163
+ "aug_policy": "flip_only",
164
+ "imbalance": "pos_weight",
165
+ "freeze_stage": 0,
166
+ "loss": "bce",
167
+ "optimizer": "adamw",
168
+ "scheduler": "cosine",
169
+ "epochs": 40,
170
+ "early_stop_patience": 8,
171
+ "dropout": 0.0
172
+ },
173
+ "best_val_auroc": 0.9637226666666667,
174
+ "best_epoch": 6,
175
+ "out_dir": "/app/sweep_runs/c07_aug_flip_only",
176
+ "returncode": 0
177
+ },
178
+ {
179
+ "name": "c08_aug_strong",
180
+ "config": {
181
+ "backbone": "timm:resnet18.a1_in1k",
182
+ "lr": 0.0002,
183
+ "weight_decay": 0.0001,
184
+ "batch_size": 32,
185
+ "aug_policy": "medical_strong",
186
+ "imbalance": "pos_weight",
187
+ "freeze_stage": 0,
188
+ "loss": "bce",
189
+ "optimizer": "adamw",
190
+ "scheduler": "cosine",
191
+ "epochs": 40,
192
+ "early_stop_patience": 8,
193
+ "dropout": 0.0
194
+ },
195
+ "best_val_auroc": 0.9609386666666667,
196
+ "best_epoch": 8,
197
+ "out_dir": "/app/sweep_runs/c08_aug_strong",
198
+ "returncode": 0
199
+ },
200
+ {
201
+ "name": "c09_imb_none",
202
+ "config": {
203
+ "backbone": "timm:resnet18.a1_in1k",
204
+ "lr": 0.0002,
205
+ "weight_decay": 0.0001,
206
+ "batch_size": 32,
207
+ "aug_policy": "medical_default",
208
+ "imbalance": "none",
209
+ "freeze_stage": 0,
210
+ "loss": "bce",
211
+ "optimizer": "adamw",
212
+ "scheduler": "cosine",
213
+ "epochs": 40,
214
+ "early_stop_patience": 8,
215
+ "dropout": 0.0
216
+ },
217
+ "best_val_auroc": 0.9739093333333334,
218
+ "best_epoch": 6,
219
+ "out_dir": "/app/sweep_runs/c09_imb_none",
220
+ "returncode": 0
221
+ },
222
+ {
223
+ "name": "c10_imb_sampler",
224
+ "config": {
225
+ "backbone": "timm:resnet18.a1_in1k",
226
+ "lr": 0.0002,
227
+ "weight_decay": 0.0001,
228
+ "batch_size": 32,
229
+ "aug_policy": "medical_default",
230
+ "imbalance": "sampler",
231
+ "freeze_stage": 0,
232
+ "loss": "bce",
233
+ "optimizer": "adamw",
234
+ "scheduler": "cosine",
235
+ "epochs": 40,
236
+ "early_stop_patience": 8,
237
+ "dropout": 0.0
238
+ },
239
+ "best_val_auroc": 0.9696640000000001,
240
+ "best_epoch": 13,
241
+ "out_dir": "/app/sweep_runs/c10_imb_sampler",
242
+ "returncode": 0
243
+ },
244
+ {
245
+ "name": "c11_freeze1",
246
+ "config": {
247
+ "backbone": "timm:resnet18.a1_in1k",
248
+ "lr": 0.0002,
249
+ "weight_decay": 0.0001,
250
+ "batch_size": 32,
251
+ "aug_policy": "medical_default",
252
+ "imbalance": "pos_weight",
253
+ "freeze_stage": 1,
254
+ "loss": "bce",
255
+ "optimizer": "adamw",
256
+ "scheduler": "cosine",
257
+ "epochs": 40,
258
+ "early_stop_patience": 8,
259
+ "dropout": 0.0
260
+ },
261
+ "best_val_auroc": 0.9671786666666665,
262
+ "best_epoch": 6,
263
+ "out_dir": "/app/sweep_runs/c11_freeze1",
264
+ "returncode": 0
265
+ },
266
+ {
267
+ "name": "c12_loss_focal",
268
+ "config": {
269
+ "backbone": "timm:resnet18.a1_in1k",
270
+ "lr": 0.0002,
271
+ "weight_decay": 0.0001,
272
+ "batch_size": 32,
273
+ "aug_policy": "medical_default",
274
+ "imbalance": "none",
275
+ "freeze_stage": 0,
276
+ "loss": "focal",
277
+ "optimizer": "adamw",
278
+ "scheduler": "cosine",
279
+ "epochs": 40,
280
+ "early_stop_patience": 8,
281
+ "dropout": 0.0,
282
+ "focal_gamma": 1.0
283
+ },
284
+ "best_val_auroc": 0.9756053333333333,
285
+ "best_epoch": 6,
286
+ "out_dir": "/app/sweep_runs/c12_loss_focal",
287
+ "returncode": 0
288
+ },
289
+ {
290
+ "name": "c13_lr1e-4_wd1e-3_drop",
291
+ "config": {
292
+ "backbone": "timm:resnet18.a1_in1k",
293
+ "lr": 0.0001,
294
+ "weight_decay": 0.001,
295
+ "batch_size": 32,
296
+ "aug_policy": "medical_default",
297
+ "imbalance": "pos_weight",
298
+ "freeze_stage": 0,
299
+ "loss": "bce",
300
+ "optimizer": "adamw",
301
+ "scheduler": "cosine",
302
+ "epochs": 40,
303
+ "early_stop_patience": 8,
304
+ "dropout": 0.2
305
+ },
306
+ "best_val_auroc": 0.965696,
307
+ "best_epoch": 11,
308
+ "out_dir": "/app/sweep_runs/c13_lr1e-4_wd1e-3_drop",
309
+ "returncode": 0
310
+ }
311
+ ]
results/tables/test_metrics_with_ci.csv ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ metric,point_estimate,ci_low,ci_high
2
+ auroc,0.937144,0.920204,0.952817
3
+ sensitivity,0.904241,0.882353,0.924761
4
+ specificity,0.795539,0.743494,0.843866
5
+ ppv,0.923184,0.905422,0.940088
6
+ npv,0.753521,0.712270,0.797907
7
+ accuracy,0.875000,0.854000,0.895000
8
+ f1,0.913614,0.899095,0.927778
9
+ brier,0.082285,,
10
+ ece,0.031438,,
results/tables/test_metrics_with_ci.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Final Test Metrics (locked model + locked threshold)
2
+
3
+ - Selected run: c12_loss_focal | selection val AUROC: 0.9756
4
+ - Backbone: timm:resnet18.a1_in1k | Calibration: temperature scaling (T=0.5646, used)
5
+ - Locked threshold (val sens>=0.95): 0.7113 | probabilities: calibrated
6
+ - CI method: stratified bootstrap, 2000 resamples, seed=42
7
+
8
+ | Metric | Point estimate | 95% CI |
9
+ |--------|---------------:|:------:|
10
+ | AUROC | 0.9371 | [0.9202, 0.9528] |
11
+ | SENSITIVITY | 0.9042 | [0.8824, 0.9248] |
12
+ | SPECIFICITY | 0.7955 | [0.7435, 0.8439] |
13
+ | PPV | 0.9232 | [0.9054, 0.9401] |
14
+ | NPV | 0.7535 | [0.7123, 0.7979] |
15
+ | ACCURACY | 0.8750 | [0.8540, 0.8950] |
16
+ | F1 | 0.9136 | [0.8991, 0.9278] |
17
+ | BRIER | 0.0823 | — |
18
+ | ECE | 0.0314 | — |
19
+
20
+ **Confusion matrix (Test):** TN=214, FP=55, FN=70, TP=661
results/test_predictions.csv ADDED
@@ -0,0 +1,1001 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ image_id,true_label,true_class,probability_malignant,predicted_label,predicted_class
2
+ 000001,0,Benign,0.376765,0,Benign
3
+ 000006,0,Benign,0.018830,0,Benign
4
+ 000009,0,Benign,0.516381,0,Benign
5
+ 000011,0,Benign,0.011112,0,Benign
6
+ 000016,0,Benign,0.016702,0,Benign
7
+ 000025,0,Benign,0.572745,0,Benign
8
+ 000035,0,Benign,0.045335,0,Benign
9
+ 000040,0,Benign,0.387496,0,Benign
10
+ 000044,0,Benign,0.532899,0,Benign
11
+ 000046,0,Benign,0.932031,1,Malignant
12
+ 000048,0,Benign,0.997977,1,Malignant
13
+ 000054,0,Benign,0.960895,1,Malignant
14
+ 000060,0,Benign,0.025679,0,Benign
15
+ 000083,0,Benign,0.948330,1,Malignant
16
+ 000086,0,Benign,0.003489,0,Benign
17
+ 000093,0,Benign,0.135352,0,Benign
18
+ 000105,0,Benign,0.092530,0,Benign
19
+ 000120,0,Benign,0.103708,0,Benign
20
+ 000123,0,Benign,0.068463,0,Benign
21
+ 000126,0,Benign,0.251543,0,Benign
22
+ 000131,0,Benign,0.780844,1,Malignant
23
+ 000137,0,Benign,0.925188,1,Malignant
24
+ 000149,0,Benign,0.019343,0,Benign
25
+ 000153,0,Benign,0.062035,0,Benign
26
+ 000154,0,Benign,0.090431,0,Benign
27
+ 000161,0,Benign,0.834368,1,Malignant
28
+ 000169,0,Benign,0.386561,0,Benign
29
+ 000174,0,Benign,0.879822,1,Malignant
30
+ 000177,0,Benign,0.786450,1,Malignant
31
+ 000184,0,Benign,0.034857,0,Benign
32
+ 000190,0,Benign,0.463699,0,Benign
33
+ 000197,0,Benign,0.021787,0,Benign
34
+ 000202,0,Benign,0.902157,1,Malignant
35
+ 000212,0,Benign,0.056520,0,Benign
36
+ 000222,0,Benign,0.439280,0,Benign
37
+ 000223,0,Benign,0.586564,0,Benign
38
+ 000224,0,Benign,0.935356,1,Malignant
39
+ 000225,0,Benign,0.972133,1,Malignant
40
+ 000228,0,Benign,0.506554,0,Benign
41
+ 000244,0,Benign,0.970782,1,Malignant
42
+ 000246,0,Benign,0.079815,0,Benign
43
+ 000276,0,Benign,0.007983,0,Benign
44
+ 000300,0,Benign,0.402881,0,Benign
45
+ 000304,0,Benign,0.050980,0,Benign
46
+ 000307,0,Benign,0.103203,0,Benign
47
+ 000318,0,Benign,0.750014,1,Malignant
48
+ 000323,0,Benign,0.100095,0,Benign
49
+ 000337,0,Benign,0.706239,0,Benign
50
+ 000339,0,Benign,0.869558,1,Malignant
51
+ 000342,0,Benign,0.042517,0,Benign
52
+ 000349,0,Benign,0.835838,1,Malignant
53
+ 000361,0,Benign,0.180529,0,Benign
54
+ 000363,0,Benign,0.101132,0,Benign
55
+ 000366,0,Benign,0.556296,0,Benign
56
+ 000371,0,Benign,0.993220,1,Malignant
57
+ 000379,0,Benign,0.003288,0,Benign
58
+ 000382,0,Benign,0.885058,1,Malignant
59
+ 000385,0,Benign,0.916005,1,Malignant
60
+ 000387,0,Benign,0.148785,0,Benign
61
+ 000400,0,Benign,0.691709,0,Benign
62
+ 000405,0,Benign,0.366323,0,Benign
63
+ 000410,0,Benign,0.004203,0,Benign
64
+ 000413,0,Benign,0.156769,0,Benign
65
+ 000415,0,Benign,0.502721,0,Benign
66
+ 000421,0,Benign,0.000583,0,Benign
67
+ 000430,0,Benign,0.010474,0,Benign
68
+ 000432,0,Benign,0.203537,0,Benign
69
+ 000433,0,Benign,0.081840,0,Benign
70
+ 000438,0,Benign,0.364286,0,Benign
71
+ 000445,0,Benign,0.799353,1,Malignant
72
+ 000451,0,Benign,0.647562,0,Benign
73
+ 000455,0,Benign,0.154093,0,Benign
74
+ 000459,0,Benign,0.011427,0,Benign
75
+ 000460,0,Benign,0.008466,0,Benign
76
+ 000461,0,Benign,0.014595,0,Benign
77
+ 000468,0,Benign,0.125134,0,Benign
78
+ 000487,0,Benign,0.008266,0,Benign
79
+ 000496,0,Benign,0.022297,0,Benign
80
+ 000519,0,Benign,0.108617,0,Benign
81
+ 000537,0,Benign,0.018078,0,Benign
82
+ 000541,0,Benign,0.017684,0,Benign
83
+ 000545,0,Benign,0.007944,0,Benign
84
+ 000546,0,Benign,0.078626,0,Benign
85
+ 000554,0,Benign,0.012844,0,Benign
86
+ 000566,0,Benign,0.013038,0,Benign
87
+ 000573,0,Benign,0.060264,0,Benign
88
+ 000576,0,Benign,0.128162,0,Benign
89
+ 000579,0,Benign,0.001836,0,Benign
90
+ 000582,0,Benign,0.165779,0,Benign
91
+ 000601,0,Benign,0.046323,0,Benign
92
+ 000605,0,Benign,0.930099,1,Malignant
93
+ 000606,0,Benign,0.978089,1,Malignant
94
+ 000607,0,Benign,0.797922,1,Malignant
95
+ 000623,0,Benign,0.892158,1,Malignant
96
+ 000630,0,Benign,0.219395,0,Benign
97
+ 000634,0,Benign,0.897862,1,Malignant
98
+ 000635,0,Benign,0.832107,1,Malignant
99
+ 000637,0,Benign,0.829636,1,Malignant
100
+ 000641,0,Benign,0.029093,0,Benign
101
+ 000649,0,Benign,0.095892,0,Benign
102
+ 000651,0,Benign,0.558030,0,Benign
103
+ 000652,0,Benign,0.074146,0,Benign
104
+ 000669,0,Benign,0.007034,0,Benign
105
+ 001729,0,Benign,0.034281,0,Benign
106
+ 001732,0,Benign,0.721947,1,Malignant
107
+ 001750,0,Benign,0.070680,0,Benign
108
+ 001751,0,Benign,0.048564,0,Benign
109
+ 001762,0,Benign,0.156416,0,Benign
110
+ 001763,0,Benign,0.029954,0,Benign
111
+ 001764,0,Benign,0.151097,0,Benign
112
+ 001765,0,Benign,0.010756,0,Benign
113
+ 001766,0,Benign,0.900002,1,Malignant
114
+ 001769,0,Benign,0.142776,0,Benign
115
+ 001770,0,Benign,0.628850,0,Benign
116
+ 001771,0,Benign,0.003992,0,Benign
117
+ 001773,0,Benign,0.336814,0,Benign
118
+ 001775,0,Benign,0.001492,0,Benign
119
+ 001776,0,Benign,0.005223,0,Benign
120
+ 001781,0,Benign,0.186179,0,Benign
121
+ 001785,0,Benign,0.025104,0,Benign
122
+ 001786,0,Benign,0.749363,1,Malignant
123
+ 001790,0,Benign,0.640222,0,Benign
124
+ 001795,0,Benign,0.008402,0,Benign
125
+ 001807,0,Benign,0.013915,0,Benign
126
+ 001812,0,Benign,0.032009,0,Benign
127
+ 001819,0,Benign,0.408997,0,Benign
128
+ 001823,0,Benign,0.015756,0,Benign
129
+ 001831,0,Benign,0.019138,0,Benign
130
+ 001834,0,Benign,0.013611,0,Benign
131
+ 001847,0,Benign,0.004663,0,Benign
132
+ 001858,0,Benign,0.066907,0,Benign
133
+ 001871,0,Benign,0.405174,0,Benign
134
+ 001874,0,Benign,0.780143,1,Malignant
135
+ 001875,0,Benign,0.089945,0,Benign
136
+ 001876,0,Benign,0.982950,1,Malignant
137
+ 001878,0,Benign,0.078918,0,Benign
138
+ 001879,0,Benign,0.068803,0,Benign
139
+ 001885,0,Benign,0.059237,0,Benign
140
+ 001886,0,Benign,0.161767,0,Benign
141
+ 001891,0,Benign,0.864839,1,Malignant
142
+ 001892,0,Benign,0.023491,0,Benign
143
+ 001898,0,Benign,0.002351,0,Benign
144
+ 001908,0,Benign,0.139884,0,Benign
145
+ 001918,0,Benign,0.958023,1,Malignant
146
+ 001919,0,Benign,0.115347,0,Benign
147
+ 001920,0,Benign,0.080368,0,Benign
148
+ 001928,0,Benign,0.000542,0,Benign
149
+ 001937,0,Benign,0.183525,0,Benign
150
+ 001941,0,Benign,0.004168,0,Benign
151
+ 001942,0,Benign,0.057188,0,Benign
152
+ 001944,0,Benign,0.000413,0,Benign
153
+ 002339,0,Benign,0.122457,0,Benign
154
+ 002340,0,Benign,0.001082,0,Benign
155
+ 002343,0,Benign,0.053878,0,Benign
156
+ 002356,0,Benign,0.003285,0,Benign
157
+ 002359,0,Benign,0.309704,0,Benign
158
+ 002362,0,Benign,0.871511,1,Malignant
159
+ 002364,0,Benign,0.273575,0,Benign
160
+ 002365,0,Benign,0.163321,0,Benign
161
+ 002366,0,Benign,0.402194,0,Benign
162
+ 002367,0,Benign,0.151131,0,Benign
163
+ 002368,0,Benign,0.160100,0,Benign
164
+ 002373,0,Benign,0.014142,0,Benign
165
+ 002374,0,Benign,0.024371,0,Benign
166
+ 002381,0,Benign,0.011476,0,Benign
167
+ 002382,0,Benign,0.040132,0,Benign
168
+ 002396,0,Benign,0.032057,0,Benign
169
+ 002420,0,Benign,0.000711,0,Benign
170
+ 002429,0,Benign,0.332105,0,Benign
171
+ 002435,0,Benign,0.618278,0,Benign
172
+ 002448,0,Benign,0.958286,1,Malignant
173
+ 002451,0,Benign,0.969416,1,Malignant
174
+ 002456,0,Benign,0.020082,0,Benign
175
+ 002459,0,Benign,0.141170,0,Benign
176
+ 002461,0,Benign,0.168457,0,Benign
177
+ 002465,0,Benign,0.017178,0,Benign
178
+ 002474,0,Benign,0.012569,0,Benign
179
+ 002483,0,Benign,0.068126,0,Benign
180
+ 002517,0,Benign,0.002582,0,Benign
181
+ 002588,0,Benign,0.893838,1,Malignant
182
+ 002606,0,Benign,0.086433,0,Benign
183
+ 002791,0,Benign,0.352876,0,Benign
184
+ 002858,0,Benign,0.022251,0,Benign
185
+ 002863,0,Benign,0.167775,0,Benign
186
+ 002875,0,Benign,0.009737,0,Benign
187
+ 002877,0,Benign,0.001938,0,Benign
188
+ 002878,0,Benign,0.007933,0,Benign
189
+ 002882,0,Benign,0.841961,1,Malignant
190
+ 002883,0,Benign,0.071421,0,Benign
191
+ 002889,0,Benign,0.840128,1,Malignant
192
+ 002899,0,Benign,0.020725,0,Benign
193
+ 002912,0,Benign,0.720952,1,Malignant
194
+ 002916,0,Benign,0.027808,0,Benign
195
+ 002918,0,Benign,0.653529,0,Benign
196
+ 002928,0,Benign,0.003498,0,Benign
197
+ 002931,0,Benign,0.007220,0,Benign
198
+ 002936,0,Benign,0.395898,0,Benign
199
+ 002939,0,Benign,0.015212,0,Benign
200
+ 002945,0,Benign,0.994249,1,Malignant
201
+ 002956,0,Benign,0.028150,0,Benign
202
+ 002959,0,Benign,0.135803,0,Benign
203
+ 002964,0,Benign,0.002368,0,Benign
204
+ 002966,0,Benign,0.001806,0,Benign
205
+ 002971,0,Benign,0.067422,0,Benign
206
+ 003010,0,Benign,0.168619,0,Benign
207
+ 003031,0,Benign,0.051348,0,Benign
208
+ 003040,0,Benign,0.167214,0,Benign
209
+ 003048,0,Benign,0.012287,0,Benign
210
+ 003115,0,Benign,0.035206,0,Benign
211
+ 003139,0,Benign,0.111779,0,Benign
212
+ 003443,0,Benign,0.000723,0,Benign
213
+ 003445,0,Benign,0.145899,0,Benign
214
+ 003456,0,Benign,0.192768,0,Benign
215
+ 003469,0,Benign,0.009274,0,Benign
216
+ 003471,0,Benign,0.801371,1,Malignant
217
+ 003474,0,Benign,0.905377,1,Malignant
218
+ 003475,0,Benign,0.201188,0,Benign
219
+ 003477,0,Benign,0.049918,0,Benign
220
+ 003485,0,Benign,0.416427,0,Benign
221
+ 003489,0,Benign,0.222432,0,Benign
222
+ 003505,0,Benign,0.820943,1,Malignant
223
+ 003506,0,Benign,0.015627,0,Benign
224
+ 003510,0,Benign,0.005326,0,Benign
225
+ 003526,0,Benign,0.545514,0,Benign
226
+ 003527,0,Benign,0.182499,0,Benign
227
+ 003541,0,Benign,0.164417,0,Benign
228
+ 003545,0,Benign,0.136935,0,Benign
229
+ 003547,0,Benign,0.566342,0,Benign
230
+ 003558,0,Benign,0.011882,0,Benign
231
+ 003559,0,Benign,0.028955,0,Benign
232
+ 003573,0,Benign,0.709600,0,Benign
233
+ 003575,0,Benign,0.650911,0,Benign
234
+ 004078,0,Benign,0.164860,0,Benign
235
+ 004081,0,Benign,0.431349,0,Benign
236
+ 004096,0,Benign,0.431688,0,Benign
237
+ 004097,0,Benign,0.008423,0,Benign
238
+ 004099,0,Benign,0.036861,0,Benign
239
+ 004105,0,Benign,0.379387,0,Benign
240
+ 004110,0,Benign,0.526580,0,Benign
241
+ 004115,0,Benign,0.665016,0,Benign
242
+ 004122,0,Benign,0.207223,0,Benign
243
+ 004126,0,Benign,0.815986,1,Malignant
244
+ 004130,0,Benign,0.004614,0,Benign
245
+ 004136,0,Benign,0.093367,0,Benign
246
+ 004140,0,Benign,0.003487,0,Benign
247
+ 004142,0,Benign,0.012029,0,Benign
248
+ 004146,0,Benign,0.843166,1,Malignant
249
+ 004147,0,Benign,0.001027,0,Benign
250
+ 004151,0,Benign,0.177867,0,Benign
251
+ 004165,0,Benign,0.292728,0,Benign
252
+ 004599,0,Benign,0.310957,0,Benign
253
+ 004626,0,Benign,0.336940,0,Benign
254
+ 004628,0,Benign,0.910667,1,Malignant
255
+ 004631,0,Benign,0.674330,0,Benign
256
+ 004640,0,Benign,0.981575,1,Malignant
257
+ 004641,0,Benign,0.003257,0,Benign
258
+ 004650,0,Benign,0.105583,0,Benign
259
+ 004660,0,Benign,0.487054,0,Benign
260
+ 004667,0,Benign,0.719977,1,Malignant
261
+ 004668,0,Benign,0.918459,1,Malignant
262
+ 004670,0,Benign,0.718596,1,Malignant
263
+ 004671,0,Benign,0.854799,1,Malignant
264
+ 004677,0,Benign,0.529348,0,Benign
265
+ 004678,0,Benign,0.891073,1,Malignant
266
+ 004680,0,Benign,0.839841,1,Malignant
267
+ 004693,0,Benign,0.054126,0,Benign
268
+ 004712,0,Benign,0.042257,0,Benign
269
+ 004715,0,Benign,0.661028,0,Benign
270
+ 004718,0,Benign,0.020256,0,Benign
271
+ 000076,1,Malignant,0.988106,1,Malignant
272
+ 000101,1,Malignant,0.987695,1,Malignant
273
+ 000115,1,Malignant,0.761836,1,Malignant
274
+ 000147,1,Malignant,0.801051,1,Malignant
275
+ 000180,1,Malignant,0.999960,1,Malignant
276
+ 000183,1,Malignant,0.549757,0,Benign
277
+ 000194,1,Malignant,0.996759,1,Malignant
278
+ 000195,1,Malignant,0.683322,0,Benign
279
+ 000240,1,Malignant,0.993892,1,Malignant
280
+ 000416,1,Malignant,0.993512,1,Malignant
281
+ 000483,1,Malignant,0.522804,0,Benign
282
+ 000512,1,Malignant,0.605400,0,Benign
283
+ 000513,1,Malignant,0.831612,1,Malignant
284
+ 000646,1,Malignant,0.754426,1,Malignant
285
+ 000689,1,Malignant,0.999956,1,Malignant
286
+ 000690,1,Malignant,0.954565,1,Malignant
287
+ 000691,1,Malignant,0.927895,1,Malignant
288
+ 000695,1,Malignant,0.992469,1,Malignant
289
+ 000702,1,Malignant,0.924252,1,Malignant
290
+ 000709,1,Malignant,0.265470,0,Benign
291
+ 000710,1,Malignant,0.993988,1,Malignant
292
+ 000712,1,Malignant,0.997112,1,Malignant
293
+ 000714,1,Malignant,0.989430,1,Malignant
294
+ 000728,1,Malignant,0.951929,1,Malignant
295
+ 000735,1,Malignant,0.101958,0,Benign
296
+ 000743,1,Malignant,0.818556,1,Malignant
297
+ 000745,1,Malignant,0.907677,1,Malignant
298
+ 000750,1,Malignant,0.993474,1,Malignant
299
+ 000756,1,Malignant,0.992416,1,Malignant
300
+ 000759,1,Malignant,0.989789,1,Malignant
301
+ 000760,1,Malignant,0.882096,1,Malignant
302
+ 000762,1,Malignant,0.879353,1,Malignant
303
+ 000773,1,Malignant,0.989654,1,Malignant
304
+ 000774,1,Malignant,0.982784,1,Malignant
305
+ 000776,1,Malignant,0.802452,1,Malignant
306
+ 000779,1,Malignant,0.997673,1,Malignant
307
+ 000785,1,Malignant,0.997894,1,Malignant
308
+ 000788,1,Malignant,0.992033,1,Malignant
309
+ 000793,1,Malignant,0.253489,0,Benign
310
+ 000797,1,Malignant,0.786995,1,Malignant
311
+ 000798,1,Malignant,0.628023,0,Benign
312
+ 000802,1,Malignant,0.985165,1,Malignant
313
+ 000807,1,Malignant,0.991284,1,Malignant
314
+ 000809,1,Malignant,0.969051,1,Malignant
315
+ 000815,1,Malignant,0.970573,1,Malignant
316
+ 000818,1,Malignant,0.852901,1,Malignant
317
+ 000822,1,Malignant,0.985544,1,Malignant
318
+ 000832,1,Malignant,0.952743,1,Malignant
319
+ 000841,1,Malignant,0.992124,1,Malignant
320
+ 000842,1,Malignant,0.818830,1,Malignant
321
+ 000844,1,Malignant,0.992149,1,Malignant
322
+ 000845,1,Malignant,0.992757,1,Malignant
323
+ 000846,1,Malignant,0.701353,0,Benign
324
+ 000852,1,Malignant,0.861300,1,Malignant
325
+ 000855,1,Malignant,0.997560,1,Malignant
326
+ 000858,1,Malignant,0.763017,1,Malignant
327
+ 000861,1,Malignant,0.970913,1,Malignant
328
+ 000870,1,Malignant,0.958173,1,Malignant
329
+ 000880,1,Malignant,0.990729,1,Malignant
330
+ 000882,1,Malignant,0.994648,1,Malignant
331
+ 000890,1,Malignant,0.999057,1,Malignant
332
+ 000897,1,Malignant,0.994762,1,Malignant
333
+ 000899,1,Malignant,0.998380,1,Malignant
334
+ 000900,1,Malignant,0.982477,1,Malignant
335
+ 000908,1,Malignant,0.400199,0,Benign
336
+ 000914,1,Malignant,0.995443,1,Malignant
337
+ 000918,1,Malignant,0.962861,1,Malignant
338
+ 000933,1,Malignant,0.982846,1,Malignant
339
+ 000939,1,Malignant,0.985140,1,Malignant
340
+ 000941,1,Malignant,0.945525,1,Malignant
341
+ 000942,1,Malignant,0.933974,1,Malignant
342
+ 000949,1,Malignant,0.976629,1,Malignant
343
+ 000952,1,Malignant,0.715553,1,Malignant
344
+ 000955,1,Malignant,0.166852,0,Benign
345
+ 000957,1,Malignant,0.989746,1,Malignant
346
+ 000966,1,Malignant,0.670455,0,Benign
347
+ 000968,1,Malignant,0.880955,1,Malignant
348
+ 000985,1,Malignant,0.509547,0,Benign
349
+ 000988,1,Malignant,0.703400,0,Benign
350
+ 000997,1,Malignant,0.986491,1,Malignant
351
+ 000998,1,Malignant,0.955285,1,Malignant
352
+ 001001,1,Malignant,0.994848,1,Malignant
353
+ 001012,1,Malignant,0.998219,1,Malignant
354
+ 001014,1,Malignant,0.995197,1,Malignant
355
+ 001021,1,Malignant,0.950214,1,Malignant
356
+ 001030,1,Malignant,0.810742,1,Malignant
357
+ 001038,1,Malignant,0.972109,1,Malignant
358
+ 001041,1,Malignant,0.861759,1,Malignant
359
+ 001043,1,Malignant,0.969137,1,Malignant
360
+ 001046,1,Malignant,0.950824,1,Malignant
361
+ 001049,1,Malignant,0.359347,0,Benign
362
+ 001055,1,Malignant,0.996275,1,Malignant
363
+ 001058,1,Malignant,0.944533,1,Malignant
364
+ 001064,1,Malignant,0.773933,1,Malignant
365
+ 001065,1,Malignant,0.996695,1,Malignant
366
+ 001072,1,Malignant,0.909806,1,Malignant
367
+ 001075,1,Malignant,0.306582,0,Benign
368
+ 001078,1,Malignant,0.998626,1,Malignant
369
+ 001085,1,Malignant,0.998469,1,Malignant
370
+ 001088,1,Malignant,0.964923,1,Malignant
371
+ 001093,1,Malignant,0.986920,1,Malignant
372
+ 001094,1,Malignant,0.716891,1,Malignant
373
+ 001096,1,Malignant,0.988449,1,Malignant
374
+ 001097,1,Malignant,0.962583,1,Malignant
375
+ 001101,1,Malignant,0.847014,1,Malignant
376
+ 001105,1,Malignant,0.985394,1,Malignant
377
+ 001116,1,Malignant,0.986063,1,Malignant
378
+ 001119,1,Malignant,0.980341,1,Malignant
379
+ 001122,1,Malignant,0.740422,1,Malignant
380
+ 001125,1,Malignant,0.564939,0,Benign
381
+ 001140,1,Malignant,0.970276,1,Malignant
382
+ 001146,1,Malignant,0.998717,1,Malignant
383
+ 001153,1,Malignant,0.990983,1,Malignant
384
+ 001154,1,Malignant,0.701181,0,Benign
385
+ 001155,1,Malignant,0.848377,1,Malignant
386
+ 001156,1,Malignant,0.279686,0,Benign
387
+ 001161,1,Malignant,0.951136,1,Malignant
388
+ 001166,1,Malignant,0.993566,1,Malignant
389
+ 001176,1,Malignant,0.977631,1,Malignant
390
+ 001178,1,Malignant,0.668678,0,Benign
391
+ 001181,1,Malignant,0.992727,1,Malignant
392
+ 001184,1,Malignant,0.925752,1,Malignant
393
+ 001186,1,Malignant,0.999712,1,Malignant
394
+ 001200,1,Malignant,0.997661,1,Malignant
395
+ 001204,1,Malignant,0.989757,1,Malignant
396
+ 001214,1,Malignant,0.992934,1,Malignant
397
+ 001215,1,Malignant,0.035125,0,Benign
398
+ 001224,1,Malignant,0.894715,1,Malignant
399
+ 001229,1,Malignant,0.997480,1,Malignant
400
+ 001240,1,Malignant,0.951197,1,Malignant
401
+ 001241,1,Malignant,0.927314,1,Malignant
402
+ 001242,1,Malignant,0.994040,1,Malignant
403
+ 001243,1,Malignant,0.994712,1,Malignant
404
+ 001251,1,Malignant,0.978656,1,Malignant
405
+ 001253,1,Malignant,0.963836,1,Malignant
406
+ 001255,1,Malignant,0.935158,1,Malignant
407
+ 001261,1,Malignant,0.997486,1,Malignant
408
+ 001263,1,Malignant,0.919578,1,Malignant
409
+ 001264,1,Malignant,0.965588,1,Malignant
410
+ 001266,1,Malignant,0.995602,1,Malignant
411
+ 001270,1,Malignant,0.382946,0,Benign
412
+ 001272,1,Malignant,0.960045,1,Malignant
413
+ 001278,1,Malignant,0.985634,1,Malignant
414
+ 001280,1,Malignant,0.919253,1,Malignant
415
+ 001283,1,Malignant,0.711554,1,Malignant
416
+ 001284,1,Malignant,0.996118,1,Malignant
417
+ 001288,1,Malignant,0.979226,1,Malignant
418
+ 001295,1,Malignant,0.999642,1,Malignant
419
+ 001299,1,Malignant,0.991094,1,Malignant
420
+ 001300,1,Malignant,0.916153,1,Malignant
421
+ 001304,1,Malignant,0.995783,1,Malignant
422
+ 001305,1,Malignant,0.999014,1,Malignant
423
+ 001307,1,Malignant,0.898630,1,Malignant
424
+ 001310,1,Malignant,0.999654,1,Malignant
425
+ 001314,1,Malignant,0.934021,1,Malignant
426
+ 001321,1,Malignant,0.985513,1,Malignant
427
+ 001322,1,Malignant,0.439595,0,Benign
428
+ 001326,1,Malignant,0.827354,1,Malignant
429
+ 001332,1,Malignant,0.848429,1,Malignant
430
+ 001336,1,Malignant,0.954777,1,Malignant
431
+ 001337,1,Malignant,0.175104,0,Benign
432
+ 001347,1,Malignant,0.878802,1,Malignant
433
+ 001349,1,Malignant,0.997743,1,Malignant
434
+ 001352,1,Malignant,0.987397,1,Malignant
435
+ 001359,1,Malignant,0.999165,1,Malignant
436
+ 001361,1,Malignant,0.993477,1,Malignant
437
+ 001365,1,Malignant,0.986184,1,Malignant
438
+ 001368,1,Malignant,0.998460,1,Malignant
439
+ 001387,1,Malignant,0.518177,0,Benign
440
+ 001394,1,Malignant,0.919435,1,Malignant
441
+ 001403,1,Malignant,0.606351,0,Benign
442
+ 001407,1,Malignant,0.999603,1,Malignant
443
+ 001408,1,Malignant,0.264139,0,Benign
444
+ 001411,1,Malignant,0.985361,1,Malignant
445
+ 001420,1,Malignant,0.959923,1,Malignant
446
+ 001435,1,Malignant,0.125190,0,Benign
447
+ 001436,1,Malignant,0.841843,1,Malignant
448
+ 001442,1,Malignant,0.999696,1,Malignant
449
+ 001446,1,Malignant,0.943408,1,Malignant
450
+ 001456,1,Malignant,0.998087,1,Malignant
451
+ 001459,1,Malignant,0.990286,1,Malignant
452
+ 001461,1,Malignant,0.999761,1,Malignant
453
+ 001462,1,Malignant,0.993879,1,Malignant
454
+ 001475,1,Malignant,0.997832,1,Malignant
455
+ 001481,1,Malignant,0.981934,1,Malignant
456
+ 001482,1,Malignant,0.993141,1,Malignant
457
+ 001486,1,Malignant,0.926612,1,Malignant
458
+ 001491,1,Malignant,0.633059,0,Benign
459
+ 001495,1,Malignant,0.996593,1,Malignant
460
+ 001501,1,Malignant,0.942648,1,Malignant
461
+ 001503,1,Malignant,0.957815,1,Malignant
462
+ 001519,1,Malignant,0.847699,1,Malignant
463
+ 001521,1,Malignant,0.999766,1,Malignant
464
+ 001528,1,Malignant,0.999351,1,Malignant
465
+ 001535,1,Malignant,0.987446,1,Malignant
466
+ 001549,1,Malignant,0.999527,1,Malignant
467
+ 001554,1,Malignant,0.996131,1,Malignant
468
+ 001556,1,Malignant,0.603066,0,Benign
469
+ 001562,1,Malignant,0.984052,1,Malignant
470
+ 001572,1,Malignant,0.997382,1,Malignant
471
+ 001579,1,Malignant,0.877011,1,Malignant
472
+ 001581,1,Malignant,0.997358,1,Malignant
473
+ 001583,1,Malignant,0.999952,1,Malignant
474
+ 001586,1,Malignant,0.999749,1,Malignant
475
+ 001587,1,Malignant,0.999874,1,Malignant
476
+ 001589,1,Malignant,0.995647,1,Malignant
477
+ 001597,1,Malignant,0.961708,1,Malignant
478
+ 001601,1,Malignant,0.999945,1,Malignant
479
+ 001603,1,Malignant,0.875569,1,Malignant
480
+ 001610,1,Malignant,0.955721,1,Malignant
481
+ 001612,1,Malignant,0.902527,1,Malignant
482
+ 001613,1,Malignant,0.649988,0,Benign
483
+ 001615,1,Malignant,0.908007,1,Malignant
484
+ 001618,1,Malignant,0.999179,1,Malignant
485
+ 001620,1,Malignant,0.906765,1,Malignant
486
+ 001625,1,Malignant,0.968676,1,Malignant
487
+ 001626,1,Malignant,0.917611,1,Malignant
488
+ 001633,1,Malignant,0.779418,1,Malignant
489
+ 001638,1,Malignant,0.981539,1,Malignant
490
+ 001653,1,Malignant,0.997676,1,Malignant
491
+ 001657,1,Malignant,0.988760,1,Malignant
492
+ 001665,1,Malignant,0.963141,1,Malignant
493
+ 001666,1,Malignant,0.967508,1,Malignant
494
+ 001668,1,Malignant,0.998697,1,Malignant
495
+ 001671,1,Malignant,0.993516,1,Malignant
496
+ 001672,1,Malignant,0.966728,1,Malignant
497
+ 001674,1,Malignant,0.992781,1,Malignant
498
+ 001677,1,Malignant,0.944523,1,Malignant
499
+ 001684,1,Malignant,0.946992,1,Malignant
500
+ 001687,1,Malignant,0.982495,1,Malignant
501
+ 001688,1,Malignant,0.982285,1,Malignant
502
+ 001692,1,Malignant,0.516141,0,Benign
503
+ 001695,1,Malignant,0.952865,1,Malignant
504
+ 001697,1,Malignant,0.992004,1,Malignant
505
+ 001705,1,Malignant,0.981087,1,Malignant
506
+ 001712,1,Malignant,0.999790,1,Malignant
507
+ 001716,1,Malignant,0.476895,0,Benign
508
+ 001719,1,Malignant,0.994026,1,Malignant
509
+ 001722,1,Malignant,0.978666,1,Malignant
510
+ 001746,1,Malignant,0.994993,1,Malignant
511
+ 001760,1,Malignant,0.999107,1,Malignant
512
+ 001788,1,Malignant,0.902727,1,Malignant
513
+ 001789,1,Malignant,0.999329,1,Malignant
514
+ 001810,1,Malignant,0.549603,0,Benign
515
+ 001822,1,Malignant,0.856507,1,Malignant
516
+ 001839,1,Malignant,0.986939,1,Malignant
517
+ 001948,1,Malignant,0.741596,1,Malignant
518
+ 001951,1,Malignant,0.997830,1,Malignant
519
+ 001957,1,Malignant,0.998183,1,Malignant
520
+ 001959,1,Malignant,0.998670,1,Malignant
521
+ 001965,1,Malignant,0.798207,1,Malignant
522
+ 001966,1,Malignant,0.312766,0,Benign
523
+ 001974,1,Malignant,0.991981,1,Malignant
524
+ 001983,1,Malignant,0.990025,1,Malignant
525
+ 001985,1,Malignant,0.988131,1,Malignant
526
+ 001999,1,Malignant,0.995642,1,Malignant
527
+ 002004,1,Malignant,0.995988,1,Malignant
528
+ 002005,1,Malignant,0.976291,1,Malignant
529
+ 002012,1,Malignant,0.996132,1,Malignant
530
+ 002016,1,Malignant,0.969541,1,Malignant
531
+ 002019,1,Malignant,0.993095,1,Malignant
532
+ 002026,1,Malignant,0.974777,1,Malignant
533
+ 002033,1,Malignant,0.784744,1,Malignant
534
+ 002036,1,Malignant,0.997118,1,Malignant
535
+ 002044,1,Malignant,0.991402,1,Malignant
536
+ 002048,1,Malignant,0.984623,1,Malignant
537
+ 002052,1,Malignant,0.999388,1,Malignant
538
+ 002060,1,Malignant,0.921639,1,Malignant
539
+ 002062,1,Malignant,0.986420,1,Malignant
540
+ 002069,1,Malignant,0.995716,1,Malignant
541
+ 002071,1,Malignant,0.998633,1,Malignant
542
+ 002072,1,Malignant,0.959817,1,Malignant
543
+ 002079,1,Malignant,0.980674,1,Malignant
544
+ 002082,1,Malignant,0.998629,1,Malignant
545
+ 002084,1,Malignant,0.968700,1,Malignant
546
+ 002085,1,Malignant,0.871633,1,Malignant
547
+ 002088,1,Malignant,0.996773,1,Malignant
548
+ 002092,1,Malignant,0.997441,1,Malignant
549
+ 002097,1,Malignant,0.756661,1,Malignant
550
+ 002098,1,Malignant,0.992359,1,Malignant
551
+ 002099,1,Malignant,0.057134,0,Benign
552
+ 002110,1,Malignant,0.384655,0,Benign
553
+ 002116,1,Malignant,0.141618,0,Benign
554
+ 002124,1,Malignant,0.983205,1,Malignant
555
+ 002128,1,Malignant,0.043400,0,Benign
556
+ 002130,1,Malignant,0.971986,1,Malignant
557
+ 002135,1,Malignant,0.770992,1,Malignant
558
+ 002137,1,Malignant,0.896156,1,Malignant
559
+ 002149,1,Malignant,0.999679,1,Malignant
560
+ 002155,1,Malignant,0.954614,1,Malignant
561
+ 002166,1,Malignant,0.990101,1,Malignant
562
+ 002171,1,Malignant,0.927778,1,Malignant
563
+ 002175,1,Malignant,0.964621,1,Malignant
564
+ 002179,1,Malignant,0.993388,1,Malignant
565
+ 002180,1,Malignant,0.996387,1,Malignant
566
+ 002193,1,Malignant,0.957576,1,Malignant
567
+ 002196,1,Malignant,0.913209,1,Malignant
568
+ 002201,1,Malignant,0.988991,1,Malignant
569
+ 002205,1,Malignant,0.998076,1,Malignant
570
+ 002212,1,Malignant,0.958494,1,Malignant
571
+ 002215,1,Malignant,0.484798,0,Benign
572
+ 002225,1,Malignant,0.985488,1,Malignant
573
+ 002231,1,Malignant,0.849781,1,Malignant
574
+ 002235,1,Malignant,0.997120,1,Malignant
575
+ 002237,1,Malignant,0.996022,1,Malignant
576
+ 002238,1,Malignant,0.716588,1,Malignant
577
+ 002242,1,Malignant,0.998746,1,Malignant
578
+ 002243,1,Malignant,0.765547,1,Malignant
579
+ 002247,1,Malignant,0.982860,1,Malignant
580
+ 002248,1,Malignant,0.991889,1,Malignant
581
+ 002253,1,Malignant,0.054042,0,Benign
582
+ 002261,1,Malignant,0.994336,1,Malignant
583
+ 002264,1,Malignant,0.980897,1,Malignant
584
+ 002272,1,Malignant,0.827028,1,Malignant
585
+ 002275,1,Malignant,0.990638,1,Malignant
586
+ 002277,1,Malignant,0.738599,1,Malignant
587
+ 002278,1,Malignant,0.793623,1,Malignant
588
+ 002286,1,Malignant,0.999520,1,Malignant
589
+ 002290,1,Malignant,0.999678,1,Malignant
590
+ 002294,1,Malignant,0.996064,1,Malignant
591
+ 002295,1,Malignant,0.948572,1,Malignant
592
+ 002306,1,Malignant,0.984173,1,Malignant
593
+ 002310,1,Malignant,0.968276,1,Malignant
594
+ 002317,1,Malignant,0.995948,1,Malignant
595
+ 002326,1,Malignant,0.397195,0,Benign
596
+ 002328,1,Malignant,0.950099,1,Malignant
597
+ 002335,1,Malignant,0.999909,1,Malignant
598
+ 002496,1,Malignant,0.996390,1,Malignant
599
+ 002498,1,Malignant,0.832269,1,Malignant
600
+ 002503,1,Malignant,0.684462,0,Benign
601
+ 002513,1,Malignant,0.996308,1,Malignant
602
+ 002515,1,Malignant,0.983988,1,Malignant
603
+ 002518,1,Malignant,0.999673,1,Malignant
604
+ 002519,1,Malignant,0.914439,1,Malignant
605
+ 002521,1,Malignant,0.999960,1,Malignant
606
+ 002545,1,Malignant,0.989904,1,Malignant
607
+ 002546,1,Malignant,0.918163,1,Malignant
608
+ 002551,1,Malignant,0.995326,1,Malignant
609
+ 002554,1,Malignant,0.999841,1,Malignant
610
+ 002562,1,Malignant,0.991804,1,Malignant
611
+ 002563,1,Malignant,0.941257,1,Malignant
612
+ 002571,1,Malignant,0.994461,1,Malignant
613
+ 002575,1,Malignant,0.910867,1,Malignant
614
+ 002577,1,Malignant,0.890422,1,Malignant
615
+ 002578,1,Malignant,0.980778,1,Malignant
616
+ 002581,1,Malignant,0.995798,1,Malignant
617
+ 002582,1,Malignant,0.961563,1,Malignant
618
+ 002604,1,Malignant,0.973968,1,Malignant
619
+ 002610,1,Malignant,0.938452,1,Malignant
620
+ 002618,1,Malignant,0.999519,1,Malignant
621
+ 002621,1,Malignant,0.959532,1,Malignant
622
+ 002623,1,Malignant,0.939649,1,Malignant
623
+ 002631,1,Malignant,0.998530,1,Malignant
624
+ 002637,1,Malignant,0.908542,1,Malignant
625
+ 002646,1,Malignant,0.974289,1,Malignant
626
+ 002653,1,Malignant,0.998822,1,Malignant
627
+ 002660,1,Malignant,0.457287,0,Benign
628
+ 002662,1,Malignant,0.999520,1,Malignant
629
+ 002664,1,Malignant,0.998301,1,Malignant
630
+ 002665,1,Malignant,0.997695,1,Malignant
631
+ 002669,1,Malignant,0.957053,1,Malignant
632
+ 002675,1,Malignant,0.999279,1,Malignant
633
+ 002678,1,Malignant,0.999403,1,Malignant
634
+ 002680,1,Malignant,0.973139,1,Malignant
635
+ 002682,1,Malignant,0.998670,1,Malignant
636
+ 002683,1,Malignant,0.999524,1,Malignant
637
+ 002686,1,Malignant,0.977987,1,Malignant
638
+ 002689,1,Malignant,0.954236,1,Malignant
639
+ 002690,1,Malignant,0.910959,1,Malignant
640
+ 002697,1,Malignant,0.993698,1,Malignant
641
+ 002700,1,Malignant,0.999971,1,Malignant
642
+ 002701,1,Malignant,0.995512,1,Malignant
643
+ 002702,1,Malignant,0.998439,1,Malignant
644
+ 002707,1,Malignant,0.941048,1,Malignant
645
+ 002722,1,Malignant,0.996735,1,Malignant
646
+ 002724,1,Malignant,0.922314,1,Malignant
647
+ 002726,1,Malignant,0.999215,1,Malignant
648
+ 002730,1,Malignant,0.995685,1,Malignant
649
+ 002733,1,Malignant,0.597124,0,Benign
650
+ 002735,1,Malignant,0.999092,1,Malignant
651
+ 002736,1,Malignant,0.999845,1,Malignant
652
+ 002738,1,Malignant,0.999632,1,Malignant
653
+ 002744,1,Malignant,0.994965,1,Malignant
654
+ 002749,1,Malignant,0.974954,1,Malignant
655
+ 002751,1,Malignant,0.682394,0,Benign
656
+ 002759,1,Malignant,0.926965,1,Malignant
657
+ 002771,1,Malignant,0.984243,1,Malignant
658
+ 002780,1,Malignant,0.671717,0,Benign
659
+ 002785,1,Malignant,0.790536,1,Malignant
660
+ 002788,1,Malignant,0.997999,1,Malignant
661
+ 002794,1,Malignant,0.987351,1,Malignant
662
+ 002799,1,Malignant,0.995547,1,Malignant
663
+ 002807,1,Malignant,0.995296,1,Malignant
664
+ 002811,1,Malignant,0.986844,1,Malignant
665
+ 002813,1,Malignant,0.996474,1,Malignant
666
+ 002827,1,Malignant,0.877527,1,Malignant
667
+ 002829,1,Malignant,0.800691,1,Malignant
668
+ 002830,1,Malignant,0.912302,1,Malignant
669
+ 002832,1,Malignant,0.915515,1,Malignant
670
+ 002836,1,Malignant,0.997616,1,Malignant
671
+ 002838,1,Malignant,0.993679,1,Malignant
672
+ 002847,1,Malignant,0.989258,1,Malignant
673
+ 002985,1,Malignant,0.996471,1,Malignant
674
+ 002989,1,Malignant,0.996722,1,Malignant
675
+ 002996,1,Malignant,0.969577,1,Malignant
676
+ 003001,1,Malignant,0.159474,0,Benign
677
+ 003004,1,Malignant,0.948242,1,Malignant
678
+ 003006,1,Malignant,0.990829,1,Malignant
679
+ 003020,1,Malignant,0.996957,1,Malignant
680
+ 003023,1,Malignant,0.992238,1,Malignant
681
+ 003026,1,Malignant,0.998613,1,Malignant
682
+ 003036,1,Malignant,0.999495,1,Malignant
683
+ 003053,1,Malignant,0.996434,1,Malignant
684
+ 003054,1,Malignant,0.999291,1,Malignant
685
+ 003055,1,Malignant,0.999713,1,Malignant
686
+ 003075,1,Malignant,0.934325,1,Malignant
687
+ 003077,1,Malignant,0.999183,1,Malignant
688
+ 003079,1,Malignant,0.326411,0,Benign
689
+ 003080,1,Malignant,0.989506,1,Malignant
690
+ 003090,1,Malignant,0.966555,1,Malignant
691
+ 003092,1,Malignant,0.933176,1,Malignant
692
+ 003106,1,Malignant,0.850177,1,Malignant
693
+ 003116,1,Malignant,0.901322,1,Malignant
694
+ 003119,1,Malignant,0.990059,1,Malignant
695
+ 003122,1,Malignant,0.672870,0,Benign
696
+ 003134,1,Malignant,0.996894,1,Malignant
697
+ 003138,1,Malignant,0.999338,1,Malignant
698
+ 003154,1,Malignant,0.871454,1,Malignant
699
+ 003165,1,Malignant,0.985871,1,Malignant
700
+ 003169,1,Malignant,0.973861,1,Malignant
701
+ 003171,1,Malignant,0.994774,1,Malignant
702
+ 003172,1,Malignant,0.994695,1,Malignant
703
+ 003175,1,Malignant,0.992313,1,Malignant
704
+ 003187,1,Malignant,0.989373,1,Malignant
705
+ 003192,1,Malignant,0.978116,1,Malignant
706
+ 003200,1,Malignant,0.975344,1,Malignant
707
+ 003204,1,Malignant,0.997120,1,Malignant
708
+ 003205,1,Malignant,0.999778,1,Malignant
709
+ 003219,1,Malignant,0.818023,1,Malignant
710
+ 003231,1,Malignant,0.998308,1,Malignant
711
+ 003235,1,Malignant,0.980066,1,Malignant
712
+ 003238,1,Malignant,0.972372,1,Malignant
713
+ 003246,1,Malignant,0.995843,1,Malignant
714
+ 003249,1,Malignant,0.810772,1,Malignant
715
+ 003252,1,Malignant,0.981918,1,Malignant
716
+ 003254,1,Malignant,0.699511,0,Benign
717
+ 003256,1,Malignant,0.995698,1,Malignant
718
+ 003257,1,Malignant,0.980700,1,Malignant
719
+ 003261,1,Malignant,0.995665,1,Malignant
720
+ 003263,1,Malignant,0.960137,1,Malignant
721
+ 003273,1,Malignant,0.997457,1,Malignant
722
+ 003284,1,Malignant,0.987562,1,Malignant
723
+ 003302,1,Malignant,0.860129,1,Malignant
724
+ 003312,1,Malignant,0.994609,1,Malignant
725
+ 003313,1,Malignant,0.996348,1,Malignant
726
+ 003323,1,Malignant,0.995337,1,Malignant
727
+ 003326,1,Malignant,0.984143,1,Malignant
728
+ 003329,1,Malignant,0.988739,1,Malignant
729
+ 003338,1,Malignant,0.997918,1,Malignant
730
+ 003348,1,Malignant,0.986497,1,Malignant
731
+ 003355,1,Malignant,0.988062,1,Malignant
732
+ 003356,1,Malignant,0.939997,1,Malignant
733
+ 003358,1,Malignant,0.997436,1,Malignant
734
+ 003359,1,Malignant,0.993993,1,Malignant
735
+ 003360,1,Malignant,0.997454,1,Malignant
736
+ 003369,1,Malignant,0.989870,1,Malignant
737
+ 003383,1,Malignant,0.808382,1,Malignant
738
+ 003386,1,Malignant,0.995125,1,Malignant
739
+ 003392,1,Malignant,0.507590,0,Benign
740
+ 003397,1,Malignant,0.995170,1,Malignant
741
+ 003401,1,Malignant,0.984205,1,Malignant
742
+ 003410,1,Malignant,0.995402,1,Malignant
743
+ 003425,1,Malignant,0.998939,1,Malignant
744
+ 003426,1,Malignant,0.999688,1,Malignant
745
+ 003580,1,Malignant,0.988681,1,Malignant
746
+ 003592,1,Malignant,0.997179,1,Malignant
747
+ 003597,1,Malignant,0.999456,1,Malignant
748
+ 003617,1,Malignant,0.639132,0,Benign
749
+ 003621,1,Malignant,0.690308,0,Benign
750
+ 003629,1,Malignant,0.943446,1,Malignant
751
+ 003632,1,Malignant,0.995875,1,Malignant
752
+ 003635,1,Malignant,0.987601,1,Malignant
753
+ 003643,1,Malignant,0.997313,1,Malignant
754
+ 003647,1,Malignant,0.998844,1,Malignant
755
+ 003649,1,Malignant,0.998234,1,Malignant
756
+ 003657,1,Malignant,0.989901,1,Malignant
757
+ 003658,1,Malignant,0.989495,1,Malignant
758
+ 003663,1,Malignant,0.540762,0,Benign
759
+ 003670,1,Malignant,0.998295,1,Malignant
760
+ 003677,1,Malignant,0.990845,1,Malignant
761
+ 003682,1,Malignant,0.993433,1,Malignant
762
+ 003683,1,Malignant,0.986892,1,Malignant
763
+ 003701,1,Malignant,0.940627,1,Malignant
764
+ 003706,1,Malignant,0.896658,1,Malignant
765
+ 003712,1,Malignant,0.876649,1,Malignant
766
+ 003717,1,Malignant,0.989501,1,Malignant
767
+ 003730,1,Malignant,0.781957,1,Malignant
768
+ 003733,1,Malignant,0.992679,1,Malignant
769
+ 003741,1,Malignant,0.954810,1,Malignant
770
+ 003746,1,Malignant,0.964377,1,Malignant
771
+ 003758,1,Malignant,0.994429,1,Malignant
772
+ 003761,1,Malignant,0.986761,1,Malignant
773
+ 003762,1,Malignant,0.991594,1,Malignant
774
+ 003763,1,Malignant,0.901967,1,Malignant
775
+ 003764,1,Malignant,0.999482,1,Malignant
776
+ 003774,1,Malignant,0.837600,1,Malignant
777
+ 003776,1,Malignant,0.629712,0,Benign
778
+ 003777,1,Malignant,0.979540,1,Malignant
779
+ 003781,1,Malignant,0.994604,1,Malignant
780
+ 003782,1,Malignant,0.999094,1,Malignant
781
+ 003787,1,Malignant,0.753626,1,Malignant
782
+ 003802,1,Malignant,0.971741,1,Malignant
783
+ 003804,1,Malignant,0.993994,1,Malignant
784
+ 003806,1,Malignant,0.932448,1,Malignant
785
+ 003808,1,Malignant,0.805702,1,Malignant
786
+ 003809,1,Malignant,0.949941,1,Malignant
787
+ 003814,1,Malignant,0.545864,0,Benign
788
+ 003817,1,Malignant,0.899031,1,Malignant
789
+ 003826,1,Malignant,0.998683,1,Malignant
790
+ 003827,1,Malignant,0.993627,1,Malignant
791
+ 003830,1,Malignant,0.995162,1,Malignant
792
+ 003838,1,Malignant,0.968450,1,Malignant
793
+ 003842,1,Malignant,0.986040,1,Malignant
794
+ 003843,1,Malignant,0.960795,1,Malignant
795
+ 003846,1,Malignant,0.999783,1,Malignant
796
+ 003848,1,Malignant,0.996053,1,Malignant
797
+ 003853,1,Malignant,0.992551,1,Malignant
798
+ 003862,1,Malignant,0.969106,1,Malignant
799
+ 003874,1,Malignant,0.997460,1,Malignant
800
+ 003881,1,Malignant,0.533142,0,Benign
801
+ 003882,1,Malignant,0.567271,0,Benign
802
+ 003883,1,Malignant,0.999837,1,Malignant
803
+ 003887,1,Malignant,0.888906,1,Malignant
804
+ 003888,1,Malignant,0.995096,1,Malignant
805
+ 003891,1,Malignant,0.995519,1,Malignant
806
+ 003904,1,Malignant,0.998775,1,Malignant
807
+ 003906,1,Malignant,0.993712,1,Malignant
808
+ 003908,1,Malignant,0.957494,1,Malignant
809
+ 003916,1,Malignant,0.974722,1,Malignant
810
+ 003917,1,Malignant,0.956523,1,Malignant
811
+ 003925,1,Malignant,0.993435,1,Malignant
812
+ 003933,1,Malignant,0.993865,1,Malignant
813
+ 003936,1,Malignant,0.841002,1,Malignant
814
+ 003942,1,Malignant,0.956218,1,Malignant
815
+ 003943,1,Malignant,0.585758,0,Benign
816
+ 003952,1,Malignant,0.908020,1,Malignant
817
+ 003958,1,Malignant,0.985517,1,Malignant
818
+ 003960,1,Malignant,0.916541,1,Malignant
819
+ 003961,1,Malignant,0.930404,1,Malignant
820
+ 003963,1,Malignant,0.972279,1,Malignant
821
+ 003974,1,Malignant,0.443081,0,Benign
822
+ 003975,1,Malignant,0.994700,1,Malignant
823
+ 003989,1,Malignant,0.974055,1,Malignant
824
+ 003990,1,Malignant,0.998841,1,Malignant
825
+ 003992,1,Malignant,0.907041,1,Malignant
826
+ 004000,1,Malignant,0.997292,1,Malignant
827
+ 004001,1,Malignant,0.997343,1,Malignant
828
+ 004013,1,Malignant,0.860234,1,Malignant
829
+ 004017,1,Malignant,0.939375,1,Malignant
830
+ 004025,1,Malignant,0.887041,1,Malignant
831
+ 004046,1,Malignant,0.957682,1,Malignant
832
+ 004049,1,Malignant,0.997451,1,Malignant
833
+ 004060,1,Malignant,0.784335,1,Malignant
834
+ 004062,1,Malignant,0.984212,1,Malignant
835
+ 004063,1,Malignant,0.992217,1,Malignant
836
+ 004092,1,Malignant,0.951426,1,Malignant
837
+ 004094,1,Malignant,0.995090,1,Malignant
838
+ 004095,1,Malignant,0.998346,1,Malignant
839
+ 004102,1,Malignant,0.983566,1,Malignant
840
+ 004167,1,Malignant,0.966429,1,Malignant
841
+ 004168,1,Malignant,0.975831,1,Malignant
842
+ 004170,1,Malignant,0.986467,1,Malignant
843
+ 004176,1,Malignant,0.667180,0,Benign
844
+ 004181,1,Malignant,0.963405,1,Malignant
845
+ 004189,1,Malignant,0.964328,1,Malignant
846
+ 004191,1,Malignant,0.990573,1,Malignant
847
+ 004200,1,Malignant,0.924130,1,Malignant
848
+ 004205,1,Malignant,0.988639,1,Malignant
849
+ 004213,1,Malignant,0.895263,1,Malignant
850
+ 004227,1,Malignant,0.956177,1,Malignant
851
+ 004230,1,Malignant,0.959090,1,Malignant
852
+ 004235,1,Malignant,0.996873,1,Malignant
853
+ 004236,1,Malignant,0.743295,1,Malignant
854
+ 004242,1,Malignant,0.706343,0,Benign
855
+ 004248,1,Malignant,0.993908,1,Malignant
856
+ 004250,1,Malignant,0.994452,1,Malignant
857
+ 004258,1,Malignant,0.972373,1,Malignant
858
+ 004270,1,Malignant,0.979028,1,Malignant
859
+ 004272,1,Malignant,0.929851,1,Malignant
860
+ 004284,1,Malignant,0.941676,1,Malignant
861
+ 004287,1,Malignant,0.995832,1,Malignant
862
+ 004289,1,Malignant,0.980922,1,Malignant
863
+ 004292,1,Malignant,0.982074,1,Malignant
864
+ 004299,1,Malignant,0.607361,0,Benign
865
+ 004300,1,Malignant,0.914814,1,Malignant
866
+ 004310,1,Malignant,0.999523,1,Malignant
867
+ 004313,1,Malignant,0.994058,1,Malignant
868
+ 004315,1,Malignant,0.987371,1,Malignant
869
+ 004322,1,Malignant,0.994938,1,Malignant
870
+ 004325,1,Malignant,0.996415,1,Malignant
871
+ 004326,1,Malignant,0.987359,1,Malignant
872
+ 004333,1,Malignant,0.996591,1,Malignant
873
+ 004336,1,Malignant,0.995491,1,Malignant
874
+ 004338,1,Malignant,0.561299,0,Benign
875
+ 004345,1,Malignant,0.945713,1,Malignant
876
+ 004355,1,Malignant,0.998180,1,Malignant
877
+ 004358,1,Malignant,0.999135,1,Malignant
878
+ 004365,1,Malignant,0.915637,1,Malignant
879
+ 004369,1,Malignant,0.997844,1,Malignant
880
+ 004370,1,Malignant,0.999912,1,Malignant
881
+ 004371,1,Malignant,0.822876,1,Malignant
882
+ 004373,1,Malignant,0.999125,1,Malignant
883
+ 004380,1,Malignant,0.780788,1,Malignant
884
+ 004381,1,Malignant,0.901580,1,Malignant
885
+ 004382,1,Malignant,0.964811,1,Malignant
886
+ 004388,1,Malignant,0.839605,1,Malignant
887
+ 004394,1,Malignant,0.919946,1,Malignant
888
+ 004395,1,Malignant,0.517038,0,Benign
889
+ 004397,1,Malignant,0.998060,1,Malignant
890
+ 004398,1,Malignant,0.977605,1,Malignant
891
+ 004400,1,Malignant,0.864222,1,Malignant
892
+ 004404,1,Malignant,0.862405,1,Malignant
893
+ 004412,1,Malignant,0.778374,1,Malignant
894
+ 004413,1,Malignant,0.647297,0,Benign
895
+ 004416,1,Malignant,0.998082,1,Malignant
896
+ 004417,1,Malignant,0.997732,1,Malignant
897
+ 004419,1,Malignant,0.799195,1,Malignant
898
+ 004420,1,Malignant,0.911417,1,Malignant
899
+ 004422,1,Malignant,0.993832,1,Malignant
900
+ 004430,1,Malignant,0.996149,1,Malignant
901
+ 004431,1,Malignant,0.999748,1,Malignant
902
+ 004435,1,Malignant,0.475521,0,Benign
903
+ 004437,1,Malignant,0.998239,1,Malignant
904
+ 004438,1,Malignant,0.999856,1,Malignant
905
+ 004439,1,Malignant,0.985957,1,Malignant
906
+ 004448,1,Malignant,0.848213,1,Malignant
907
+ 004451,1,Malignant,0.985415,1,Malignant
908
+ 004456,1,Malignant,0.999498,1,Malignant
909
+ 004465,1,Malignant,0.942840,1,Malignant
910
+ 004475,1,Malignant,0.999605,1,Malignant
911
+ 004478,1,Malignant,0.999948,1,Malignant
912
+ 004481,1,Malignant,0.998458,1,Malignant
913
+ 004485,1,Malignant,0.991646,1,Malignant
914
+ 004486,1,Malignant,0.995032,1,Malignant
915
+ 004487,1,Malignant,0.950961,1,Malignant
916
+ 004489,1,Malignant,0.990908,1,Malignant
917
+ 004491,1,Malignant,0.987165,1,Malignant
918
+ 004496,1,Malignant,0.997650,1,Malignant
919
+ 004497,1,Malignant,0.987072,1,Malignant
920
+ 004500,1,Malignant,0.968979,1,Malignant
921
+ 004506,1,Malignant,0.994432,1,Malignant
922
+ 004509,1,Malignant,0.996898,1,Malignant
923
+ 004510,1,Malignant,0.926083,1,Malignant
924
+ 004521,1,Malignant,0.991403,1,Malignant
925
+ 004522,1,Malignant,0.921021,1,Malignant
926
+ 004533,1,Malignant,0.998629,1,Malignant
927
+ 004543,1,Malignant,0.954704,1,Malignant
928
+ 004549,1,Malignant,0.888949,1,Malignant
929
+ 004552,1,Malignant,0.935036,1,Malignant
930
+ 004553,1,Malignant,0.998911,1,Malignant
931
+ 004565,1,Malignant,0.890689,1,Malignant
932
+ 004570,1,Malignant,0.124649,0,Benign
933
+ 004574,1,Malignant,0.992323,1,Malignant
934
+ 004580,1,Malignant,0.998202,1,Malignant
935
+ 004586,1,Malignant,0.996039,1,Malignant
936
+ 004587,1,Malignant,0.998868,1,Malignant
937
+ 004588,1,Malignant,0.996154,1,Malignant
938
+ 004589,1,Malignant,0.734379,1,Malignant
939
+ 004594,1,Malignant,0.995579,1,Malignant
940
+ 004636,1,Malignant,0.935300,1,Malignant
941
+ 004637,1,Malignant,0.998134,1,Malignant
942
+ 004638,1,Malignant,0.997470,1,Malignant
943
+ 004645,1,Malignant,0.993321,1,Malignant
944
+ 004724,1,Malignant,0.897559,1,Malignant
945
+ 004726,1,Malignant,0.996057,1,Malignant
946
+ 004727,1,Malignant,0.991684,1,Malignant
947
+ 004728,1,Malignant,0.998960,1,Malignant
948
+ 004729,1,Malignant,0.980055,1,Malignant
949
+ 004730,1,Malignant,0.988399,1,Malignant
950
+ 004740,1,Malignant,0.767038,1,Malignant
951
+ 004742,1,Malignant,0.938034,1,Malignant
952
+ 004744,1,Malignant,0.940004,1,Malignant
953
+ 004746,1,Malignant,0.855076,1,Malignant
954
+ 004750,1,Malignant,0.992189,1,Malignant
955
+ 004752,1,Malignant,0.836708,1,Malignant
956
+ 004756,1,Malignant,0.971239,1,Malignant
957
+ 004760,1,Malignant,0.960529,1,Malignant
958
+ 004762,1,Malignant,0.981579,1,Malignant
959
+ 004766,1,Malignant,0.999515,1,Malignant
960
+ 004770,1,Malignant,0.999660,1,Malignant
961
+ 004782,1,Malignant,0.998493,1,Malignant
962
+ 004788,1,Malignant,0.887674,1,Malignant
963
+ 004792,1,Malignant,0.999655,1,Malignant
964
+ 004801,1,Malignant,0.998809,1,Malignant
965
+ 004807,1,Malignant,0.654411,0,Benign
966
+ 004808,1,Malignant,0.977022,1,Malignant
967
+ 004810,1,Malignant,0.995222,1,Malignant
968
+ 004812,1,Malignant,0.991110,1,Malignant
969
+ 004829,1,Malignant,0.714231,1,Malignant
970
+ 004830,1,Malignant,0.873680,1,Malignant
971
+ 004835,1,Malignant,0.978168,1,Malignant
972
+ 004840,1,Malignant,0.962467,1,Malignant
973
+ 004842,1,Malignant,0.987063,1,Malignant
974
+ 004850,1,Malignant,0.851278,1,Malignant
975
+ 004875,1,Malignant,0.993820,1,Malignant
976
+ 004882,1,Malignant,0.999927,1,Malignant
977
+ 004889,1,Malignant,0.854955,1,Malignant
978
+ 004890,1,Malignant,0.975908,1,Malignant
979
+ 004896,1,Malignant,0.767198,1,Malignant
980
+ 004898,1,Malignant,0.985588,1,Malignant
981
+ 004906,1,Malignant,0.979915,1,Malignant
982
+ 004911,1,Malignant,0.997864,1,Malignant
983
+ 004916,1,Malignant,0.977970,1,Malignant
984
+ 004929,1,Malignant,0.999784,1,Malignant
985
+ 004955,1,Malignant,0.974640,1,Malignant
986
+ 004959,1,Malignant,0.998328,1,Malignant
987
+ 004960,1,Malignant,0.994223,1,Malignant
988
+ 004963,1,Malignant,0.989633,1,Malignant
989
+ 004969,1,Malignant,0.806766,1,Malignant
990
+ 004971,1,Malignant,0.779123,1,Malignant
991
+ 004973,1,Malignant,0.992185,1,Malignant
992
+ 004974,1,Malignant,0.999426,1,Malignant
993
+ 004982,1,Malignant,0.999585,1,Malignant
994
+ 004985,1,Malignant,0.992441,1,Malignant
995
+ 004986,1,Malignant,0.969312,1,Malignant
996
+ 004988,1,Malignant,0.999734,1,Malignant
997
+ 004990,1,Malignant,0.980178,1,Malignant
998
+ 004993,1,Malignant,0.998539,1,Malignant
999
+ 004995,1,Malignant,0.933142,1,Malignant
1000
+ 004996,1,Malignant,0.998271,1,Malignant
1001
+ 004999,1,Malignant,0.992998,1,Malignant
results/valid_predictions.csv ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ image_id,true_label,true_class,probability_malignant,predicted_label,predicted_class
2
+ 000019,0,Benign,0.387281,0,Benign
3
+ 000038,0,Benign,0.062997,0,Benign
4
+ 000055,0,Benign,0.679069,0,Benign
5
+ 000072,0,Benign,0.121674,0,Benign
6
+ 000099,0,Benign,0.041968,0,Benign
7
+ 000113,0,Benign,0.794337,1,Malignant
8
+ 000138,0,Benign,0.939260,1,Malignant
9
+ 000151,0,Benign,0.045493,0,Benign
10
+ 000201,0,Benign,0.520551,0,Benign
11
+ 000203,0,Benign,0.216831,0,Benign
12
+ 000210,0,Benign,0.067449,0,Benign
13
+ 000236,0,Benign,0.115158,0,Benign
14
+ 000238,0,Benign,0.479351,0,Benign
15
+ 000245,0,Benign,0.944340,1,Malignant
16
+ 000284,0,Benign,0.018240,0,Benign
17
+ 000287,0,Benign,0.229929,0,Benign
18
+ 000305,0,Benign,0.951172,1,Malignant
19
+ 000317,0,Benign,0.648442,0,Benign
20
+ 000331,0,Benign,0.002365,0,Benign
21
+ 000345,0,Benign,0.048424,0,Benign
22
+ 000369,0,Benign,0.085227,0,Benign
23
+ 000374,0,Benign,0.543083,0,Benign
24
+ 000389,0,Benign,0.242428,0,Benign
25
+ 000396,0,Benign,0.101504,0,Benign
26
+ 000409,0,Benign,0.030582,0,Benign
27
+ 000420,0,Benign,0.001326,0,Benign
28
+ 000465,0,Benign,0.311004,0,Benign
29
+ 000471,0,Benign,0.031065,0,Benign
30
+ 000474,0,Benign,0.168879,0,Benign
31
+ 000502,0,Benign,0.074407,0,Benign
32
+ 000523,0,Benign,0.121968,0,Benign
33
+ 000536,0,Benign,0.006251,0,Benign
34
+ 000548,0,Benign,0.419118,0,Benign
35
+ 000549,0,Benign,0.024952,0,Benign
36
+ 000556,0,Benign,0.030170,0,Benign
37
+ 000561,0,Benign,0.030486,0,Benign
38
+ 000565,0,Benign,0.017267,0,Benign
39
+ 000569,0,Benign,0.195708,0,Benign
40
+ 000580,0,Benign,0.020466,0,Benign
41
+ 000583,0,Benign,0.379398,0,Benign
42
+ 000587,0,Benign,0.001430,0,Benign
43
+ 000598,0,Benign,0.009525,0,Benign
44
+ 000624,0,Benign,0.028617,0,Benign
45
+ 000638,0,Benign,0.558968,0,Benign
46
+ 000655,0,Benign,0.177185,0,Benign
47
+ 000677,0,Benign,0.013310,0,Benign
48
+ 001731,0,Benign,0.085332,0,Benign
49
+ 001734,0,Benign,0.182752,0,Benign
50
+ 001741,0,Benign,0.058499,0,Benign
51
+ 001754,0,Benign,0.110054,0,Benign
52
+ 001772,0,Benign,0.026569,0,Benign
53
+ 001797,0,Benign,0.037475,0,Benign
54
+ 001799,0,Benign,0.676092,0,Benign
55
+ 001801,0,Benign,0.156940,0,Benign
56
+ 001811,0,Benign,0.010308,0,Benign
57
+ 001817,0,Benign,0.043788,0,Benign
58
+ 001829,0,Benign,0.237723,0,Benign
59
+ 001854,0,Benign,0.042435,0,Benign
60
+ 001855,0,Benign,0.010479,0,Benign
61
+ 001862,0,Benign,0.764704,1,Malignant
62
+ 001887,0,Benign,0.046733,0,Benign
63
+ 001890,0,Benign,0.003490,0,Benign
64
+ 001894,0,Benign,0.007490,0,Benign
65
+ 001902,0,Benign,0.035927,0,Benign
66
+ 001903,0,Benign,0.373132,0,Benign
67
+ 001921,0,Benign,0.229835,0,Benign
68
+ 001924,0,Benign,0.603180,0,Benign
69
+ 001926,0,Benign,0.077111,0,Benign
70
+ 001932,0,Benign,0.038921,0,Benign
71
+ 002342,0,Benign,0.678385,0,Benign
72
+ 002351,0,Benign,0.004603,0,Benign
73
+ 002354,0,Benign,0.051213,0,Benign
74
+ 002357,0,Benign,0.379581,0,Benign
75
+ 002376,0,Benign,0.387769,0,Benign
76
+ 002419,0,Benign,0.047453,0,Benign
77
+ 002421,0,Benign,0.000408,0,Benign
78
+ 002424,0,Benign,0.099354,0,Benign
79
+ 002428,0,Benign,0.015846,0,Benign
80
+ 002431,0,Benign,0.008843,0,Benign
81
+ 002434,0,Benign,0.624946,0,Benign
82
+ 002460,0,Benign,0.016944,0,Benign
83
+ 002462,0,Benign,0.234696,0,Benign
84
+ 002478,0,Benign,0.600433,0,Benign
85
+ 002487,0,Benign,0.527726,0,Benign
86
+ 002572,0,Benign,0.016822,0,Benign
87
+ 002661,0,Benign,0.002315,0,Benign
88
+ 002854,0,Benign,0.177960,0,Benign
89
+ 002868,0,Benign,0.533999,0,Benign
90
+ 002901,0,Benign,0.026087,0,Benign
91
+ 002903,0,Benign,0.119333,0,Benign
92
+ 002911,0,Benign,0.074131,0,Benign
93
+ 002932,0,Benign,0.130335,0,Benign
94
+ 002961,0,Benign,0.003093,0,Benign
95
+ 002970,0,Benign,0.257603,0,Benign
96
+ 002978,0,Benign,0.059331,0,Benign
97
+ 003086,0,Benign,0.011522,0,Benign
98
+ 003468,0,Benign,0.856581,1,Malignant
99
+ 003472,0,Benign,0.912035,1,Malignant
100
+ 003476,0,Benign,0.934736,1,Malignant
101
+ 003488,0,Benign,0.033851,0,Benign
102
+ 003509,0,Benign,0.001938,0,Benign
103
+ 003531,0,Benign,0.033524,0,Benign
104
+ 003534,0,Benign,0.006998,0,Benign
105
+ 003539,0,Benign,0.078468,0,Benign
106
+ 003556,0,Benign,0.009779,0,Benign
107
+ 003566,0,Benign,0.611938,0,Benign
108
+ 003572,0,Benign,0.054567,0,Benign
109
+ 003578,0,Benign,0.646698,0,Benign
110
+ 004107,0,Benign,0.834480,1,Malignant
111
+ 004127,0,Benign,0.820747,1,Malignant
112
+ 004128,0,Benign,0.034664,0,Benign
113
+ 004131,0,Benign,0.549792,0,Benign
114
+ 004138,0,Benign,0.868963,1,Malignant
115
+ 004148,0,Benign,0.538186,0,Benign
116
+ 004156,0,Benign,0.092962,0,Benign
117
+ 004158,0,Benign,0.006836,0,Benign
118
+ 004161,0,Benign,0.949909,1,Malignant
119
+ 004633,0,Benign,0.015397,0,Benign
120
+ 004665,0,Benign,0.045830,0,Benign
121
+ 004679,0,Benign,0.906385,1,Malignant
122
+ 004684,0,Benign,0.009066,0,Benign
123
+ 004710,0,Benign,0.526296,0,Benign
124
+ 004717,0,Benign,0.004666,0,Benign
125
+ 004720,0,Benign,0.370934,0,Benign
126
+ 004723,0,Benign,0.009995,0,Benign
127
+ 000066,1,Malignant,0.993485,1,Malignant
128
+ 000079,1,Malignant,0.392704,0,Benign
129
+ 000094,1,Malignant,0.975690,1,Malignant
130
+ 000109,1,Malignant,0.998271,1,Malignant
131
+ 000110,1,Malignant,0.993965,1,Malignant
132
+ 000164,1,Malignant,0.947308,1,Malignant
133
+ 000170,1,Malignant,0.999071,1,Malignant
134
+ 000298,1,Malignant,0.997954,1,Malignant
135
+ 000321,1,Malignant,0.986230,1,Malignant
136
+ 000386,1,Malignant,0.976108,1,Malignant
137
+ 000412,1,Malignant,0.999406,1,Malignant
138
+ 000482,1,Malignant,0.973097,1,Malignant
139
+ 000563,1,Malignant,0.996247,1,Malignant
140
+ 000627,1,Malignant,0.984402,1,Malignant
141
+ 000685,1,Malignant,0.999806,1,Malignant
142
+ 000698,1,Malignant,0.911930,1,Malignant
143
+ 000701,1,Malignant,0.963718,1,Malignant
144
+ 000704,1,Malignant,0.991786,1,Malignant
145
+ 000713,1,Malignant,0.968341,1,Malignant
146
+ 000717,1,Malignant,0.997639,1,Malignant
147
+ 000722,1,Malignant,0.968839,1,Malignant
148
+ 000730,1,Malignant,0.981428,1,Malignant
149
+ 000758,1,Malignant,0.978231,1,Malignant
150
+ 000765,1,Malignant,0.912167,1,Malignant
151
+ 000771,1,Malignant,0.981199,1,Malignant
152
+ 000772,1,Malignant,0.986472,1,Malignant
153
+ 000784,1,Malignant,0.998426,1,Malignant
154
+ 000787,1,Malignant,0.962361,1,Malignant
155
+ 000795,1,Malignant,0.877663,1,Malignant
156
+ 000812,1,Malignant,0.948511,1,Malignant
157
+ 000823,1,Malignant,0.961643,1,Malignant
158
+ 000826,1,Malignant,0.997290,1,Malignant
159
+ 000831,1,Malignant,0.880871,1,Malignant
160
+ 000847,1,Malignant,0.860789,1,Malignant
161
+ 000850,1,Malignant,0.997068,1,Malignant
162
+ 000862,1,Malignant,0.881968,1,Malignant
163
+ 000865,1,Malignant,0.974072,1,Malignant
164
+ 000885,1,Malignant,0.966868,1,Malignant
165
+ 000887,1,Malignant,0.982732,1,Malignant
166
+ 000895,1,Malignant,0.995623,1,Malignant
167
+ 000896,1,Malignant,0.997866,1,Malignant
168
+ 000901,1,Malignant,0.985213,1,Malignant
169
+ 000905,1,Malignant,0.964824,1,Malignant
170
+ 000913,1,Malignant,0.986834,1,Malignant
171
+ 000932,1,Malignant,0.996755,1,Malignant
172
+ 000944,1,Malignant,0.942766,1,Malignant
173
+ 000956,1,Malignant,0.982945,1,Malignant
174
+ 000964,1,Malignant,0.999472,1,Malignant
175
+ 000971,1,Malignant,0.988736,1,Malignant
176
+ 000973,1,Malignant,0.996567,1,Malignant
177
+ 000974,1,Malignant,0.994878,1,Malignant
178
+ 000975,1,Malignant,0.974703,1,Malignant
179
+ 001003,1,Malignant,0.952650,1,Malignant
180
+ 001005,1,Malignant,0.616048,0,Benign
181
+ 001006,1,Malignant,0.922225,1,Malignant
182
+ 001011,1,Malignant,0.139962,0,Benign
183
+ 001025,1,Malignant,0.995767,1,Malignant
184
+ 001028,1,Malignant,0.993395,1,Malignant
185
+ 001061,1,Malignant,0.987865,1,Malignant
186
+ 001066,1,Malignant,0.996522,1,Malignant
187
+ 001076,1,Malignant,0.951404,1,Malignant
188
+ 001083,1,Malignant,0.985492,1,Malignant
189
+ 001113,1,Malignant,0.495338,0,Benign
190
+ 001120,1,Malignant,0.890909,1,Malignant
191
+ 001129,1,Malignant,0.752179,1,Malignant
192
+ 001131,1,Malignant,0.977786,1,Malignant
193
+ 001139,1,Malignant,0.999149,1,Malignant
194
+ 001151,1,Malignant,0.772084,1,Malignant
195
+ 001171,1,Malignant,0.996123,1,Malignant
196
+ 001173,1,Malignant,0.996236,1,Malignant
197
+ 001182,1,Malignant,0.998429,1,Malignant
198
+ 001185,1,Malignant,0.900645,1,Malignant
199
+ 001199,1,Malignant,0.966592,1,Malignant
200
+ 001202,1,Malignant,0.954136,1,Malignant
201
+ 001217,1,Malignant,0.982801,1,Malignant
202
+ 001226,1,Malignant,0.975301,1,Malignant
203
+ 001227,1,Malignant,0.988381,1,Malignant
204
+ 001233,1,Malignant,0.918507,1,Malignant
205
+ 001238,1,Malignant,0.988839,1,Malignant
206
+ 001245,1,Malignant,0.975408,1,Malignant
207
+ 001293,1,Malignant,0.143003,0,Benign
208
+ 001316,1,Malignant,0.938141,1,Malignant
209
+ 001328,1,Malignant,0.997835,1,Malignant
210
+ 001344,1,Malignant,0.988967,1,Malignant
211
+ 001406,1,Malignant,0.997181,1,Malignant
212
+ 001412,1,Malignant,0.762491,1,Malignant
213
+ 001443,1,Malignant,0.999642,1,Malignant
214
+ 001452,1,Malignant,0.945174,1,Malignant
215
+ 001455,1,Malignant,0.973271,1,Malignant
216
+ 001466,1,Malignant,0.986426,1,Malignant
217
+ 001471,1,Malignant,0.980855,1,Malignant
218
+ 001478,1,Malignant,0.953676,1,Malignant
219
+ 001500,1,Malignant,0.970171,1,Malignant
220
+ 001502,1,Malignant,0.969985,1,Malignant
221
+ 001510,1,Malignant,0.999471,1,Malignant
222
+ 001529,1,Malignant,0.934806,1,Malignant
223
+ 001536,1,Malignant,0.993730,1,Malignant
224
+ 001537,1,Malignant,0.964988,1,Malignant
225
+ 001542,1,Malignant,0.978624,1,Malignant
226
+ 001550,1,Malignant,0.998453,1,Malignant
227
+ 001553,1,Malignant,0.999799,1,Malignant
228
+ 001555,1,Malignant,0.998741,1,Malignant
229
+ 001566,1,Malignant,0.989323,1,Malignant
230
+ 001596,1,Malignant,0.993235,1,Malignant
231
+ 001607,1,Malignant,0.443932,0,Benign
232
+ 001621,1,Malignant,0.947348,1,Malignant
233
+ 001640,1,Malignant,0.965515,1,Malignant
234
+ 001643,1,Malignant,0.998730,1,Malignant
235
+ 001660,1,Malignant,0.996300,1,Malignant
236
+ 001661,1,Malignant,0.982810,1,Malignant
237
+ 001669,1,Malignant,0.997381,1,Malignant
238
+ 001675,1,Malignant,0.972770,1,Malignant
239
+ 001681,1,Malignant,0.925273,1,Malignant
240
+ 001702,1,Malignant,0.997202,1,Malignant
241
+ 001708,1,Malignant,0.941180,1,Malignant
242
+ 001709,1,Malignant,0.981210,1,Malignant
243
+ 001714,1,Malignant,0.989479,1,Malignant
244
+ 001774,1,Malignant,0.992550,1,Malignant
245
+ 001804,1,Malignant,0.994218,1,Malignant
246
+ 001808,1,Malignant,0.920840,1,Malignant
247
+ 001821,1,Malignant,0.487217,0,Benign
248
+ 001870,1,Malignant,0.994932,1,Malignant
249
+ 001884,1,Malignant,0.023947,0,Benign
250
+ 001904,1,Malignant,0.999020,1,Malignant
251
+ 001949,1,Malignant,0.998657,1,Malignant
252
+ 001961,1,Malignant,0.711314,1,Malignant
253
+ 001964,1,Malignant,0.995892,1,Malignant
254
+ 001970,1,Malignant,0.939822,1,Malignant
255
+ 001972,1,Malignant,0.994984,1,Malignant
256
+ 001979,1,Malignant,0.615314,0,Benign
257
+ 001980,1,Malignant,0.958194,1,Malignant
258
+ 001981,1,Malignant,0.927563,1,Malignant
259
+ 001988,1,Malignant,0.959771,1,Malignant
260
+ 001990,1,Malignant,0.862600,1,Malignant
261
+ 001991,1,Malignant,0.963877,1,Malignant
262
+ 001992,1,Malignant,0.989394,1,Malignant
263
+ 001996,1,Malignant,0.996645,1,Malignant
264
+ 001998,1,Malignant,0.777489,1,Malignant
265
+ 002013,1,Malignant,0.962771,1,Malignant
266
+ 002028,1,Malignant,0.951218,1,Malignant
267
+ 002047,1,Malignant,0.999922,1,Malignant
268
+ 002066,1,Malignant,0.995408,1,Malignant
269
+ 002077,1,Malignant,0.993160,1,Malignant
270
+ 002087,1,Malignant,0.994055,1,Malignant
271
+ 002102,1,Malignant,0.988949,1,Malignant
272
+ 002114,1,Malignant,0.951617,1,Malignant
273
+ 002136,1,Malignant,0.993738,1,Malignant
274
+ 002142,1,Malignant,0.995946,1,Malignant
275
+ 002153,1,Malignant,0.979807,1,Malignant
276
+ 002183,1,Malignant,0.821714,1,Malignant
277
+ 002185,1,Malignant,0.515379,0,Benign
278
+ 002206,1,Malignant,0.998406,1,Malignant
279
+ 002210,1,Malignant,0.998672,1,Malignant
280
+ 002213,1,Malignant,0.977704,1,Malignant
281
+ 002258,1,Malignant,0.998083,1,Malignant
282
+ 002266,1,Malignant,0.973419,1,Malignant
283
+ 002268,1,Malignant,0.962770,1,Malignant
284
+ 002288,1,Malignant,0.999359,1,Malignant
285
+ 002291,1,Malignant,0.881381,1,Malignant
286
+ 002307,1,Malignant,0.958153,1,Malignant
287
+ 002315,1,Malignant,0.968310,1,Malignant
288
+ 002318,1,Malignant,0.996061,1,Malignant
289
+ 002322,1,Malignant,0.981137,1,Malignant
290
+ 002332,1,Malignant,0.863663,1,Malignant
291
+ 002334,1,Malignant,0.790510,1,Malignant
292
+ 002494,1,Malignant,0.977316,1,Malignant
293
+ 002495,1,Malignant,0.994649,1,Malignant
294
+ 002497,1,Malignant,0.998809,1,Malignant
295
+ 002504,1,Malignant,0.962371,1,Malignant
296
+ 002505,1,Malignant,0.481627,0,Benign
297
+ 002506,1,Malignant,0.966266,1,Malignant
298
+ 002516,1,Malignant,0.935815,1,Malignant
299
+ 002520,1,Malignant,0.985208,1,Malignant
300
+ 002526,1,Malignant,0.999814,1,Malignant
301
+ 002527,1,Malignant,0.997584,1,Malignant
302
+ 002529,1,Malignant,0.998031,1,Malignant
303
+ 002533,1,Malignant,0.999243,1,Malignant
304
+ 002538,1,Malignant,0.989732,1,Malignant
305
+ 002580,1,Malignant,0.974431,1,Malignant
306
+ 002592,1,Malignant,0.966971,1,Malignant
307
+ 002599,1,Malignant,0.999943,1,Malignant
308
+ 002601,1,Malignant,0.996603,1,Malignant
309
+ 002614,1,Malignant,0.995410,1,Malignant
310
+ 002622,1,Malignant,0.972892,1,Malignant
311
+ 002625,1,Malignant,0.981396,1,Malignant
312
+ 002639,1,Malignant,0.619600,0,Benign
313
+ 002652,1,Malignant,0.995043,1,Malignant
314
+ 002666,1,Malignant,0.999196,1,Malignant
315
+ 002677,1,Malignant,0.999174,1,Malignant
316
+ 002684,1,Malignant,0.999632,1,Malignant
317
+ 002688,1,Malignant,0.957585,1,Malignant
318
+ 002717,1,Malignant,0.996240,1,Malignant
319
+ 002776,1,Malignant,0.995559,1,Malignant
320
+ 002815,1,Malignant,0.996464,1,Malignant
321
+ 002816,1,Malignant,0.869906,1,Malignant
322
+ 002819,1,Malignant,0.952106,1,Malignant
323
+ 002821,1,Malignant,0.997302,1,Malignant
324
+ 002822,1,Malignant,0.993221,1,Malignant
325
+ 002835,1,Malignant,0.996129,1,Malignant
326
+ 002842,1,Malignant,0.996020,1,Malignant
327
+ 002843,1,Malignant,0.990913,1,Malignant
328
+ 002986,1,Malignant,0.976309,1,Malignant
329
+ 002991,1,Malignant,0.957319,1,Malignant
330
+ 003003,1,Malignant,0.925761,1,Malignant
331
+ 003005,1,Malignant,0.979471,1,Malignant
332
+ 003022,1,Malignant,0.854833,1,Malignant
333
+ 003027,1,Malignant,0.995498,1,Malignant
334
+ 003034,1,Malignant,0.981327,1,Malignant
335
+ 003035,1,Malignant,0.983234,1,Malignant
336
+ 003059,1,Malignant,0.839371,1,Malignant
337
+ 003071,1,Malignant,0.998835,1,Malignant
338
+ 003074,1,Malignant,0.971335,1,Malignant
339
+ 003082,1,Malignant,0.941155,1,Malignant
340
+ 003097,1,Malignant,0.999811,1,Malignant
341
+ 003127,1,Malignant,0.726051,1,Malignant
342
+ 003137,1,Malignant,0.763365,1,Malignant
343
+ 003160,1,Malignant,0.956501,1,Malignant
344
+ 003190,1,Malignant,0.976107,1,Malignant
345
+ 003191,1,Malignant,0.914092,1,Malignant
346
+ 003222,1,Malignant,0.815161,1,Malignant
347
+ 003232,1,Malignant,0.803291,1,Malignant
348
+ 003237,1,Malignant,0.997355,1,Malignant
349
+ 003239,1,Malignant,0.966361,1,Malignant
350
+ 003240,1,Malignant,0.991806,1,Malignant
351
+ 003251,1,Malignant,0.975122,1,Malignant
352
+ 003267,1,Malignant,0.997212,1,Malignant
353
+ 003276,1,Malignant,0.994591,1,Malignant
354
+ 003278,1,Malignant,0.977191,1,Malignant
355
+ 003279,1,Malignant,0.979742,1,Malignant
356
+ 003287,1,Malignant,0.895884,1,Malignant
357
+ 003288,1,Malignant,0.981201,1,Malignant
358
+ 003314,1,Malignant,0.980365,1,Malignant
359
+ 003317,1,Malignant,0.959575,1,Malignant
360
+ 003319,1,Malignant,0.955607,1,Malignant
361
+ 003327,1,Malignant,0.915734,1,Malignant
362
+ 003328,1,Malignant,0.999948,1,Malignant
363
+ 003330,1,Malignant,0.984279,1,Malignant
364
+ 003343,1,Malignant,0.991889,1,Malignant
365
+ 003349,1,Malignant,0.995506,1,Malignant
366
+ 003351,1,Malignant,0.952905,1,Malignant
367
+ 003362,1,Malignant,0.973198,1,Malignant
368
+ 003364,1,Malignant,0.996079,1,Malignant
369
+ 003377,1,Malignant,0.999852,1,Malignant
370
+ 003378,1,Malignant,0.940930,1,Malignant
371
+ 003393,1,Malignant,0.607435,0,Benign
372
+ 003403,1,Malignant,0.944609,1,Malignant
373
+ 003405,1,Malignant,0.991853,1,Malignant
374
+ 003437,1,Malignant,0.937323,1,Malignant
375
+ 003601,1,Malignant,0.992880,1,Malignant
376
+ 003611,1,Malignant,0.998025,1,Malignant
377
+ 003612,1,Malignant,0.926110,1,Malignant
378
+ 003620,1,Malignant,0.999628,1,Malignant
379
+ 003624,1,Malignant,0.998621,1,Malignant
380
+ 003654,1,Malignant,0.803938,1,Malignant
381
+ 003655,1,Malignant,0.995213,1,Malignant
382
+ 003656,1,Malignant,0.999012,1,Malignant
383
+ 003681,1,Malignant,0.982621,1,Malignant
384
+ 003703,1,Malignant,0.989486,1,Malignant
385
+ 003704,1,Malignant,0.678671,0,Benign
386
+ 003707,1,Malignant,0.984178,1,Malignant
387
+ 003732,1,Malignant,0.964608,1,Malignant
388
+ 003745,1,Malignant,0.865759,1,Malignant
389
+ 003756,1,Malignant,0.999931,1,Malignant
390
+ 003757,1,Malignant,0.941315,1,Malignant
391
+ 003760,1,Malignant,0.949414,1,Malignant
392
+ 003789,1,Malignant,0.892808,1,Malignant
393
+ 003810,1,Malignant,0.919709,1,Malignant
394
+ 003821,1,Malignant,0.979100,1,Malignant
395
+ 003824,1,Malignant,0.757743,1,Malignant
396
+ 003833,1,Malignant,0.992178,1,Malignant
397
+ 003834,1,Malignant,0.992941,1,Malignant
398
+ 003836,1,Malignant,0.775179,1,Malignant
399
+ 003847,1,Malignant,0.999048,1,Malignant
400
+ 003855,1,Malignant,0.655440,0,Benign
401
+ 003863,1,Malignant,0.918345,1,Malignant
402
+ 003870,1,Malignant,0.984393,1,Malignant
403
+ 003875,1,Malignant,0.987469,1,Malignant
404
+ 003897,1,Malignant,0.998630,1,Malignant
405
+ 003901,1,Malignant,0.998393,1,Malignant
406
+ 003926,1,Malignant,0.970938,1,Malignant
407
+ 003932,1,Malignant,0.976214,1,Malignant
408
+ 003959,1,Malignant,0.423781,0,Benign
409
+ 003967,1,Malignant,0.980966,1,Malignant
410
+ 003981,1,Malignant,0.993071,1,Malignant
411
+ 003987,1,Malignant,0.974980,1,Malignant
412
+ 003993,1,Malignant,0.930377,1,Malignant
413
+ 003994,1,Malignant,0.980157,1,Malignant
414
+ 004023,1,Malignant,0.759148,1,Malignant
415
+ 004034,1,Malignant,0.992803,1,Malignant
416
+ 004036,1,Malignant,0.999078,1,Malignant
417
+ 004039,1,Malignant,0.997604,1,Malignant
418
+ 004061,1,Malignant,0.506222,0,Benign
419
+ 004084,1,Malignant,0.999368,1,Malignant
420
+ 004175,1,Malignant,0.999422,1,Malignant
421
+ 004178,1,Malignant,0.995541,1,Malignant
422
+ 004179,1,Malignant,0.928364,1,Malignant
423
+ 004190,1,Malignant,0.893910,1,Malignant
424
+ 004195,1,Malignant,0.991185,1,Malignant
425
+ 004206,1,Malignant,0.994625,1,Malignant
426
+ 004210,1,Malignant,0.893928,1,Malignant
427
+ 004212,1,Malignant,0.984733,1,Malignant
428
+ 004216,1,Malignant,0.997782,1,Malignant
429
+ 004224,1,Malignant,0.997015,1,Malignant
430
+ 004243,1,Malignant,0.988051,1,Malignant
431
+ 004244,1,Malignant,0.983893,1,Malignant
432
+ 004257,1,Malignant,0.993675,1,Malignant
433
+ 004278,1,Malignant,0.995409,1,Malignant
434
+ 004297,1,Malignant,0.938453,1,Malignant
435
+ 004298,1,Malignant,0.991459,1,Malignant
436
+ 004303,1,Malignant,0.981134,1,Malignant
437
+ 004306,1,Malignant,0.875113,1,Malignant
438
+ 004307,1,Malignant,0.991860,1,Malignant
439
+ 004328,1,Malignant,0.843277,1,Malignant
440
+ 004346,1,Malignant,0.980378,1,Malignant
441
+ 004349,1,Malignant,0.987017,1,Malignant
442
+ 004363,1,Malignant,0.996992,1,Malignant
443
+ 004375,1,Malignant,0.980458,1,Malignant
444
+ 004378,1,Malignant,0.993926,1,Malignant
445
+ 004392,1,Malignant,0.984863,1,Malignant
446
+ 004399,1,Malignant,0.928852,1,Malignant
447
+ 004403,1,Malignant,0.898591,1,Malignant
448
+ 004410,1,Malignant,0.997732,1,Malignant
449
+ 004414,1,Malignant,0.977467,1,Malignant
450
+ 004426,1,Malignant,0.885507,1,Malignant
451
+ 004436,1,Malignant,0.990488,1,Malignant
452
+ 004461,1,Malignant,0.987587,1,Malignant
453
+ 004463,1,Malignant,0.997002,1,Malignant
454
+ 004468,1,Malignant,0.928077,1,Malignant
455
+ 004479,1,Malignant,0.992864,1,Malignant
456
+ 004515,1,Malignant,0.938283,1,Malignant
457
+ 004517,1,Malignant,0.967930,1,Malignant
458
+ 004518,1,Malignant,0.995571,1,Malignant
459
+ 004520,1,Malignant,0.996082,1,Malignant
460
+ 004535,1,Malignant,0.947997,1,Malignant
461
+ 004538,1,Malignant,0.981330,1,Malignant
462
+ 004563,1,Malignant,0.997586,1,Malignant
463
+ 004576,1,Malignant,0.981701,1,Malignant
464
+ 004592,1,Malignant,0.940132,1,Malignant
465
+ 004597,1,Malignant,0.995031,1,Malignant
466
+ 004613,1,Malignant,0.982742,1,Malignant
467
+ 004647,1,Malignant,0.988146,1,Malignant
468
+ 004664,1,Malignant,0.994281,1,Malignant
469
+ 004739,1,Malignant,0.998700,1,Malignant
470
+ 004775,1,Malignant,0.977282,1,Malignant
471
+ 004793,1,Malignant,0.999082,1,Malignant
472
+ 004797,1,Malignant,0.981884,1,Malignant
473
+ 004802,1,Malignant,0.991508,1,Malignant
474
+ 004806,1,Malignant,0.023197,0,Benign
475
+ 004823,1,Malignant,0.999941,1,Malignant
476
+ 004825,1,Malignant,0.999410,1,Malignant
477
+ 004826,1,Malignant,0.998237,1,Malignant
478
+ 004843,1,Malignant,0.987593,1,Malignant
479
+ 004844,1,Malignant,0.957767,1,Malignant
480
+ 004845,1,Malignant,0.979598,1,Malignant
481
+ 004856,1,Malignant,0.979316,1,Malignant
482
+ 004859,1,Malignant,0.880817,1,Malignant
483
+ 004867,1,Malignant,0.990999,1,Malignant
484
+ 004868,1,Malignant,0.986867,1,Malignant
485
+ 004871,1,Malignant,0.993395,1,Malignant
486
+ 004872,1,Malignant,0.993593,1,Malignant
487
+ 004874,1,Malignant,0.988520,1,Malignant
488
+ 004891,1,Malignant,0.960777,1,Malignant
489
+ 004894,1,Malignant,0.923604,1,Malignant
490
+ 004895,1,Malignant,0.987956,1,Malignant
491
+ 004907,1,Malignant,0.994634,1,Malignant
492
+ 004922,1,Malignant,0.999635,1,Malignant
493
+ 004923,1,Malignant,0.978561,1,Malignant
494
+ 004933,1,Malignant,0.986324,1,Malignant
495
+ 004938,1,Malignant,0.949794,1,Malignant
496
+ 004941,1,Malignant,0.973917,1,Malignant
497
+ 004946,1,Malignant,0.986527,1,Malignant
498
+ 004953,1,Malignant,0.986625,1,Malignant
499
+ 004957,1,Malignant,0.970218,1,Malignant
500
+ 004981,1,Malignant,0.998157,1,Malignant
501
+ 005000,1,Malignant,0.991685,1,Malignant
results/winning_run_history.json ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "epoch": 0,
4
+ "train_loss": 0.1598212777716773,
5
+ "val_loss": 0.5469009280204773,
6
+ "val_auroc": 0.8194133333333333,
7
+ "val_sens@0.5": 0.9813333333333333,
8
+ "val_spec@0.5": 0.12,
9
+ "val_ppv@0.5": 0.7698744769874477,
10
+ "val_npv@0.5": 0.6818181818181818,
11
+ "val_ece@0.5": 0.190321617603302,
12
+ "val_brier": 0.17975325882434845,
13
+ "lr": 0.0001,
14
+ "epoch_time_s": 9.1
15
+ },
16
+ {
17
+ "epoch": 1,
18
+ "train_loss": 0.12178190640040806,
19
+ "val_loss": 0.4364696443080902,
20
+ "val_auroc": 0.899616,
21
+ "val_sens@0.5": 0.9386666666666666,
22
+ "val_spec@0.5": 0.608,
23
+ "val_ppv@0.5": 0.8778054862842892,
24
+ "val_npv@0.5": 0.7676767676767676,
25
+ "val_ece@0.5": 0.1629413467645645,
26
+ "val_brier": 0.13361606001853943,
27
+ "lr": 0.0002,
28
+ "epoch_time_s": 8.1
29
+ },
30
+ {
31
+ "epoch": 2,
32
+ "train_loss": 0.09681233019488199,
33
+ "val_loss": 0.6704103946685791,
34
+ "val_auroc": 0.8971306666666666,
35
+ "val_sens@0.5": 0.34933333333333333,
36
+ "val_spec@0.5": 0.984,
37
+ "val_ppv@0.5": 0.9849624060150376,
38
+ "val_npv@0.5": 0.335149863760218,
39
+ "val_ece@0.5": 0.35036509992182263,
40
+ "val_brier": 0.24114809930324554,
41
+ "lr": 0.0001996917333733128,
42
+ "epoch_time_s": 8.0
43
+ },
44
+ {
45
+ "epoch": 3,
46
+ "train_loss": 0.08150971835000174,
47
+ "val_loss": 0.39045241475105286,
48
+ "val_auroc": 0.959968,
49
+ "val_sens@0.5": 0.84,
50
+ "val_spec@0.5": 0.912,
51
+ "val_ppv@0.5": 0.9662576687116564,
52
+ "val_npv@0.5": 0.6551724137931034,
53
+ "val_ece@0.5": 0.20656415994465355,
54
+ "val_brier": 0.11692728847265244,
55
+ "lr": 0.00019876883405951377,
56
+ "epoch_time_s": 8.3
57
+ },
58
+ {
59
+ "epoch": 4,
60
+ "train_loss": 0.0718498518552099,
61
+ "val_loss": 0.29067134857177734,
62
+ "val_auroc": 0.9701013333333334,
63
+ "val_sens@0.5": 0.9626666666666667,
64
+ "val_spec@0.5": 0.816,
65
+ "val_ppv@0.5": 0.9401041666666666,
66
+ "val_npv@0.5": 0.8793103448275862,
67
+ "val_ece@0.5": 0.13937596596777438,
68
+ "val_brier": 0.07864873111248016,
69
+ "lr": 0.00019723699203976766,
70
+ "epoch_time_s": 8.4
71
+ },
72
+ {
73
+ "epoch": 5,
74
+ "train_loss": 0.06457222277351789,
75
+ "val_loss": 0.3043011724948883,
76
+ "val_auroc": 0.9652586666666667,
77
+ "val_sens@0.5": 0.8933333333333333,
78
+ "val_spec@0.5": 0.904,
79
+ "val_ppv@0.5": 0.9654178674351584,
80
+ "val_npv@0.5": 0.738562091503268,
81
+ "val_ece@0.5": 0.14274779457598924,
82
+ "val_brier": 0.08610112220048904,
83
+ "lr": 0.00019510565162951534,
84
+ "epoch_time_s": 8.2
85
+ },
86
+ {
87
+ "epoch": 6,
88
+ "train_loss": 0.05656171679496765,
89
+ "val_loss": 0.22200419008731842,
90
+ "val_auroc": 0.9756053333333333,
91
+ "val_sens@0.5": 0.9733333333333334,
92
+ "val_spec@0.5": 0.76,
93
+ "val_ppv@0.5": 0.9240506329113924,
94
+ "val_npv@0.5": 0.9047619047619048,
95
+ "val_ece@0.5": 0.08342214265465736,
96
+ "val_brier": 0.05918317288160324,
97
+ "lr": 0.00019238795325112864,
98
+ "epoch_time_s": 8.2
99
+ },
100
+ {
101
+ "epoch": 7,
102
+ "train_loss": 0.05262019157835415,
103
+ "val_loss": 0.24622531235218048,
104
+ "val_auroc": 0.9653013333333333,
105
+ "val_sens@0.5": 0.9386666666666666,
106
+ "val_spec@0.5": 0.856,
107
+ "val_ppv@0.5": 0.9513513513513514,
108
+ "val_npv@0.5": 0.823076923076923,
109
+ "val_ece@0.5": 0.09611700309067965,
110
+ "val_brier": 0.06746554374694824,
111
+ "lr": 0.00018910065241883674,
112
+ "epoch_time_s": 8.2
113
+ },
114
+ {
115
+ "epoch": 8,
116
+ "train_loss": 0.045101860872336794,
117
+ "val_loss": 0.2303643375635147,
118
+ "val_auroc": 0.9690666666666666,
119
+ "val_sens@0.5": 0.9493333333333334,
120
+ "val_spec@0.5": 0.832,
121
+ "val_ppv@0.5": 0.9442970822281167,
122
+ "val_npv@0.5": 0.8455284552845529,
123
+ "val_ece@0.5": 0.07282097344845534,
124
+ "val_brier": 0.06332194060087204,
125
+ "lr": 0.0001852640164354092,
126
+ "epoch_time_s": 8.1
127
+ },
128
+ {
129
+ "epoch": 9,
130
+ "train_loss": 0.04153951165505818,
131
+ "val_loss": 0.22570541501045227,
132
+ "val_auroc": 0.9694506666666667,
133
+ "val_sens@0.5": 0.9573333333333334,
134
+ "val_spec@0.5": 0.832,
135
+ "val_ppv@0.5": 0.9447368421052632,
136
+ "val_npv@0.5": 0.8666666666666667,
137
+ "val_ece@0.5": 0.07430721400678159,
138
+ "val_brier": 0.06271392852067947,
139
+ "lr": 0.00018090169943749473,
140
+ "epoch_time_s": 8.2
141
+ },
142
+ {
143
+ "epoch": 10,
144
+ "train_loss": 0.03941814090098653,
145
+ "val_loss": 0.20154055953025818,
146
+ "val_auroc": 0.9754239999999998,
147
+ "val_sens@0.5": 0.9733333333333334,
148
+ "val_spec@0.5": 0.832,
149
+ "val_ppv@0.5": 0.9455958549222798,
150
+ "val_npv@0.5": 0.9122807017543859,
151
+ "val_ece@0.5": 0.06357789923250676,
152
+ "val_brier": 0.05432131513953209,
153
+ "lr": 0.00017604059656000309,
154
+ "epoch_time_s": 8.2
155
+ },
156
+ {
157
+ "epoch": 11,
158
+ "train_loss": 0.03350917380622455,
159
+ "val_loss": 0.20909833908081055,
160
+ "val_auroc": 0.9710933333333334,
161
+ "val_sens@0.5": 0.9573333333333334,
162
+ "val_spec@0.5": 0.84,
163
+ "val_ppv@0.5": 0.9472295514511874,
164
+ "val_npv@0.5": 0.8677685950413223,
165
+ "val_ece@0.5": 0.06616889646649358,
166
+ "val_brier": 0.057766642421483994,
167
+ "lr": 0.00017071067811865473,
168
+ "epoch_time_s": 8.2
169
+ },
170
+ {
171
+ "epoch": 12,
172
+ "train_loss": 0.030914436317980288,
173
+ "val_loss": 0.20893579721450806,
174
+ "val_auroc": 0.9663893333333333,
175
+ "val_sens@0.5": 0.976,
176
+ "val_spec@0.5": 0.816,
177
+ "val_ppv@0.5": 0.9408740359897172,
178
+ "val_npv@0.5": 0.918918918918919,
179
+ "val_ece@0.5": 0.05971383413672446,
180
+ "val_brier": 0.0571916438639164,
181
+ "lr": 0.00016494480483301836,
182
+ "epoch_time_s": 8.4
183
+ },
184
+ {
185
+ "epoch": 13,
186
+ "train_loss": 0.028774067768028804,
187
+ "val_loss": 0.2109844982624054,
188
+ "val_auroc": 0.968,
189
+ "val_sens@0.5": 0.968,
190
+ "val_spec@0.5": 0.768,
191
+ "val_ppv@0.5": 0.9260204081632653,
192
+ "val_npv@0.5": 0.8888888888888888,
193
+ "val_ece@0.5": 0.04835819691419602,
194
+ "val_brier": 0.05989755317568779,
195
+ "lr": 0.0001587785252292473,
196
+ "epoch_time_s": 8.3
197
+ },
198
+ {
199
+ "epoch": 14,
200
+ "train_loss": 0.025121769504887717,
201
+ "val_loss": 0.21120241284370422,
202
+ "val_auroc": 0.9669226666666666,
203
+ "val_sens@0.5": 0.9573333333333334,
204
+ "val_spec@0.5": 0.864,
205
+ "val_ppv@0.5": 0.9547872340425532,
206
+ "val_npv@0.5": 0.8709677419354839,
207
+ "val_ece@0.5": 0.051551883660256835,
208
+ "val_brier": 0.05882710963487625,
209
+ "lr": 0.00015224985647159484,
210
+ "epoch_time_s": 8.0
211
+ }
212
+ ]
sweep.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Focused hyperparameter sweep for the thyroid ResNet-18 classifier, optimized
4
+ for **validation AUROC**. Each trial is a full train.py run (logged to Trackio).
5
+ Trial val AUROCs are collected; the best config is reported.
6
+
7
+ Search dimensions (kept deliberately small to avoid overfitting the val set):
8
+ - backbone variant (torchvision, timm a1/a2)
9
+ - learning rate
10
+ - weight decay
11
+ - batch size
12
+ - augmentation policy
13
+ - class-imbalance strategy
14
+ - fine-tune depth (freeze_stage)
15
+ - loss (bce vs focal)
16
+
17
+ The sweep is staged: a base set of one-factor-at-a-time trials around a sensible
18
+ center (informed by literature), so each dimension is isolated.
19
+ """
20
+ import argparse
21
+ import json
22
+ import subprocess
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ DATA_DIR = "/app/TN5000"
27
+ SPACE = "Johnyquest7/Trakio_agentic_thyroid"
28
+ DSET = "Johnyquest7/Trakio_agentic_thyroid_dataset"
29
+ PROJECT = "agentic_thyroid_resnet18"
30
+
31
+ # center config (literature-informed)
32
+ CENTER = dict(backbone="timm:resnet18.a1_in1k", lr=2e-4, weight_decay=1e-4,
33
+ batch_size=32, aug_policy="medical_default", imbalance="pos_weight",
34
+ freeze_stage=0, loss="bce", optimizer="adamw", scheduler="cosine",
35
+ epochs=40, early_stop_patience=8, dropout=0.0)
36
+
37
+ # Each trial = (name, overrides-dict). One-factor-at-a-time around CENTER.
38
+ TRIALS = [
39
+ ("c00_center_a1", {}),
40
+ ("c01_backbone_torchvision", {"backbone": "torchvision"}),
41
+ ("c02_backbone_a2", {"backbone": "timm:resnet18.a2_in1k"}),
42
+ ("c03_lr_1e-4", {"lr": 1e-4}),
43
+ ("c04_lr_5e-4", {"lr": 5e-4}),
44
+ ("c05_wd_1e-3", {"weight_decay": 1e-3}),
45
+ ("c06_bs_64", {"batch_size": 64}),
46
+ ("c07_aug_flip_only", {"aug_policy": "flip_only"}),
47
+ ("c08_aug_strong", {"aug_policy": "medical_strong"}),
48
+ ("c09_imb_none", {"imbalance": "none"}),
49
+ ("c10_imb_sampler", {"imbalance": "sampler"}),
50
+ ("c11_freeze1", {"freeze_stage": 1}),
51
+ ("c12_loss_focal", {"loss": "focal", "focal_gamma": 1.0, "imbalance": "none"}),
52
+ ("c13_lr1e-4_wd1e-3_drop", {"lr": 1e-4, "weight_decay": 1e-3, "dropout": 0.2}),
53
+ ]
54
+
55
+
56
+ def run_trial(name, overrides, out_root, seed):
57
+ cfg = dict(CENTER); cfg.update(overrides)
58
+ out_dir = str(Path(out_root) / name)
59
+ cmd = [sys.executable, "train.py",
60
+ "--data_dir", DATA_DIR, "--output_dir", out_dir,
61
+ "--run_name", name, "--seed", str(seed),
62
+ "--trackio_project", PROJECT, "--trackio_space_id", SPACE,
63
+ "--trackio_dataset_id", DSET,
64
+ "--num_workers", "4"]
65
+ for k, v in cfg.items():
66
+ if isinstance(v, bool):
67
+ if v:
68
+ cmd.append(f"--{k}")
69
+ else:
70
+ cmd += [f"--{k}", str(v)]
71
+ print(f"\n===== TRIAL {name} =====\n{' '.join(cmd)}", flush=True)
72
+ r = subprocess.run(cmd, capture_output=True, text=True)
73
+ # echo tail for visibility
74
+ print(r.stdout[-1500:], flush=True)
75
+ if r.returncode != 0:
76
+ print("STDERR tail:", r.stderr[-1500:], flush=True)
77
+ summ_path = Path(out_dir) / "final_summary.json"
78
+ best = -1.0; best_epoch = -1
79
+ if summ_path.exists():
80
+ s = json.load(open(summ_path))
81
+ best = s.get("best_val_auroc", -1.0); best_epoch = s.get("best_epoch", -1)
82
+ return {"name": name, "config": cfg, "best_val_auroc": best,
83
+ "best_epoch": best_epoch, "out_dir": out_dir, "returncode": r.returncode}
84
+
85
+
86
+ def main():
87
+ ap = argparse.ArgumentParser()
88
+ ap.add_argument("--out_root", default="/app/sweep_runs")
89
+ ap.add_argument("--seed", type=int, default=42)
90
+ ap.add_argument("--only", default=None, help="comma list of trial names to run")
91
+ args = ap.parse_args()
92
+
93
+ Path(args.out_root).mkdir(parents=True, exist_ok=True)
94
+ trials = TRIALS
95
+ if args.only:
96
+ names = set(args.only.split(","))
97
+ trials = [t for t in TRIALS if t[0] in names]
98
+
99
+ results = []
100
+ res_path = Path(args.out_root) / "sweep_results.json"
101
+ if res_path.exists():
102
+ results = json.load(open(res_path))
103
+ done = {r["name"] for r in results}
104
+ trials = [t for t in trials if t[0] not in done]
105
+
106
+ for name, ov in trials:
107
+ res = run_trial(name, ov, args.out_root, args.seed)
108
+ results.append(res)
109
+ json.dump(results, open(res_path, "w"), indent=2)
110
+ print(f">>> {name}: val_auroc={res['best_val_auroc']:.4f} rc={res['returncode']}", flush=True)
111
+
112
+ results_sorted = sorted(results, key=lambda r: r["best_val_auroc"], reverse=True)
113
+ print("\n===== SWEEP LEADERBOARD =====")
114
+ for r in results_sorted:
115
+ print(f"{r['best_val_auroc']:.4f} {r['name']:28s} (epoch {r['best_epoch']}, rc {r['returncode']})")
116
+ json.dump(results_sorted, open(Path(args.out_root) / "sweep_leaderboard.json", "w"), indent=2)
117
+ if results_sorted:
118
+ print("\nBEST:", results_sorted[0]["name"], results_sorted[0]["best_val_auroc"])
119
+
120
+
121
+ if __name__ == "__main__":
122
+ main()
thyroid_lib.py ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Shared library for the agentic thyroid ResNet-18 experiment.
4
+
5
+ Centralizes everything that train.py / evaluate.py / evaluate_external.py must
6
+ share so that preprocessing, model construction, calibration, and thresholding
7
+ are guaranteed identical across training, validation, test, and external use.
8
+
9
+ Positive class = Malignant (label 1). Benign = 0.
10
+ """
11
+ import json
12
+ import os
13
+ import random
14
+ from dataclasses import dataclass, field, asdict
15
+ from pathlib import Path
16
+ from typing import Optional, List, Tuple
17
+
18
+ import numpy as np
19
+
20
+ IMAGENET_MEAN = [0.485, 0.456, 0.406]
21
+ IMAGENET_STD = [0.229, 0.224, 0.225]
22
+ CLASS_TO_IDX = {"Benign": 0, "Malignant": 1}
23
+ IDX_TO_CLASS = {0: "Benign", 1: "Malignant"}
24
+
25
+
26
+ # --------------------------------------------------------------------------- #
27
+ # Reproducibility
28
+ # --------------------------------------------------------------------------- #
29
+ def set_determinism(seed: int, strict: bool = True):
30
+ """Set all RNG seeds and (optionally) enforce deterministic algorithms."""
31
+ import torch
32
+ os.environ["PYTHONHASHSEED"] = str(seed)
33
+ random.seed(seed)
34
+ np.random.seed(seed)
35
+ torch.manual_seed(seed)
36
+ torch.cuda.manual_seed_all(seed)
37
+ if strict:
38
+ # cuBLAS workspace config required for deterministic matmul on CUDA.
39
+ os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
40
+ torch.backends.cudnn.deterministic = True
41
+ torch.backends.cudnn.benchmark = False
42
+ try:
43
+ torch.use_deterministic_algorithms(True, warn_only=True)
44
+ except Exception:
45
+ torch.use_deterministic_algorithms(True)
46
+ else:
47
+ torch.backends.cudnn.benchmark = True
48
+
49
+
50
+ def seed_worker(worker_id):
51
+ import torch
52
+ worker_seed = torch.initial_seed() % 2 ** 32
53
+ np.random.seed(worker_seed)
54
+ random.seed(worker_seed)
55
+
56
+
57
+ def collect_env_info():
58
+ """Return a dict of package versions and hardware/CUDA settings for logging."""
59
+ info = {}
60
+ try:
61
+ import torch
62
+ info["torch"] = torch.__version__
63
+ info["cuda_available"] = torch.cuda.is_available()
64
+ info["cuda_version"] = torch.version.cuda
65
+ info["cudnn_version"] = torch.backends.cudnn.version() if torch.backends.cudnn.is_available() else None
66
+ info["cudnn_deterministic"] = torch.backends.cudnn.deterministic
67
+ info["cudnn_benchmark"] = torch.backends.cudnn.benchmark
68
+ if torch.cuda.is_available():
69
+ info["gpu_name"] = torch.cuda.get_device_name(0)
70
+ info["gpu_count"] = torch.cuda.device_count()
71
+ props = torch.cuda.get_device_properties(0)
72
+ info["gpu_total_mem_gb"] = round(props.total_memory / 1e9, 2)
73
+ except Exception as e:
74
+ info["torch_error"] = repr(e)
75
+ for mod in ["torchvision", "timm", "sklearn", "numpy", "PIL", "trackio"]:
76
+ try:
77
+ m = __import__(mod)
78
+ info[mod] = getattr(m, "__version__", "?")
79
+ except Exception:
80
+ info[mod] = None
81
+ info["cublas_workspace_config"] = os.environ.get("CUBLAS_WORKSPACE_CONFIG")
82
+ info["pythonhashseed"] = os.environ.get("PYTHONHASHSEED")
83
+ return info
84
+
85
+
86
+ # --------------------------------------------------------------------------- #
87
+ # Preprocessing / augmentation
88
+ # --------------------------------------------------------------------------- #
89
+ @dataclass
90
+ class PreprocessConfig:
91
+ """Locked preprocessing config saved with the final model.
92
+
93
+ Eval/inference path is fully deterministic: resize to image_size, ToTensor,
94
+ Normalize with the given mean/std. No augmentation at eval time.
95
+ """
96
+ image_size: int = 224
97
+ mean: List[float] = field(default_factory=lambda: list(IMAGENET_MEAN))
98
+ std: List[float] = field(default_factory=lambda: list(IMAGENET_STD))
99
+ interpolation: str = "bilinear" # 'bilinear' (torchvision) or 'bicubic' (timm a1/a2/a3)
100
+
101
+ def to_dict(self):
102
+ return asdict(self)
103
+
104
+ @staticmethod
105
+ def from_dict(d):
106
+ fields = {"image_size", "mean", "std", "interpolation"}
107
+ return PreprocessConfig(**{k: v for k, v in d.items() if k in fields})
108
+
109
+
110
+ def _interp(name):
111
+ from torchvision.transforms import InterpolationMode
112
+ return {"bilinear": InterpolationMode.BILINEAR,
113
+ "bicubic": InterpolationMode.BICUBIC}[name]
114
+
115
+
116
+ def build_eval_transform(pp: PreprocessConfig):
117
+ """Deterministic eval/inference transform (NO augmentation)."""
118
+ import torchvision.transforms as T
119
+ return T.Compose([
120
+ T.Resize((pp.image_size, pp.image_size), interpolation=_interp(pp.interpolation)),
121
+ T.ToTensor(),
122
+ T.Normalize(pp.mean, pp.std),
123
+ ])
124
+
125
+
126
+ def build_train_transform(pp: PreprocessConfig, policy: str = "medical_default"):
127
+ """Training augmentation. Medically plausible ultrasound augmentations only.
128
+
129
+ Policies:
130
+ none : eval transform (no augmentation) — baseline ablation.
131
+ flip_only : horizontal flip only.
132
+ medical_default : flip + mild affine(rot<=10,trans5%,scale0.9-1.1) + mild
133
+ brightness/contrast + occasional light gaussian blur.
134
+ medical_strong : medical_default + mild speckle/gaussian noise +
135
+ narrow random-resized-crop (scale 0.8-1.0).
136
+ clahe : medical_default + CLAHE applied as preprocessing (ablation).
137
+
138
+ Explicitly AVOIDED: vertical flip, large rotation (>15deg), aggressive crop
139
+ (<0.8 scale), shear, heavy blur, any color/HSV jitter beyond mild
140
+ brightness/contrast — all of which distort ultrasound texture or nodule
141
+ morphology (per MediAug arXiv:2504.18983 and thyroid-US best practice).
142
+ """
143
+ import torch
144
+ import torchvision.transforms as T
145
+ interp = _interp(pp.interpolation)
146
+ norm = T.Normalize(pp.mean, pp.std)
147
+
148
+ if policy == "none":
149
+ return build_eval_transform(pp)
150
+
151
+ if policy == "flip_only":
152
+ return T.Compose([
153
+ T.Resize((pp.image_size, pp.image_size), interpolation=interp),
154
+ T.RandomHorizontalFlip(0.5),
155
+ T.ToTensor(), norm,
156
+ ])
157
+
158
+ if policy == "medical_default":
159
+ return T.Compose([
160
+ T.Resize((pp.image_size, pp.image_size), interpolation=interp),
161
+ T.RandomHorizontalFlip(0.5),
162
+ T.RandomApply([T.RandomAffine(degrees=10, translate=(0.05, 0.05),
163
+ scale=(0.9, 1.1), interpolation=interp)], p=0.5),
164
+ T.ColorJitter(brightness=0.15, contrast=0.15),
165
+ T.RandomApply([T.GaussianBlur(3, sigma=(0.1, 1.0))], p=0.2),
166
+ T.ToTensor(), norm,
167
+ ])
168
+
169
+ if policy == "medical_strong":
170
+ class AddSpeckle:
171
+ def __init__(self, sigma=0.05, p=0.2):
172
+ self.sigma, self.p = sigma, p
173
+ def __call__(self, x):
174
+ if random.random() < self.p:
175
+ return x + x * (self.sigma * torch.randn_like(x))
176
+ return x
177
+ return T.Compose([
178
+ T.RandomResizedCrop(pp.image_size, scale=(0.8, 1.0), ratio=(0.9, 1.1),
179
+ interpolation=interp),
180
+ T.RandomHorizontalFlip(0.5),
181
+ T.RandomApply([T.RandomAffine(degrees=10, translate=(0.05, 0.05),
182
+ scale=(0.9, 1.1), interpolation=interp)], p=0.5),
183
+ T.ColorJitter(brightness=0.15, contrast=0.15),
184
+ T.RandomApply([T.GaussianBlur(3, sigma=(0.1, 1.0))], p=0.2),
185
+ T.ToTensor(),
186
+ AddSpeckle(sigma=0.05, p=0.2),
187
+ norm,
188
+ ])
189
+
190
+ if policy == "clahe":
191
+ from PIL import Image
192
+ class CLAHE:
193
+ def __call__(self, img):
194
+ import numpy as _np
195
+ try:
196
+ import cv2
197
+ arr = _np.asarray(img.convert("L"))
198
+ cl = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(arr)
199
+ return Image.fromarray(cl).convert("RGB")
200
+ except Exception:
201
+ return img
202
+ return T.Compose([
203
+ CLAHE(),
204
+ T.Resize((pp.image_size, pp.image_size), interpolation=interp),
205
+ T.RandomHorizontalFlip(0.5),
206
+ T.RandomApply([T.RandomAffine(degrees=10, translate=(0.05, 0.05),
207
+ scale=(0.9, 1.1), interpolation=interp)], p=0.5),
208
+ T.ColorJitter(brightness=0.15, contrast=0.15),
209
+ T.ToTensor(), norm,
210
+ ])
211
+
212
+ raise ValueError(f"Unknown augmentation policy: {policy}")
213
+
214
+
215
+ # --------------------------------------------------------------------------- #
216
+ # Dataset
217
+ # --------------------------------------------------------------------------- #
218
+ class ThyroidImageFolder:
219
+ """Lightweight ImageFolder that also returns the image filename id.
220
+
221
+ Layout: <root>/<Benign|Malignant>/<id>.png
222
+ Returns (tensor, label, image_id).
223
+ """
224
+ def __init__(self, root, transform):
225
+ from PIL import Image
226
+ self.Image = Image
227
+ self.root = Path(root)
228
+ self.transform = transform
229
+ self.samples: List[Tuple[Path, int, str]] = []
230
+ for cls, idx in CLASS_TO_IDX.items():
231
+ d = self.root / cls
232
+ if d.is_dir():
233
+ for p in sorted(d.glob("*.png")):
234
+ self.samples.append((p, idx, p.stem))
235
+ if not self.samples:
236
+ raise RuntimeError(f"No images found under {root}")
237
+ self.targets = [s[1] for s in self.samples]
238
+
239
+ def __len__(self):
240
+ return len(self.samples)
241
+
242
+ def __getitem__(self, i):
243
+ path, label, img_id = self.samples[i]
244
+ with self.Image.open(path) as im:
245
+ im = im.convert("RGB")
246
+ x = self.transform(im)
247
+ return x, label, img_id
248
+
249
+
250
+ def class_counts(targets):
251
+ n_pos = int(sum(1 for t in targets if t == 1))
252
+ n_neg = int(sum(1 for t in targets if t == 0))
253
+ return n_neg, n_pos
254
+
255
+
256
+ # --------------------------------------------------------------------------- #
257
+ # Model
258
+ # --------------------------------------------------------------------------- #
259
+ def build_model(backbone: str, freeze_stage: int = 0, dropout: float = 0.0):
260
+ """Build a single-logit ResNet-18 classifier.
261
+
262
+ backbone:
263
+ 'torchvision' -> torchvision resnet18 ImageNet1K_V1
264
+ 'timm:resnet18.a1_in1k' -> any timm tag after 'timm:'
265
+ freeze_stage: 0 = full fine-tune; 1 = freeze stem+layer1; 2 = +layer2; etc.
266
+ Returns (model, preprocess_config).
267
+ """
268
+ import torch
269
+ import torch.nn as nn
270
+
271
+ if backbone == "torchvision":
272
+ from torchvision.models import resnet18, ResNet18_Weights
273
+ weights = ResNet18_Weights.IMAGENET1K_V1
274
+ model = resnet18(weights=weights)
275
+ in_f = model.fc.in_features
276
+ model.fc = nn.Sequential(nn.Dropout(dropout), nn.Linear(in_f, 1)) if dropout > 0 \
277
+ else nn.Linear(in_f, 1)
278
+ pp = PreprocessConfig(image_size=224, mean=list(IMAGENET_MEAN),
279
+ std=list(IMAGENET_STD), interpolation="bilinear")
280
+ _freeze_resnet(model, freeze_stage)
281
+ return model, pp
282
+
283
+ if backbone.startswith("timm:"):
284
+ import timm
285
+ from timm.data import resolve_model_data_config
286
+ tag = backbone.split("timm:", 1)[1]
287
+ model = timm.create_model(tag, pretrained=True, num_classes=1, drop_rate=dropout)
288
+ cfg = resolve_model_data_config(model)
289
+ mean = list(cfg.get("mean", IMAGENET_MEAN))
290
+ std = list(cfg.get("std", IMAGENET_STD))
291
+ interp = cfg.get("interpolation", "bicubic")
292
+ size = cfg.get("input_size", (3, 224, 224))[-1]
293
+ pp = PreprocessConfig(image_size=int(size), mean=mean, std=std,
294
+ interpolation=interp if interp in ("bilinear", "bicubic") else "bicubic")
295
+ _freeze_timm_resnet(model, freeze_stage)
296
+ return model, pp
297
+
298
+ raise ValueError(f"Unknown backbone: {backbone}")
299
+
300
+
301
+ def _freeze_resnet(model, stage):
302
+ if stage <= 0:
303
+ return
304
+ to_freeze = []
305
+ if stage >= 1:
306
+ to_freeze += [model.conv1, model.bn1, model.layer1]
307
+ if stage >= 2:
308
+ to_freeze += [model.layer2]
309
+ if stage >= 3:
310
+ to_freeze += [model.layer3]
311
+ for m in to_freeze:
312
+ for p in m.parameters():
313
+ p.requires_grad = False
314
+
315
+
316
+ def _freeze_timm_resnet(model, stage):
317
+ if stage <= 0:
318
+ return
319
+ name_prefixes = []
320
+ if stage >= 1:
321
+ name_prefixes += ["conv1", "bn1", "layer1"]
322
+ if stage >= 2:
323
+ name_prefixes += ["layer2"]
324
+ if stage >= 3:
325
+ name_prefixes += ["layer3"]
326
+ for n, p in model.named_parameters():
327
+ if any(n.startswith(pref) for pref in name_prefixes):
328
+ p.requires_grad = False
329
+
330
+
331
+ # --------------------------------------------------------------------------- #
332
+ # Loss
333
+ # --------------------------------------------------------------------------- #
334
+ def build_loss(name: str, pos_weight: Optional[float], focal_gamma: float = 2.0,
335
+ focal_alpha: float = 0.5):
336
+ import torch
337
+ import torch.nn as nn
338
+
339
+ if name == "bce":
340
+ pw = torch.tensor([pos_weight]) if pos_weight is not None else None
341
+ return nn.BCEWithLogitsLoss(pos_weight=pw)
342
+
343
+ if name == "focal":
344
+ class FocalLoss(nn.Module):
345
+ def __init__(self, gamma, alpha):
346
+ super().__init__()
347
+ self.gamma, self.alpha = gamma, alpha
348
+ def forward(self, logits, targets):
349
+ logits = logits.view(-1)
350
+ targets = targets.view(-1).float()
351
+ p = torch.sigmoid(logits)
352
+ ce = nn.functional.binary_cross_entropy_with_logits(logits, targets, reduction="none")
353
+ p_t = p * targets + (1 - p) * (1 - targets)
354
+ alpha_t = self.alpha * targets + (1 - self.alpha) * (1 - targets)
355
+ loss = alpha_t * (1 - p_t) ** self.gamma * ce
356
+ return loss.mean()
357
+ return FocalLoss(focal_gamma, focal_alpha)
358
+
359
+ raise ValueError(f"Unknown loss: {name}")
360
+
361
+
362
+ # --------------------------------------------------------------------------- #
363
+ # Inference: collect logits/probs/labels/ids
364
+ # --------------------------------------------------------------------------- #
365
+ def collect_logits(model, loader, device, amp=False):
366
+ import torch
367
+ model.eval()
368
+ logits_all, labels_all, ids_all = [], [], []
369
+ use_ac = amp and device == "cuda"
370
+ with torch.no_grad():
371
+ for x, y, ids in loader:
372
+ x = x.to(device, non_blocking=True)
373
+ if use_ac:
374
+ with torch.autocast(device_type="cuda", dtype=torch.float16):
375
+ out = model(x).view(-1)
376
+ else:
377
+ out = model(x).view(-1)
378
+ logits_all.append(out.float().cpu().numpy())
379
+ labels_all.append(np.asarray(y))
380
+ ids_all.extend(list(ids))
381
+ return (np.concatenate(logits_all), np.concatenate(labels_all).astype(int), ids_all)
382
+
383
+
384
+ # --------------------------------------------------------------------------- #
385
+ # Calibration (temperature scaling)
386
+ # --------------------------------------------------------------------------- #
387
+ def fit_temperature(val_logits: np.ndarray, val_labels: np.ndarray) -> float:
388
+ """Fit single-parameter temperature on validation logits (minimize NLL)."""
389
+ import torch
390
+ import torch.nn as nn
391
+ logits = torch.tensor(val_logits, dtype=torch.float32)
392
+ labels = torch.tensor(val_labels, dtype=torch.float32)
393
+ T = nn.Parameter(torch.ones(1))
394
+ opt = torch.optim.LBFGS([T], lr=0.01, max_iter=200)
395
+ bce = nn.BCEWithLogitsLoss()
396
+
397
+ def closure():
398
+ opt.zero_grad()
399
+ loss = bce(logits / T.clamp(min=1e-3), labels)
400
+ loss.backward()
401
+ return loss
402
+ opt.step(closure)
403
+ return float(T.detach().clamp(min=1e-3).item())
404
+
405
+
406
+ def apply_temperature(logits: np.ndarray, T: float) -> np.ndarray:
407
+ return 1.0 / (1.0 + np.exp(-(logits / T)))
408
+
409
+
410
+ def sigmoid(logits: np.ndarray) -> np.ndarray:
411
+ return 1.0 / (1.0 + np.exp(-logits))
412
+
413
+
414
+ # --------------------------------------------------------------------------- #
415
+ # Calibration metrics
416
+ # --------------------------------------------------------------------------- #
417
+ def expected_calibration_error(y_true, y_prob, n_bins=15):
418
+ y_true = np.asarray(y_true); y_prob = np.asarray(y_prob)
419
+ bins = np.linspace(0, 1, n_bins + 1)
420
+ ece = 0.0
421
+ for lo, hi in zip(bins[:-1], bins[1:]):
422
+ m = (y_prob > lo) & (y_prob <= hi)
423
+ if m.sum() > 0:
424
+ ece += (m.sum() / len(y_prob)) * abs(y_true[m].mean() - y_prob[m].mean())
425
+ return float(ece)
426
+
427
+
428
+ def brier(y_true, y_prob):
429
+ from sklearn.metrics import brier_score_loss
430
+ return float(brier_score_loss(np.asarray(y_true), np.asarray(y_prob)))
431
+
432
+
433
+ # --------------------------------------------------------------------------- #
434
+ # Thresholding
435
+ # --------------------------------------------------------------------------- #
436
+ def threshold_for_sensitivity(y_true, y_prob, target_sens=0.95):
437
+ """Highest-specificity threshold achieving sensitivity >= target on these data.
438
+
439
+ Returns (threshold, achieved_sens, achieved_spec, achievable_flag).
440
+ """
441
+ from sklearn.metrics import roc_curve
442
+ y_true = np.asarray(y_true); y_prob = np.asarray(y_prob)
443
+ fpr, tpr, thr = roc_curve(y_true, y_prob)
444
+ spec = 1 - fpr
445
+ ok = tpr >= target_sens
446
+ if ok.any():
447
+ cand = np.where(ok)[0]
448
+ best = cand[np.argmax(spec[cand])]
449
+ return float(thr[best]), float(tpr[best]), float(spec[best]), True
450
+ best = int(np.argmax(tpr))
451
+ return float(thr[best]), float(tpr[best]), float(spec[best]), False
452
+
453
+
454
+ def youden_threshold(y_true, y_prob):
455
+ from sklearn.metrics import roc_curve
456
+ fpr, tpr, thr = roc_curve(np.asarray(y_true), np.asarray(y_prob))
457
+ j = tpr - fpr
458
+ best = int(np.argmax(j))
459
+ return float(thr[best]), float(tpr[best]), float(1 - fpr[best])
460
+
461
+
462
+ # --------------------------------------------------------------------------- #
463
+ # Metrics + bootstrap CIs
464
+ # --------------------------------------------------------------------------- #
465
+ def point_metrics(y_true, y_prob, thr):
466
+ from sklearn.metrics import roc_auc_score, f1_score, accuracy_score
467
+ y_true = np.asarray(y_true); y_prob = np.asarray(y_prob)
468
+ pred = (y_prob >= thr).astype(int)
469
+ tp = int(((pred == 1) & (y_true == 1)).sum())
470
+ tn = int(((pred == 0) & (y_true == 0)).sum())
471
+ fp = int(((pred == 1) & (y_true == 0)).sum())
472
+ fn = int(((pred == 0) & (y_true == 1)).sum())
473
+ sens = tp / (tp + fn) if (tp + fn) else float("nan")
474
+ spec = tn / (tn + fp) if (tn + fp) else float("nan")
475
+ ppv = tp / (tp + fp) if (tp + fp) else float("nan")
476
+ npv = tn / (tn + fn) if (tn + fn) else float("nan")
477
+ return {
478
+ "auroc": float(roc_auc_score(y_true, y_prob)),
479
+ "accuracy": float(accuracy_score(y_true, pred)),
480
+ "sensitivity": float(sens),
481
+ "specificity": float(spec),
482
+ "ppv": float(ppv),
483
+ "npv": float(npv),
484
+ "f1": float(f1_score(y_true, pred, zero_division=0)),
485
+ "brier": brier(y_true, y_prob),
486
+ "ece": expected_calibration_error(y_true, y_prob),
487
+ "tp": tp, "tn": tn, "fp": fp, "fn": fn,
488
+ "threshold": float(thr),
489
+ "n": int(len(y_true)),
490
+ "n_pos": int((y_true == 1).sum()),
491
+ "n_neg": int((y_true == 0).sum()),
492
+ }
493
+
494
+
495
+ def bootstrap_ci(y_true, y_prob, thr, n_boot=2000, seed=42):
496
+ """Stratified bootstrap 95% CIs for AUROC, sens, spec, ppv, npv, acc, f1."""
497
+ from sklearn.metrics import roc_auc_score, f1_score, accuracy_score
498
+ y_true = np.asarray(y_true); y_prob = np.asarray(y_prob)
499
+ rng = np.random.default_rng(seed)
500
+ pos = np.where(y_true == 1)[0]
501
+ neg = np.where(y_true == 0)[0]
502
+ keys = ["auroc", "sensitivity", "specificity", "ppv", "npv", "accuracy", "f1"]
503
+ acc = {k: [] for k in keys}
504
+ for _ in range(n_boot):
505
+ idx = np.concatenate([rng.choice(pos, len(pos), replace=True),
506
+ rng.choice(neg, len(neg), replace=True)])
507
+ yt = y_true[idx]; yp = y_prob[idx]
508
+ pred = (yp >= thr).astype(int)
509
+ try:
510
+ acc["auroc"].append(roc_auc_score(yt, yp))
511
+ except Exception:
512
+ acc["auroc"].append(np.nan)
513
+ tp = ((pred == 1) & (yt == 1)).sum(); tn = ((pred == 0) & (yt == 0)).sum()
514
+ fp = ((pred == 1) & (yt == 0)).sum(); fn = ((pred == 0) & (yt == 1)).sum()
515
+ acc["sensitivity"].append(tp / (tp + fn) if (tp + fn) else np.nan)
516
+ acc["specificity"].append(tn / (tn + fp) if (tn + fp) else np.nan)
517
+ acc["ppv"].append(tp / (tp + fp) if (tp + fp) else np.nan)
518
+ acc["npv"].append(tn / (tn + fn) if (tn + fn) else np.nan)
519
+ acc["accuracy"].append(accuracy_score(yt, pred))
520
+ acc["f1"].append(f1_score(yt, pred, zero_division=0))
521
+ out = {}
522
+ for k in keys:
523
+ v = np.asarray(acc[k], dtype=float)
524
+ out[k] = (float(np.nanpercentile(v, 2.5)), float(np.nanpercentile(v, 97.5)))
525
+ return out
526
+
527
+
528
+ # --------------------------------------------------------------------------- #
529
+ # JSON helpers
530
+ # --------------------------------------------------------------------------- #
531
+ def save_json(obj, path):
532
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
533
+ with open(path, "w") as f:
534
+ json.dump(obj, f, indent=2)
535
+
536
+
537
+ def load_json(path):
538
+ with open(path) as f:
539
+ return json.load(f)
train.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """
3
+ Reproducible training for the ResNet-18 thyroid ultrasound malignancy classifier.
4
+
5
+ Trains on Train only; selects the best checkpoint by **validation AUROC**.
6
+ Logs everything to Trackio (losses, val AUROC/sens/spec/PPV/NPV/ECE/Brier, LR,
7
+ epoch, hyperparameters, env info) and emits trackio.alert() at decision points.
8
+
9
+ Single-command reproduction:
10
+ python train.py --config configs/final_config.yaml
11
+
12
+ All CLI args override config values. The exact command line, resolved config,
13
+ seed, package versions and hardware info are saved to the output dir.
14
+
15
+ Dataset is loaded directly from the Train/Valid/Test FOLDER structure of the
16
+ Hub repo (NOT the flattened datasets-viewer 'train' split), so the predefined
17
+ splits are respected.
18
+ """
19
+ import argparse
20
+ import os
21
+ import sys
22
+ import time
23
+ from pathlib import Path
24
+
25
+ import numpy as np
26
+ import yaml
27
+
28
+ import thyroid_lib as L
29
+
30
+
31
+ def parse_args():
32
+ ap = argparse.ArgumentParser()
33
+ ap.add_argument("--config", default=None, help="YAML config path")
34
+ ap.add_argument("--dataset_id", default="Johnyquest7/TN5000-thyroid-nodule-classification")
35
+ ap.add_argument("--data_dir", default=None, help="Local TN5000 dir; else downloaded from Hub")
36
+ ap.add_argument("--output_dir", default="run_out")
37
+ ap.add_argument("--backbone", default="timm:resnet18.a1_in1k")
38
+ ap.add_argument("--freeze_stage", type=int, default=0)
39
+ ap.add_argument("--dropout", type=float, default=0.0)
40
+ ap.add_argument("--aug_policy", default="medical_default")
41
+ ap.add_argument("--loss", default="bce", choices=["bce", "focal"])
42
+ ap.add_argument("--focal_gamma", type=float, default=2.0)
43
+ ap.add_argument("--focal_alpha", type=float, default=0.5)
44
+ ap.add_argument("--imbalance", default="pos_weight",
45
+ choices=["pos_weight", "none", "sampler"])
46
+ ap.add_argument("--optimizer", default="adamw", choices=["adamw", "sgd"])
47
+ ap.add_argument("--lr", type=float, default=2e-4)
48
+ ap.add_argument("--weight_decay", type=float, default=1e-4)
49
+ ap.add_argument("--batch_size", type=int, default=32)
50
+ ap.add_argument("--epochs", type=int, default=40)
51
+ ap.add_argument("--scheduler", default="cosine", choices=["cosine", "plateau", "none"])
52
+ ap.add_argument("--warmup_epochs", type=int, default=2)
53
+ ap.add_argument("--early_stop_patience", type=int, default=8)
54
+ ap.add_argument("--amp", action="store_true", default=True)
55
+ ap.add_argument("--no_amp", dest="amp", action="store_false")
56
+ ap.add_argument("--num_workers", type=int, default=4)
57
+ ap.add_argument("--seed", type=int, default=42)
58
+ ap.add_argument("--strict_determinism", action="store_true", default=True)
59
+ ap.add_argument("--no_strict_determinism", dest="strict_determinism", action="store_false")
60
+ ap.add_argument("--trackio_project", default="agentic_thyroid_resnet18")
61
+ ap.add_argument("--trackio_space_id", default="Johnyquest7/Trakio_agentic_thyroid")
62
+ ap.add_argument("--trackio_dataset_id", default="Johnyquest7/Trakio_agentic_thyroid_dataset")
63
+ ap.add_argument("--run_name", default=None)
64
+ ap.add_argument("--no_trackio", action="store_true")
65
+ return ap.parse_args()
66
+
67
+
68
+ def merge_config(args):
69
+ if args.config and Path(args.config).exists():
70
+ with open(args.config) as f:
71
+ cfg = yaml.safe_load(f) or {}
72
+ passed = {a.split("=")[0].lstrip("-").replace("-", "_") for a in sys.argv[1:] if a.startswith("--")}
73
+ for k, v in cfg.items():
74
+ if k not in passed and hasattr(args, k):
75
+ setattr(args, k, v)
76
+ return args
77
+
78
+
79
+ def main():
80
+ args = parse_args()
81
+ args = merge_config(args)
82
+ out = Path(args.output_dir)
83
+ out.mkdir(parents=True, exist_ok=True)
84
+
85
+ import torch
86
+ from torch.utils.data import DataLoader, WeightedRandomSampler
87
+ from sklearn.metrics import roc_auc_score
88
+ import torch.nn.functional as F
89
+
90
+ L.set_determinism(args.seed, strict=args.strict_determinism)
91
+ env = L.collect_env_info()
92
+ device = "cuda" if torch.cuda.is_available() else "cpu"
93
+
94
+ if args.data_dir:
95
+ data_dir = Path(args.data_dir)
96
+ else:
97
+ from huggingface_hub import snapshot_download
98
+ data_dir = Path(snapshot_download(repo_id=args.dataset_id, repo_type="dataset",
99
+ local_dir=str(out / "_data"),
100
+ allow_patterns=["Train/**", "Valid/**", "Test/**"]))
101
+
102
+ model, pp = L.build_model(args.backbone, freeze_stage=args.freeze_stage, dropout=args.dropout)
103
+ model = model.to(device)
104
+
105
+ train_tf = L.build_train_transform(pp, args.aug_policy)
106
+ eval_tf = L.build_eval_transform(pp)
107
+ train_ds = L.ThyroidImageFolder(data_dir / "Train", train_tf)
108
+ valid_ds = L.ThyroidImageFolder(data_dir / "Valid", eval_tf)
109
+
110
+ n_neg, n_pos = L.class_counts(train_ds.targets)
111
+ pos_weight = (n_neg / n_pos) if (args.imbalance == "pos_weight" and n_pos) else None
112
+
113
+ g = torch.Generator(); g.manual_seed(args.seed)
114
+ if args.imbalance == "sampler":
115
+ cw = np.array([1.0 / n_neg, 1.0 / n_pos])
116
+ sw = np.array([cw[t] for t in train_ds.targets])
117
+ sampler = WeightedRandomSampler(torch.tensor(sw, dtype=torch.double),
118
+ num_samples=len(sw), replacement=True, generator=g)
119
+ train_loader = DataLoader(train_ds, batch_size=args.batch_size, sampler=sampler,
120
+ num_workers=args.num_workers, worker_init_fn=L.seed_worker,
121
+ generator=g, pin_memory=(device == "cuda"), drop_last=False)
122
+ else:
123
+ train_loader = DataLoader(train_ds, batch_size=args.batch_size, shuffle=True,
124
+ num_workers=args.num_workers, worker_init_fn=L.seed_worker,
125
+ generator=g, pin_memory=(device == "cuda"), drop_last=False)
126
+ valid_loader = DataLoader(valid_ds, batch_size=64, shuffle=False,
127
+ num_workers=args.num_workers, pin_memory=(device == "cuda"))
128
+
129
+ criterion = L.build_loss(args.loss, pos_weight if args.loss == "bce" else None,
130
+ args.focal_gamma, args.focal_alpha).to(device)
131
+
132
+ params = [p for p in model.parameters() if p.requires_grad]
133
+ if args.optimizer == "adamw":
134
+ optimizer = torch.optim.AdamW(params, lr=args.lr, weight_decay=args.weight_decay)
135
+ else:
136
+ optimizer = torch.optim.SGD(params, lr=args.lr, momentum=0.9,
137
+ weight_decay=args.weight_decay, nesterov=True)
138
+
139
+ if args.scheduler == "cosine":
140
+ scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.epochs)
141
+ elif args.scheduler == "plateau":
142
+ scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode="max",
143
+ factor=0.5, patience=3)
144
+ else:
145
+ scheduler = None
146
+
147
+ scaler = torch.amp.GradScaler("cuda", enabled=(args.amp and device == "cuda"))
148
+
149
+ run_name = args.run_name or (
150
+ f"{args.backbone.replace('timm:','tm_').replace('.','_')}_lr{args.lr}_wd{args.weight_decay}"
151
+ f"_bs{args.batch_size}_{args.aug_policy}_{args.loss}_{args.imbalance}_fz{args.freeze_stage}")
152
+
153
+ resolved = vars(args).copy()
154
+ resolved.update({"pos_weight": pos_weight, "n_train_neg": n_neg, "n_train_pos": n_pos,
155
+ "preprocess": pp.to_dict(), "device": device})
156
+ L.save_json(resolved, out / "resolved_config.json")
157
+ L.save_json(env, out / "env_info.json")
158
+ with open(out / "command_line.txt", "w") as f:
159
+ f.write("python " + " ".join(sys.argv) + "\n")
160
+ with open(out / "config_used.yaml", "w") as f:
161
+ yaml.safe_dump({k: v for k, v in resolved.items() if not isinstance(v, dict) or k == "preprocess"}, f)
162
+
163
+ use_trackio = not args.no_trackio
164
+ if use_trackio:
165
+ import trackio
166
+ try:
167
+ from trackio.alerts import AlertLevel
168
+ _LV = {"info": AlertLevel.INFO, "warn": AlertLevel.WARN, "error": AlertLevel.ERROR}
169
+ except Exception:
170
+ _LV = {"info": "info", "warn": "warn", "error": "error"}
171
+
172
+ def _alert(title, text, level="info"):
173
+ try:
174
+ trackio.alert(title, text, level=_LV.get(level, level))
175
+ except Exception as e:
176
+ print(f"[alert-failed] {title}: {text} ({e})", flush=True)
177
+
178
+ trackio.init(project=args.trackio_project, name=run_name,
179
+ space_id=args.trackio_space_id,
180
+ dataset_id=args.trackio_dataset_id,
181
+ config={k: v for k, v in resolved.items() if k != "preprocess"})
182
+ _alert("Run started",
183
+ f"{run_name} | backbone={args.backbone} loss={args.loss} "
184
+ f"imb={args.imbalance} lr={args.lr} wd={args.weight_decay} "
185
+ f"bs={args.batch_size} aug={args.aug_policy} fz={args.freeze_stage} "
186
+ f"pos_weight={pos_weight} device={env.get('gpu_name')}",
187
+ "info")
188
+
189
+ best_auroc = -1.0
190
+ best_epoch = -1
191
+ epochs_no_improve = 0
192
+ history = []
193
+ global_step = 0
194
+ n_warmup_steps = args.warmup_epochs * max(1, len(train_loader))
195
+ base_lr = args.lr
196
+
197
+ for epoch in range(args.epochs):
198
+ model.train()
199
+ t0 = time.time()
200
+ running = 0.0
201
+ for x, y, _ in train_loader:
202
+ x = x.to(device, non_blocking=True)
203
+ y = y.to(device, non_blocking=True).float()
204
+ if global_step < n_warmup_steps and args.warmup_epochs > 0:
205
+ for pg in optimizer.param_groups:
206
+ pg["lr"] = base_lr * (global_step + 1) / n_warmup_steps
207
+ optimizer.zero_grad(set_to_none=True)
208
+ with torch.autocast(device_type="cuda", dtype=torch.float16,
209
+ enabled=(args.amp and device == "cuda")):
210
+ out_logits = model(x).view(-1)
211
+ loss = criterion(out_logits, y)
212
+ scaler.scale(loss).backward()
213
+ scaler.step(optimizer)
214
+ scaler.update()
215
+ running += loss.item() * x.size(0)
216
+ global_step += 1
217
+ train_loss = running / len(train_ds)
218
+
219
+ val_logits, val_labels, _ = L.collect_logits(model, valid_loader, device, amp=args.amp)
220
+ val_probs = L.sigmoid(val_logits)
221
+ val_auroc = float(roc_auc_score(val_labels, val_probs))
222
+ val_loss = float(F.binary_cross_entropy_with_logits(
223
+ torch.tensor(val_logits), torch.tensor(val_labels, dtype=torch.float32)).item())
224
+ m = L.point_metrics(val_labels, val_probs, 0.5)
225
+ cur_lr = optimizer.param_groups[0]["lr"]
226
+
227
+ if scheduler is not None:
228
+ if args.scheduler == "plateau":
229
+ scheduler.step(val_auroc)
230
+ elif global_step >= n_warmup_steps:
231
+ scheduler.step()
232
+
233
+ row = {"epoch": epoch, "train_loss": train_loss, "val_loss": val_loss,
234
+ "val_auroc": val_auroc, "val_sens@0.5": m["sensitivity"],
235
+ "val_spec@0.5": m["specificity"], "val_ppv@0.5": m["ppv"],
236
+ "val_npv@0.5": m["npv"], "val_ece@0.5": m["ece"], "val_brier": m["brier"],
237
+ "lr": cur_lr, "epoch_time_s": round(time.time() - t0, 1)}
238
+ history.append(row)
239
+ print(f"[epoch {epoch}] train_loss={train_loss:.4f} val_loss={val_loss:.4f} "
240
+ f"val_auroc={val_auroc:.4f} lr={cur_lr:.2e} ({row['epoch_time_s']}s)", flush=True)
241
+
242
+ if use_trackio:
243
+ trackio.log({"train_loss": train_loss, "val_loss": val_loss, "val_auroc": val_auroc,
244
+ "val_sensitivity": m["sensitivity"], "val_specificity": m["specificity"],
245
+ "val_ppv": m["ppv"], "val_npv": m["npv"], "val_ece": m["ece"],
246
+ "val_brier": m["brier"], "lr": cur_lr, "epoch": epoch})
247
+
248
+ improved = val_auroc > best_auroc + 1e-5
249
+ if improved:
250
+ best_auroc = val_auroc
251
+ best_epoch = epoch
252
+ epochs_no_improve = 0
253
+ torch.save({"model_state": model.state_dict(),
254
+ "backbone": args.backbone, "freeze_stage": args.freeze_stage,
255
+ "dropout": args.dropout, "preprocess": pp.to_dict(),
256
+ "epoch": epoch, "val_auroc": val_auroc},
257
+ out / "best_model.pt")
258
+ L.save_json({"best_epoch": best_epoch, "best_val_auroc": best_auroc,
259
+ "val_metrics_at_0.5": m}, out / "best_val_summary.json")
260
+ else:
261
+ epochs_no_improve += 1
262
+
263
+ if use_trackio and (not np.isfinite(train_loss) or train_loss > 1e3):
264
+ _alert("Training diverged",
265
+ f"train_loss={train_loss} at epoch {epoch} — lr likely too high, try x0.1",
266
+ "error")
267
+
268
+ if epochs_no_improve >= args.early_stop_patience:
269
+ print(f"Early stopping at epoch {epoch} (no val AUROC improvement for "
270
+ f"{args.early_stop_patience} epochs).", flush=True)
271
+ if use_trackio:
272
+ _alert("Early stopping",
273
+ f"No val AUROC gain for {args.early_stop_patience} epochs; "
274
+ f"best={best_auroc:.4f} @ epoch {best_epoch}. Consider lr x0.5.",
275
+ "warn")
276
+ break
277
+
278
+ L.save_json(history, out / "history.json")
279
+ L.save_json({"best_val_auroc": best_auroc, "best_epoch": best_epoch,
280
+ "run_name": run_name, "backbone": args.backbone},
281
+ out / "final_summary.json")
282
+
283
+ if use_trackio:
284
+ trackio.log({"best_val_auroc": best_auroc, "best_epoch": best_epoch})
285
+ _alert("Run complete",
286
+ f"{run_name}: best val AUROC={best_auroc:.4f} @ epoch {best_epoch}",
287
+ "info")
288
+ trackio.finish()
289
+
290
+ print(f"DONE best_val_auroc={best_auroc:.4f} best_epoch={best_epoch}", flush=True)
291
+ return best_auroc
292
+
293
+
294
+ if __name__ == "__main__":
295
+ main()