clane9 commited on
Commit
84ca7cf
·
verified ·
1 Parent(s): 3a5be0d

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +42 -0
  2. checkpoints/pretrain_full_90_10_h100/config.yaml +69 -0
  3. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00000.png +3 -0
  4. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00001.png +3 -0
  5. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00002.png +3 -0
  6. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00003.png +3 -0
  7. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00004.png +3 -0
  8. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00005.png +3 -0
  9. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00006.png +3 -0
  10. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00007.png +3 -0
  11. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00008.png +3 -0
  12. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00009.png +3 -0
  13. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00010.png +3 -0
  14. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00011.png +3 -0
  15. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00012.png +3 -0
  16. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00013.png +3 -0
  17. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00014.png +3 -0
  18. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00015.png +3 -0
  19. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00016.png +3 -0
  20. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00017.png +3 -0
  21. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00018.png +3 -0
  22. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00019.png +3 -0
  23. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00020.png +3 -0
  24. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00021.png +3 -0
  25. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00022.png +3 -0
  26. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00023.png +3 -0
  27. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00024.png +3 -0
  28. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00025.png +3 -0
  29. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00026.png +3 -0
  30. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00027.png +3 -0
  31. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00028.png +3 -0
  32. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00029.png +3 -0
  33. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00030.png +3 -0
  34. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00031.png +3 -0
  35. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00032.png +3 -0
  36. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00033.png +3 -0
  37. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00034.png +3 -0
  38. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00035.png +3 -0
  39. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00037.png +3 -0
  40. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00095.png +3 -0
  41. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00096.png +3 -0
  42. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00097.png +3 -0
  43. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00098.png +3 -0
  44. checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00099.png +3 -0
  45. finetune/fomo_tune_baseline/output/task1/build/Apptainer.def +31 -0
  46. finetune/fomo_tune_baseline/output/task1/build/fomo_tune/README.md +239 -0
  47. finetune/fomo_tune_baseline/output/task1/build/fomo_tune/backbone.py +153 -0
  48. finetune/fomo_tune_baseline/output/task1/build/fomo_tune/datasets.py +205 -0
  49. finetune/fomo_tune_baseline/output/task1/build/fomo_tune/main_task1.py +253 -0
  50. finetune/fomo_tune_baseline/output/task1/build/fomo_tune/main_task3.py +241 -0
.gitattributes CHANGED
@@ -33,3 +33,45 @@ 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
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00096.png filter=lfs diff=lfs merge=lfs -text
37
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00095.png filter=lfs diff=lfs merge=lfs -text
38
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00099.png filter=lfs diff=lfs merge=lfs -text
39
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00098.png filter=lfs diff=lfs merge=lfs -text
40
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00097.png filter=lfs diff=lfs merge=lfs -text
41
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00000.png filter=lfs diff=lfs merge=lfs -text
42
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00001.png filter=lfs diff=lfs merge=lfs -text
43
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00002.png filter=lfs diff=lfs merge=lfs -text
44
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00004.png filter=lfs diff=lfs merge=lfs -text
45
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00003.png filter=lfs diff=lfs merge=lfs -text
46
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00005.png filter=lfs diff=lfs merge=lfs -text
47
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00007.png filter=lfs diff=lfs merge=lfs -text
48
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00006.png filter=lfs diff=lfs merge=lfs -text
49
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00008.png filter=lfs diff=lfs merge=lfs -text
50
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00011.png filter=lfs diff=lfs merge=lfs -text
51
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00009.png filter=lfs diff=lfs merge=lfs -text
52
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00010.png filter=lfs diff=lfs merge=lfs -text
53
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00012.png filter=lfs diff=lfs merge=lfs -text
54
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00013.png filter=lfs diff=lfs merge=lfs -text
55
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00014.png filter=lfs diff=lfs merge=lfs -text
56
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00015.png filter=lfs diff=lfs merge=lfs -text
57
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00016.png filter=lfs diff=lfs merge=lfs -text
58
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00017.png filter=lfs diff=lfs merge=lfs -text
59
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00018.png filter=lfs diff=lfs merge=lfs -text
60
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00019.png filter=lfs diff=lfs merge=lfs -text
61
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00020.png filter=lfs diff=lfs merge=lfs -text
62
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00021.png filter=lfs diff=lfs merge=lfs -text
63
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00023.png filter=lfs diff=lfs merge=lfs -text
64
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00024.png filter=lfs diff=lfs merge=lfs -text
65
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00022.png filter=lfs diff=lfs merge=lfs -text
66
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00026.png filter=lfs diff=lfs merge=lfs -text
67
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00025.png filter=lfs diff=lfs merge=lfs -text
68
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00027.png filter=lfs diff=lfs merge=lfs -text
69
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00029.png filter=lfs diff=lfs merge=lfs -text
70
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00028.png filter=lfs diff=lfs merge=lfs -text
71
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00032.png filter=lfs diff=lfs merge=lfs -text
72
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00030.png filter=lfs diff=lfs merge=lfs -text
73
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00031.png filter=lfs diff=lfs merge=lfs -text
74
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00033.png filter=lfs diff=lfs merge=lfs -text
75
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00034.png filter=lfs diff=lfs merge=lfs -text
76
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00035.png filter=lfs diff=lfs merge=lfs -text
77
+ checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00037.png filter=lfs diff=lfs merge=lfs -text
checkpoints/pretrain_full_90_10_h100/config.yaml ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: pretrain_full_90_10_h100
2
+ notes: null
3
+ output_dir: checkpoints/pretrain_full_90_10_h100
4
+ img_size:
5
+ - 208
6
+ - 240
7
+ - 208
8
+ patch_size: 8
9
+ mask_ratio: 0.8
10
+ pred_mask_ratio: null
11
+ model: mae_vit_large
12
+ model_kwargs:
13
+ decoding: attn
14
+ target_norm: none
15
+ no_decode_pos: false
16
+ mask_drop_scale: false
17
+ class_token: true
18
+ reg_tokens: 0
19
+ no_embed_class: false
20
+ decoder_depth: 4
21
+ drop_path_rate: 0.0
22
+ datasets:
23
+ fomo_train:
24
+ url: datasets/FOMO300/wds/shard.{000000..001020}.tar
25
+ samples_per_epoch: 153000
26
+ shuffle: true
27
+ buffer_size: 8000
28
+ drop_last: true
29
+ fomo_val:
30
+ url: datasets/FOMO300/wds/shard.{001021..001134}.tar
31
+ samples_per_epoch: 17000
32
+ shuffle: false
33
+ buffer_size: 1000
34
+ drop_last: true
35
+ train_dataset: fomo_train
36
+ eval_datasets:
37
+ - fomo_val
38
+ num_workers: 16
39
+ prefetch_factor: 8
40
+ presend_cuda: false
41
+ epochs: 100
42
+ batch_size: 64
43
+ accum_iter: 1
44
+ base_lr: 0.001
45
+ min_lr: 1.0e-06
46
+ warmup_epochs: 10
47
+ weight_decay: 0.05
48
+ betas:
49
+ - 0.9
50
+ - 0.95
51
+ clip_grad: 1.0
52
+ amp: true
53
+ amp_dtype: bfloat16
54
+ compile: false
55
+ ckpt: null
56
+ resume: false
57
+ auto_resume: true
58
+ start_epoch: 0
59
+ checkpoint_period: 5
60
+ max_checkpoints: 5
61
+ r2_sync: null
62
+ device: cuda
63
+ distributed: false
64
+ seed: 7338
65
+ eval_seed: 7338
66
+ debug: false
67
+ wandb: true
68
+ wandb_entity: null
69
+ wandb_project: smri-fm
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00000.png ADDED

Git LFS Details

  • SHA256: 049cf8178d9e2156e6abcd30309a2c0cb24bf1de8e30965867a973f66d90fb3e
  • Pointer size: 131 Bytes
  • Size of remote file: 168 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00001.png ADDED

Git LFS Details

  • SHA256: 93b0e03d74dd349ee07ad64dd9a22daa7ffc3d2faf196dbbf285529013224c91
  • Pointer size: 131 Bytes
  • Size of remote file: 171 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00002.png ADDED

Git LFS Details

  • SHA256: a19c10fd992b23cd2477ac1ba5c0955ecb825eb04a8141139c95fef66525639c
  • Pointer size: 131 Bytes
  • Size of remote file: 156 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00003.png ADDED

Git LFS Details

  • SHA256: aec6e39208f27baed70a07a7e71fe37b7f780afc7a0fc02cce8b19a5fbf49c06
  • Pointer size: 131 Bytes
  • Size of remote file: 146 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00004.png ADDED

Git LFS Details

  • SHA256: 8fb6368a4e9397a0cf77c73aa1a097138daa6946626f7bbc9685f858b85ba171
  • Pointer size: 131 Bytes
  • Size of remote file: 166 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00005.png ADDED

Git LFS Details

  • SHA256: bd96ed3939119b141c698f95ef9d1d5930d103d2ed981580186d78cf8b4217fa
  • Pointer size: 131 Bytes
  • Size of remote file: 186 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00006.png ADDED

Git LFS Details

  • SHA256: 63884ae4f9f68e71842e79733379740dcd2e6966bb94ce391297c603812f88aa
  • Pointer size: 131 Bytes
  • Size of remote file: 157 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00007.png ADDED

Git LFS Details

  • SHA256: fb0078b35e62a86af3522cad31e933a947612e5ec73c23b2169f1776e6ecf596
  • Pointer size: 131 Bytes
  • Size of remote file: 157 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00008.png ADDED

Git LFS Details

  • SHA256: 20e69a2a66963a4556a59769d149a2da28d712dc0ad082a8e9d53494ec273627
  • Pointer size: 131 Bytes
  • Size of remote file: 170 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00009.png ADDED

Git LFS Details

  • SHA256: 626e9eee4f3ec6e6f983e59a3cd208c7055cae83e0654920b513b896639cc4b3
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00010.png ADDED

Git LFS Details

  • SHA256: 52530ffdfa0a5f3e819b26a2060814b6046e9860377fcd8849fe84ca52d7f14b
  • Pointer size: 131 Bytes
  • Size of remote file: 129 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00011.png ADDED

Git LFS Details

  • SHA256: badbe2ca7c350002e21ac4db097b30291942d89695de24c57f2ea7cad86e7ed4
  • Pointer size: 131 Bytes
  • Size of remote file: 174 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00012.png ADDED

Git LFS Details

  • SHA256: a1bd8f797e6851097c8cc451eb12eab2b492085625300fe90cd6e511f1280581
  • Pointer size: 131 Bytes
  • Size of remote file: 213 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00013.png ADDED

Git LFS Details

  • SHA256: bd7d579bf99a7507eca6a7405038a99d058150012f8a84dd4001c0c8817bf28c
  • Pointer size: 131 Bytes
  • Size of remote file: 161 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00014.png ADDED

Git LFS Details

  • SHA256: e27d6fb6a3fd96c36e5500ba330957c5939422df641bb97e0f952013eb3f85e9
  • Pointer size: 131 Bytes
  • Size of remote file: 175 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00015.png ADDED

Git LFS Details

  • SHA256: d6ec4cd6bc053f81bcbb281d95069430852c6307b38439b5e2fdf26e3d5082b2
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00016.png ADDED

Git LFS Details

  • SHA256: 960834d50638ff587f94a221bd0f861e26067c21aa74370800c42e99442eb526
  • Pointer size: 131 Bytes
  • Size of remote file: 171 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00017.png ADDED

Git LFS Details

  • SHA256: 323c2cb77512bbe35fca84809da55d284a38c06febd86ed21f97fcd41282a4bf
  • Pointer size: 131 Bytes
  • Size of remote file: 180 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00018.png ADDED

Git LFS Details

  • SHA256: 421cd0dba7f54db47f5d12a5e112d8de5cd4ae4f4f11387faab8a66c392ca248
  • Pointer size: 131 Bytes
  • Size of remote file: 173 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00019.png ADDED

Git LFS Details

  • SHA256: 4459a00be9bc4a36168a0effcfff6231091a3065c4846f1d991ecf2bf9a85ac3
  • Pointer size: 131 Bytes
  • Size of remote file: 188 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00020.png ADDED

Git LFS Details

  • SHA256: 212c328d27cae6cabface81892e00d6d569316ddc652a6156617aea69ec44661
  • Pointer size: 131 Bytes
  • Size of remote file: 186 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00021.png ADDED

Git LFS Details

  • SHA256: 3cc10385580c13cabf1e49dcf26978f10151ce1da77a4fd78694919591194475
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00022.png ADDED

Git LFS Details

  • SHA256: 2ac767a2dc077a38fd035546e9f3f92ede4a54200a5615014b4b6581591d9448
  • Pointer size: 131 Bytes
  • Size of remote file: 156 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00023.png ADDED

Git LFS Details

  • SHA256: 766bc14165574913b46130a535113a9734d010c45a2a920b08dfa88f340dae6e
  • Pointer size: 131 Bytes
  • Size of remote file: 162 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00024.png ADDED

Git LFS Details

  • SHA256: dc9e9935055243a930c62ce9748dfad6a31f1356edbf3575740a0514acc22702
  • Pointer size: 131 Bytes
  • Size of remote file: 184 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00025.png ADDED

Git LFS Details

  • SHA256: ac256ad0ad9e27e24af7016fb1446d32ae8318b5cb184cef67ebbef2884abe9b
  • Pointer size: 131 Bytes
  • Size of remote file: 154 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00026.png ADDED

Git LFS Details

  • SHA256: 8fd791a441713393e6826adf429369b12746c9ac6f230683732de50a0ebcb7c6
  • Pointer size: 131 Bytes
  • Size of remote file: 188 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00027.png ADDED

Git LFS Details

  • SHA256: 948d69578f96217cbb20723e191d0ed8b9d6e22d36e5392c121fc28aabf8f1c3
  • Pointer size: 131 Bytes
  • Size of remote file: 171 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00028.png ADDED

Git LFS Details

  • SHA256: c1708de62264b8d5c0b602cdb68bc6bfee15ebd250ca2a62a294d8c1bdbe5c10
  • Pointer size: 131 Bytes
  • Size of remote file: 148 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00029.png ADDED

Git LFS Details

  • SHA256: 97881a5f9c8936fa9d165ed216601a6f60401f358802a6ce04bd11b4815acc1c
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00030.png ADDED

Git LFS Details

  • SHA256: 823b3dc511af86fc4faaaf9b6e742c4542e5135ee03a7961ec184d2b84d0c3df
  • Pointer size: 131 Bytes
  • Size of remote file: 167 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00031.png ADDED

Git LFS Details

  • SHA256: 8135344dcad2e9b43ac32a2e23754327ca65b1a1360aa017cc3c8cdc97ef1ca9
  • Pointer size: 131 Bytes
  • Size of remote file: 171 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00032.png ADDED

Git LFS Details

  • SHA256: 8ce9e81c38e7f2ac954a8b14b7f4976eda6fcda7155a4828170d174835a73355
  • Pointer size: 131 Bytes
  • Size of remote file: 151 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00033.png ADDED

Git LFS Details

  • SHA256: 5cdc7be4fbda272a3c7f9edd4faba6b52e7d8eea1c90cc52490fbc85bf60b5cd
  • Pointer size: 131 Bytes
  • Size of remote file: 172 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00034.png ADDED

Git LFS Details

  • SHA256: 8f712bcd47e0212994bcfffbf49b2e18c5c49769ac8f9e39d09f4723248e5eef
  • Pointer size: 131 Bytes
  • Size of remote file: 175 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00035.png ADDED

Git LFS Details

  • SHA256: 0421b22ba16fb23f21f11f338875cb8b55831ae7dbb28ebe903d091b57d71abc
  • Pointer size: 131 Bytes
  • Size of remote file: 172 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00037.png ADDED

Git LFS Details

  • SHA256: 5979cda6e1ee43acd9ac309552c12270315c99417e6699383596eaa23a1d1c74
  • Pointer size: 131 Bytes
  • Size of remote file: 188 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00095.png ADDED

Git LFS Details

  • SHA256: a840e88a256264ee300d9919553895d75b0eff24635e7605c90d1704d2602d9a
  • Pointer size: 131 Bytes
  • Size of remote file: 161 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00096.png ADDED

Git LFS Details

  • SHA256: 39f6c0c8215fff05a1cd93275ff5b9a865dfd802cfdea831444a2087ad200540
  • Pointer size: 131 Bytes
  • Size of remote file: 220 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00097.png ADDED

Git LFS Details

  • SHA256: 1b2f9ad4760703a016c487a673626d35eb7dc24c95888e7204ac4d3b23ebb712
  • Pointer size: 131 Bytes
  • Size of remote file: 146 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00098.png ADDED

Git LFS Details

  • SHA256: 1a42d0f98da08c0faf6babd70a619a73629f4b10d486f87a6db103ca54e3b331
  • Pointer size: 131 Bytes
  • Size of remote file: 236 kB
checkpoints/pretrain_full_90_10_h100/eval__fomo_val__mask_pred__00099.png ADDED

Git LFS Details

  • SHA256: 33cc7b0fbb9637feb22cfa6b47156a808deee594a05e9e2b6df4bbbe7f605f43
  • Pointer size: 131 Bytes
  • Size of remote file: 159 kB
finetune/fomo_tune_baseline/output/task1/build/Apptainer.def ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Bootstrap: docker
2
+ From: python:3.11-slim
3
+
4
+ # NOT buildable where it sits: the %files paths below are relative to the build cwd, which is the
5
+ # staging dir `build.py` writes. Build it with `python -m fomo_tune.build <run_dir>`, not by
6
+ # pointing apptainer at this file.
7
+ #
8
+ # Versions are pinned to the training environment: numpy, scikit-learn and joblib because they
9
+ # unpickle `head.joblib`, torch because that is what the checkpoint was written by.
10
+
11
+ %files
12
+ fomo_tune /app/fomo_tune
13
+ smri_mae /app/smri_mae
14
+ model /app/model
15
+ predict.py /app/predict.py
16
+
17
+ %post
18
+ pip install --no-cache-dir \
19
+ torch==2.8.0 \
20
+ numpy==2.4.6 \
21
+ nibabel==5.4.2 \
22
+ einops==0.8.2 \
23
+ jaxtyping==0.3.10 \
24
+ timm==1.0.27 \
25
+ huggingface-hub==0.36.2 \
26
+ scikit-learn==1.8.0 \
27
+ joblib==1.5.3 \
28
+ omegaconf==2.3.0
29
+
30
+ %runscript
31
+ exec python /app/predict.py "$@"
finetune/fomo_tune_baseline/output/task1/build/fomo_tune/README.md ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # fomo_tune
2
+
3
+ The five FOMO26 challenge tasks, one script each, tuned independently.
4
+
5
+ This is a spinoff of `nanobrain.eval`, which scored every backbone on every task through one fixed
6
+ probe. That was the right shape for a benchmark and the wrong shape for a competition: here we care
7
+ about one backbone (sMRI MAE) and five scores, and each task wants a different method. **Nothing
8
+ here imports `nanobrain.eval`, and it should stay that way** — this package may be shared with
9
+ people who won't get the eval suite.
10
+
11
+ ## Layout
12
+
13
+ | File | |
14
+ |---|---|
15
+ | `datasets.py` | core, **frozen**. One `load_fomo_task<k>()` per task, streaming the challenge zips into an HF dataset. Raw niftis, no resampling — the backbone transform does that. |
16
+ | `backbone.py` | core, **frozen**. `load_backbone(ckpt_path) -> (SmriMaeBackbone, SmriMaeTransform)`. Frozen sMRI MAE encoder; the transform canonicalizes to RAS, rescales to 1mm, fits to the pretraining shape, z-scores in a mean-threshold brain mask. |
17
+ | `utils.py` | core. `set_seed`, `git_sha`, `setup_logging`. |
18
+ | `main_task<k>.py` | shell. One task, end to end. Task 1 is the worked example; copy it. |
19
+ | `build.py` + `Apptainer.def` | shell. Package a run dir into the challenge `.sif`. Shared by every task. |
20
+
21
+ `datasets.py` and `backbone.py` are settled and their caches are warm. Treat them as read-only:
22
+ new work goes in `main_task<k>.py`. If one of them genuinely needs to change, that is a
23
+ conversation first, because it invalidates every score already recorded.
24
+
25
+ ## The pattern
26
+
27
+ `main_task1.py` is in three sections, and the split is the point of the whole design.
28
+
29
+ **`Task1Method` — the part we tune.** Features, head, hyperparameters, anything that might move
30
+ the score. Its interface is:
31
+
32
+ ```python
33
+ method.fit(rows) # rows are dataset records: subject, label, images
34
+ method.predict(images) # -> the challenge's output for one subject
35
+ method.save(model_dir) # config.yaml + head.joblib
36
+ Task1Method.load(model_dir, **overrides)
37
+ ```
38
+
39
+ **The protocol — fixed.** Pool out-of-fold predictions over all subjects, bootstrap subjects for
40
+ the CI. No repeats, no stratification; the bootstrap is the only variance estimate. Splitting is
41
+ per-task but fixed within a task — leave-one-out where n is tiny (task 1, n=21), **20-fold** where
42
+ it isn't (tasks 3 and 5), which is close enough to LOO without paying for 494 refits. Once a task's
43
+ scheme is set, hold it or scores stop being comparable across iterations. That is also why
44
+ `cross_validate` seeds its shuffle at 0 rather than from `cfg.seed`: the folds are part of the
45
+ protocol, so tuning the run's seed must not silently redraw them.
46
+
47
+ **Two entrypoints.** `train` runs the protocol then fits a head on all subjects and saves it;
48
+ `predict` is the challenge CLI. Both go through `Method.predict`, which is why every fold
49
+ exercises the code the submission will run.
50
+
51
+ That last point is the load-bearing one. `predict` is not a wrapper written at packaging time — it
52
+ is the same call cross-validation already made once per held-out subject. When you add a task, keep
53
+ that property.
54
+
55
+ ```bash
56
+ uv run python -m fomo_tune.main_task1 train modalities=[dwi_b1000,flair] name=task1_dwi_flair
57
+ uv run python -m fomo_tune.main_task1 predict --model-dir output/fomo_tune/task1_dwi/model \
58
+ --adc adc.nii.gz --dwi dwi.nii.gz --flair flair.nii.gz --output prob.txt
59
+ ```
60
+
61
+ `train` takes omegaconf dotlist overrides against the `Config` dataclass at the top of the file.
62
+ It writes `config.yaml`, `log.txt`, `metrics.json`, and `model/` into `{output_root}/{name}/`.
63
+
64
+ ## Status
65
+
66
+ Tasks 1, 5 and 3 are drafted and verified. Task 1 is also packaged — its container passes the
67
+ challenge validator; 5 and 3 have not been built yet. **Tasks 2 and 4 are tabled** — both are
68
+ segmentation, both need `predict` to emit a nifti on the input grid, and neither is worth opening
69
+ until the classification and regression tasks are settled.
70
+
71
+ All three on `vitl_fomo300`, one H100, wall being the cross-validation loop:
72
+
73
+ | run | result | wall |
74
+ |---|---|---|
75
+ | `task1_dwi`, dwi_b1000, n=21, LOO | AUROC **0.990** [0.944, 1.000] | 25s |
76
+ | `task5_t1w`, t1w, n=48, 20-fold | AUROC **0.984** [0.953, 1.000] | 73s |
77
+ | `task3_t1w`, t1w, n=494, 20-fold | r **0.962** [0.956, 0.968], MAE **3.71y** [3.45, 3.97] | 260s |
78
+
79
+ **Task 3's row is one fold-seed stale.** It was measured before `cross_validate` froze its shuffle
80
+ at 0, so it is a 20-fold run with `random_state=4466`. Task 1 (LOO) and task 5 are current. The
81
+ re-run is cheap — 260s on a GPU — it just has not been done. Expect a shift of the same order task
82
+ 5 saw when its folds moved (0.948 → 0.984, i.e. inside the CI but not negligible).
83
+
84
+ Task 1's earlier probe sweep got 0.954 [0.861, 1.000] on the same checkpoint
85
+ (`experiments/eval_global_0728`), so it roughly reproduces — the gap is LOO vs 5×5 stratified CV,
86
+ one interpolation instead of two, and a head selected on AUROC instead of balanced accuracy.
87
+
88
+ Two checks worth repeating per task — `.claude/scratch/verify_task1.py` and
89
+ `.claude/scratch/verify_task35.py <k>` do both:
90
+ - features are **bit-identical** whether the nifti comes from the HF dataset wrapper or from
91
+ `nib.load` off disk, so CV numbers transfer to the container
92
+ - the `predict` CLI agrees with the in-process method
93
+
94
+ ## What changes per task
95
+
96
+ Counts and modalities, read from the local zips:
97
+
98
+ | Task | n | Inputs | Output | Split | Notes |
99
+ |---|---|---|---|---|---|
100
+ | 1 infarct | 21 | adc, dwi_b1000, flair (+t2s/swi) | probability | LOO | done |
101
+ | 5 polymicrogyria | 48 | t1w | probability | 20-fold | done |
102
+ | 3 brain age | 494 | t1w | age in years | 20-fold | done — RidgeCV head, scored by **Pearson r and MAE**, each with its own bootstrap CI |
103
+ | 2 meningioma | 23 | dwi_b1000, flair (+t2s/swi) | mask, input grid | — | tabled |
104
+ | 4 trigeminal | 40 | t2w | mask, labels 1=nerve 2=vessel | — | tabled |
105
+
106
+ Tasks 5 and 3 diverge from task 1 only where that table says. `cross_validate` over a shuffled
107
+ `KFold` replaces `leave_one_out`; both take one modality, so `features` loses the
108
+ concat-over-modalities loop and `Config` loses `modalities`; the challenge CLI flag is `--t1` for
109
+ both, and it is `--t1` for task 3 too even though the file in the zip is `t1w.nii.gz`.
110
+
111
+ Task 3 is the first regression, so its `score` loops over the two metrics rather than returning
112
+ one, and it drops task 1's guard against bootstrap resamples with fewer than two distinct labels.
113
+ The analogous degenerate case for regression is a resample with no spread in `y`, where Pearson r
114
+ is undefined rather than merely unstable — at n=494 it does not happen.
115
+
116
+ When tasks 2 and 4 come back: `predict` must write a nifti on the input's grid, and the method
117
+ needs localized features rather than a pooled vector — `backbone.forward` returns `patch_coords`
118
+ in world mm for exactly that. Task 4's label order (1=nerve, 2=vessel) is still a guess and needs
119
+ confirming against the challenge data before per-class numbers mean anything.
120
+
121
+ ## Gotchas
122
+
123
+ **Raw niftis are on disk** at `data/fomo_eval/Task_<k>/preprocessed/<sub>/ses-01/`, which is the
124
+ easy way to exercise `predict` on a real file rather than one written out of the dataset:
125
+
126
+ ```bash
127
+ uv run python -m fomo_tune.main_task1 predict \
128
+ --model-dir output/fomo_tune/task1_dwi/model \
129
+ --adc data/fomo_eval/Task_1/preprocessed/sub-20/ses-01/adc.nii.gz \
130
+ --dwi data/fomo_eval/Task_1/preprocessed/sub-20/ses-01/dwi_b1000.nii.gz \
131
+ --flair data/fomo_eval/Task_1/preprocessed/sub-20/ses-01/flair.nii.gz \
132
+ --output /tmp/prob.txt
133
+ ```
134
+
135
+ Task 5 breaks the naming: `Task_5/preprocessed/sub_01/ses_01/t1.nii.gz` — underscores throughout,
136
+ and `t1` not `t1w`. `datasets.py` already handles it; anything you write by hand won't.
137
+
138
+ ```bash
139
+ uv run python -m fomo_tune.main_task5 predict --model-dir output/fomo_tune/task5_t1w/model \
140
+ --t1 data/fomo_eval/Task_5/preprocessed/sub_01/ses_01/t1.nii.gz --output /tmp/prob.txt
141
+ ```
142
+
143
+ **Volumes are wildly anisotropic.** Task 1's DWI is 0.46×0.46×**5.6**mm, so the transform
144
+ upsamples z by 5.6× to reach 1mm iso. Nothing is wrong, but don't read the 1mm grid as real
145
+ resolution.
146
+
147
+ **The backbone never saw skull or neck.** Pretraining used a SynthSeg brain mask; the transform
148
+ substitutes a mean-intensity threshold, which keeps both. Known fidelity gap — see
149
+ `.claude/memory/smri-mae-preprocessing-gap.md`.
150
+
151
+ **Probabilities are not calibrated.** `LogisticRegressionCV` on ~20 samples × 1024 features shrinks
152
+ hard; task 1's out-of-fold probabilities all land in 0.48–0.52 with near-perfect ranking. Fine for
153
+ AUROC, which is what the challenge scores, but don't read them as probabilities. Task 5's do span
154
+ 0–1, which is n=48 rather than n=21 and not evidence of calibration.
155
+
156
+ **n is tiny, so the CI is the result.** Task 1's is ~0.06 wide at the top of the range. Most tuning
157
+ deltas you chase will be inside it. `.claude/NOTES.md` thread 1 has the longer argument.
158
+
159
+ **GPUs need an allocation** — the login node has no driver. See the `gpu-session` skill.
160
+
161
+ ## Submission
162
+
163
+ `build.py` packages a run dir into the `.sif` the challenge wants. One command, taking the run dir
164
+ the shipped head was saved into:
165
+
166
+ ```bash
167
+ uv run python -m fomo_tune.build output/fomo_tune/task1_dwi
168
+ ```
169
+
170
+ It stages `/app`, then builds from there:
171
+
172
+ ```
173
+ /app/predict.py # shim: calls fomo_tune.main_task<k> predict
174
+ /app/model/config.yaml # from the run dir
175
+ /app/model/head.joblib # from the run dir
176
+ /app/model/backbone.pth # stripped checkpoint, --ckpt-path points here
177
+ ```
178
+
179
+ **Both `build.py` and `Apptainer.def` are shared across tasks**, which they can be because nothing
180
+ in staging or in the dependency list is task-specific. The one thing that does vary is the module
181
+ the shim imports, and that comes from `task` in the run's saved config — so a run dir knows which
182
+ task it belongs to, and `build.py` never needs telling.
183
+
184
+ `predict.py` is **generated at build time** rather than checked in. It is eight lines whose whole
185
+ meaning is the container layout staged around it, so there is nowhere outside a container to run
186
+ it. This does not weaken the point above about `predict` not being written at packaging time: the
187
+ logic still lives in `main_task<k>.py`, exercised once per fold, and the shim only picks the
188
+ subcommand and two paths.
189
+
190
+ **`Apptainer.def` is not buildable where it sits.** Its `%files` paths are relative to the build
191
+ cwd, which is the staging dir. Pointing `apptainer build` at it in the repo fails confusingly; go
192
+ through `build.py`.
193
+
194
+ The run dir deliberately does *not* carry backbone weights — that checkpoint is 3.9G and would be
195
+ copied on every run. `--ckpt-path` overrides what `config.yaml` recorded, so the saved config stays
196
+ a faithful record of what trained rather than being rewritten at build time.
197
+
198
+ **The staged checkpoint is stripped to `model` and `args`**, which is 3.9G → 1.3G because the rest
199
+ is optimizer state inference never reads. `load_backbone` needs no change for this, and `predict`
200
+ gives a bit-identical probability either way (0.524739 on `sub-20`, checked on GPU).
201
+
202
+ **The base image is `python:3.11-slim`, not a CUDA image.** The PyPI torch wheel *is* the cu128
203
+ build and vendors the whole CUDA userspace as `nvidia-*` packages, so all the container needs from
204
+ the host is the driver, which `--nv`/`--nvccli` binds in. That keeps the SIF at 5.3G (4.0G of
205
+ image, 1.3G of weights) against roughly double for `pytorch/pytorch` and far more for NGC.
206
+ Versions are pinned to the training environment
207
+ mostly so `head.joblib` unpickles against the numpy/sklearn that wrote it.
208
+
209
+ Apptainer caches the bootstrap layers but **always re-runs `%post`**, so every build re-downloads
210
+ ~3G of wheels. If that gets annoying, bake a deps-only base SIF and `Bootstrap: localimage` off it.
211
+
212
+ ### Validating
213
+
214
+ `third_party/container-validator` is the challenge's own validator, test niftis included:
215
+
216
+ ```bash
217
+ python third_party/container-validator/container_validator/validate.py \
218
+ --task task1 --sif output/fomo_tune/task1_dwi/task1.sif
219
+ ```
220
+
221
+ It runs `python /app/predict.py --flair /input/… --adc … --dwi … --swi … --output /output/<sid>.txt`
222
+ inside an `apptainer instance` with `/input`, `/output` and `/tmp` bound — which is exactly the
223
+ shim's contract, so nothing in `predict.py` is guessing at the interface.
224
+
225
+ One thing it does that is easy to miss: it takes GPU via `--nvccli` rather than `--nv`, and one of
226
+ its tests runs `nvidia-smi -L` **inside** the container. `python:3.11-slim` ships no `nvidia-smi`,
227
+ so that test passes only because `--nvccli` injects the host one — a CUDA base image would hide
228
+ that dependency rather than remove it.
229
+
230
+ **The `task1_dwi` container passes all 20 validator tests**, and `predict` inside it returns
231
+ 0.524739 on `sub-20`, identical to the same call outside the container. So the packaging is
232
+ verified end to end, not just built.
233
+
234
+ **Run it on a compute node with apptainer, which as of 2026-08-11 means `n-6`** — `salloc
235
+ --nodelist=n-6`. The other nodes fail the validator's preflight. The login node has apptainer but
236
+ no driver, and
237
+ `predict` there dies inside `can_use_cudnn_attention` — the jagged-SDPA path reaches into CUDA even
238
+ when the tensors are on CPU, so a driver-less host fails at the forward pass rather than falling
239
+ back. That is the CPU gap worth remembering; it is not a container problem.
finetune/fomo_tune_baseline/output/task1/build/fomo_tune/backbone.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+
3
+ import nibabel as nib
4
+ import numpy as np
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ from einops import rearrange
9
+ from torch import Tensor
10
+
11
+ import smri_mae.model_mae as models_mae
12
+
13
+
14
+ class SmriMaeBackbone(nn.Module):
15
+ grid_coords: Tensor
16
+
17
+ def __init__(self, encoder: models_mae.MaskedEncoder):
18
+ super().__init__()
19
+ self.encoder = encoder
20
+ self.img_size = self.encoder.patchify.img_size
21
+
22
+ grid_size = self.encoder.patchify.grid_size
23
+ patch_size = np.array(self.encoder.patchify.patch_size)
24
+ grid_coords = rearrange(np.indices(grid_size), "c x y z -> (x y z) c")
25
+ grid_coords = grid_coords * patch_size + (patch_size - 1) / 2
26
+ grid_coords = torch.as_tensor(grid_coords, dtype=torch.float32)
27
+ self.register_buffer("grid_coords", grid_coords)
28
+
29
+ def forward(self, batch: dict[str, Tensor]) -> dict[str, Tensor]:
30
+ images = batch["image"]
31
+ mask = batch["mask"]
32
+ affine = batch["affine"]
33
+
34
+ B, C, X, Y, Z = images.shape
35
+ assert (X, Y, Z) == self.img_size, f"expected {self.img_size}, got {(X, Y, Z)}"
36
+
37
+ _, _, patch_embeds, _, patch_ids, token_mask = self.encoder(images, mask=mask)
38
+
39
+ # [B, L, 3] world xyz coords of embeddings
40
+ patch_coords = self.grid_coords[patch_ids, :]
41
+ rot = affine[:, :3, :3]
42
+ trans = affine[:, :3, 3]
43
+ patch_coords = patch_coords @ rot.transpose(1, 2) + trans[:, None, :]
44
+
45
+ return {
46
+ "patch_embeds": patch_embeds,
47
+ "patch_ids": patch_ids,
48
+ "token_mask": token_mask,
49
+ "patch_coords": patch_coords,
50
+ }
51
+
52
+
53
+ class SmriMaeTransform:
54
+ def __init__(
55
+ self,
56
+ img_size: tuple[int, int, int] = (208, 240, 208),
57
+ spacing: tuple[float, float, float] = (1.0, 1.0, 1.0),
58
+ ):
59
+ self.img_size = img_size
60
+ self.spacing = spacing
61
+
62
+ def __call__(self, img: nib.Nifti1Image) -> dict[str, Tensor]:
63
+ # repack image to handle incomplete hf Nifti interface
64
+ img = nib.Nifti1Image(img.dataobj, img.affine, img.header)
65
+ img = nib.as_closest_canonical(img)
66
+
67
+ data = torch.from_numpy(np.ascontiguousarray(img.get_fdata(dtype=np.float32)))
68
+ affine = np.asarray(img.affine)
69
+
70
+ spacing = img.header.get_zooms()
71
+ if max(abs(s - s_) for s, s_ in zip(spacing, self.spacing)) > 0.05:
72
+ data, affine = rescale(data, affine, spacing, self.spacing)
73
+
74
+ data, affine = fit_to_shape(data, affine, target_shape=self.img_size)
75
+
76
+ # mean threshold, not the SynthSeg mask used in pretraining, so skull and neck stay in
77
+ mask = data > data.mean()
78
+ brain = data[mask]
79
+ mean = brain.mean()
80
+ # population std (correction=0) to match the pretraining normalization
81
+ std = brain.std(correction=0).clamp_min(1e-6)
82
+ data = torch.where(mask, (data - mean) / std, 0.0)
83
+
84
+ return {
85
+ "image": data.unsqueeze(0),
86
+ "mask": mask.unsqueeze(0),
87
+ "affine": torch.as_tensor(affine, dtype=torch.float32),
88
+ }
89
+
90
+
91
+ def rescale(
92
+ x: torch.Tensor,
93
+ affine: np.ndarray,
94
+ spacing: tuple[float, ...],
95
+ target_spacing: tuple[float, ...] = (1.0, 1.0, 1.0),
96
+ ) -> tuple[torch.Tensor, np.ndarray]:
97
+ scales = tuple([current / target for current, target in zip(spacing, target_spacing)])
98
+ resampled = F.interpolate(x[None, None], scale_factor=scales, mode="trilinear").squeeze(0, 1)
99
+
100
+ # align_corners=False reads output voxel j from input voxel (j + 0.5) / scale - 0.5
101
+ scale = np.asarray(scales, dtype=float)
102
+ step = np.diag([*(1 / scale), 1.0])
103
+ step[:3, 3] = 0.5 / scale - 0.5
104
+ return resampled, affine @ step
105
+
106
+
107
+ def fit_to_shape(
108
+ x: torch.Tensor, affine: np.ndarray, target_shape: tuple[int, ...]
109
+ ) -> tuple[torch.Tensor, np.ndarray]:
110
+ """Centre the volume in `target_shape`, padding the short axes and cropping the long ones."""
111
+ pads = [target - size for size, target in zip(x.shape, target_shape)]
112
+ padding = [side for pad in reversed(pads) for side in (pad // 2, pad - pad // 2)]
113
+
114
+ # a crop is a negative pad, so output voxel k came from input voxel k - pad // 2 either way
115
+ step = np.eye(4)
116
+ step[:3, 3] = [-(pad // 2) for pad in pads]
117
+ return F.pad(x, padding), affine @ step
118
+
119
+
120
+ def resolve_ckpt(ckpt_path: str) -> str:
121
+ """A local path for a checkpoint, downloading it if it is an hf://<org>/<repo>/<file> URI."""
122
+ from huggingface_hub import hf_hub_download
123
+
124
+ if ckpt_path.startswith("hf://"):
125
+ org, repo, *rest = ckpt_path.removeprefix("hf://").split("/")
126
+ return hf_hub_download(f"{org}/{repo}", "/".join(rest))
127
+
128
+ return ckpt_path
129
+
130
+
131
+ def load_backbone(ckpt_path: str) -> tuple[SmriMaeBackbone, SmriMaeTransform]:
132
+ path = resolve_ckpt(ckpt_path)
133
+ ckpt = torch.load(path, map_location="cpu", weights_only=True, mmap=True)
134
+ args = ckpt["args"]
135
+
136
+ model_fn = models_mae.__dict__[args["model"]]
137
+ model: models_mae.MaskedAutoencoderViT = model_fn(
138
+ img_size=args["img_size"],
139
+ in_chans=args.get("in_chans", 1),
140
+ patch_size=args["patch_size"],
141
+ # older checkpoints carry training flags the current model_mae no longer takes
142
+ **filter_kwargs(models_mae.MaskedAutoencoderViT, args.get("model_kwargs") or {}),
143
+ )
144
+ model.load_state_dict(ckpt["model"])
145
+ backbone = SmriMaeBackbone(model.encoder)
146
+ transform = SmriMaeTransform(img_size=args["img_size"])
147
+ return backbone, transform
148
+
149
+
150
+ def filter_kwargs(func, kwargs):
151
+ signature = inspect.signature(func)
152
+ kwargs = {k: v for k, v in kwargs.items() if k in signature.parameters}
153
+ return kwargs
finetune/fomo_tune_baseline/output/task1/build/fomo_tune/datasets.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import tempfile
4
+ import zipfile
5
+ from collections.abc import Generator
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+
9
+ import fsspec
10
+ from datasets import Dataset, Features, Nifti, Value
11
+
12
+ FOMO_EVAL_BASE_URL = os.getenv(
13
+ "FOMO_EVAL_BASE_URL",
14
+ "https://sid.erda.dk/share_redirect/fmeuvo1EdF",
15
+ )
16
+ FOMO_EVAL_TASK5_URL = os.getenv(
17
+ "FOMO_EVAL_TASK5_URL",
18
+ "https://huggingface.co/datasets/medarc/smri-fm/resolve/main/fomo_eval/Task_5.zip",
19
+ )
20
+
21
+
22
+ @contextmanager
23
+ def open_zip(url: str) -> Generator[zipfile.ZipFile, None, None]:
24
+ """Open a task zip, copying a remote url to a temp file first."""
25
+ with tempfile.TemporaryDirectory() as tmp:
26
+ local = Path(url)
27
+ if not local.exists():
28
+ local = Path(tmp) / "task.zip"
29
+ with fsspec.open(url) as src, local.open("wb") as dst:
30
+ shutil.copyfileobj(src, dst)
31
+ with zipfile.ZipFile(local) as zf:
32
+ yield zf
33
+
34
+
35
+ def subject_ids(zf: zipfile.ZipFile) -> list[str]:
36
+ return sorted({name.split("/")[2] for name in zf.namelist() if name.endswith(".nii.gz")})
37
+
38
+
39
+ # ---- Task 1: acute infarct (classification; positives also carry a lesion mask) --------
40
+
41
+
42
+ def load_fomo_task1() -> Dataset:
43
+ # No 4th modality: it is swi on 16 subjects and t2s on the other 5.
44
+ suffixes = ("adc", "dwi_b1000", "flair")
45
+ features = Features(
46
+ {
47
+ "subject": Value("string"),
48
+ "label": Value("int32"),
49
+ **{suffix: Nifti() for suffix in suffixes},
50
+ }
51
+ )
52
+ dataset = Dataset.from_generator(
53
+ _fomo_task1_generator,
54
+ features=features,
55
+ gen_kwargs={"suffixes": suffixes},
56
+ writer_batch_size=16,
57
+ )
58
+ return dataset
59
+
60
+
61
+ def _fomo_task1_generator(suffixes: tuple[str, ...]):
62
+ url = f"{FOMO_EVAL_BASE_URL}/Task_1.zip"
63
+ with open_zip(url) as zf:
64
+ for sub in subject_ids(zf):
65
+ label = int(zf.read(f"Task_1/labels/{sub}/ses-01/label.txt").strip())
66
+ sample = {"subject": sub, "label": label}
67
+ for suffix in suffixes:
68
+ name = f"Task_1/preprocessed/{sub}/ses-01/{suffix}.nii.gz"
69
+ sample[suffix] = {"path": None, "bytes": zf.read(name)}
70
+ yield sample
71
+
72
+
73
+ # ---- Task 2: meningioma segmentation ---------------------------------------------------
74
+
75
+
76
+ def load_fomo_task2() -> Dataset:
77
+ # No 4th modality: it is t2s on 15 subjects and swi on the other 8.
78
+ suffixes = ("dwi_b1000", "flair")
79
+ features = Features(
80
+ {
81
+ "subject": Value("string"),
82
+ **{suffix: Nifti() for suffix in suffixes},
83
+ "seg": Nifti(),
84
+ }
85
+ )
86
+ dataset = Dataset.from_generator(
87
+ _fomo_task2_generator,
88
+ features=features,
89
+ gen_kwargs={"suffixes": suffixes},
90
+ writer_batch_size=16,
91
+ )
92
+ return dataset
93
+
94
+
95
+ def _fomo_task2_generator(suffixes: tuple[str, ...]):
96
+ url = f"{FOMO_EVAL_BASE_URL}/Task_2.zip"
97
+ with open_zip(url) as zf:
98
+ for sub in subject_ids(zf):
99
+ sample = {"subject": sub}
100
+ for suffix in suffixes:
101
+ name = f"Task_2/preprocessed/{sub}/ses-01/{suffix}.nii.gz"
102
+ sample[suffix] = {"path": None, "bytes": zf.read(name)}
103
+ # Seg is on the image grid (shapes match) but its affine differs by up to 0.03mm.
104
+ name = f"Task_2/labels/{sub}/ses-01/seg.nii.gz"
105
+ sample["seg"] = {"path": None, "bytes": zf.read(name)}
106
+ yield sample
107
+
108
+
109
+ # ---- Task 3: brain age regression ------------------------------------------------------
110
+
111
+
112
+ def load_fomo_task3() -> Dataset:
113
+ features = Features(
114
+ {
115
+ "subject": Value("string"),
116
+ "age": Value("int32"),
117
+ "t1w": Nifti(),
118
+ }
119
+ )
120
+ dataset = Dataset.from_generator(
121
+ _fomo_task3_generator,
122
+ features=features,
123
+ writer_batch_size=16,
124
+ )
125
+ return dataset
126
+
127
+
128
+ def _fomo_task3_generator():
129
+ url = f"{FOMO_EVAL_BASE_URL}/Task_3.zip"
130
+ with open_zip(url) as zf:
131
+ for sub in subject_ids(zf):
132
+ age = int(zf.read(f"Task_3/labels/{sub}/ses-01/labels.txt").strip())
133
+ image_gz = zf.read(f"Task_3/preprocessed/{sub}/ses-01/t1w.nii.gz")
134
+ sample = {
135
+ "subject": sub,
136
+ "age": age,
137
+ "t1w": {"path": None, "bytes": image_gz},
138
+ }
139
+ yield sample
140
+
141
+
142
+ # ---- Task 4: trigeminal nerve/vessel segmentation --------------------------------------
143
+
144
+
145
+ def load_fomo_task4() -> Dataset:
146
+ # Volumes are uncropped 0.5mm near-iso, ~360x512x512; crop before feeding a model.
147
+ features = Features(
148
+ {
149
+ "subject": Value("string"),
150
+ "t2w": Nifti(),
151
+ "seg": Nifti(),
152
+ }
153
+ )
154
+ dataset = Dataset.from_generator(
155
+ _fomo_task4_generator,
156
+ features=features,
157
+ writer_batch_size=16,
158
+ )
159
+ return dataset
160
+
161
+
162
+ def _fomo_task4_generator():
163
+ url = f"{FOMO_EVAL_BASE_URL}/Task_4.zip"
164
+ with open_zip(url) as zf:
165
+ for sub in subject_ids(zf):
166
+ image_gz = zf.read(f"Task_4/preprocessed/{sub}/ses-01/t2w.nii.gz")
167
+ seg_gz = zf.read(f"Task_4/labels/{sub}/ses-01/seg.nii.gz")
168
+ sample = {
169
+ "subject": sub,
170
+ "t2w": {"path": None, "bytes": image_gz},
171
+ "seg": {"path": None, "bytes": seg_gz},
172
+ }
173
+ yield sample
174
+
175
+
176
+ # ---- Task 5: polymicrogyria classification ---------------------------------------------
177
+
178
+
179
+ def load_fomo_task5() -> Dataset:
180
+ features = Features(
181
+ {
182
+ "subject": Value("string"),
183
+ "label": Value("int32"),
184
+ "t1w": Nifti(),
185
+ }
186
+ )
187
+ dataset = Dataset.from_generator(
188
+ _fomo_task5_generator,
189
+ features=features,
190
+ writer_batch_size=16,
191
+ )
192
+ return dataset
193
+
194
+
195
+ def _fomo_task5_generator():
196
+ with open_zip(FOMO_EVAL_TASK5_URL) as zf:
197
+ for sub in subject_ids(zf):
198
+ label = int(zf.read(f"Task_5/labels/{sub}/ses_01/labels.txt").strip())
199
+ image_gz = zf.read(f"Task_5/preprocessed/{sub}/ses_01/t1.nii.gz")
200
+ sample = {
201
+ "subject": sub,
202
+ "label": label,
203
+ "t1w": {"path": None, "bytes": image_gz},
204
+ }
205
+ yield sample
finetune/fomo_tune_baseline/output/task1/build/fomo_tune/main_task1.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FOMO task 1: acute infarct classification, scored by AUROC as the challenge scores it.
2
+
3
+ `Task1Method` is the part we tune -- features, head, hyperparameters. The protocol below it is
4
+ fixed so scores stay comparable across iterations: leave one subject out, pool the out-of-fold
5
+ predictions, bootstrap subjects for the CI.
6
+
7
+ `train` runs that protocol then fits and saves a head; `predict` is the challenge contract,
8
+ modality paths in and one probability out. Both go through `Task1Method.predict`, so every fold
9
+ exercises the path the submission will run.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import time
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+
19
+ import joblib
20
+ import nibabel as nib
21
+ import numpy as np
22
+ import torch
23
+ from omegaconf import OmegaConf
24
+ from sklearn.linear_model import LogisticRegressionCV
25
+ from sklearn.metrics import roc_auc_score
26
+ from sklearn.pipeline import make_pipeline
27
+ from sklearn.preprocessing import StandardScaler
28
+
29
+ from fomo_tune.backbone import load_backbone
30
+ from fomo_tune.utils import git_sha, set_seed, setup_logging
31
+
32
+ logger = logging.getLogger("fomo_tune")
33
+
34
+ Images = dict[str, nib.Nifti1Image]
35
+
36
+
37
+ @dataclass
38
+ class Config:
39
+ task: str = "task1"
40
+ ckpt_path: str = (
41
+ "/data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth"
42
+ )
43
+ modalities: list[str] = field(default_factory=lambda: ["dwi_b1000"])
44
+ output_root: str = "output/fomo_tune"
45
+ name: str = "task1"
46
+ device: str = "cuda"
47
+ seed: int = 4466
48
+
49
+
50
+ # ---- method: the part we tune -----------------------------------------------------------
51
+
52
+
53
+ class Task1Method:
54
+ """Frozen sMRI MAE, mean-pooled tokens per modality concatenated, logistic head."""
55
+
56
+ def __init__(self, cfg: Config):
57
+ self.cfg = cfg
58
+ self.backbone, self.transform = load_backbone(cfg.ckpt_path)
59
+ self.device = torch.device(cfg.device)
60
+ self.backbone.to(self.device).eval().requires_grad_(False)
61
+ self.modalities = list(cfg.modalities)
62
+ self.cache: dict[str, np.ndarray] = {}
63
+ self.head = None
64
+
65
+ @torch.inference_mode()
66
+ def features(self, images: Images) -> np.ndarray:
67
+ """(D,) per subject. A pure function of the images, so training and inference agree."""
68
+ pooled = []
69
+ for modality in self.modalities:
70
+ sample = self.transform(images[modality])
71
+ batch = {key: value[None].to(self.device) for key, value in sample.items()}
72
+
73
+ with torch.autocast("cuda", torch.bfloat16, enabled=self.device.type == "cuda"):
74
+ out = self.backbone(batch)
75
+
76
+ patch_embeds = out["patch_embeds"]
77
+ token_mask = out["token_mask"].bool().unsqueeze(-1)
78
+ embed = (patch_embeds * token_mask).sum(dim=1) / token_mask.sum(dim=1)
79
+ pooled.append(embed[0].float().cpu())
80
+
81
+ return torch.cat(pooled).numpy()
82
+
83
+ def cached_features(self, row: dict) -> np.ndarray:
84
+ if row["subject"] not in self.cache:
85
+ self.cache[row["subject"]] = self.features(row)
86
+ return self.cache[row["subject"]]
87
+
88
+ def fit(self, rows: list[dict]) -> None:
89
+ X = np.stack([self.cached_features(row) for row in rows])
90
+ y = np.array([row["label"] for row in rows])
91
+
92
+ clf = LogisticRegressionCV(
93
+ Cs=10,
94
+ class_weight="balanced",
95
+ scoring="roc_auc",
96
+ max_iter=1000,
97
+ l1_ratios=(0,),
98
+ use_legacy_attributes=False,
99
+ )
100
+ self.head = make_pipeline(StandardScaler(), clf)
101
+ self.head.fit(X, y)
102
+ self.positive = list(self.head.classes_).index(1)
103
+
104
+ def predict(self, images: Images) -> float:
105
+ """Positive-class probability. Indexes `classes_` rather than assuming column 1, which
106
+ would silently score the wrong class if the label order differed."""
107
+ X = self.features(images)[None]
108
+ probs = self.head.predict_proba(X)[0]
109
+ return float(probs[self.positive])
110
+
111
+ def save(self, model_dir: Path) -> None:
112
+ """Everything `load` needs but the backbone weights, which stay wherever `ckpt_path`
113
+ points -- a few hundred KB, so a run saves one without copying a 3.7G checkpoint."""
114
+ model_dir.mkdir(parents=True, exist_ok=True)
115
+ OmegaConf.save(self.cfg, model_dir / "config.yaml")
116
+ joblib.dump({"head": self.head, "positive": self.positive}, model_dir / "head.joblib")
117
+
118
+ @classmethod
119
+ def load(cls, model_dir: Path, **overrides) -> "Task1Method":
120
+ """Rebuild a fitted method from `save`. Overrides are Config fields, for what differs
121
+ between here and the container -- the backbone path, the device."""
122
+ cfg = OmegaConf.merge(
123
+ OmegaConf.structured(Config), OmegaConf.load(model_dir / "config.yaml"), overrides
124
+ )
125
+ method = cls(cfg)
126
+ state = joblib.load(model_dir / "head.joblib")
127
+ method.head, method.positive = state["head"], state["positive"]
128
+ return method
129
+
130
+
131
+ # ---- protocol: the part we hold fixed ---------------------------------------------------
132
+
133
+ # Every image the task ships. The method picks which of them it wants, as at inference, where
134
+ # the challenge hands over all four modalities whether or not a model uses them.
135
+ IMAGE_COLS = ("adc", "dwi_b1000", "flair")
136
+
137
+
138
+ def leave_one_out(rows: list[dict], method: Task1Method) -> tuple[np.ndarray, np.ndarray]:
139
+ """Out-of-fold score for every subject, each predicted by a head fit on the other n-1."""
140
+ y = np.array([row["label"] for row in rows])
141
+ oof = np.zeros(len(rows), dtype=float)
142
+ start = time.perf_counter()
143
+ for held_out, row in enumerate(rows):
144
+ method.fit([r for r in rows if r["subject"] != row["subject"]])
145
+ oof[held_out] = method.predict({key: row[key] for key in IMAGE_COLS})
146
+ logger.info(
147
+ f"fold {held_out + 1}/{len(rows)} {row['subject']} "
148
+ f"y={y[held_out]} p={oof[held_out]:.3f} ({time.perf_counter() - start:.0f}s)"
149
+ )
150
+ return y, oof
151
+
152
+
153
+ def score(
154
+ y: np.ndarray, oof: np.ndarray, seed: int = 0, n_boot: int = 2000, alpha: float = 0.05
155
+ ) -> dict:
156
+ """AUROC, the challenge metric, plus a percentile CI resampling subjects with replacement."""
157
+ rng = np.random.default_rng(seed)
158
+ samples = []
159
+ for _ in range(n_boot):
160
+ rows = rng.integers(0, len(y), size=len(y))
161
+ if len(np.unique(y[rows])) < 2:
162
+ continue
163
+ samples.append(roc_auc_score(y[rows], oof[rows]))
164
+
165
+ low, high = np.percentile(samples, [100 * alpha / 2, 100 * (1 - alpha / 2)])
166
+ return {
167
+ "auroc": float(roc_auc_score(y, oof)),
168
+ "auroc_ci_low": float(low),
169
+ "auroc_ci_high": float(high),
170
+ }
171
+
172
+
173
+ # ---- entrypoints ------------------------------------------------------------------------
174
+
175
+
176
+ def train(args: argparse.Namespace) -> None:
177
+ # imported here, not at the top, so the container needs no dataset stack to run `predict`
178
+ from fomo_tune.datasets import load_fomo_task1
179
+
180
+ cfg = OmegaConf.merge(OmegaConf.structured(Config), OmegaConf.from_dotlist(args.overrides))
181
+ run_dir = Path(cfg.output_root) / cfg.name
182
+ run_dir.mkdir(parents=True, exist_ok=True)
183
+
184
+ setup_logging(run_dir)
185
+ set_seed(cfg.seed)
186
+ logger.info(f"run {cfg.name} (git {git_sha()})")
187
+ logger.info(f"config:\n{OmegaConf.to_yaml(cfg).rstrip()}")
188
+ OmegaConf.save(cfg, run_dir / "config.yaml")
189
+
190
+ # decoded once: leave-one-out revisits every subject n times, and the niftis are small
191
+ rows = list(load_fomo_task1())
192
+ logger.info(f"dataset: {len(rows)} subjects, {sum(r['label'] for r in rows)} positive")
193
+
194
+ method = Task1Method(cfg)
195
+ start = time.perf_counter()
196
+ y, oof = leave_one_out(rows, method)
197
+ run_time = time.perf_counter() - start
198
+ summary = score(y, oof)
199
+
200
+ # the shipped head sees all n subjects, so it is not any of the models scored above
201
+ method.fit(rows)
202
+ method.save(run_dir / "model")
203
+
204
+ record = {"name": cfg.name, **summary, "run_time": round(run_time, 1)}
205
+ (run_dir / "metrics.json").write_text(json.dumps(record) + "\n")
206
+ scores = " ".join(f"{k}={v:.4f}" for k, v in summary.items())
207
+ logger.info(f"result: {scores} ({run_time:.0f}s)")
208
+
209
+
210
+ def predict(args: argparse.Namespace) -> None:
211
+ """The challenge contract: modality paths in, one probability written to `--output`.
212
+
213
+ `/app/predict.py` in the container is a shim over this, so what scores the submission is the
214
+ code leave-one-out already ran, not something generated at build time.
215
+ """
216
+ overrides = {"device": args.device}
217
+ if args.ckpt_path:
218
+ overrides["ckpt_path"] = args.ckpt_path
219
+ method = Task1Method.load(args.model_dir, **overrides)
220
+
221
+ # every image the challenge hands over, as in `leave_one_out`; the method takes what it uses
222
+ paths = {"adc": args.adc, "dwi_b1000": args.dwi, "flair": args.flair}
223
+ probability = method.predict({key: nib.load(path) for key, path in paths.items()})
224
+
225
+ args.output.write_text(f"{probability:.6f}\n")
226
+
227
+
228
+ def main() -> None:
229
+ parser = argparse.ArgumentParser()
230
+ modes = parser.add_subparsers(required=True)
231
+
232
+ train_parser = modes.add_parser("train", help="leave-one-out over the task, then fit and save")
233
+ train_parser.add_argument("overrides", nargs="*", help="config overrides, e.g. device=cpu")
234
+ train_parser.set_defaults(run=train)
235
+
236
+ predict_parser = modes.add_parser("predict", help="one subject, one probability")
237
+ for flag in ("--flair", "--adc", "--dwi"):
238
+ predict_parser.add_argument(flag, type=Path, required=True)
239
+ # accepted and ignored: the 4th modality is swi on some subjects and t2s on others
240
+ for flag in ("--t2s", "--swi"):
241
+ predict_parser.add_argument(flag, type=Path)
242
+ predict_parser.add_argument("--output", type=Path, required=True)
243
+ predict_parser.add_argument("--model-dir", type=Path, default=Path("/app/model"))
244
+ predict_parser.add_argument("--ckpt-path", help="overrides the trained config's backbone path")
245
+ predict_parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
246
+ predict_parser.set_defaults(run=predict)
247
+
248
+ args = parser.parse_args()
249
+ args.run(args)
250
+
251
+
252
+ if __name__ == "__main__":
253
+ main()
finetune/fomo_tune_baseline/output/task1/build/fomo_tune/main_task3.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FOMO task 3: brain age regression, scored by Pearson r and MAE as the challenge scores it.
2
+
3
+ `Task3Method` is the part we tune -- features, head, hyperparameters. The protocol below it is
4
+ fixed so scores stay comparable across iterations: 20-fold over the 494 subjects, pool the
5
+ out-of-fold predictions, bootstrap subjects for the CI.
6
+
7
+ `train` runs that protocol then fits and saves a head; `predict` is the challenge contract, one t1
8
+ path in and one age out. Both go through `Task3Method.predict`, so every fold exercises the path
9
+ the submission will run.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import logging
15
+ import time
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ import joblib
20
+ import nibabel as nib
21
+ import numpy as np
22
+ import torch
23
+ from omegaconf import OmegaConf
24
+ from sklearn.linear_model import RidgeCV
25
+ from sklearn.model_selection import KFold
26
+ from sklearn.pipeline import make_pipeline
27
+ from sklearn.preprocessing import StandardScaler
28
+
29
+ from fomo_tune.backbone import load_backbone
30
+ from fomo_tune.utils import git_sha, set_seed, setup_logging
31
+
32
+ logger = logging.getLogger("fomo_tune")
33
+
34
+ Images = dict[str, nib.Nifti1Image]
35
+
36
+
37
+ @dataclass
38
+ class Config:
39
+ task: str = "task3"
40
+ ckpt_path: str = (
41
+ "/data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth"
42
+ )
43
+ output_root: str = "output/fomo_tune"
44
+ name: str = "task3"
45
+ device: str = "cuda"
46
+ seed: int = 4466
47
+
48
+
49
+ # ---- method: the part we tune -----------------------------------------------------------
50
+
51
+
52
+ class Task3Method:
53
+ """Frozen sMRI MAE, mean-pooled tokens over the t1w, ridge head."""
54
+
55
+ def __init__(self, cfg: Config):
56
+ self.cfg = cfg
57
+ self.backbone, self.transform = load_backbone(cfg.ckpt_path)
58
+ self.device = torch.device(cfg.device)
59
+ self.backbone.to(self.device).eval().requires_grad_(False)
60
+ self.cache: dict[str, np.ndarray] = {}
61
+ self.head = None
62
+
63
+ @torch.inference_mode()
64
+ def features(self, images: Images) -> np.ndarray:
65
+ """(D,) per subject. A pure function of the images, so training and inference agree."""
66
+ sample = self.transform(images["t1w"])
67
+ batch = {key: value[None].to(self.device) for key, value in sample.items()}
68
+
69
+ with torch.autocast("cuda", torch.bfloat16, enabled=self.device.type == "cuda"):
70
+ out = self.backbone(batch)
71
+
72
+ patch_embeds = out["patch_embeds"]
73
+ token_mask = out["token_mask"].bool().unsqueeze(-1)
74
+ embed = (patch_embeds * token_mask).sum(dim=1) / token_mask.sum(dim=1)
75
+ return embed[0].float().cpu().numpy()
76
+
77
+ def cached_features(self, row: dict) -> np.ndarray:
78
+ if row["subject"] not in self.cache:
79
+ self.cache[row["subject"]] = self.features(row)
80
+ return self.cache[row["subject"]]
81
+
82
+ def fit(self, rows: list[dict]) -> None:
83
+ X = np.stack([self.cached_features(row) for row in rows])
84
+ y = np.array([row["age"] for row in rows], dtype=float)
85
+
86
+ # RidgeCV picks alpha by its own efficient leave-one-out, so the fold's own split is
87
+ # never touched by model selection
88
+ self.head = make_pipeline(StandardScaler(), RidgeCV(alphas=np.logspace(-3, 6, 19)))
89
+ self.head.fit(X, y)
90
+
91
+ def predict(self, images: Images) -> float:
92
+ """Age in years."""
93
+ X = self.features(images)[None]
94
+ return float(self.head.predict(X)[0])
95
+
96
+ def save(self, model_dir: Path) -> None:
97
+ """Everything `load` needs but the backbone weights, which stay wherever `ckpt_path`
98
+ points -- a few hundred KB, so a run saves one without copying a 3.7G checkpoint."""
99
+ model_dir.mkdir(parents=True, exist_ok=True)
100
+ OmegaConf.save(self.cfg, model_dir / "config.yaml")
101
+ joblib.dump(self.head, model_dir / "head.joblib")
102
+
103
+ @classmethod
104
+ def load(cls, model_dir: Path, **overrides) -> "Task3Method":
105
+ """Rebuild a fitted method from `save`. Overrides are Config fields, for what differs
106
+ between here and the container -- the backbone path, the device."""
107
+ cfg = OmegaConf.merge(
108
+ OmegaConf.structured(Config), OmegaConf.load(model_dir / "config.yaml"), overrides
109
+ )
110
+ method = cls(cfg)
111
+ method.head = joblib.load(model_dir / "head.joblib")
112
+ return method
113
+
114
+
115
+ # ---- protocol: the part we hold fixed ---------------------------------------------------
116
+
117
+ # Every image the task ships. The method picks which of them it wants, as at inference, where the
118
+ # challenge hands over the modalities whether or not a model uses them.
119
+ IMAGE_COLS = ("t1w",)
120
+
121
+
122
+ def cross_validate(
123
+ rows: list[dict], method: Task3Method, seed: int = 0, n_folds: int = 20
124
+ ) -> tuple[np.ndarray, np.ndarray]:
125
+ """Out-of-fold age for every subject, each predicted by a head fit on the other folds."""
126
+ y = np.array([row["age"] for row in rows], dtype=float)
127
+ oof = np.zeros(len(rows), dtype=float)
128
+ folds = KFold(n_splits=n_folds, shuffle=True, random_state=seed)
129
+ start = time.perf_counter()
130
+ for fold, (train, test) in enumerate(folds.split(rows)):
131
+ method.fit([rows[i] for i in train])
132
+ for i in test:
133
+ oof[i] = method.predict({key: rows[i][key] for key in IMAGE_COLS})
134
+ logger.info(
135
+ f"fold {fold + 1}/{n_folds} n={len(test)} mae={np.abs(y[test] - oof[test]).mean():.2f} "
136
+ f"({time.perf_counter() - start:.0f}s)"
137
+ )
138
+ return y, oof
139
+
140
+
141
+ def metrics(y: np.ndarray, oof: np.ndarray) -> dict:
142
+ return {
143
+ "pearson_r": float(np.corrcoef(y, oof)[0, 1]),
144
+ "mae": float(np.abs(y - oof).mean()),
145
+ }
146
+
147
+
148
+ def score(
149
+ y: np.ndarray, oof: np.ndarray, seed: int = 0, n_boot: int = 2000, alpha: float = 0.05
150
+ ) -> dict:
151
+ """Both challenge metrics, each with a percentile CI resampling subjects with replacement."""
152
+ rng = np.random.default_rng(seed)
153
+ resamples = rng.integers(0, len(y), size=(n_boot, len(y)))
154
+
155
+ summary = {}
156
+ for name, point in metrics(y, oof).items():
157
+ samples = [metrics(y[rows], oof[rows])[name] for rows in resamples]
158
+ low, high = np.percentile(samples, [100 * alpha / 2, 100 * (1 - alpha / 2)])
159
+ summary[name] = point
160
+ summary[f"{name}_ci_low"] = float(low)
161
+ summary[f"{name}_ci_high"] = float(high)
162
+ return summary
163
+
164
+
165
+ # ---- entrypoints ------------------------------------------------------------------------
166
+
167
+
168
+ def train(args: argparse.Namespace) -> None:
169
+ # imported here, not at the top, so the container needs no dataset stack to run `predict`
170
+ from fomo_tune.datasets import load_fomo_task3
171
+
172
+ cfg = OmegaConf.merge(OmegaConf.structured(Config), OmegaConf.from_dotlist(args.overrides))
173
+ run_dir = Path(cfg.output_root) / cfg.name
174
+ run_dir.mkdir(parents=True, exist_ok=True)
175
+
176
+ setup_logging(run_dir)
177
+ set_seed(cfg.seed)
178
+ logger.info(f"run {cfg.name} (git {git_sha()})")
179
+ logger.info(f"config:\n{OmegaConf.to_yaml(cfg).rstrip()}")
180
+ OmegaConf.save(cfg, run_dir / "config.yaml")
181
+
182
+ rows = list(load_fomo_task3())
183
+ ages = np.array([row["age"] for row in rows])
184
+ logger.info(
185
+ f"dataset: {len(rows)} subjects, age {ages.min()}-{ages.max()} mean {ages.mean():.1f}"
186
+ )
187
+
188
+ method = Task3Method(cfg)
189
+ start = time.perf_counter()
190
+ y, oof = cross_validate(rows, method)
191
+ run_time = time.perf_counter() - start
192
+ summary = score(y, oof)
193
+
194
+ # the shipped head sees all n subjects, so it is not any of the models scored above
195
+ method.fit(rows)
196
+ method.save(run_dir / "model")
197
+
198
+ record = {"name": cfg.name, **summary, "run_time": round(run_time, 1)}
199
+ (run_dir / "metrics.json").write_text(json.dumps(record) + "\n")
200
+ scores = " ".join(f"{k}={v:.4f}" for k, v in summary.items())
201
+ logger.info(f"result: {scores} ({run_time:.0f}s)")
202
+
203
+
204
+ def predict(args: argparse.Namespace) -> None:
205
+ """The challenge contract: a t1 path in, one age written to `--output`.
206
+
207
+ `/app/predict.py` in the container is a shim over this, so what scores the submission is the
208
+ code cross-validation already ran, not something generated at build time.
209
+ """
210
+ overrides = {"device": args.device}
211
+ if args.ckpt_path:
212
+ overrides["ckpt_path"] = args.ckpt_path
213
+ method = Task3Method.load(args.model_dir, **overrides)
214
+
215
+ age = method.predict({"t1w": nib.load(args.t1)})
216
+
217
+ args.output.write_text(f"{age:.6f}\n")
218
+
219
+
220
+ def main() -> None:
221
+ parser = argparse.ArgumentParser()
222
+ modes = parser.add_subparsers(required=True)
223
+
224
+ train_parser = modes.add_parser("train", help="cross-validate over the task, then fit and save")
225
+ train_parser.add_argument("overrides", nargs="*", help="config overrides, e.g. device=cpu")
226
+ train_parser.set_defaults(run=train)
227
+
228
+ predict_parser = modes.add_parser("predict", help="one subject, one age in years")
229
+ predict_parser.add_argument("--t1", type=Path, required=True)
230
+ predict_parser.add_argument("--output", type=Path, required=True)
231
+ predict_parser.add_argument("--model-dir", type=Path, default=Path("/app/model"))
232
+ predict_parser.add_argument("--ckpt-path", help="overrides the trained config's backbone path")
233
+ predict_parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
234
+ predict_parser.set_defaults(run=predict)
235
+
236
+ args = parser.parse_args()
237
+ args.run(args)
238
+
239
+
240
+ if __name__ == "__main__":
241
+ main()