Add full reproducible thyroid ResNet-18 experiment: weights, scripts, configs, calibration, locked threshold, test eval w/ CIs, figures, data exploration, README, LOG
Browse files- .gitattributes +6 -0
- LOG.md +185 -0
- README.md +198 -1
- configs/calibration.json +13 -0
- configs/env_info.json +19 -0
- configs/final_config.yaml +45 -0
- configs/preprocess.json +17 -0
- configs/threshold.json +14 -0
- configs/winning_run_command_line.txt +1 -0
- configs/winning_run_resolved_config.json +49 -0
- data_exploration_report.md +101 -0
- evaluate.py +123 -0
- evaluate_external.py +217 -0
- explore_data.py +313 -0
- final_model.pt +3 -0
- finalize.py +277 -0
- requirements.txt +15 -0
- results/figures/class_distribution.png +0 -0
- results/figures/grid_Test_Benign.png +3 -0
- results/figures/grid_Test_Malignant.png +3 -0
- results/figures/grid_Train_Benign.png +3 -0
- results/figures/grid_Train_Malignant.png +3 -0
- results/figures/grid_Valid_Benign.png +3 -0
- results/figures/grid_Valid_Malignant.png +3 -0
- results/figures/intensity_distribution.png +0 -0
- results/figures/test_calibration.png +0 -0
- results/figures/test_confusion_counts.png +0 -0
- results/figures/test_confusion_normalized.png +0 -0
- results/figures/test_pr.png +0 -0
- results/figures/test_roc.png +0 -0
- results/figures/valid_calibration.png +0 -0
- results/final_results.json +115 -0
- results/tables/class_distribution.csv +5 -0
- results/tables/data_exploration_summary.json +60 -0
- results/tables/sweep_leaderboard.json +311 -0
- results/tables/sweep_results.json +311 -0
- results/tables/test_metrics_with_ci.csv +10 -0
- results/tables/test_metrics_with_ci.md +20 -0
- results/test_predictions.csv +1001 -0
- results/valid_predictions.csv +501 -0
- results/winning_run_history.json +212 -0
- sweep.py +122 -0
- thyroid_lib.py +539 -0
- 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 78 |
+
|
| 79 |
+
**Train / Malignant**
|
| 80 |
+
|
| 81 |
+

|
| 82 |
+
|
| 83 |
+
**Valid / Benign**
|
| 84 |
+
|
| 85 |
+

|
| 86 |
+
|
| 87 |
+
**Valid / Malignant**
|
| 88 |
+
|
| 89 |
+

|
| 90 |
+
|
| 91 |
+
**Test / Benign**
|
| 92 |
+
|
| 93 |
+

|
| 94 |
+
|
| 95 |
+
**Test / Malignant**
|
| 96 |
+
|
| 97 |
+

|
| 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("\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\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"\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
|
results/figures/grid_Test_Malignant.png
ADDED
|
Git LFS Details
|
results/figures/grid_Train_Benign.png
ADDED
|
Git LFS Details
|
results/figures/grid_Train_Malignant.png
ADDED
|
Git LFS Details
|
results/figures/grid_Valid_Benign.png
ADDED
|
Git LFS Details
|
results/figures/grid_Valid_Malignant.png
ADDED
|
Git LFS Details
|
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()
|