Add files using upload-large-folder tool
Browse files- checkpoints/pretrain_full_90_10_h100/log.json +100 -0
- checkpoints/pretrain_full_90_10_h100/log.txt +0 -0
- checkpoints/walnut-v0-1/README.md +21 -0
- checkpoints/walnut-v0-1/vitb/sub-52k/config.yaml +83 -0
- checkpoints/walnut-v0-1/vitl/sub-52k/config.yaml +83 -0
- checkpoints/walnut-v0-1/vitl/sub-52k/ddep8/config.yaml +83 -0
- finetune/fomo_tune_baseline/build.sh +31 -0
- finetune/fomo_tune_baseline/launch.sh +43 -0
- finetune/fomo_tune_baseline/output/task1/config.yaml +8 -0
- finetune/fomo_tune_baseline/output/task1/log.txt +33 -0
- finetune/fomo_tune_baseline/output/task1/metrics.json +1 -0
- finetune/fomo_tune_baseline/output/task3/build/Apptainer.def +31 -0
- finetune/fomo_tune_baseline/output/task3/build/fomo_tune/README.md +239 -0
- finetune/fomo_tune_baseline/output/task3/build/fomo_tune/backbone.py +153 -0
- finetune/fomo_tune_baseline/output/task3/build/fomo_tune/datasets.py +205 -0
- finetune/fomo_tune_baseline/output/task3/build/fomo_tune/main_task1.py +253 -0
- finetune/fomo_tune_baseline/output/task3/build/fomo_tune/main_task3.py +241 -0
- finetune/fomo_tune_baseline/output/task3/build/fomo_tune/main_task5.py +245 -0
- finetune/fomo_tune_baseline/output/task3/build/fomo_tune/utils.py +33 -0
- finetune/fomo_tune_baseline/output/task3/build/model/config.yaml +6 -0
- finetune/fomo_tune_baseline/output/task3/build/predict.py +16 -0
- finetune/fomo_tune_baseline/output/task3/build/smri_mae/main_pretrain.py +486 -0
- finetune/fomo_tune_baseline/output/task3/build/smri_mae/model_mae.py +916 -0
- finetune/fomo_tune_baseline/output/task3/config.yaml +6 -0
- finetune/fomo_tune_baseline/output/task3/log.txt +30 -0
- finetune/fomo_tune_baseline/output/task3/metrics.json +1 -0
- finetune/fomo_tune_baseline/output/task3/model/config.yaml +6 -0
- finetune/fomo_tune_baseline/output/task5/build/Apptainer.def +31 -0
- finetune/fomo_tune_baseline/output/task5/build/fomo_tune/README.md +239 -0
- finetune/fomo_tune_baseline/output/task5/build/fomo_tune/backbone.py +153 -0
- finetune/fomo_tune_baseline/output/task5/build/fomo_tune/datasets.py +205 -0
- finetune/fomo_tune_baseline/output/task5/build/fomo_tune/main_task1.py +253 -0
- finetune/fomo_tune_baseline/output/task5/build/fomo_tune/main_task3.py +241 -0
- finetune/fomo_tune_baseline/output/task5/build/fomo_tune/main_task5.py +245 -0
- finetune/fomo_tune_baseline/output/task5/build/fomo_tune/utils.py +33 -0
- finetune/fomo_tune_baseline/output/task5/build/model/config.yaml +6 -0
- finetune/fomo_tune_baseline/output/task5/build/predict.py +16 -0
- finetune/fomo_tune_baseline/output/task5/build/smri_mae/config/default_pretrain.yaml +98 -0
- finetune/fomo_tune_baseline/output/task5/build/smri_mae/main_pretrain.py +486 -0
- finetune/fomo_tune_baseline/output/task5/build/smri_mae/masking.py +80 -0
- finetune/fomo_tune_baseline/output/task5/build/smri_mae/model_mae.py +916 -0
- finetune/fomo_tune_baseline/output/task5/build/smri_mae/modules.py +453 -0
- finetune/fomo_tune_baseline/output/task5/build/smri_mae/utils.py +581 -0
- finetune/fomo_tune_baseline/output/task5/build/smri_mae/visualization.py +400 -0
- finetune/fomo_tune_baseline/output/task5/config.yaml +6 -0
- finetune/fomo_tune_baseline/output/task5/log.txt +30 -0
- finetune/fomo_tune_baseline/output/task5/metrics.json +1 -0
- finetune/fomo_tune_baseline/output/task5/model/config.yaml +6 -0
- finetune/fomo_tune_baseline/slurms/build.log +718 -0
- finetune/fomo_tune_baseline/slurms/slurm-378508.out +100 -0
checkpoints/pretrain_full_90_10_h100/log.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"epoch": 0, "train/lr": 1.25586844637851e-05, "train/grad": 2.920640599727631, "train/loss": 0.7872715425491333, "eval/fomo_val/loss": 0.5462666508161796}
|
| 2 |
+
{"epoch": 1, "train/lr": 3.755973053265827e-05, "train/grad": 1.3527106428146363, "train/loss": 0.5261555862426758, "eval/fomo_val/loss": 0.42567361694461897}
|
| 3 |
+
{"epoch": 2, "train/lr": 6.256077660153144e-05, "train/grad": 0.7325714993476867, "train/loss": 0.46626012682914736, "eval/fomo_val/loss": 0.3726151780137476}
|
| 4 |
+
{"epoch": 3, "train/lr": 8.756182267040462e-05, "train/grad": 0.46042932987213137, "train/loss": 0.4258356285095215, "eval/fomo_val/loss": 0.33146386214022366}
|
| 5 |
+
{"epoch": 4, "train/lr": 0.0001125628687392778, "train/grad": 0.35649376034736635, "train/loss": 0.37943224906921386, "eval/fomo_val/loss": 0.299087589079479}
|
| 6 |
+
{"epoch": 5, "train/lr": 0.00013756391480815097, "train/grad": 0.3172145819664001, "train/loss": 0.3558504378795624, "eval/fomo_val/loss": 0.2806159214028772}
|
| 7 |
+
{"epoch": 6, "train/lr": 0.0001625649608770241, "train/grad": 0.2512174040079117, "train/loss": 0.34055978536605835, "eval/fomo_val/loss": 0.263444991437894}
|
| 8 |
+
{"epoch": 7, "train/lr": 0.00018756600694589727, "train/grad": 0.227120401263237, "train/loss": 0.32977490186691283, "eval/fomo_val/loss": 0.2502139743206636}
|
| 9 |
+
{"epoch": 8, "train/lr": 0.0002125670530147705, "train/grad": 0.2262219125032425, "train/loss": 0.31398472309112546, "eval/fomo_val/loss": 0.24116477066615843}
|
| 10 |
+
{"epoch": 9, "train/lr": 0.00023756768065609442, "train/grad": 0.19973687052726746, "train/loss": 0.31021475315093994, "eval/fomo_val/loss": 0.23339828995038878}
|
| 11 |
+
{"epoch": 10, "train/lr": 0.00024997397135702543, "train/grad": 0.19415394365787506, "train/loss": 0.2942377561330795, "eval/fomo_val/loss": 0.22347858396341216}
|
| 12 |
+
{"epoch": 11, "train/lr": 0.00024982196612385177, "train/grad": 0.18079570591449737, "train/loss": 0.2809535217285156, "eval/fomo_val/loss": 0.21499103560762586}
|
| 13 |
+
{"epoch": 12, "train/lr": 0.000249518493725287, "train/grad": 0.17159102141857147, "train/loss": 0.2636483603715897, "eval/fomo_val/loss": 0.20624621943482813}
|
| 14 |
+
{"epoch": 13, "train/lr": 0.0002490639238957025, "train/grad": 0.16183650910854339, "train/loss": 0.26090574383735654, "eval/fomo_val/loss": 0.20108123466653643}
|
| 15 |
+
{"epoch": 14, "train/lr": 0.0002484588104584144, "train/grad": 0.16673315227031707, "train/loss": 0.2611585813760757, "eval/fomo_val/loss": 0.19515288882660417}
|
| 16 |
+
{"epoch": 15, "train/lr": 0.00024770389065093576, "train/grad": 0.1701878774166107, "train/loss": 0.24610809803009034, "eval/fomo_val/loss": 0.18857810705337885}
|
| 17 |
+
{"epoch": 16, "train/lr": 0.00024680008422676503, "train/grad": 0.15014985650777818, "train/loss": 0.2394261348247528, "eval/fomo_val/loss": 0.18335753156329102}
|
| 18 |
+
{"epoch": 17, "train/lr": 0.0002457484923348096, "train/grad": 0.14950336307287215, "train/loss": 0.2503662085533142, "eval/fomo_val/loss": 0.17991795455509763}
|
| 19 |
+
{"epoch": 18, "train/lr": 0.00024455039617780444, "train/grad": 0.1615193596482277, "train/loss": 0.23103986382484437, "eval/fomo_val/loss": 0.17753474532433275}
|
| 20 |
+
{"epoch": 19, "train/lr": 0.00024320725545136444, "train/grad": 0.14509602308273314, "train/loss": 0.2324778312444687, "eval/fomo_val/loss": 0.17450881507599128}
|
| 21 |
+
{"epoch": 20, "train/lr": 0.00024172070656556968, "train/grad": 0.16689604490995408, "train/loss": 0.22850071251392365, "eval/fomo_val/loss": 0.17120197977659837}
|
| 22 |
+
{"epoch": 21, "train/lr": 0.00024009256065125222, "train/grad": 0.15709128856658935, "train/loss": 0.22197725176811217, "eval/fomo_val/loss": 0.16947166334907965}
|
| 23 |
+
{"epoch": 22, "train/lr": 0.00023832480135341203, "train/grad": 0.1612587684392929, "train/loss": 0.22163174211978912, "eval/fomo_val/loss": 0.16690312148827427}
|
| 24 |
+
{"epoch": 23, "train/lr": 0.00023641958241445106, "train/grad": 0.16475949883461, "train/loss": 0.22064274191856384, "eval/fomo_val/loss": 0.1649576800892938}
|
| 25 |
+
{"epoch": 24, "train/lr": 0.00023437922505016993, "train/grad": 0.1586805135011673, "train/loss": 0.2258121520280838, "eval/fomo_val/loss": 0.16478188597368745}
|
| 26 |
+
{"epoch": 25, "train/lr": 0.00023220621512172403, "train/grad": 0.1567085862159729, "train/loss": 0.21691338539123536, "eval/fomo_val/loss": 0.16126508369760692}
|
| 27 |
+
{"epoch": 26, "train/lr": 0.00022990320010698485, "train/grad": 0.1585501727461815, "train/loss": 0.21671274185180664, "eval/fomo_val/loss": 0.16039053063909964}
|
| 28 |
+
{"epoch": 27, "train/lr": 0.00022747298587499527, "train/grad": 0.15190839976072312, "train/loss": 0.21496180653572083, "eval/fomo_val/loss": 0.15931141266845308}
|
| 29 |
+
{"epoch": 28, "train/lr": 0.00022491853326745144, "train/grad": 0.15507292598485947, "train/loss": 0.21460922420024872, "eval/fomo_val/loss": 0.157214418756512}
|
| 30 |
+
{"epoch": 29, "train/lr": 0.00022224295449137222, "train/grad": 0.16469780147075652, "train/loss": 0.20705958902835847, "eval/fomo_val/loss": 0.1577240099603275}
|
| 31 |
+
{"epoch": 30, "train/lr": 0.00021944950932735515, "train/grad": 0.14905590534210206, "train/loss": 0.20592523217201233, "eval/fomo_val/loss": 0.15570834981945325}
|
| 32 |
+
{"epoch": 31, "train/lr": 0.00021654160115803497, "train/grad": 0.16514489948749542, "train/loss": 0.2095154505968094, "eval/fomo_val/loss": 0.15465696760506}
|
| 33 |
+
{"epoch": 32, "train/lr": 0.0002135227728215875, "train/grad": 0.1505313554406166, "train/loss": 0.21015099346637725, "eval/fomo_val/loss": 0.15362778120445755}
|
| 34 |
+
{"epoch": 33, "train/lr": 0.00021039670229532564, "train/grad": 0.15690734952688218, "train/loss": 0.21043111860752106, "eval/fomo_val/loss": 0.15289746829361286}
|
| 35 |
+
{"epoch": 34, "train/lr": 0.00020716719821465125, "train/grad": 0.14784382790327072, "train/loss": 0.20424923300743103, "eval/fomo_val/loss": 0.1520626851410236}
|
| 36 |
+
{"epoch": 35, "train/lr": 0.00020383819523282006, "train/grad": 0.1598421823978424, "train/loss": 0.2076873528957367, "eval/fomo_val/loss": 0.1513240284796031}
|
| 37 |
+
{"epoch": 36, "train/lr": 0.00020041374922717139, "train/grad": 0.15387804478406905, "train/loss": 0.20155590653419495, "eval/fomo_val/loss": 0.15059169218787608}
|
| 38 |
+
{"epoch": 37, "train/lr": 0.00019689803235766793, "train/grad": 0.1508965849876404, "train/loss": 0.19935197472572327, "eval/fomo_val/loss": 0.14958625525798439}
|
| 39 |
+
{"epoch": 38, "train/lr": 0.0001932953279837605, "train/grad": 0.14703434854745864, "train/loss": 0.20475524961948394, "eval/fomo_val/loss": 0.14901817755879096}
|
| 40 |
+
{"epoch": 39, "train/lr": 0.0001896100254457745, "train/grad": 0.1346307560801506, "train/loss": 0.19926900923252105, "eval/fomo_val/loss": 0.14799074038582027}
|
| 41 |
+
{"epoch": 40, "train/lr": 0.00018584661471717515, "train/grad": 0.14164321571588517, "train/loss": 0.20554547607898713, "eval/fomo_val/loss": 0.14782850649559273}
|
| 42 |
+
{"epoch": 41, "train/lr": 0.00018200968093422642, "train/grad": 0.14752665281295776, "train/loss": 0.19935064673423766, "eval/fomo_val/loss": 0.14740161482455597}
|
| 43 |
+
{"epoch": 42, "train/lr": 0.00017810389880970831, "train/grad": 0.15093021750450134, "train/loss": 0.20380199790000916, "eval/fomo_val/loss": 0.14630420196731136}
|
| 44 |
+
{"epoch": 43, "train/lr": 0.00017413402693750008, "train/grad": 0.15112090498209, "train/loss": 0.19148373246192932, "eval/fomo_val/loss": 0.14608781576718924}
|
| 45 |
+
{"epoch": 44, "train/lr": 0.00017010490199496597, "train/grad": 0.13140210419893264, "train/loss": 0.1884577304124832, "eval/fomo_val/loss": 0.1453945238354071}
|
| 46 |
+
{"epoch": 45, "train/lr": 0.00016602143285020948, "train/grad": 0.13969567716121672, "train/loss": 0.198207226395607, "eval/fomo_val/loss": 0.14440294616627244}
|
| 47 |
+
{"epoch": 46, "train/lr": 0.00016188859458137324, "train/grad": 0.1501631945371628, "train/loss": 0.19735783517360686, "eval/fomo_val/loss": 0.1443709095131676}
|
| 48 |
+
{"epoch": 47, "train/lr": 0.0001577114224152729, "train/grad": 0.1411166822910309, "train/loss": 0.1969533234834671, "eval/fomo_val/loss": 0.14403169548736428}
|
| 49 |
+
{"epoch": 48, "train/lr": 0.00015340826572017794, "train/grad": 0.13484682080646357, "train/loss": 0.19229870662093163, "eval/fomo_val/loss": 0.14312048478508896}
|
| 50 |
+
{"epoch": 49, "train/lr": 0.00014915707728428194, "train/grad": 0.14023476342360178, "train/loss": 0.19414550065994263, "eval/fomo_val/loss": 0.14291474344033114}
|
| 51 |
+
{"epoch": 50, "train/lr": 0.0001448770663438084, "train/grad": 0.14675075436631838, "train/loss": 0.19627596127490202, "eval/fomo_val/loss": 0.14252478741812255}
|
| 52 |
+
{"epoch": 51, "train/lr": 0.00014057344743280325, "train/grad": 0.14737429935485125, "train/loss": 0.19208722313245138, "eval/fomo_val/loss": 0.1418645646212236}
|
| 53 |
+
{"epoch": 52, "train/lr": 0.00013625146384798776, "train/grad": 0.1358876507729292, "train/loss": 0.1927537719408671, "eval/fomo_val/loss": 0.14128415702648883}
|
| 54 |
+
{"epoch": 53, "train/lr": 0.00013191638126060958, "train/grad": 0.12969130308677754, "train/loss": 0.19298341187338033, "eval/fomo_val/loss": 0.14083823333933668}
|
| 55 |
+
{"epoch": 54, "train/lr": 0.00012757348130103307, "train/grad": 0.12538257210204998, "train/loss": 0.1926837439338366, "eval/fomo_val/loss": 0.14034455472005988}
|
| 56 |
+
{"epoch": 55, "train/lr": 0.00012322805512388657, "train/grad": 0.13841872910658518, "train/loss": 0.18828583943347135, "eval/fomo_val/loss": 0.14036549324134612}
|
| 57 |
+
{"epoch": 56, "train/lr": 0.00011888539696160531, "train/grad": 0.12802624329924583, "train/loss": 0.18928068690001965, "eval/fomo_val/loss": 0.1397463431616999}
|
| 58 |
+
{"epoch": 57, "train/lr": 0.00011455079767422488, "train/grad": 0.13150859965632358, "train/loss": 0.1854710274686416, "eval/fomo_val/loss": 0.13939857676906406}
|
| 59 |
+
{"epoch": 58, "train/lr": 0.00011022953830328308, "train/grad": 0.13126523792743683, "train/loss": 0.19039718496302763, "eval/fomo_val/loss": 0.13924091649505327}
|
| 60 |
+
{"epoch": 59, "train/lr": 0.00010592688363768446, "train/grad": 0.14349377558877072, "train/loss": 0.18032679396371046, "eval/fomo_val/loss": 0.1388494759516896}
|
| 61 |
+
{"epoch": 60, "train/lr": 0.00010164807579936582, "train/grad": 0.12802154074112573, "train/loss": 0.1817970940222343, "eval/fomo_val/loss": 0.13819859115020283}
|
| 62 |
+
{"epoch": 61, "train/lr": 9.739832785657841e-05, "train/grad": 0.1388087049126625, "train/loss": 0.18076220775643984, "eval/fomo_val/loss": 0.13775559689076441}
|
| 63 |
+
{"epoch": 62, "train/lr": 9.3182817472567e-05, "train/grad": 0.12375997193157673, "train/loss": 0.1885116429378589, "eval/fomo_val/loss": 0.1378685231197555}
|
| 64 |
+
{"epoch": 63, "train/lr": 8.900668059738495e-05, "train/grad": 0.12581131172676882, "train/loss": 0.18472245956460634, "eval/fomo_val/loss": 0.13720959821399653}
|
| 65 |
+
{"epoch": 64, "train/lr": 8.487500521053002e-05, "train/grad": 0.11724819460262854, "train/loss": 0.18963311364253363, "eval/fomo_val/loss": 0.13685027062892913}
|
| 66 |
+
{"epoch": 65, "train/lr": 8.079282512202535e-05, "train/grad": 0.1248181788250804, "train/loss": 0.1769704738010963, "eval/fomo_val/loss": 0.13661445061553198}
|
| 67 |
+
{"epoch": 66, "train/lr": 7.676511383949712e-05, "train/grad": 0.11684403289109468, "train/loss": 0.1830657379080852, "eval/fomo_val/loss": 0.13631079151945294}
|
| 68 |
+
{"epoch": 67, "train/lr": 7.279677850872176e-05, "train/grad": 0.12013472771892945, "train/loss": 0.18565905342499414, "eval/fomo_val/loss": 0.13585144320748888}
|
| 69 |
+
{"epoch": 68, "train/lr": 6.889265393502457e-05, "train/grad": 0.13088354685654244, "train/loss": 0.18833177971343198, "eval/fomo_val/loss": 0.13563257132498724}
|
| 70 |
+
{"epoch": 69, "train/lr": 6.505749669281434e-05, "train/grad": 0.1235970901325345, "train/loss": 0.18656270764768124, "eval/fomo_val/loss": 0.13527863334934667}
|
| 71 |
+
{"epoch": 70, "train/lr": 6.129597933043005e-05, "train/grad": 0.12204912304878235, "train/loss": 0.18639984975258508, "eval/fomo_val/loss": 0.1351662516875087}
|
| 72 |
+
{"epoch": 71, "train/lr": 5.7612684677360404e-05, "train/grad": 0.12089106285323699, "train/loss": 0.1839581529299418, "eval/fomo_val/loss": 0.13469430298175453}
|
| 73 |
+
{"epoch": 72, "train/lr": 5.401210026077211e-05, "train/grad": 0.12930735231687626, "train/loss": 0.181477606917421, "eval/fomo_val/loss": 0.13472259134616493}
|
| 74 |
+
{"epoch": 73, "train/lr": 5.049861283814926e-05, "train/grad": 0.11069933945933978, "train/loss": 0.1823968899746736, "eval/fomo_val/loss": 0.13440711593852853}
|
| 75 |
+
{"epoch": 74, "train/lr": 4.70765030527051e-05, "train/grad": 0.10915143756816785, "train/loss": 0.17597144097089767, "eval/fomo_val/loss": 0.13389452583384964}
|
| 76 |
+
{"epoch": 75, "train/lr": 4.374994021807759e-05, "train/grad": 0.11530119584252436, "train/loss": 0.1775289699435234, "eval/fomo_val/loss": 0.13390670171323812}
|
| 77 |
+
{"epoch": 76, "train/lr": 4.052297723866298e-05, "train/grad": 0.1125206748644511, "train/loss": 0.18334729907413325, "eval/fomo_val/loss": 0.13359323290159117}
|
| 78 |
+
{"epoch": 77, "train/lr": 3.7399545671776155e-05, "train/grad": 0.11088902006546657, "train/loss": 0.179920989399155, "eval/fomo_val/loss": 0.1332294022418418}
|
| 79 |
+
{"epoch": 78, "train/lr": 3.438345093765363e-05, "train/grad": 0.11161495838314295, "train/loss": 0.17800653353333473, "eval/fomo_val/loss": 0.1330351065633432}
|
| 80 |
+
{"epoch": 79, "train/lr": 3.147836768313514e-05, "train/grad": 0.10522990642736356, "train/loss": 0.17757239130636057, "eval/fomo_val/loss": 0.1331123783340994}
|
| 81 |
+
{"epoch": 80, "train/lr": 2.868783530467256e-05, "train/grad": 0.09980769486476977, "train/loss": 0.17774887258807817, "eval/fomo_val/loss": 0.13267944176804344}
|
| 82 |
+
{"epoch": 81, "train/lr": 2.6015253636120466e-05, "train/grad": 0.10013275531431039, "train/loss": 0.17619845954080424, "eval/fomo_val/loss": 0.13248468544685615}
|
| 83 |
+
{"epoch": 82, "train/lr": 2.3463878806562344e-05, "train/grad": 0.10081137996166945, "train/loss": 0.1800100685407718, "eval/fomo_val/loss": 0.13219575589557864}
|
| 84 |
+
{"epoch": 83, "train/lr": 2.1036819273218863e-05, "train/grad": 0.10347515996545553, "train/loss": 0.17568077395359674, "eval/fomo_val/loss": 0.13195786242777446}
|
| 85 |
+
{"epoch": 84, "train/lr": 1.8737032034271527e-05, "train/grad": 0.10440930351614952, "train/loss": 0.17420727759599686, "eval/fomo_val/loss": 0.13185373426608318}
|
| 86 |
+
{"epoch": 85, "train/lr": 1.656731902621594e-05, "train/grad": 0.10837028237680595, "train/loss": 0.18037758581340313, "eval/fomo_val/loss": 0.13179254700552742}
|
| 87 |
+
{"epoch": 86, "train/lr": 1.453032371013373e-05, "train/grad": 0.09870245152463515, "train/loss": 0.1831672458599011, "eval/fomo_val/loss": 0.131568816950861}
|
| 88 |
+
{"epoch": 87, "train/lr": 1.2628527851042481e-05, "train/grad": 0.08623698353767395, "train/loss": 0.171711598833402, "eval/fomo_val/loss": 0.1315375879688083}
|
| 89 |
+
{"epoch": 88, "train/lr": 1.0864248494247304e-05, "train/grad": 0.09079501374314229, "train/loss": 0.17652994766831398, "eval/fomo_val/loss": 0.13138468673206724}
|
| 90 |
+
{"epoch": 89, "train/lr": 9.239635142378045e-06, "train/grad": 0.08837371009091537, "train/loss": 0.1798645822952191, "eval/fomo_val/loss": 0.13128385861527245}
|
| 91 |
+
{"epoch": 90, "train/lr": 7.756667136551455e-06, "train/grad": 0.096140431240201, "train/loss": 0.1783239816625913, "eval/fomo_val/loss": 0.13115769554421586}
|
| 92 |
+
{"epoch": 91, "train/lr": 6.41715124484893e-06, "train/grad": 0.08655263110995293, "train/loss": 0.17633002437651157, "eval/fomo_val/loss": 0.1311222384560783}
|
| 93 |
+
{"epoch": 92, "train/lr": 5.222719461047898e-06, "train/grad": 0.08044332855691512, "train/loss": 0.1724255457520485, "eval/fomo_val/loss": 0.13100630379510375}
|
| 94 |
+
{"epoch": 93, "train/lr": 4.174827016288814e-06, "train/grad": 0.0795227784352998, "train/loss": 0.17683392018079758, "eval/fomo_val/loss": 0.13084294014381911}
|
| 95 |
+
{"epoch": 94, "train/lr": 3.2747506061001546e-06, "train/grad": 0.0814427686855197, "train/loss": 0.18071980526049933, "eval/fomo_val/loss": 0.13090927108841122}
|
| 96 |
+
{"epoch": 95, "train/lr": 2.5235868349415963e-06, "train/grad": 0.08623279072344303, "train/loss": 0.1820303381731113, "eval/fomo_val/loss": 0.1308269995563435}
|
| 97 |
+
{"epoch": 96, "train/lr": 1.922250880160383e-06, "train/grad": 0.07390511563668649, "train/loss": 0.16746863164007664, "eval/fomo_val/loss": 0.13083390045278478}
|
| 98 |
+
{"epoch": 97, "train/lr": 1.4714753769887204e-06, "train/grad": 0.07653172205512722, "train/loss": 0.17233146354556084, "eval/fomo_val/loss": 0.1307800050895169}
|
| 99 |
+
{"epoch": 98, "train/lr": 1.1718095259405694e-06, "train/grad": 0.07844715456788738, "train/loss": 0.17596244563659033, "eval/fomo_val/loss": 0.13069988840593483}
|
| 100 |
+
{"epoch": 99, "train/lr": 1.0236184236954529e-06, "train/grad": 0.07888089073821902, "train/loss": 0.18072639467815557, "eval/fomo_val/loss": 0.13072822161440578}
|
checkpoints/pretrain_full_90_10_h100/log.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
checkpoints/walnut-v0-1/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: pytorch
|
| 3 |
+
tags:
|
| 4 |
+
- medical-imaging
|
| 5 |
+
- vision-transformer
|
| 6 |
+
- masked-autoencoder
|
| 7 |
+
- mri
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Walnut v0.1
|
| 11 |
+
|
| 12 |
+
Pretrained 3D ViT-MAE checkpoints for structural MRI.
|
| 13 |
+
|
| 14 |
+
The `vitb` and `vitl` directories contain models trained with subject-specific
|
| 15 |
+
sampling at four dataset scales. These runs used a fixed compute budget of
|
| 16 |
+
99,000 optimizer steps with global batch size 256. Each directory contains the
|
| 17 |
+
final checkpoint and its training configuration.
|
| 18 |
+
|
| 19 |
+
`vitl/sub-52k/ddep8` is the corresponding full-subject ViT-L experiment with an
|
| 20 |
+
eight-layer decoder.
|
| 21 |
+
|
checkpoints/walnut-v0-1/vitb/sub-52k/config.yaml
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: scale_subjects_vitb_n52643_resume_v1_mb64_gbs256_s7338
|
| 2 |
+
notes: unique_subject_scaling_resume_v1_microbatch64
|
| 3 |
+
output_dir: /admin/home/mihir.neal/smri-fm/checkpoints/pretrain_subject_scaling_fixed_compute_v5_resumed/scale_subjects_vitb_n52643_resume_v1_mb64_gbs256_s7338
|
| 4 |
+
img_size:
|
| 5 |
+
- 208
|
| 6 |
+
- 240
|
| 7 |
+
- 208
|
| 8 |
+
patch_size: 8
|
| 9 |
+
mask_ratio: 0.8
|
| 10 |
+
pred_mask_ratio: null
|
| 11 |
+
pad_to_multiple: 32
|
| 12 |
+
masking_strategy: random
|
| 13 |
+
block_mask_fraction: 0.7
|
| 14 |
+
block_mask_min_size: 2
|
| 15 |
+
block_mask_max_size: 6
|
| 16 |
+
model: mae_vit_base
|
| 17 |
+
model_kwargs:
|
| 18 |
+
target_norm: none
|
| 19 |
+
no_decode_pos: false
|
| 20 |
+
mask_drop_scale: false
|
| 21 |
+
class_token: true
|
| 22 |
+
reg_tokens: 0
|
| 23 |
+
no_embed_class: false
|
| 24 |
+
decoder_depth: 4
|
| 25 |
+
drop_path_rate: 0.0
|
| 26 |
+
datasets:
|
| 27 |
+
fomo_train:
|
| 28 |
+
url: /data/smri-datasets/FOMO300/train/shard.*.tar
|
| 29 |
+
subject_index: /admin/home/mihir.neal/smri-fm-pretrain-scaling/artifacts/pretrain_scaling_v1/fomo300_train.subject-index.json
|
| 30 |
+
subject_manifest: null
|
| 31 |
+
subject_count: 52643
|
| 32 |
+
subset_seed: 7338
|
| 33 |
+
include_modalities: null
|
| 34 |
+
samples_per_epoch: 253440
|
| 35 |
+
read_ahead: 2048
|
| 36 |
+
drop_last: true
|
| 37 |
+
fomo_val:
|
| 38 |
+
url: /data/smri-datasets/FOMO300/val/shard.*.tar
|
| 39 |
+
samples_per_epoch: 29278
|
| 40 |
+
shuffle: false
|
| 41 |
+
buffer_size: 0
|
| 42 |
+
drop_last: false
|
| 43 |
+
train_dataset: fomo_train
|
| 44 |
+
eval_datasets:
|
| 45 |
+
- fomo_val
|
| 46 |
+
num_workers: 4
|
| 47 |
+
prefetch_factor: 2
|
| 48 |
+
presend_cuda: true
|
| 49 |
+
epochs: 100
|
| 50 |
+
batch_size: 64
|
| 51 |
+
accum_iter: 1
|
| 52 |
+
base_lr: 0.001
|
| 53 |
+
min_lr: 1.0e-06
|
| 54 |
+
warmup_epochs: 10
|
| 55 |
+
weight_decay: 0.05
|
| 56 |
+
betas:
|
| 57 |
+
- 0.9
|
| 58 |
+
- 0.95
|
| 59 |
+
clip_grad: 1.0
|
| 60 |
+
amp: true
|
| 61 |
+
amp_dtype: bfloat16
|
| 62 |
+
ckpt: /admin/home/mihir.neal/smri-fm/checkpoints/pretrain_subject_scaling_fixed_compute_v1/scale_subjects_vitb_n52643_steps99000_s7338/checkpoint-last.pth
|
| 63 |
+
resume: true
|
| 64 |
+
auto_resume: false
|
| 65 |
+
start_epoch: 0
|
| 66 |
+
checkpoint_period: 10
|
| 67 |
+
max_checkpoints: 0
|
| 68 |
+
eval_period: 10
|
| 69 |
+
r2_sync: null
|
| 70 |
+
device: cuda
|
| 71 |
+
distributed: true
|
| 72 |
+
seed: 7338
|
| 73 |
+
data_seed: 7338
|
| 74 |
+
eval_seed: 7338
|
| 75 |
+
debug: false
|
| 76 |
+
wandb: true
|
| 77 |
+
wandb_entity: null
|
| 78 |
+
wandb_project: smri-fm
|
| 79 |
+
lr: null
|
| 80 |
+
rank: 0
|
| 81 |
+
world_size: 4
|
| 82 |
+
gpu: 0
|
| 83 |
+
dist_backend: nccl
|
checkpoints/walnut-v0-1/vitl/sub-52k/config.yaml
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: scale_subjects_vitl_n52643_resume_v1_mb64_gbs256_s7338
|
| 2 |
+
notes: unique_subject_scaling_resume_v1_microbatch64
|
| 3 |
+
output_dir: /admin/home/mihir.neal/smri-fm/checkpoints/pretrain_subject_scaling_fixed_compute_v5_resumed/scale_subjects_vitl_n52643_resume_v1_mb64_gbs256_s7338
|
| 4 |
+
img_size:
|
| 5 |
+
- 208
|
| 6 |
+
- 240
|
| 7 |
+
- 208
|
| 8 |
+
patch_size: 8
|
| 9 |
+
mask_ratio: 0.8
|
| 10 |
+
pred_mask_ratio: null
|
| 11 |
+
pad_to_multiple: 32
|
| 12 |
+
masking_strategy: random
|
| 13 |
+
block_mask_fraction: 0.7
|
| 14 |
+
block_mask_min_size: 2
|
| 15 |
+
block_mask_max_size: 6
|
| 16 |
+
model: mae_vit_large
|
| 17 |
+
model_kwargs:
|
| 18 |
+
target_norm: none
|
| 19 |
+
no_decode_pos: false
|
| 20 |
+
mask_drop_scale: false
|
| 21 |
+
class_token: true
|
| 22 |
+
reg_tokens: 0
|
| 23 |
+
no_embed_class: false
|
| 24 |
+
decoder_depth: 4
|
| 25 |
+
drop_path_rate: 0.0
|
| 26 |
+
datasets:
|
| 27 |
+
fomo_train:
|
| 28 |
+
url: /data/smri-datasets/FOMO300/train/shard.*.tar
|
| 29 |
+
subject_index: /admin/home/mihir.neal/smri-fm-pretrain-scaling/artifacts/pretrain_scaling_v1/fomo300_train.subject-index.json
|
| 30 |
+
subject_manifest: null
|
| 31 |
+
subject_count: 52643
|
| 32 |
+
subset_seed: 7338
|
| 33 |
+
include_modalities: null
|
| 34 |
+
samples_per_epoch: 253440
|
| 35 |
+
read_ahead: 2048
|
| 36 |
+
drop_last: true
|
| 37 |
+
fomo_val:
|
| 38 |
+
url: /data/smri-datasets/FOMO300/val/shard.*.tar
|
| 39 |
+
samples_per_epoch: 29278
|
| 40 |
+
shuffle: false
|
| 41 |
+
buffer_size: 0
|
| 42 |
+
drop_last: false
|
| 43 |
+
train_dataset: fomo_train
|
| 44 |
+
eval_datasets:
|
| 45 |
+
- fomo_val
|
| 46 |
+
num_workers: 4
|
| 47 |
+
prefetch_factor: 2
|
| 48 |
+
presend_cuda: true
|
| 49 |
+
epochs: 100
|
| 50 |
+
batch_size: 64
|
| 51 |
+
accum_iter: 1
|
| 52 |
+
base_lr: 0.001
|
| 53 |
+
min_lr: 1.0e-06
|
| 54 |
+
warmup_epochs: 10
|
| 55 |
+
weight_decay: 0.05
|
| 56 |
+
betas:
|
| 57 |
+
- 0.9
|
| 58 |
+
- 0.95
|
| 59 |
+
clip_grad: 1.0
|
| 60 |
+
amp: true
|
| 61 |
+
amp_dtype: bfloat16
|
| 62 |
+
ckpt: /admin/home/mihir.neal/smri-fm/checkpoints/pretrain_subject_scaling_fixed_compute_v1/scale_subjects_vitl_n52643_steps99000_s7338/checkpoint-last.pth
|
| 63 |
+
resume: true
|
| 64 |
+
auto_resume: false
|
| 65 |
+
start_epoch: 0
|
| 66 |
+
checkpoint_period: 10
|
| 67 |
+
max_checkpoints: 0
|
| 68 |
+
eval_period: 10
|
| 69 |
+
r2_sync: null
|
| 70 |
+
device: cuda
|
| 71 |
+
distributed: true
|
| 72 |
+
seed: 7338
|
| 73 |
+
data_seed: 7338
|
| 74 |
+
eval_seed: 7338
|
| 75 |
+
debug: false
|
| 76 |
+
wandb: true
|
| 77 |
+
wandb_entity: null
|
| 78 |
+
wandb_project: smri-fm
|
| 79 |
+
lr: null
|
| 80 |
+
rank: 0
|
| 81 |
+
world_size: 4
|
| 82 |
+
gpu: 0
|
| 83 |
+
dist_backend: nccl
|
checkpoints/walnut-v0-1/vitl/sub-52k/ddep8/config.yaml
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: vitl_n52k_ddep8
|
| 2 |
+
notes: vitl_full_subjects_mae_decoder_depth8_fixed_compute
|
| 3 |
+
output_dir: /admin/home/mihir.neal/smri-fm/checkpoints/vitl_n52k_ddep8
|
| 4 |
+
img_size:
|
| 5 |
+
- 208
|
| 6 |
+
- 240
|
| 7 |
+
- 208
|
| 8 |
+
patch_size: 8
|
| 9 |
+
mask_ratio: 0.8
|
| 10 |
+
pred_mask_ratio: null
|
| 11 |
+
pad_to_multiple: 32
|
| 12 |
+
masking_strategy: random
|
| 13 |
+
block_mask_fraction: 0.7
|
| 14 |
+
block_mask_min_size: 2
|
| 15 |
+
block_mask_max_size: 6
|
| 16 |
+
model: mae_vit_large
|
| 17 |
+
model_kwargs:
|
| 18 |
+
target_norm: none
|
| 19 |
+
no_decode_pos: false
|
| 20 |
+
mask_drop_scale: false
|
| 21 |
+
class_token: true
|
| 22 |
+
reg_tokens: 0
|
| 23 |
+
no_embed_class: false
|
| 24 |
+
decoder_depth: 8
|
| 25 |
+
drop_path_rate: 0.0
|
| 26 |
+
datasets:
|
| 27 |
+
fomo_train:
|
| 28 |
+
url: /data/smri-datasets/FOMO300/train/shard.*.tar
|
| 29 |
+
subject_index: /admin/home/mihir.neal/smri-fm-pretrain-scaling/artifacts/pretrain_scaling_v1/fomo300_train.subject-index.json
|
| 30 |
+
subject_manifest: null
|
| 31 |
+
subject_count: 52643
|
| 32 |
+
subset_seed: 7338
|
| 33 |
+
include_modalities: null
|
| 34 |
+
samples_per_epoch: 253440
|
| 35 |
+
read_ahead: 2048
|
| 36 |
+
drop_last: true
|
| 37 |
+
fomo_val:
|
| 38 |
+
url: /data/smri-datasets/FOMO300/val/shard.*.tar
|
| 39 |
+
samples_per_epoch: 29278
|
| 40 |
+
shuffle: false
|
| 41 |
+
buffer_size: 0
|
| 42 |
+
drop_last: false
|
| 43 |
+
train_dataset: fomo_train
|
| 44 |
+
eval_datasets:
|
| 45 |
+
- fomo_val
|
| 46 |
+
num_workers: 4
|
| 47 |
+
prefetch_factor: 2
|
| 48 |
+
presend_cuda: true
|
| 49 |
+
epochs: 100
|
| 50 |
+
batch_size: 32
|
| 51 |
+
accum_iter: 2
|
| 52 |
+
base_lr: 0.001
|
| 53 |
+
min_lr: 1.0e-06
|
| 54 |
+
warmup_epochs: 10
|
| 55 |
+
weight_decay: 0.05
|
| 56 |
+
betas:
|
| 57 |
+
- 0.9
|
| 58 |
+
- 0.95
|
| 59 |
+
clip_grad: 1.0
|
| 60 |
+
amp: true
|
| 61 |
+
amp_dtype: bfloat16
|
| 62 |
+
ckpt: null
|
| 63 |
+
resume: false
|
| 64 |
+
auto_resume: true
|
| 65 |
+
start_epoch: 0
|
| 66 |
+
checkpoint_period: 10
|
| 67 |
+
max_checkpoints: 0
|
| 68 |
+
eval_period: 10
|
| 69 |
+
r2_sync: null
|
| 70 |
+
device: cuda
|
| 71 |
+
distributed: true
|
| 72 |
+
seed: 7338
|
| 73 |
+
data_seed: 7338
|
| 74 |
+
eval_seed: 7338
|
| 75 |
+
debug: false
|
| 76 |
+
wandb: true
|
| 77 |
+
wandb_entity: null
|
| 78 |
+
wandb_project: smri-fm
|
| 79 |
+
lr: null
|
| 80 |
+
rank: 0
|
| 81 |
+
world_size: 4
|
| 82 |
+
gpu: 0
|
| 83 |
+
dist_backend: nccl
|
finetune/fomo_tune_baseline/build.sh
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Package each trained run into its challenge .sif. Run on the login node, after launch.sh:
|
| 3 |
+
# apptainer lives there and on n-6 only, and a build needs no GPU driver.
|
| 4 |
+
#
|
| 5 |
+
# Slow. Apptainer always re-runs %post, so each of the three re-downloads ~3G of wheels.
|
| 6 |
+
|
| 7 |
+
set -euo pipefail
|
| 8 |
+
|
| 9 |
+
ROOT="/data/connor/nanobrain.1"
|
| 10 |
+
cd $ROOT
|
| 11 |
+
|
| 12 |
+
EXP_DIR="experiments/fomo_tune_baseline"
|
| 13 |
+
OUT_DIR="${EXP_DIR}/output"
|
| 14 |
+
|
| 15 |
+
runs=(task1 task5 task3)
|
| 16 |
+
|
| 17 |
+
for name in "${runs[@]}"; do
|
| 18 |
+
# build.py names the sif after `task` in the run's saved config, which is the run name here
|
| 19 |
+
sif="${OUT_DIR}/${name}/${name}.sif"
|
| 20 |
+
|
| 21 |
+
if [[ -f "${sif}" ]]; then
|
| 22 |
+
echo "sif ${sif} exists; skipping"
|
| 23 |
+
continue
|
| 24 |
+
fi
|
| 25 |
+
|
| 26 |
+
echo "=== ${name} ==="
|
| 27 |
+
uv run --no-sync python -m fomo_tune.build "${OUT_DIR}/${name}"
|
| 28 |
+
done
|
| 29 |
+
|
| 30 |
+
echo "=== sifs ==="
|
| 31 |
+
ls -lh "${OUT_DIR}"/*/*.sif
|
finetune/fomo_tune_baseline/launch.sh
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
#SBATCH --job-name=fomo_tune
|
| 3 |
+
#SBATCH --nodes=1
|
| 4 |
+
#SBATCH --ntasks-per-node=1
|
| 5 |
+
#SBATCH --gpus-per-task=1
|
| 6 |
+
#SBATCH --time=2:00:00
|
| 7 |
+
#SBATCH --partition=main
|
| 8 |
+
#SBATCH --output=slurms/slurm-%j.out
|
| 9 |
+
#SBATCH --account=sophont
|
| 10 |
+
|
| 11 |
+
set -euo pipefail
|
| 12 |
+
|
| 13 |
+
ROOT="/data/connor/nanobrain.1"
|
| 14 |
+
cd $ROOT
|
| 15 |
+
|
| 16 |
+
EXP_DIR="experiments/fomo_tune_baseline"
|
| 17 |
+
OUT_DIR="${EXP_DIR}/output"
|
| 18 |
+
|
| 19 |
+
# name, module. Cheapest first, so a broken environment fails in 90s.
|
| 20 |
+
# One job, run sequentially: the whole grid is ~12 minutes and each task loads the same
|
| 21 |
+
# 3.9G checkpoint, so an array would only add queue time and cache contention.
|
| 22 |
+
runs=(
|
| 23 |
+
"task1 main_task1"
|
| 24 |
+
"task5 main_task5"
|
| 25 |
+
"task3 main_task3"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
for run in "${runs[@]}"; do
|
| 29 |
+
read -r name module <<<"${run}"
|
| 30 |
+
|
| 31 |
+
if [[ -f "${OUT_DIR}/${name}/metrics.json" ]]; then
|
| 32 |
+
echo "result ${name} exists; skipping"
|
| 33 |
+
continue
|
| 34 |
+
fi
|
| 35 |
+
|
| 36 |
+
echo "=== ${name} ==="
|
| 37 |
+
uv run --no-sync python -m "fomo_tune.${module}" train \
|
| 38 |
+
output_root="${OUT_DIR}" \
|
| 39 |
+
name="${name}"
|
| 40 |
+
done
|
| 41 |
+
|
| 42 |
+
echo "=== results ==="
|
| 43 |
+
cat "${OUT_DIR}"/*/metrics.json
|
finetune/fomo_tune_baseline/output/task1/config.yaml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task: task1
|
| 2 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 3 |
+
modalities:
|
| 4 |
+
- dwi_b1000
|
| 5 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 6 |
+
name: task1
|
| 7 |
+
device: cuda
|
| 8 |
+
seed: 4466
|
finetune/fomo_tune_baseline/output/task1/log.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
16:31:38 run task1 (git 1df2e5d)
|
| 2 |
+
16:31:38 config:
|
| 3 |
+
task: task1
|
| 4 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 5 |
+
modalities:
|
| 6 |
+
- dwi_b1000
|
| 7 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 8 |
+
name: task1
|
| 9 |
+
device: cuda
|
| 10 |
+
seed: 4466
|
| 11 |
+
16:31:40 dataset: 21 subjects, 13 positive
|
| 12 |
+
16:31:48 fold 1/21 sub-01 y=1 p=0.512 (4s)
|
| 13 |
+
16:31:49 fold 2/21 sub-02 y=1 p=0.514 (4s)
|
| 14 |
+
16:31:49 fold 3/21 sub-03 y=0 p=0.486 (5s)
|
| 15 |
+
16:31:49 fold 4/21 sub-04 y=1 p=0.499 (5s)
|
| 16 |
+
16:31:50 fold 5/21 sub-05 y=1 p=0.507 (5s)
|
| 17 |
+
16:31:50 fold 6/21 sub-06 y=1 p=0.493 (6s)
|
| 18 |
+
16:31:50 fold 7/21 sub-07 y=0 p=0.489 (6s)
|
| 19 |
+
16:31:51 fold 8/21 sub-08 y=0 p=0.490 (6s)
|
| 20 |
+
16:31:51 fold 9/21 sub-09 y=1 p=0.523 (7s)
|
| 21 |
+
16:31:52 fold 10/21 sub-10 y=1 p=0.525 (7s)
|
| 22 |
+
16:31:52 fold 11/21 sub-11 y=0 p=0.479 (7s)
|
| 23 |
+
16:31:52 fold 12/21 sub-12 y=0 p=0.474 (8s)
|
| 24 |
+
16:31:53 fold 13/21 sub-13 y=1 p=0.516 (8s)
|
| 25 |
+
16:31:53 fold 14/21 sub-14 y=1 p=0.536 (9s)
|
| 26 |
+
16:31:53 fold 15/21 sub-15 y=0 p=0.486 (9s)
|
| 27 |
+
16:31:54 fold 16/21 sub-16 y=0 p=0.482 (9s)
|
| 28 |
+
16:31:54 fold 17/21 sub-17 y=1 p=0.520 (10s)
|
| 29 |
+
16:31:54 fold 18/21 sub-18 y=1 p=0.518 (10s)
|
| 30 |
+
16:31:55 fold 19/21 sub-19 y=1 p=0.520 (10s)
|
| 31 |
+
16:31:55 fold 20/21 sub-20 y=1 p=0.519 (11s)
|
| 32 |
+
16:31:56 fold 21/21 sub-21 y=0 p=0.498 (11s)
|
| 33 |
+
16:31:57 result: auroc=0.9904 auroc_ci_low=0.9444 auroc_ci_high=1.0000 (11s)
|
finetune/fomo_tune_baseline/output/task1/metrics.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"name": "task1", "auroc": 0.9903846153846154, "auroc_ci_low": 0.9444444444444445, "auroc_ci_high": 1.0, "run_time": 11.2}
|
finetune/fomo_tune_baseline/output/task3/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/task3/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/task3/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/task3/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/task3/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/task3/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()
|
finetune/fomo_tune_baseline/output/task3/build/fomo_tune/main_task5.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FOMO task 5: polymicrogyria classification, scored by AUROC as the challenge scores it.
|
| 2 |
+
|
| 3 |
+
`Task5Method` 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 48 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 probability out. Both go through `Task5Method.predict`, so every fold exercises
|
| 9 |
+
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
|
| 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.model_selection import KFold
|
| 27 |
+
from sklearn.pipeline import make_pipeline
|
| 28 |
+
from sklearn.preprocessing import StandardScaler
|
| 29 |
+
|
| 30 |
+
from fomo_tune.backbone import load_backbone
|
| 31 |
+
from fomo_tune.utils import git_sha, set_seed, setup_logging
|
| 32 |
+
|
| 33 |
+
logger = logging.getLogger("fomo_tune")
|
| 34 |
+
|
| 35 |
+
Images = dict[str, nib.Nifti1Image]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class Config:
|
| 40 |
+
task: str = "task5"
|
| 41 |
+
ckpt_path: str = (
|
| 42 |
+
"/data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth"
|
| 43 |
+
)
|
| 44 |
+
output_root: str = "output/fomo_tune"
|
| 45 |
+
name: str = "task5"
|
| 46 |
+
device: str = "cuda"
|
| 47 |
+
seed: int = 4466
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ---- method: the part we tune -----------------------------------------------------------
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Task5Method:
|
| 54 |
+
"""Frozen sMRI MAE, mean-pooled tokens over the t1w, 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.cache: dict[str, np.ndarray] = {}
|
| 62 |
+
self.head = None
|
| 63 |
+
|
| 64 |
+
@torch.inference_mode()
|
| 65 |
+
def features(self, images: Images) -> np.ndarray:
|
| 66 |
+
"""(D,) per subject. A pure function of the images, so training and inference agree."""
|
| 67 |
+
sample = self.transform(images["t1w"])
|
| 68 |
+
batch = {key: value[None].to(self.device) for key, value in sample.items()}
|
| 69 |
+
|
| 70 |
+
with torch.autocast("cuda", torch.bfloat16, enabled=self.device.type == "cuda"):
|
| 71 |
+
out = self.backbone(batch)
|
| 72 |
+
|
| 73 |
+
patch_embeds = out["patch_embeds"]
|
| 74 |
+
token_mask = out["token_mask"].bool().unsqueeze(-1)
|
| 75 |
+
embed = (patch_embeds * token_mask).sum(dim=1) / token_mask.sum(dim=1)
|
| 76 |
+
return embed[0].float().cpu().numpy()
|
| 77 |
+
|
| 78 |
+
def cached_features(self, row: dict) -> np.ndarray:
|
| 79 |
+
if row["subject"] not in self.cache:
|
| 80 |
+
self.cache[row["subject"]] = self.features(row)
|
| 81 |
+
return self.cache[row["subject"]]
|
| 82 |
+
|
| 83 |
+
def fit(self, rows: list[dict]) -> None:
|
| 84 |
+
X = np.stack([self.cached_features(row) for row in rows])
|
| 85 |
+
y = np.array([row["label"] for row in rows])
|
| 86 |
+
|
| 87 |
+
clf = LogisticRegressionCV(
|
| 88 |
+
Cs=10,
|
| 89 |
+
class_weight="balanced",
|
| 90 |
+
scoring="roc_auc",
|
| 91 |
+
max_iter=1000,
|
| 92 |
+
l1_ratios=(0,),
|
| 93 |
+
use_legacy_attributes=False,
|
| 94 |
+
)
|
| 95 |
+
self.head = make_pipeline(StandardScaler(), clf)
|
| 96 |
+
self.head.fit(X, y)
|
| 97 |
+
self.positive = list(self.head.classes_).index(1)
|
| 98 |
+
|
| 99 |
+
def predict(self, images: Images) -> float:
|
| 100 |
+
"""Positive-class probability. Indexes `classes_` rather than assuming column 1, which
|
| 101 |
+
would silently score the wrong class if the label order differed."""
|
| 102 |
+
X = self.features(images)[None]
|
| 103 |
+
probs = self.head.predict_proba(X)[0]
|
| 104 |
+
return float(probs[self.positive])
|
| 105 |
+
|
| 106 |
+
def save(self, model_dir: Path) -> None:
|
| 107 |
+
"""Everything `load` needs but the backbone weights, which stay wherever `ckpt_path`
|
| 108 |
+
points -- a few hundred KB, so a run saves one without copying a 3.7G checkpoint."""
|
| 109 |
+
model_dir.mkdir(parents=True, exist_ok=True)
|
| 110 |
+
OmegaConf.save(self.cfg, model_dir / "config.yaml")
|
| 111 |
+
joblib.dump({"head": self.head, "positive": self.positive}, model_dir / "head.joblib")
|
| 112 |
+
|
| 113 |
+
@classmethod
|
| 114 |
+
def load(cls, model_dir: Path, **overrides) -> "Task5Method":
|
| 115 |
+
"""Rebuild a fitted method from `save`. Overrides are Config fields, for what differs
|
| 116 |
+
between here and the container -- the backbone path, the device."""
|
| 117 |
+
cfg = OmegaConf.merge(
|
| 118 |
+
OmegaConf.structured(Config), OmegaConf.load(model_dir / "config.yaml"), overrides
|
| 119 |
+
)
|
| 120 |
+
method = cls(cfg)
|
| 121 |
+
state = joblib.load(model_dir / "head.joblib")
|
| 122 |
+
method.head, method.positive = state["head"], state["positive"]
|
| 123 |
+
return method
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# ---- protocol: the part we hold fixed ---------------------------------------------------
|
| 127 |
+
|
| 128 |
+
# Every image the task ships. The method picks which of them it wants, as at inference, where the
|
| 129 |
+
# challenge hands over the modalities whether or not a model uses them.
|
| 130 |
+
IMAGE_COLS = ("t1w",)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def cross_validate(
|
| 134 |
+
rows: list[dict], method: Task5Method, seed: int = 0, n_folds: int = 20
|
| 135 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 136 |
+
"""Out-of-fold score for every subject, each predicted by a head fit on the other folds."""
|
| 137 |
+
y = np.array([row["label"] for row in rows])
|
| 138 |
+
oof = np.zeros(len(rows), dtype=float)
|
| 139 |
+
folds = KFold(n_splits=n_folds, shuffle=True, random_state=seed)
|
| 140 |
+
start = time.perf_counter()
|
| 141 |
+
for fold, (train, test) in enumerate(folds.split(rows)):
|
| 142 |
+
method.fit([rows[i] for i in train])
|
| 143 |
+
for i in test:
|
| 144 |
+
oof[i] = method.predict({key: rows[i][key] for key in IMAGE_COLS})
|
| 145 |
+
logger.info(
|
| 146 |
+
f"fold {fold + 1}/{n_folds} n={len(test)} y={y[test]} "
|
| 147 |
+
f"p={np.round(oof[test], 3)} ({time.perf_counter() - start:.0f}s)"
|
| 148 |
+
)
|
| 149 |
+
return y, oof
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def score(
|
| 153 |
+
y: np.ndarray, oof: np.ndarray, seed: int = 0, n_boot: int = 2000, alpha: float = 0.05
|
| 154 |
+
) -> dict:
|
| 155 |
+
"""AUROC, the challenge metric, plus a percentile CI resampling subjects with replacement."""
|
| 156 |
+
rng = np.random.default_rng(seed)
|
| 157 |
+
samples = []
|
| 158 |
+
for _ in range(n_boot):
|
| 159 |
+
rows = rng.integers(0, len(y), size=len(y))
|
| 160 |
+
if len(np.unique(y[rows])) < 2:
|
| 161 |
+
continue
|
| 162 |
+
samples.append(roc_auc_score(y[rows], oof[rows]))
|
| 163 |
+
|
| 164 |
+
low, high = np.percentile(samples, [100 * alpha / 2, 100 * (1 - alpha / 2)])
|
| 165 |
+
return {
|
| 166 |
+
"auroc": float(roc_auc_score(y, oof)),
|
| 167 |
+
"auroc_ci_low": float(low),
|
| 168 |
+
"auroc_ci_high": float(high),
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
# ---- entrypoints ------------------------------------------------------------------------
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def train(args: argparse.Namespace) -> None:
|
| 176 |
+
# imported here, not at the top, so the container needs no dataset stack to run `predict`
|
| 177 |
+
from fomo_tune.datasets import load_fomo_task5
|
| 178 |
+
|
| 179 |
+
cfg = OmegaConf.merge(OmegaConf.structured(Config), OmegaConf.from_dotlist(args.overrides))
|
| 180 |
+
run_dir = Path(cfg.output_root) / cfg.name
|
| 181 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 182 |
+
|
| 183 |
+
setup_logging(run_dir)
|
| 184 |
+
set_seed(cfg.seed)
|
| 185 |
+
logger.info(f"run {cfg.name} (git {git_sha()})")
|
| 186 |
+
logger.info(f"config:\n{OmegaConf.to_yaml(cfg).rstrip()}")
|
| 187 |
+
OmegaConf.save(cfg, run_dir / "config.yaml")
|
| 188 |
+
|
| 189 |
+
rows = list(load_fomo_task5())
|
| 190 |
+
logger.info(f"dataset: {len(rows)} subjects, {sum(r['label'] for r in rows)} positive")
|
| 191 |
+
|
| 192 |
+
method = Task5Method(cfg)
|
| 193 |
+
start = time.perf_counter()
|
| 194 |
+
y, oof = cross_validate(rows, method)
|
| 195 |
+
run_time = time.perf_counter() - start
|
| 196 |
+
summary = score(y, oof)
|
| 197 |
+
|
| 198 |
+
# the shipped head sees all n subjects, so it is not any of the models scored above
|
| 199 |
+
method.fit(rows)
|
| 200 |
+
method.save(run_dir / "model")
|
| 201 |
+
|
| 202 |
+
record = {"name": cfg.name, **summary, "run_time": round(run_time, 1)}
|
| 203 |
+
(run_dir / "metrics.json").write_text(json.dumps(record) + "\n")
|
| 204 |
+
scores = " ".join(f"{k}={v:.4f}" for k, v in summary.items())
|
| 205 |
+
logger.info(f"result: {scores} ({run_time:.0f}s)")
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def predict(args: argparse.Namespace) -> None:
|
| 209 |
+
"""The challenge contract: a t1 path in, one probability written to `--output`.
|
| 210 |
+
|
| 211 |
+
`/app/predict.py` in the container is a shim over this, so what scores the submission is the
|
| 212 |
+
code cross-validation already ran, not something generated at build time.
|
| 213 |
+
"""
|
| 214 |
+
overrides = {"device": args.device}
|
| 215 |
+
if args.ckpt_path:
|
| 216 |
+
overrides["ckpt_path"] = args.ckpt_path
|
| 217 |
+
method = Task5Method.load(args.model_dir, **overrides)
|
| 218 |
+
|
| 219 |
+
probability = method.predict({"t1w": nib.load(args.t1)})
|
| 220 |
+
|
| 221 |
+
args.output.write_text(f"{probability:.6f}\n")
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def main() -> None:
|
| 225 |
+
parser = argparse.ArgumentParser()
|
| 226 |
+
modes = parser.add_subparsers(required=True)
|
| 227 |
+
|
| 228 |
+
train_parser = modes.add_parser("train", help="cross-validate over the task, then fit and save")
|
| 229 |
+
train_parser.add_argument("overrides", nargs="*", help="config overrides, e.g. device=cpu")
|
| 230 |
+
train_parser.set_defaults(run=train)
|
| 231 |
+
|
| 232 |
+
predict_parser = modes.add_parser("predict", help="one subject, one probability")
|
| 233 |
+
predict_parser.add_argument("--t1", type=Path, required=True)
|
| 234 |
+
predict_parser.add_argument("--output", type=Path, required=True)
|
| 235 |
+
predict_parser.add_argument("--model-dir", type=Path, default=Path("/app/model"))
|
| 236 |
+
predict_parser.add_argument("--ckpt-path", help="overrides the trained config's backbone path")
|
| 237 |
+
predict_parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 238 |
+
predict_parser.set_defaults(run=predict)
|
| 239 |
+
|
| 240 |
+
args = parser.parse_args()
|
| 241 |
+
args.run(args)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
if __name__ == "__main__":
|
| 245 |
+
main()
|
finetune/fomo_tune_baseline/output/task3/build/fomo_tune/utils.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import random
|
| 3 |
+
import subprocess
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger("fomo_tune")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def set_seed(seed: int) -> None:
|
| 14 |
+
random.seed(seed)
|
| 15 |
+
np.random.seed(seed)
|
| 16 |
+
torch.manual_seed(seed)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def git_sha() -> str:
|
| 20 |
+
kwargs = dict(cwd=Path(__file__).parent, capture_output=True, text=True, check=True)
|
| 21 |
+
sha = subprocess.run(["git", "rev-parse", "--short", "HEAD"], **kwargs).stdout.strip()
|
| 22 |
+
dirty = subprocess.run(["git", "status", "--porcelain", "-uno"], **kwargs).stdout.strip()
|
| 23 |
+
return f"{sha}-dirty" if dirty else sha
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def setup_logging(run_dir: Path) -> None:
|
| 27 |
+
handlers = [logging.StreamHandler(sys.stdout), logging.FileHandler(run_dir / "log.txt")]
|
| 28 |
+
logger.setLevel(logging.INFO)
|
| 29 |
+
logger.handlers.clear()
|
| 30 |
+
for handler in handlers:
|
| 31 |
+
handler.setFormatter(logging.Formatter("%(asctime)s %(message)s", datefmt="%H:%M:%S"))
|
| 32 |
+
logger.addHandler(handler)
|
| 33 |
+
logger.propagate = False
|
finetune/fomo_tune_baseline/output/task3/build/model/config.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task: task3
|
| 2 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 3 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 4 |
+
name: task3
|
| 5 |
+
device: cuda
|
| 6 |
+
seed: 4466
|
finetune/fomo_tune_baseline/output/task3/build/predict.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
from fomo_tune.main_task3 import main
|
| 5 |
+
|
| 6 |
+
sys.argv = [
|
| 7 |
+
sys.argv[0],
|
| 8 |
+
"predict",
|
| 9 |
+
*sys.argv[1:],
|
| 10 |
+
"--model-dir",
|
| 11 |
+
"/app/model",
|
| 12 |
+
"--ckpt-path",
|
| 13 |
+
"/app/model/backbone.pth",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
main()
|
finetune/fomo_tune_baseline/output/task3/build/smri_mae/main_pretrain.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Sophont, Inc
|
| 2 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 3 |
+
#
|
| 4 |
+
# References:
|
| 5 |
+
# deit: https://github.com/facebookresearch/deit/blob/main/main.py
|
| 6 |
+
# capi: https://github.com/facebookresearch/capi/blob/main/train_capi.py
|
| 7 |
+
|
| 8 |
+
import argparse
|
| 9 |
+
import datetime
|
| 10 |
+
import json
|
| 11 |
+
import math
|
| 12 |
+
import random
|
| 13 |
+
import subprocess
|
| 14 |
+
import time
|
| 15 |
+
from contextlib import nullcontext
|
| 16 |
+
from functools import partial
|
| 17 |
+
from itertools import islice
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Iterable, Sequence
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn as nn
|
| 23 |
+
import wandb
|
| 24 |
+
import webdataset as wds
|
| 25 |
+
from omegaconf import DictConfig, OmegaConf
|
| 26 |
+
from PIL import Image
|
| 27 |
+
|
| 28 |
+
from matplotlib import pyplot as plt
|
| 29 |
+
from torch import Tensor
|
| 30 |
+
|
| 31 |
+
import data.mri_data as mri_data
|
| 32 |
+
import smri_mae.model_mae as models_mae
|
| 33 |
+
import smri_mae.utils as ut
|
| 34 |
+
import smri_mae.visualization as vis
|
| 35 |
+
|
| 36 |
+
DEFAULT_CONFIG = Path(__file__).parent / "config/default_pretrain.yaml"
|
| 37 |
+
|
| 38 |
+
MODELS_DICT = models_mae.__dict__
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def main(args: DictConfig):
|
| 42 |
+
# setup
|
| 43 |
+
ut.init_distributed_mode(args)
|
| 44 |
+
global_rank = ut.get_rank()
|
| 45 |
+
is_master = global_rank == 0
|
| 46 |
+
world_size = ut.get_world_size()
|
| 47 |
+
device = torch.device(args.device)
|
| 48 |
+
ut.configure_flash_sdpa()
|
| 49 |
+
ut.random_seed(args.seed, rank=global_rank)
|
| 50 |
+
|
| 51 |
+
if args.name and not args.output_dir.endswith(args.name):
|
| 52 |
+
args.output_dir = f"{args.output_dir}/{args.name}"
|
| 53 |
+
output_dir = Path(args.output_dir)
|
| 54 |
+
|
| 55 |
+
if is_master:
|
| 56 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 57 |
+
out_cfg_path = output_dir / "config.yaml"
|
| 58 |
+
if out_cfg_path.exists():
|
| 59 |
+
prev_cfg = OmegaConf.load(out_cfg_path)
|
| 60 |
+
assert args == prev_cfg, "current config doesn't match previous config"
|
| 61 |
+
else:
|
| 62 |
+
OmegaConf.save(args, out_cfg_path)
|
| 63 |
+
|
| 64 |
+
if args.wandb:
|
| 65 |
+
wandb.init(
|
| 66 |
+
entity=args.wandb_entity,
|
| 67 |
+
project=args.wandb_project,
|
| 68 |
+
name=args.name,
|
| 69 |
+
notes=args.notes,
|
| 70 |
+
config=OmegaConf.to_container(args),
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
ut.setup_for_distributed(log_path=output_dir / "log.txt")
|
| 74 |
+
|
| 75 |
+
print("pretraining 3D ViTMAE")
|
| 76 |
+
print(f"start: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 77 |
+
print(f"cwd: {Path.cwd()}")
|
| 78 |
+
print(ut.get_sha())
|
| 79 |
+
print("config:", OmegaConf.to_yaml(args), sep="\n")
|
| 80 |
+
|
| 81 |
+
# data loaders
|
| 82 |
+
train_loader, eval_loaders = create_data_loaders(args)
|
| 83 |
+
|
| 84 |
+
# model
|
| 85 |
+
model = MODELS_DICT[args.model](
|
| 86 |
+
img_size=args.img_size,
|
| 87 |
+
in_chans=args.get("in_chans", 1),
|
| 88 |
+
patch_size=args.patch_size,
|
| 89 |
+
**(args.get("model_kwargs") or {}),
|
| 90 |
+
)
|
| 91 |
+
model.to(device)
|
| 92 |
+
print("model:", model, sep="\n")
|
| 93 |
+
num_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 94 |
+
print(f"num params: {num_params / 1e6:.1f}M")
|
| 95 |
+
|
| 96 |
+
model_without_ddp = model
|
| 97 |
+
if args.distributed:
|
| 98 |
+
model = torch.nn.parallel.DistributedDataParallel(
|
| 99 |
+
model,
|
| 100 |
+
device_ids=[args.gpu],
|
| 101 |
+
gradient_as_bucket_view=True,
|
| 102 |
+
)
|
| 103 |
+
model_without_ddp = model.module
|
| 104 |
+
|
| 105 |
+
# optimizer
|
| 106 |
+
total_batch_size = args.batch_size * args.accum_iter * world_size
|
| 107 |
+
print(
|
| 108 |
+
f"total batch size: {total_batch_size} = "
|
| 109 |
+
f"{args.batch_size} bs per gpu x {args.accum_iter} accum x {world_size} gpus"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
if not args.get("lr"):
|
| 113 |
+
args.lr = args.base_lr * total_batch_size / 256
|
| 114 |
+
print(f"lr: {args.lr:.2e} = {args.base_lr:.2e} x {total_batch_size} / 256")
|
| 115 |
+
else:
|
| 116 |
+
print(f"lr: {args.lr:.2e}")
|
| 117 |
+
|
| 118 |
+
param_groups = ut.get_param_groups(model)
|
| 119 |
+
ut.update_lr(param_groups, args.lr)
|
| 120 |
+
ut.update_wd(param_groups, args.weight_decay)
|
| 121 |
+
# cast or else it corrupts the checkpoint
|
| 122 |
+
betas = tuple(args.betas) if args.betas is not None else None
|
| 123 |
+
optimizer = torch.optim.AdamW(param_groups, betas=betas, fused=True)
|
| 124 |
+
|
| 125 |
+
epoch_num_batches = len(train_loader)
|
| 126 |
+
steps_per_epoch = math.ceil(epoch_num_batches / args.accum_iter)
|
| 127 |
+
total_steps = args.epochs * steps_per_epoch
|
| 128 |
+
warmup_steps = args.warmup_epochs * steps_per_epoch
|
| 129 |
+
lr_schedule = ut.WarmupThenCosine(
|
| 130 |
+
base_value=args.lr,
|
| 131 |
+
final_value=args.min_lr,
|
| 132 |
+
total_iters=total_steps,
|
| 133 |
+
warmup_iters=warmup_steps,
|
| 134 |
+
)
|
| 135 |
+
print(f"full schedule: epochs = {args.epochs} (steps = {total_steps})")
|
| 136 |
+
print(f"warmup: epochs = {args.warmup_epochs} (steps = {warmup_steps})")
|
| 137 |
+
|
| 138 |
+
# loss scaling not needed for bfloat16 (according to timm)
|
| 139 |
+
if args.amp and args.amp_dtype != "bfloat16":
|
| 140 |
+
loss_scaler = torch.GradScaler(device.type)
|
| 141 |
+
else:
|
| 142 |
+
loss_scaler = None
|
| 143 |
+
|
| 144 |
+
# load checkpoint/resume training
|
| 145 |
+
ut.load_model(args, model_without_ddp, optimizer, loss_scaler)
|
| 146 |
+
|
| 147 |
+
print(f"start training for {args.epochs} epochs")
|
| 148 |
+
start_time = time.monotonic()
|
| 149 |
+
for epoch in range(args.start_epoch, args.epochs):
|
| 150 |
+
train_stats = train_one_epoch(
|
| 151 |
+
args,
|
| 152 |
+
model,
|
| 153 |
+
train_loader,
|
| 154 |
+
optimizer,
|
| 155 |
+
loss_scaler,
|
| 156 |
+
lr_schedule,
|
| 157 |
+
epoch,
|
| 158 |
+
device,
|
| 159 |
+
)
|
| 160 |
+
eval_stats = {}
|
| 161 |
+
eval_plots = {}
|
| 162 |
+
eval_period = args.get("eval_period", 1)
|
| 163 |
+
if eval_period and (epoch % eval_period == 0 or epoch == args.epochs - 1):
|
| 164 |
+
for name, loader in eval_loaders.items():
|
| 165 |
+
stats, plots = evaluate(
|
| 166 |
+
args,
|
| 167 |
+
model,
|
| 168 |
+
loader,
|
| 169 |
+
epoch,
|
| 170 |
+
device,
|
| 171 |
+
eval_name=name,
|
| 172 |
+
)
|
| 173 |
+
eval_stats.update(stats)
|
| 174 |
+
eval_plots.update(plots)
|
| 175 |
+
|
| 176 |
+
merged_stats = {"epoch": epoch, **train_stats, **eval_stats}
|
| 177 |
+
if is_master:
|
| 178 |
+
with (output_dir / "log.json").open("a") as f:
|
| 179 |
+
print(json.dumps(merged_stats), file=f)
|
| 180 |
+
|
| 181 |
+
for plot_name, img in eval_plots.items():
|
| 182 |
+
plot_name = plot_name.replace("/", "__")
|
| 183 |
+
img.save(output_dir / f"{plot_name}__{epoch:05d}.png")
|
| 184 |
+
|
| 185 |
+
ut.save_model(args, epoch, model_without_ddp, optimizer, loss_scaler)
|
| 186 |
+
sync_checkpoints_to_r2(args, output_dir)
|
| 187 |
+
|
| 188 |
+
if args.distributed:
|
| 189 |
+
torch.distributed.destroy_process_group()
|
| 190 |
+
|
| 191 |
+
total_time = time.monotonic() - start_time
|
| 192 |
+
print(f"done! training time: {datetime.timedelta(seconds=int(total_time))}")
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def create_data_loaders(args: DictConfig):
|
| 196 |
+
data_loaders = {}
|
| 197 |
+
dataset_names = [args.train_dataset] + args.eval_datasets
|
| 198 |
+
|
| 199 |
+
for dataset_name in dataset_names:
|
| 200 |
+
dataset_config = args.datasets[dataset_name].copy()
|
| 201 |
+
drop_last = dataset_config.pop("drop_last")
|
| 202 |
+
is_train = dataset_name == args.train_dataset
|
| 203 |
+
|
| 204 |
+
print(f"loading dataset: {dataset_name}\n\n{OmegaConf.to_yaml(dataset_config)}")
|
| 205 |
+
shuffle = dataset_config["shuffle"]
|
| 206 |
+
samples_per_epoch = dataset_config.pop("samples_per_epoch")
|
| 207 |
+
dataset = mri_data.make_sparse_wds_dataset(
|
| 208 |
+
dataset_config["url"],
|
| 209 |
+
shuffle=shuffle,
|
| 210 |
+
buffer_size=dataset_config["buffer_size"],
|
| 211 |
+
)
|
| 212 |
+
num_workers = int(args.num_workers)
|
| 213 |
+
loader_kwargs = {
|
| 214 |
+
"batch_size": args.batch_size,
|
| 215 |
+
"collate_fn": partial(mri_data.collate, include_meta=not is_train),
|
| 216 |
+
"shuffle": False,
|
| 217 |
+
"num_workers": num_workers,
|
| 218 |
+
"persistent_workers": num_workers > 0,
|
| 219 |
+
"pin_memory": True,
|
| 220 |
+
"drop_last": drop_last,
|
| 221 |
+
"prefetch_factor": args.prefetch_factor,
|
| 222 |
+
}
|
| 223 |
+
loader = wds.WebLoader(dataset, **loader_kwargs)
|
| 224 |
+
num_batches = samples_per_epoch // (ut.get_world_size() * args.batch_size)
|
| 225 |
+
loader = loader.with_epoch(num_batches)
|
| 226 |
+
loader = loader.with_length(num_batches, silent=True)
|
| 227 |
+
|
| 228 |
+
data_loaders[dataset_name] = loader
|
| 229 |
+
|
| 230 |
+
train_loader = data_loaders.pop(args.train_dataset)
|
| 231 |
+
return train_loader, data_loaders
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def sync_checkpoints_to_r2(args: DictConfig, output_dir: Path) -> None:
|
| 235 |
+
r2_sync_url = args.get("r2_sync")
|
| 236 |
+
if not r2_sync_url or not ut.is_main_process():
|
| 237 |
+
return
|
| 238 |
+
|
| 239 |
+
cmd = ["aws", "s3", "sync", str(output_dir), str(r2_sync_url), "--profile", "r2"]
|
| 240 |
+
print(f"syncing checkpoints to R2: {output_dir} -> {r2_sync_url}")
|
| 241 |
+
subprocess.run(cmd, check=True)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def train_one_epoch(
|
| 245 |
+
args: DictConfig,
|
| 246 |
+
model: nn.Module,
|
| 247 |
+
data_loader: Iterable,
|
| 248 |
+
optimizer: torch.optim.Optimizer,
|
| 249 |
+
loss_scaler: torch.GradScaler | None,
|
| 250 |
+
lr_schedule: Sequence[float],
|
| 251 |
+
epoch: int,
|
| 252 |
+
device: torch.device,
|
| 253 |
+
):
|
| 254 |
+
model.train()
|
| 255 |
+
|
| 256 |
+
metric_logger = ut.MetricLogger(delimiter=" ")
|
| 257 |
+
metric_logger.add_meter("lr", ut.SmoothedValue(window_size=1, fmt="{value:.6f}"))
|
| 258 |
+
metric_logger.add_meter("grad", ut.SmoothedValue())
|
| 259 |
+
header = f"Train: [{epoch}]"
|
| 260 |
+
log_wandb = args.wandb and ut.is_main_process()
|
| 261 |
+
|
| 262 |
+
epoch_num_batches = len(data_loader)
|
| 263 |
+
steps_per_epoch = math.ceil(epoch_num_batches / args.accum_iter)
|
| 264 |
+
|
| 265 |
+
print_freq = args.get("print_freq", 100) if not args.debug else 1
|
| 266 |
+
num_batches = epoch_num_batches if not args.debug else 10
|
| 267 |
+
amp_dtype = getattr(torch, args.amp_dtype)
|
| 268 |
+
use_cuda = device.type == "cuda"
|
| 269 |
+
if use_cuda and args.presend_cuda:
|
| 270 |
+
data_loader = ut.pre_send_to_cuda_wrapper(
|
| 271 |
+
data_loader, device, dtype_map={torch.float16: amp_dtype}
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
optimizer.zero_grad()
|
| 275 |
+
|
| 276 |
+
for batch_idx, batch in enumerate(
|
| 277 |
+
metric_logger.log_every(data_loader, print_freq, header, total_steps=num_batches)
|
| 278 |
+
):
|
| 279 |
+
if use_cuda and not args.presend_cuda:
|
| 280 |
+
batch = ut.send_data(batch, device, dtype_map={torch.float16: amp_dtype})
|
| 281 |
+
|
| 282 |
+
batch_step = batch_idx + 1
|
| 283 |
+
log_step = batch_step % print_freq == 0 or batch_step == num_batches
|
| 284 |
+
update_in_epoch = batch_idx // args.accum_iter
|
| 285 |
+
group_size = min(args.accum_iter, num_batches - update_in_epoch * args.accum_iter)
|
| 286 |
+
need_update = batch_step % args.accum_iter == 0 or batch_step == num_batches
|
| 287 |
+
global_step = epoch * steps_per_epoch + update_in_epoch
|
| 288 |
+
lr = lr_schedule[global_step]
|
| 289 |
+
if need_update:
|
| 290 |
+
ut.update_lr(optimizer.param_groups, lr)
|
| 291 |
+
|
| 292 |
+
images, img_mask = mri_data.densify_sparse_image_batch(
|
| 293 |
+
batch["image_values"],
|
| 294 |
+
batch["img_mask"],
|
| 295 |
+
(int(args.get("in_chans", 1)), *args.img_size),
|
| 296 |
+
dtype=amp_dtype,
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
sync_context = model.no_sync() if args.distributed and not need_update else nullcontext()
|
| 300 |
+
with sync_context:
|
| 301 |
+
with torch.autocast(device_type=device.type, dtype=amp_dtype, enabled=args.amp):
|
| 302 |
+
loss = model(
|
| 303 |
+
images,
|
| 304 |
+
img_mask=img_mask,
|
| 305 |
+
mask_ratio=args.mask_ratio,
|
| 306 |
+
pred_mask_ratio=args.pred_mask_ratio,
|
| 307 |
+
pad_to_multiple=args.pad_to_multiple,
|
| 308 |
+
with_state=False,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
loss_for_log = loss.detach()
|
| 312 |
+
torch._assert_async(torch.isfinite(loss_for_log), "non-finite loss")
|
| 313 |
+
|
| 314 |
+
grad_norm = ut.backward_step(
|
| 315 |
+
loss / group_size,
|
| 316 |
+
optimizer,
|
| 317 |
+
scaler=loss_scaler,
|
| 318 |
+
need_update=need_update,
|
| 319 |
+
max_norm=args.clip_grad,
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
if need_update and log_step:
|
| 323 |
+
loss_value = loss_for_log.item()
|
| 324 |
+
grad_norm_value = grad_norm.item()
|
| 325 |
+
metric_logger.update(loss=loss_value, lr=lr, grad=grad_norm_value)
|
| 326 |
+
if log_wandb:
|
| 327 |
+
wandb.log(
|
| 328 |
+
{
|
| 329 |
+
"train/loss": loss_value,
|
| 330 |
+
"train/lr": lr,
|
| 331 |
+
"train/grad": grad_norm_value,
|
| 332 |
+
},
|
| 333 |
+
step=int(1000 * (epoch + batch_step / epoch_num_batches)),
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
# gather the stats from all processes
|
| 337 |
+
metric_logger.synchronize_between_processes()
|
| 338 |
+
print("Averaged stats:", metric_logger)
|
| 339 |
+
return {f"train/{k}": meter.global_avg for k, meter in metric_logger.meters.items()}
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
@torch.inference_mode()
|
| 343 |
+
def evaluate(
|
| 344 |
+
args: DictConfig,
|
| 345 |
+
model: nn.Module,
|
| 346 |
+
data_loader: Iterable,
|
| 347 |
+
epoch: int,
|
| 348 |
+
device: torch.device,
|
| 349 |
+
eval_name: str,
|
| 350 |
+
):
|
| 351 |
+
model.eval()
|
| 352 |
+
|
| 353 |
+
metric_logger = ut.MetricLogger(delimiter=" ")
|
| 354 |
+
header = f"Eval ({eval_name}): [{epoch}]"
|
| 355 |
+
is_master = ut.is_main_process()
|
| 356 |
+
log_wandb = args.wandb and is_master
|
| 357 |
+
|
| 358 |
+
epoch_num_batches = len(data_loader)
|
| 359 |
+
if epoch_num_batches <= 0:
|
| 360 |
+
raise ValueError(f"eval loader {eval_name!r} has zero batches")
|
| 361 |
+
|
| 362 |
+
print_freq = args.get("print_freq", 100) if not args.debug else 1
|
| 363 |
+
num_batches = epoch_num_batches if not args.debug else 10
|
| 364 |
+
num_batches = min(num_batches, epoch_num_batches)
|
| 365 |
+
eval_seed = int(args.get("eval_seed", args.seed)) + ut.get_rank()
|
| 366 |
+
example_step = random.Random(eval_seed).randint(1, num_batches)
|
| 367 |
+
amp_dtype = getattr(torch, args.amp_dtype)
|
| 368 |
+
use_cuda = device.type == "cuda"
|
| 369 |
+
rng_state = ut.capture_rng_state()
|
| 370 |
+
torch.set_rng_state(torch.Generator().manual_seed(eval_seed).get_state())
|
| 371 |
+
if use_cuda:
|
| 372 |
+
torch.cuda.manual_seed(eval_seed)
|
| 373 |
+
if use_cuda and args.presend_cuda:
|
| 374 |
+
data_loader = ut.pre_send_to_cuda_wrapper(
|
| 375 |
+
data_loader, device, dtype_map={torch.float16: amp_dtype}
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
eval_batches = islice(data_loader, num_batches)
|
| 379 |
+
for batch_idx, batch in enumerate(
|
| 380 |
+
metric_logger.log_every(eval_batches, print_freq, header, total_steps=num_batches)
|
| 381 |
+
):
|
| 382 |
+
if use_cuda and not args.presend_cuda:
|
| 383 |
+
batch = ut.send_data(batch, device, dtype_map={torch.float16: amp_dtype})
|
| 384 |
+
|
| 385 |
+
batch_step = batch_idx + 1
|
| 386 |
+
|
| 387 |
+
images, img_mask = mri_data.densify_sparse_image_batch(
|
| 388 |
+
batch["image_values"],
|
| 389 |
+
batch["img_mask"],
|
| 390 |
+
(int(args.get("in_chans", 1)), *args.img_size),
|
| 391 |
+
dtype=amp_dtype,
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
with torch.autocast(device_type=device.type, dtype=amp_dtype, enabled=args.amp):
|
| 395 |
+
loss, state = model(
|
| 396 |
+
images,
|
| 397 |
+
img_mask=img_mask,
|
| 398 |
+
mask_ratio=args.mask_ratio,
|
| 399 |
+
pred_mask_ratio=args.pred_mask_ratio,
|
| 400 |
+
pad_to_multiple=args.pad_to_multiple,
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
loss_value = loss.detach().float().item()
|
| 404 |
+
finite = torch.tensor(int(math.isfinite(loss_value)), dtype=torch.int32, device=device)
|
| 405 |
+
if args.distributed:
|
| 406 |
+
torch.distributed.all_reduce(finite, op=torch.distributed.ReduceOp.MIN)
|
| 407 |
+
if not finite.item():
|
| 408 |
+
raise RuntimeError("non-finite validation loss detected")
|
| 409 |
+
metric_logger.meters["loss"].update(loss_value, n=int(batch["img_mask"].shape[0]))
|
| 410 |
+
|
| 411 |
+
if is_master and batch_step == example_step:
|
| 412 |
+
example_batch = {"image": images[:1], "img_mask": img_mask[:1]}
|
| 413 |
+
if "meta" in batch:
|
| 414 |
+
example_batch["meta"] = batch["meta"][:1]
|
| 415 |
+
example_state = {
|
| 416 |
+
"pred_images": state["pred_images"][:1],
|
| 417 |
+
"pred_mask": state["pred_mask"][:1],
|
| 418 |
+
}
|
| 419 |
+
example_data = {
|
| 420 |
+
"batch": ut.send_data(example_batch, "cpu"),
|
| 421 |
+
"state": ut.send_data(example_state, "cpu"),
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
# gather the stats from all processes
|
| 425 |
+
metric_logger.synchronize_between_processes()
|
| 426 |
+
print(f"Averaged stats ({eval_name}):", metric_logger)
|
| 427 |
+
stats = {f"eval/{eval_name}/{k}": meter.global_avg for k, meter in metric_logger.meters.items()}
|
| 428 |
+
|
| 429 |
+
plots = {}
|
| 430 |
+
if is_master:
|
| 431 |
+
print(f"Making plots ({eval_name}): example={example_step}")
|
| 432 |
+
plots = make_plots(args, **example_data)
|
| 433 |
+
plots = {f"eval/{eval_name}/{k}": img for k, img in plots.items()}
|
| 434 |
+
|
| 435 |
+
if log_wandb:
|
| 436 |
+
wandb.log(stats, step=1000 * (epoch + 1))
|
| 437 |
+
wandb.log(
|
| 438 |
+
{k: wandb.Image(img, caption=f"example={example_step}") for k, img in plots.items()},
|
| 439 |
+
step=1000 * (epoch + 1),
|
| 440 |
+
)
|
| 441 |
+
ut.restore_rng_state(rng_state)
|
| 442 |
+
return stats, plots
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def make_plots(
|
| 446 |
+
args: DictConfig,
|
| 447 |
+
batch: dict[str, Tensor],
|
| 448 |
+
state: dict[str, Tensor],
|
| 449 |
+
) -> dict[str, Image.Image]:
|
| 450 |
+
fig_kwargs = args.get("fig_kwargs", {})
|
| 451 |
+
|
| 452 |
+
images = batch["image"]
|
| 453 |
+
img_mask = batch.get("img_mask")
|
| 454 |
+
if img_mask is not None:
|
| 455 |
+
img_mask = img_mask.expand_as(images)
|
| 456 |
+
|
| 457 |
+
raw_mean, raw_std = vis.raw_stats_from_batch(batch)
|
| 458 |
+
|
| 459 |
+
plots = {}
|
| 460 |
+
mask_pred_fig = vis.plot_mask_pred(
|
| 461 |
+
target=images,
|
| 462 |
+
pred=state["pred_images"],
|
| 463 |
+
pred_mask=state["pred_mask"],
|
| 464 |
+
img_mask=img_mask,
|
| 465 |
+
patch_size=args.patch_size,
|
| 466 |
+
raw_mean=raw_mean,
|
| 467 |
+
raw_std=raw_std,
|
| 468 |
+
**ut.filter_kwargs(vis.plot_mask_pred, fig_kwargs),
|
| 469 |
+
)
|
| 470 |
+
plots["mask_pred"] = vis.fig2pil(mask_pred_fig)
|
| 471 |
+
plt.close(mask_pred_fig)
|
| 472 |
+
|
| 473 |
+
return plots
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
if __name__ == "__main__":
|
| 477 |
+
parser = argparse.ArgumentParser()
|
| 478 |
+
parser.add_argument("--cfg-path", type=str, default=None)
|
| 479 |
+
parser.add_argument("--overrides", type=str, default=None, nargs="+")
|
| 480 |
+
args = parser.parse_args()
|
| 481 |
+
cfg = OmegaConf.load(DEFAULT_CONFIG)
|
| 482 |
+
if args.cfg_path:
|
| 483 |
+
cfg = OmegaConf.unsafe_merge(cfg, OmegaConf.load(args.cfg_path))
|
| 484 |
+
if args.overrides:
|
| 485 |
+
cfg = OmegaConf.unsafe_merge(cfg, OmegaConf.from_dotlist(args.overrides))
|
| 486 |
+
main(cfg)
|
finetune/fomo_tune_baseline/output/task3/build/smri_mae/model_mae.py
ADDED
|
@@ -0,0 +1,916 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Sophont, Inc
|
| 2 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 3 |
+
#
|
| 4 |
+
# References:
|
| 5 |
+
# capi: https://github.com/facebookresearch/capi/blob/main/model.py
|
| 6 |
+
# timm: https://github.com/huggingface/pytorch-image-models/blob/v1.0.20/timm/models/vision_transformer.py
|
| 7 |
+
|
| 8 |
+
"""
|
| 9 |
+
From-scratch re-implementation of the original MAE model.
|
| 10 |
+
|
| 11 |
+
MaskedEncoder: standard ViT with masking
|
| 12 |
+
MaskedDecoder: standard self-attention MAE decoder
|
| 13 |
+
MaskedAutoEncoderViT: full MAE model for 3D structural MRI volumes
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from collections.abc import Sequence
|
| 17 |
+
from typing import Literal
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn as nn
|
| 21 |
+
import torch.nn.functional as F
|
| 22 |
+
from torch import Tensor
|
| 23 |
+
from torch.utils.checkpoint import checkpoint
|
| 24 |
+
from huggingface_hub import PyTorchModelHubMixin
|
| 25 |
+
from jaxtyping import Float, Int
|
| 26 |
+
|
| 27 |
+
from .modules import (
|
| 28 |
+
AbsolutePosEmbed,
|
| 29 |
+
Block,
|
| 30 |
+
LayerNorm,
|
| 31 |
+
Normalize,
|
| 32 |
+
JaggedBatch,
|
| 33 |
+
Patchify3D,
|
| 34 |
+
SeparablePosEmbed,
|
| 35 |
+
SinCosPosEmbed3D,
|
| 36 |
+
unpack_tokens,
|
| 37 |
+
)
|
| 38 |
+
from .masking import pad_patch_mask
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class MaskedEncoder(nn.Module):
|
| 42 |
+
"""
|
| 43 |
+
Masked transformer encoder.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
patchify: nn.Module,
|
| 49 |
+
patch_embed: nn.Module,
|
| 50 |
+
pos_embed: nn.Module,
|
| 51 |
+
depth: int = 12,
|
| 52 |
+
embed_dim: int = 768,
|
| 53 |
+
num_heads: int = 12,
|
| 54 |
+
qkv_bias: bool = True,
|
| 55 |
+
proj_bias: bool = True,
|
| 56 |
+
mlp_ratio: int | float = 4,
|
| 57 |
+
class_token: bool = True,
|
| 58 |
+
reg_tokens: int = 0,
|
| 59 |
+
no_embed_class: bool = False,
|
| 60 |
+
final_norm: bool = True,
|
| 61 |
+
drop_path_rate: float = 0.0,
|
| 62 |
+
mask_drop_scale: bool = False,
|
| 63 |
+
):
|
| 64 |
+
super().__init__()
|
| 65 |
+
self.num_prefix_tokens = int(class_token) + reg_tokens
|
| 66 |
+
self.num_reg_tokens = reg_tokens
|
| 67 |
+
self.has_class_token = class_token
|
| 68 |
+
self.no_embed_class = no_embed_class
|
| 69 |
+
|
| 70 |
+
# scale inputs by 1 / observed rate (like dropout)
|
| 71 |
+
self.mask_drop_scale = mask_drop_scale
|
| 72 |
+
|
| 73 |
+
# inject tokenization modules, so that the encoder doesn't specifically need to
|
| 74 |
+
# know how the data are tokenized, while still implementing a complete
|
| 75 |
+
# self-contained model.
|
| 76 |
+
self.patchify = patchify
|
| 77 |
+
self.patch_embed = patch_embed
|
| 78 |
+
self.pos_embed = pos_embed
|
| 79 |
+
|
| 80 |
+
R = reg_tokens
|
| 81 |
+
self.cls_token = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
|
| 82 |
+
self.reg_token = nn.Parameter(torch.empty(1, R, embed_dim)) if reg_tokens else None
|
| 83 |
+
|
| 84 |
+
if not no_embed_class:
|
| 85 |
+
self.cls_token_pos = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
|
| 86 |
+
self.reg_token_pos = nn.Parameter(torch.empty(1, R, embed_dim)) if reg_tokens else None
|
| 87 |
+
else:
|
| 88 |
+
self.cls_token_pos = self.reg_token_pos = None
|
| 89 |
+
|
| 90 |
+
# stochastic depth decay rule
|
| 91 |
+
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]
|
| 92 |
+
|
| 93 |
+
self.blocks = nn.ModuleList(
|
| 94 |
+
[
|
| 95 |
+
Block(
|
| 96 |
+
dim=embed_dim,
|
| 97 |
+
num_heads=num_heads,
|
| 98 |
+
qkv_bias=qkv_bias,
|
| 99 |
+
proj_bias=proj_bias,
|
| 100 |
+
mlp_ratio=mlp_ratio,
|
| 101 |
+
drop_path=dpr[ii],
|
| 102 |
+
)
|
| 103 |
+
for ii in range(depth)
|
| 104 |
+
]
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
self.norm = LayerNorm(embed_dim) if final_norm else nn.Identity()
|
| 108 |
+
|
| 109 |
+
self.reset_parameters()
|
| 110 |
+
|
| 111 |
+
def extra_repr(self):
|
| 112 |
+
return (
|
| 113 |
+
f"class_token={self.has_class_token}, reg_tokens={self.num_reg_tokens}, "
|
| 114 |
+
f"no_embed_class={self.no_embed_class}, mask_drop_scale={self.mask_drop_scale}"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
def reset_parameters(self) -> None:
|
| 118 |
+
for p in [self.cls_token, self.cls_token_pos, self.reg_token, self.reg_token_pos]:
|
| 119 |
+
if p is not None:
|
| 120 |
+
nn.init.trunc_normal_(p, std=0.02)
|
| 121 |
+
|
| 122 |
+
def cat_tokens(self, x: Tensor) -> Tensor:
|
| 123 |
+
# prepend cls and reg tokens with optional learned position embedding
|
| 124 |
+
# the cls and reg pos embedding is ofc redundant, but included in many other
|
| 125 |
+
# implementations.
|
| 126 |
+
B, _, _ = x.shape
|
| 127 |
+
|
| 128 |
+
to_cat = []
|
| 129 |
+
if self.has_class_token:
|
| 130 |
+
cls_token = self.cls_token
|
| 131 |
+
if not self.no_embed_class:
|
| 132 |
+
cls_token = cls_token + self.cls_token_pos
|
| 133 |
+
to_cat.append(cls_token.expand(B, -1, -1))
|
| 134 |
+
|
| 135 |
+
if self.num_reg_tokens:
|
| 136 |
+
reg_token = self.reg_token
|
| 137 |
+
if not self.no_embed_class:
|
| 138 |
+
reg_token = reg_token + self.reg_token_pos
|
| 139 |
+
to_cat.append(reg_token.expand(B, -1, -1))
|
| 140 |
+
|
| 141 |
+
if to_cat:
|
| 142 |
+
x = torch.cat(to_cat + [x], dim=1)
|
| 143 |
+
return x
|
| 144 |
+
|
| 145 |
+
def cat_token_mask(self, token_mask: Tensor, batch_size: int) -> Tensor:
|
| 146 |
+
if self.num_prefix_tokens:
|
| 147 |
+
prefix_mask = torch.ones(
|
| 148 |
+
(batch_size, self.num_prefix_tokens),
|
| 149 |
+
dtype=torch.bool,
|
| 150 |
+
device=token_mask.device,
|
| 151 |
+
)
|
| 152 |
+
token_mask = torch.cat([prefix_mask, token_mask], dim=1)
|
| 153 |
+
return token_mask
|
| 154 |
+
|
| 155 |
+
def chunk_tokens(self, x: Tensor) -> tuple[Tensor | None, Tensor | None, Tensor]:
|
| 156 |
+
cls_offset = int(self.has_class_token)
|
| 157 |
+
cls = x[:, :cls_offset] if self.has_class_token else None
|
| 158 |
+
if self.num_reg_tokens:
|
| 159 |
+
reg = x[:, cls_offset : self.num_prefix_tokens, :]
|
| 160 |
+
else:
|
| 161 |
+
reg = None
|
| 162 |
+
patch = x[:, self.num_prefix_tokens :, :]
|
| 163 |
+
return cls, reg, patch
|
| 164 |
+
|
| 165 |
+
def forward(
|
| 166 |
+
self,
|
| 167 |
+
x: Tensor,
|
| 168 |
+
mask: Tensor | None = None,
|
| 169 |
+
mask_ratio: float | None = None,
|
| 170 |
+
pad_to_multiple: int | None = None,
|
| 171 |
+
) -> tuple[
|
| 172 |
+
Float[Tensor, "B 1 D"] | None,
|
| 173 |
+
Float[Tensor, "B R D"] | None,
|
| 174 |
+
Float[Tensor, "B L D"],
|
| 175 |
+
Tensor | None,
|
| 176 |
+
Int[Tensor, "B L"] | None,
|
| 177 |
+
Tensor | None,
|
| 178 |
+
]:
|
| 179 |
+
"""
|
| 180 |
+
x: input data shape [B, C, D, H, W]
|
| 181 |
+
mask: visible mask, 1 = visible, 0 = invisible. broadcastable shape
|
| 182 |
+
mask_ratio: mask ratio for uniform random masking
|
| 183 |
+
|
| 184 |
+
returns:
|
| 185 |
+
- cls_embeds: [B, 1, D]
|
| 186 |
+
- reg_embeds: [B, R, D]
|
| 187 |
+
- patch_embeds: [B, L, D], where L is the number of visible patches
|
| 188 |
+
- mask: observed mask, 1 = observed, 0 = unobserved. same shape as input
|
| 189 |
+
- mask_ids: indices of visible patches [B L]
|
| 190 |
+
- token_mask: valid token mask for padded per-sample masking [B L]
|
| 191 |
+
"""
|
| 192 |
+
# apply mask to the input
|
| 193 |
+
if mask is not None:
|
| 194 |
+
mask = mask.to(device=x.device, dtype=torch.bool).expand_as(x)
|
| 195 |
+
x = x.masked_fill(~mask, 0)
|
| 196 |
+
|
| 197 |
+
# patchify input
|
| 198 |
+
x = self.patchify(x)
|
| 199 |
+
B, N, P = x.shape
|
| 200 |
+
|
| 201 |
+
# patchify mask and apply dropout style scaling
|
| 202 |
+
if mask is not None:
|
| 203 |
+
mask_patches = self.patchify(mask)
|
| 204 |
+
patch_num_obs = mask_patches.sum(dim=-1)
|
| 205 |
+
patch_mask = patch_num_obs > 0
|
| 206 |
+
if self.mask_drop_scale:
|
| 207 |
+
patch_num_obs = patch_num_obs.to(x.dtype)
|
| 208 |
+
x = x * (P / patch_num_obs.unsqueeze(-1).clamp(min=1.0))
|
| 209 |
+
elif mask_ratio is not None:
|
| 210 |
+
patch_mask = torch.ones((B, N), dtype=torch.bool, device=x.device)
|
| 211 |
+
mask_patches = patch_mask.unsqueeze(-1).expand(-1, -1, P)
|
| 212 |
+
else:
|
| 213 |
+
patch_mask = mask_patches = None
|
| 214 |
+
|
| 215 |
+
# patch and position embed
|
| 216 |
+
x = self.patch_embed(x)
|
| 217 |
+
x = self.pos_embed(x)
|
| 218 |
+
|
| 219 |
+
if mask is not None or mask_ratio is not None:
|
| 220 |
+
mask_ratio = 0.0 if mask_ratio is None else mask_ratio
|
| 221 |
+
patch_mask, mask_ids, token_mask = pad_patch_mask(
|
| 222 |
+
patch_mask,
|
| 223 |
+
mask_ratio=mask_ratio,
|
| 224 |
+
shuffle=mask_ratio > 0,
|
| 225 |
+
pad_to_multiple=pad_to_multiple,
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
mask_patches = mask_patches & patch_mask.unsqueeze(-1)
|
| 229 |
+
mask = self.patchify.unpatchify(mask_patches)
|
| 230 |
+
x = x.gather(1, mask_ids.unsqueeze(-1).expand(-1, -1, x.shape[-1]))
|
| 231 |
+
else:
|
| 232 |
+
mask_ids = None
|
| 233 |
+
token_mask = None
|
| 234 |
+
|
| 235 |
+
cls_embeds, reg_embeds, patch_embeds = self.forward_patch_embeds(
|
| 236 |
+
x,
|
| 237 |
+
token_mask=token_mask,
|
| 238 |
+
)
|
| 239 |
+
return cls_embeds, reg_embeds, patch_embeds, mask, mask_ids, token_mask
|
| 240 |
+
|
| 241 |
+
def forward_patch_embeds(
|
| 242 |
+
self,
|
| 243 |
+
x: Float[Tensor, "B L D"],
|
| 244 |
+
token_mask: Tensor | None = None,
|
| 245 |
+
) -> tuple[
|
| 246 |
+
Float[Tensor, "B 1 D"] | None,
|
| 247 |
+
Float[Tensor, "B R D"] | None,
|
| 248 |
+
Float[Tensor, "B L D"],
|
| 249 |
+
]:
|
| 250 |
+
B = x.shape[0]
|
| 251 |
+
if token_mask is None:
|
| 252 |
+
token_mask = torch.ones(x.shape[:2], dtype=torch.bool, device=x.device)
|
| 253 |
+
x = self.cat_tokens(x)
|
| 254 |
+
token_mask = self.cat_token_mask(token_mask, B)
|
| 255 |
+
jagged_batch = JaggedBatch.from_mask(token_mask)
|
| 256 |
+
x = x[token_mask]
|
| 257 |
+
for block in self.blocks:
|
| 258 |
+
x = block(x, jagged_batch=jagged_batch)
|
| 259 |
+
x = self.norm(x)
|
| 260 |
+
x = unpack_tokens(x, token_mask)
|
| 261 |
+
|
| 262 |
+
cls_embeds, reg_embeds, patch_embeds = self.chunk_tokens(x)
|
| 263 |
+
return cls_embeds, reg_embeds, patch_embeds
|
| 264 |
+
|
| 265 |
+
def forward_visible_ids(
|
| 266 |
+
self,
|
| 267 |
+
x: Tensor,
|
| 268 |
+
visible_ids: Int[Tensor, "B L"],
|
| 269 |
+
img_mask: Tensor | None = None,
|
| 270 |
+
) -> tuple[
|
| 271 |
+
Float[Tensor, "B 1 D"] | None,
|
| 272 |
+
Float[Tensor, "B R D"] | None,
|
| 273 |
+
Float[Tensor, "B L D"],
|
| 274 |
+
]:
|
| 275 |
+
if img_mask is not None:
|
| 276 |
+
img_mask = img_mask.to(device=x.device, dtype=torch.bool).expand_as(x)
|
| 277 |
+
x = x.masked_fill(~img_mask, 0)
|
| 278 |
+
|
| 279 |
+
x = self.patchify(x)
|
| 280 |
+
if self.mask_drop_scale and img_mask is not None:
|
| 281 |
+
mask_patches = self.patchify(img_mask)
|
| 282 |
+
patch_num_obs = mask_patches.sum(dim=-1).to(x.dtype)
|
| 283 |
+
x = x * (self.patchify.patch_dim / patch_num_obs.unsqueeze(-1).clamp(min=1.0))
|
| 284 |
+
x = self.patch_embed(x)
|
| 285 |
+
x = self.pos_embed(x)
|
| 286 |
+
visible_ids = visible_ids.to(device=x.device)
|
| 287 |
+
x = x.gather(1, visible_ids.unsqueeze(-1).expand(-1, -1, x.shape[-1]))
|
| 288 |
+
return self.forward_patch_embeds(x)
|
| 289 |
+
|
| 290 |
+
def forward_embedding(
|
| 291 |
+
self,
|
| 292 |
+
x: Tensor,
|
| 293 |
+
mask: Tensor | None = None,
|
| 294 |
+
mask_ratio: float | None = None,
|
| 295 |
+
):
|
| 296 |
+
cls_embeds, reg_embeds, patch_embeds, *_ = self.forward(
|
| 297 |
+
x,
|
| 298 |
+
mask=mask,
|
| 299 |
+
mask_ratio=mask_ratio,
|
| 300 |
+
)
|
| 301 |
+
return cls_embeds, reg_embeds, patch_embeds
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
class MaskedDecoder(nn.Module):
|
| 305 |
+
"""Self-attention MAE decoder supporting sparse subset decoding via pred_ids."""
|
| 306 |
+
|
| 307 |
+
def __init__(
|
| 308 |
+
self,
|
| 309 |
+
pos_embed: nn.Module,
|
| 310 |
+
head: nn.Module | None = None,
|
| 311 |
+
input_dim: int | None = None,
|
| 312 |
+
depth: int = 12,
|
| 313 |
+
embed_dim: int = 768,
|
| 314 |
+
num_heads: int = 12,
|
| 315 |
+
qkv_bias: bool = True,
|
| 316 |
+
proj_bias: bool = True,
|
| 317 |
+
mlp_ratio: int | float = 4,
|
| 318 |
+
class_token: bool = True,
|
| 319 |
+
no_embed_class: bool = False,
|
| 320 |
+
final_norm: bool = True,
|
| 321 |
+
):
|
| 322 |
+
super().__init__()
|
| 323 |
+
input_dim = embed_dim if input_dim is None else input_dim
|
| 324 |
+
self.has_class_token = class_token
|
| 325 |
+
self.no_embed_class = no_embed_class
|
| 326 |
+
|
| 327 |
+
self.cls_token = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
|
| 328 |
+
self.cls_token_pos = (
|
| 329 |
+
nn.Parameter(torch.empty(1, 1, embed_dim))
|
| 330 |
+
if class_token and not no_embed_class
|
| 331 |
+
else None
|
| 332 |
+
)
|
| 333 |
+
self.mask_token = nn.Parameter(torch.empty(1, 1, embed_dim))
|
| 334 |
+
|
| 335 |
+
# decoder position embedding, encodes query position information into masks
|
| 336 |
+
self.pos_embed = pos_embed
|
| 337 |
+
|
| 338 |
+
self.proj = nn.Identity() if input_dim == embed_dim else nn.Linear(input_dim, embed_dim)
|
| 339 |
+
|
| 340 |
+
self.blocks = nn.ModuleList(
|
| 341 |
+
[
|
| 342 |
+
Block(
|
| 343 |
+
dim=embed_dim,
|
| 344 |
+
num_heads=num_heads,
|
| 345 |
+
qkv_bias=qkv_bias,
|
| 346 |
+
proj_bias=proj_bias,
|
| 347 |
+
mlp_ratio=mlp_ratio,
|
| 348 |
+
)
|
| 349 |
+
for _ in range(depth)
|
| 350 |
+
]
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
self.norm = LayerNorm(embed_dim) if final_norm else nn.Identity()
|
| 354 |
+
|
| 355 |
+
# optional injected prediction head
|
| 356 |
+
self.head = nn.Identity() if head is None else head
|
| 357 |
+
|
| 358 |
+
self.reset_parameters()
|
| 359 |
+
|
| 360 |
+
def extra_repr(self):
|
| 361 |
+
return f"class_token={self.has_class_token}, no_embed_class={self.no_embed_class}"
|
| 362 |
+
|
| 363 |
+
def reset_parameters(self) -> None:
|
| 364 |
+
# official mae initializes decoder cls token to zeros
|
| 365 |
+
# although perhaps this was an oversight
|
| 366 |
+
if self.cls_token is not None:
|
| 367 |
+
nn.init.zeros_(self.cls_token)
|
| 368 |
+
if self.cls_token_pos is not None:
|
| 369 |
+
nn.init.trunc_normal_(self.cls_token_pos, std=0.02)
|
| 370 |
+
nn.init.trunc_normal_(self.mask_token, std=0.02)
|
| 371 |
+
|
| 372 |
+
def cat_tokens(self, x: Tensor) -> Tensor:
|
| 373 |
+
if not self.has_class_token:
|
| 374 |
+
return x
|
| 375 |
+
cls_token = self.cls_token
|
| 376 |
+
if not self.no_embed_class:
|
| 377 |
+
cls_token = cls_token + self.cls_token_pos
|
| 378 |
+
return torch.cat([cls_token.expand(x.shape[0], -1, -1), x], dim=1)
|
| 379 |
+
|
| 380 |
+
def cat_token_mask(self, token_mask: Tensor, batch_size: int) -> Tensor:
|
| 381 |
+
if self.has_class_token:
|
| 382 |
+
cls_mask = torch.ones(
|
| 383 |
+
(batch_size, 1),
|
| 384 |
+
dtype=torch.bool,
|
| 385 |
+
device=token_mask.device,
|
| 386 |
+
)
|
| 387 |
+
token_mask = torch.cat([cls_mask, token_mask], dim=1)
|
| 388 |
+
return token_mask
|
| 389 |
+
|
| 390 |
+
def chunk_tokens(self, x: Tensor) -> tuple[Tensor | None, Tensor]:
|
| 391 |
+
cls_offset = int(self.has_class_token)
|
| 392 |
+
cls = x[:, :cls_offset] if self.has_class_token else None
|
| 393 |
+
patch = x[:, cls_offset:, :]
|
| 394 |
+
return cls, patch
|
| 395 |
+
|
| 396 |
+
def forward(
|
| 397 |
+
self,
|
| 398 |
+
embeds: Float[Tensor, "B L D"],
|
| 399 |
+
embed_ids: Int[Tensor, "B L"] | None = None,
|
| 400 |
+
pred_ids: Int[Tensor, "B Q"] | None = None,
|
| 401 |
+
embed_token_mask: Tensor | None = None,
|
| 402 |
+
pred_token_mask: Tensor | None = None,
|
| 403 |
+
packed_output: bool = False,
|
| 404 |
+
) -> Float[Tensor, "B Q P"] | Float[Tensor, "T P"]:
|
| 405 |
+
"""
|
| 406 |
+
embeds: input patch embeddings.
|
| 407 |
+
embed_ids: optional patch indices for input embeddings. If not provided, no
|
| 408 |
+
position will be added to the embeddings.
|
| 409 |
+
pred_ids: patch indices of query mask positions. If None, decode *all* patches.
|
| 410 |
+
|
| 411 |
+
returns:
|
| 412 |
+
- pred [B, Q, P] where Q is the number of prediction patches and P is the output
|
| 413 |
+
dimension
|
| 414 |
+
"""
|
| 415 |
+
B, L, _ = embeds.shape
|
| 416 |
+
|
| 417 |
+
Q = self.pos_embed.num_patches if pred_ids is None else pred_ids.shape[1]
|
| 418 |
+
mask = self.mask_token.expand(B, Q, -1)
|
| 419 |
+
mask = self.pos_embed(mask, pos_ids=pred_ids)
|
| 420 |
+
|
| 421 |
+
embeds = self.proj(embeds)
|
| 422 |
+
|
| 423 |
+
if embed_ids is not None:
|
| 424 |
+
embeds = self.pos_embed(embeds, pos_ids=embed_ids)
|
| 425 |
+
if embed_token_mask is None:
|
| 426 |
+
embed_token_mask = torch.ones((B, L), dtype=torch.bool, device=embeds.device)
|
| 427 |
+
if pred_token_mask is None:
|
| 428 |
+
pred_token_mask = torch.ones((B, Q), dtype=torch.bool, device=embeds.device)
|
| 429 |
+
x = torch.cat([embeds, mask], dim=1)
|
| 430 |
+
token_mask = torch.cat([embed_token_mask, pred_token_mask], dim=1)
|
| 431 |
+
|
| 432 |
+
x = self.cat_tokens(x)
|
| 433 |
+
token_mask = self.cat_token_mask(token_mask, B)
|
| 434 |
+
jagged_batch = JaggedBatch.from_mask(token_mask)
|
| 435 |
+
x = x[token_mask]
|
| 436 |
+
# Keep headroom for rare maximum-length PSP batches.
|
| 437 |
+
checkpoint_start = max(0, len(self.blocks) - 2)
|
| 438 |
+
for block_index, block in enumerate(self.blocks):
|
| 439 |
+
if self.training and torch.is_grad_enabled() and block_index >= checkpoint_start:
|
| 440 |
+
x = checkpoint(block, x, jagged_batch, use_reentrant=False)
|
| 441 |
+
else:
|
| 442 |
+
x = block(x, jagged_batch=jagged_batch)
|
| 443 |
+
|
| 444 |
+
x = self.norm(x)
|
| 445 |
+
if packed_output:
|
| 446 |
+
pred_offset = int(self.has_class_token) + L
|
| 447 |
+
prediction_mask = F.pad(pred_token_mask, (pred_offset, 0))
|
| 448 |
+
return self.head(x[prediction_mask[token_mask]])
|
| 449 |
+
|
| 450 |
+
x = unpack_tokens(x, token_mask)
|
| 451 |
+
_, x = self.chunk_tokens(x)
|
| 452 |
+
|
| 453 |
+
pred = x[:, L:]
|
| 454 |
+
pred = pred.masked_fill(~pred_token_mask.unsqueeze(-1), 0)
|
| 455 |
+
pred = self.head(pred)
|
| 456 |
+
return pred
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
class MaskedAutoencoderViT(nn.Module, PyTorchModelHubMixin):
|
| 460 |
+
def __init__(
|
| 461 |
+
self,
|
| 462 |
+
img_size: int | tuple[int, int, int] = (208, 240, 208),
|
| 463 |
+
patch_size: int | tuple[int, int, int] = (16, 16, 16),
|
| 464 |
+
in_chans: int = 1,
|
| 465 |
+
depth: int = 12,
|
| 466 |
+
embed_dim: int = 768,
|
| 467 |
+
num_heads: int = 12,
|
| 468 |
+
decoder_depth: int = 4,
|
| 469 |
+
decoder_embed_dim: int | None = 512,
|
| 470 |
+
decoder_num_heads: int | None = 16, # default from mae, head dim = 32
|
| 471 |
+
qkv_bias: bool = True,
|
| 472 |
+
proj_bias: bool = True,
|
| 473 |
+
mlp_ratio: int | float = 4,
|
| 474 |
+
class_token: bool = True,
|
| 475 |
+
reg_tokens: int = 0,
|
| 476 |
+
no_embed_class: bool = False,
|
| 477 |
+
drop_path_rate: float = 0.0,
|
| 478 |
+
mask_drop_scale: bool = False,
|
| 479 |
+
no_decode_pos: bool = False,
|
| 480 |
+
pos_embed: Literal["abs", "sep", "sincos"] = "sincos",
|
| 481 |
+
target_norm: Literal["none", "global", "slice", "patch"] | None = None,
|
| 482 |
+
):
|
| 483 |
+
super().__init__()
|
| 484 |
+
img_size = _to_3d_tuple(img_size, "img_size")
|
| 485 |
+
patch_size = _to_3d_tuple(patch_size, "patch_size")
|
| 486 |
+
|
| 487 |
+
self.no_decode_pos = no_decode_pos # don't pos encode embeddings in decoder
|
| 488 |
+
|
| 489 |
+
# patchify reshapes input into sequence of flattened patches, shape [B, N, P]
|
| 490 |
+
ndim = 3
|
| 491 |
+
patchify = Patchify3D(img_size, patch_size, in_chans=in_chans)
|
| 492 |
+
|
| 493 |
+
# linear patch embedding P -> D
|
| 494 |
+
patch_embed = nn.Linear(patchify.patch_dim, embed_dim)
|
| 495 |
+
|
| 496 |
+
# position embedding
|
| 497 |
+
# separable position embedding decouples the first spatial axis from the
|
| 498 |
+
# others. Fixed sin/cos embeddings are the default for sMRI volumes.
|
| 499 |
+
if pos_embed == "sincos":
|
| 500 |
+
pos_embed_layer = SinCosPosEmbed3D
|
| 501 |
+
else:
|
| 502 |
+
pos_embed_layer = {"abs": AbsolutePosEmbed, "sep": SeparablePosEmbed}[pos_embed]
|
| 503 |
+
pos_embed = pos_embed_layer(embed_dim, patchify.grid_size)
|
| 504 |
+
|
| 505 |
+
# encoder. for inference, this model can be extracted and used like a regular vit
|
| 506 |
+
self.encoder = MaskedEncoder(
|
| 507 |
+
patchify=patchify,
|
| 508 |
+
patch_embed=patch_embed,
|
| 509 |
+
pos_embed=pos_embed,
|
| 510 |
+
depth=depth,
|
| 511 |
+
embed_dim=embed_dim,
|
| 512 |
+
num_heads=num_heads,
|
| 513 |
+
qkv_bias=qkv_bias,
|
| 514 |
+
proj_bias=proj_bias,
|
| 515 |
+
mlp_ratio=mlp_ratio,
|
| 516 |
+
class_token=class_token,
|
| 517 |
+
reg_tokens=reg_tokens,
|
| 518 |
+
no_embed_class=no_embed_class,
|
| 519 |
+
drop_path_rate=drop_path_rate,
|
| 520 |
+
mask_drop_scale=mask_drop_scale,
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
self.pred_patchify = patchify
|
| 524 |
+
|
| 525 |
+
# fall back to encoder architecture width
|
| 526 |
+
decoder_embed_dim = decoder_embed_dim or embed_dim
|
| 527 |
+
decoder_num_heads = decoder_num_heads or num_heads
|
| 528 |
+
|
| 529 |
+
decoder_pos_embed = pos_embed_layer(decoder_embed_dim, self.pred_patchify.grid_size)
|
| 530 |
+
# we might want to try tying the weights of the prediction head to the patch
|
| 531 |
+
# embedding at some point.
|
| 532 |
+
decoder_head = nn.Linear(decoder_embed_dim, self.pred_patchify.patch_dim)
|
| 533 |
+
|
| 534 |
+
self.decoder = MaskedDecoder(
|
| 535 |
+
pos_embed=decoder_pos_embed,
|
| 536 |
+
head=decoder_head,
|
| 537 |
+
input_dim=embed_dim,
|
| 538 |
+
depth=decoder_depth,
|
| 539 |
+
embed_dim=decoder_embed_dim,
|
| 540 |
+
num_heads=decoder_num_heads,
|
| 541 |
+
qkv_bias=qkv_bias,
|
| 542 |
+
proj_bias=proj_bias,
|
| 543 |
+
mlp_ratio=mlp_ratio,
|
| 544 |
+
class_token=class_token,
|
| 545 |
+
no_embed_class=no_embed_class,
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
# mae style target normalization
|
| 549 |
+
# dim is relative to an unflattened embedding tensor of shape [B, *grid_size, D]
|
| 550 |
+
if target_norm not in {"none", None}:
|
| 551 |
+
norm_dim = {
|
| 552 |
+
"global": tuple(range(1, ndim + 2)), # full sequence
|
| 553 |
+
"slice": tuple(range(2, ndim + 2)), # each depth slice along first dim
|
| 554 |
+
"patch": -1, # normalize each patch independently (mae pix norm loss)
|
| 555 |
+
}[target_norm]
|
| 556 |
+
self.target_norm = Normalize(self.pred_patchify.grid_size, dim=norm_dim)
|
| 557 |
+
else:
|
| 558 |
+
self.target_norm = None
|
| 559 |
+
|
| 560 |
+
self.init_weights()
|
| 561 |
+
|
| 562 |
+
def extra_repr(self):
|
| 563 |
+
return f"no_decode_pos={self.no_decode_pos}"
|
| 564 |
+
|
| 565 |
+
def init_weights(self):
|
| 566 |
+
self.apply(_init_weights)
|
| 567 |
+
|
| 568 |
+
def prepare_targets(self, images: Tensor, img_mask: Tensor | None):
|
| 569 |
+
"""
|
| 570 |
+
images: [B, C, D, H, W]
|
| 571 |
+
img_mask: mask of valid data. only used for computing correct normalization
|
| 572 |
+
stats. same shape as images.
|
| 573 |
+
"""
|
| 574 |
+
targets_patches = self.pred_patchify(images) # [B, N, P]
|
| 575 |
+
|
| 576 |
+
# target normalization
|
| 577 |
+
if self.target_norm is not None:
|
| 578 |
+
# full image data mask used for normalization stats only
|
| 579 |
+
if img_mask is not None:
|
| 580 |
+
img_mask_patches = self.pred_patchify(img_mask)
|
| 581 |
+
else:
|
| 582 |
+
img_mask_patches = None
|
| 583 |
+
targets_patches, *targets_stats = self.target_norm(targets_patches, img_mask_patches)
|
| 584 |
+
else:
|
| 585 |
+
targets_stats = None
|
| 586 |
+
|
| 587 |
+
return targets_patches, targets_stats
|
| 588 |
+
|
| 589 |
+
def prepare_masks(
|
| 590 |
+
self,
|
| 591 |
+
img_mask: Tensor,
|
| 592 |
+
visible_mask: Tensor | None,
|
| 593 |
+
pred_mask: Tensor | None,
|
| 594 |
+
device: torch.device,
|
| 595 |
+
):
|
| 596 |
+
img_mask = img_mask.to(device=device, dtype=torch.bool)
|
| 597 |
+
|
| 598 |
+
if visible_mask is None:
|
| 599 |
+
visible_mask = img_mask
|
| 600 |
+
else:
|
| 601 |
+
visible_mask = img_mask & visible_mask.to(device=device, dtype=torch.bool)
|
| 602 |
+
|
| 603 |
+
if pred_mask is None:
|
| 604 |
+
pred_mask = img_mask
|
| 605 |
+
else:
|
| 606 |
+
pred_mask = img_mask & pred_mask.to(device=device, dtype=torch.bool)
|
| 607 |
+
|
| 608 |
+
return img_mask, visible_mask, pred_mask
|
| 609 |
+
|
| 610 |
+
def prepare_pred_mask(
|
| 611 |
+
self,
|
| 612 |
+
visible_mask: Tensor,
|
| 613 |
+
pred_mask: Tensor | None = None,
|
| 614 |
+
pred_mask_ratio: float | None = None,
|
| 615 |
+
pad_to_multiple: int | None = None,
|
| 616 |
+
):
|
| 617 |
+
"""
|
| 618 |
+
prepare prediction mask by removing visible content
|
| 619 |
+
visible_mask: [B, C, D, H, W], 1 = visible, 0 = invisible
|
| 620 |
+
pred_mask: same shape, 1 = predict, 0 = don't predict
|
| 621 |
+
"""
|
| 622 |
+
if pred_mask is None:
|
| 623 |
+
pred_mask = torch.ones_like(visible_mask)
|
| 624 |
+
|
| 625 |
+
pred_mask = pred_mask & ~visible_mask
|
| 626 |
+
|
| 627 |
+
pred_mask_patches = self.pred_patchify(pred_mask)
|
| 628 |
+
pred_patch_mask = pred_mask_patches.any(dim=-1)
|
| 629 |
+
# Optionally subsample the prediction candidates.
|
| 630 |
+
mask_ratio = 0.0 if pred_mask_ratio is None else pred_mask_ratio
|
| 631 |
+
pred_patch_mask, pred_ids, pred_token_mask = pad_patch_mask(
|
| 632 |
+
pred_patch_mask,
|
| 633 |
+
mask_ratio=mask_ratio,
|
| 634 |
+
# With per-sample padding every candidate is retained when the ratio
|
| 635 |
+
# is zero, so randomizing their order is pure overhead.
|
| 636 |
+
shuffle=mask_ratio > 0,
|
| 637 |
+
pad_to_multiple=pad_to_multiple,
|
| 638 |
+
)
|
| 639 |
+
pred_mask_patches = pred_mask_patches & pred_patch_mask.unsqueeze(-1)
|
| 640 |
+
return pred_mask_patches, pred_ids, pred_token_mask
|
| 641 |
+
|
| 642 |
+
def forward_decoder(
|
| 643 |
+
self,
|
| 644 |
+
patch_embeds: Float[Tensor, "B L D"],
|
| 645 |
+
visible_ids: Int[Tensor, "B L"],
|
| 646 |
+
pred_ids: Int[Tensor, "B Q"] | None,
|
| 647 |
+
visible_token_mask: Tensor | None = None,
|
| 648 |
+
pred_token_mask: Tensor | None = None,
|
| 649 |
+
packed_output: bool = False,
|
| 650 |
+
) -> Float[Tensor, "B Q P"] | Float[Tensor, "T P"]:
|
| 651 |
+
return self.decoder(
|
| 652 |
+
patch_embeds,
|
| 653 |
+
embed_ids=None if self.no_decode_pos else visible_ids,
|
| 654 |
+
pred_ids=pred_ids,
|
| 655 |
+
embed_token_mask=visible_token_mask,
|
| 656 |
+
pred_token_mask=pred_token_mask,
|
| 657 |
+
packed_output=packed_output,
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
def forward_loss(
|
| 661 |
+
self,
|
| 662 |
+
preds: Float[Tensor, "T P"],
|
| 663 |
+
targets_patches: Float[Tensor, "B N P"],
|
| 664 |
+
pred_mask_patches: Float[Tensor, "B N P"],
|
| 665 |
+
pred_ids: Int[Tensor, "B Q"],
|
| 666 |
+
pred_token_mask: Tensor,
|
| 667 |
+
) -> Tensor:
|
| 668 |
+
"""Average valid-voxel MSE within each scan, then average across scans."""
|
| 669 |
+
batch_ids, slot_ids = pred_token_mask.nonzero(as_tuple=True)
|
| 670 |
+
patch_ids = pred_ids[batch_ids, slot_ids]
|
| 671 |
+
targets = targets_patches[batch_ids, patch_ids]
|
| 672 |
+
voxel_mask = pred_mask_patches[batch_ids, patch_ids]
|
| 673 |
+
|
| 674 |
+
patch_errors = ((preds - targets) ** 2 * voxel_mask).sum(dim=1)
|
| 675 |
+
patch_voxels = voxel_mask.sum(dim=1).to(dtype=patch_errors.dtype)
|
| 676 |
+
batch_size = targets_patches.shape[0]
|
| 677 |
+
scan_errors = patch_errors.new_zeros(batch_size).scatter_add_(0, batch_ids, patch_errors)
|
| 678 |
+
scan_voxels = patch_voxels.new_zeros(batch_size).scatter_add_(0, batch_ids, patch_voxels)
|
| 679 |
+
return (scan_errors / scan_voxels).mean()
|
| 680 |
+
|
| 681 |
+
@torch.no_grad()
|
| 682 |
+
def forward_pred_images(
|
| 683 |
+
self,
|
| 684 |
+
preds: Float[Tensor, "B Q P"],
|
| 685 |
+
pred_ids: Int[Tensor, "B Q"],
|
| 686 |
+
pred_token_mask: Tensor | None = None,
|
| 687 |
+
img_mask: Tensor | None = None,
|
| 688 |
+
targets_stats: tuple[Tensor, Tensor] | None = None,
|
| 689 |
+
) -> Tensor:
|
| 690 |
+
B, _, P = preds.shape
|
| 691 |
+
N = self.pred_patchify.num_patches
|
| 692 |
+
if pred_token_mask is not None:
|
| 693 |
+
preds = preds.masked_fill(~pred_token_mask.unsqueeze(-1), 0)
|
| 694 |
+
|
| 695 |
+
preds = torch.zeros((B, N, P), dtype=preds.dtype, device=preds.device).scatter_add_(
|
| 696 |
+
1, pred_ids.unsqueeze(-1).expand(-1, -1, P), preds
|
| 697 |
+
)
|
| 698 |
+
|
| 699 |
+
if targets_stats is not None:
|
| 700 |
+
targets_mean, targets_std = targets_stats
|
| 701 |
+
preds = preds * targets_std + targets_mean
|
| 702 |
+
|
| 703 |
+
pred_images = self.pred_patchify.unpatchify(preds)
|
| 704 |
+
if img_mask is not None:
|
| 705 |
+
pred_images = pred_images.masked_fill(~img_mask, 0)
|
| 706 |
+
return pred_images
|
| 707 |
+
|
| 708 |
+
def forward(
|
| 709 |
+
self,
|
| 710 |
+
images: Tensor,
|
| 711 |
+
img_mask: Tensor,
|
| 712 |
+
mask_ratio: float,
|
| 713 |
+
pred_mask_ratio: float | None = None,
|
| 714 |
+
pad_to_multiple: int | None = None,
|
| 715 |
+
with_state: bool = True,
|
| 716 |
+
) -> Tensor | tuple[Tensor, dict]:
|
| 717 |
+
img_mask, visible_mask, pred_mask = self.prepare_masks(
|
| 718 |
+
img_mask,
|
| 719 |
+
None,
|
| 720 |
+
None,
|
| 721 |
+
device=images.device,
|
| 722 |
+
)
|
| 723 |
+
targets_patches, targets_stats = self.prepare_targets(images, img_mask)
|
| 724 |
+
|
| 725 |
+
(
|
| 726 |
+
cls_embeds,
|
| 727 |
+
reg_embeds,
|
| 728 |
+
patch_embeds,
|
| 729 |
+
visible_mask,
|
| 730 |
+
visible_ids,
|
| 731 |
+
visible_token_mask,
|
| 732 |
+
) = self.encoder(
|
| 733 |
+
images,
|
| 734 |
+
mask=visible_mask,
|
| 735 |
+
mask_ratio=mask_ratio,
|
| 736 |
+
pad_to_multiple=pad_to_multiple,
|
| 737 |
+
)
|
| 738 |
+
|
| 739 |
+
pred_mask_patches, pred_ids, pred_token_mask = self.prepare_pred_mask(
|
| 740 |
+
visible_mask,
|
| 741 |
+
pred_mask=pred_mask,
|
| 742 |
+
pred_mask_ratio=pred_mask_ratio,
|
| 743 |
+
pad_to_multiple=pad_to_multiple,
|
| 744 |
+
)
|
| 745 |
+
|
| 746 |
+
preds = self.forward_decoder(
|
| 747 |
+
patch_embeds,
|
| 748 |
+
visible_ids,
|
| 749 |
+
pred_ids,
|
| 750 |
+
visible_token_mask=visible_token_mask,
|
| 751 |
+
pred_token_mask=pred_token_mask,
|
| 752 |
+
packed_output=not with_state,
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
+
loss_preds = preds if not with_state else preds[pred_token_mask]
|
| 756 |
+
loss = self.forward_loss(
|
| 757 |
+
loss_preds,
|
| 758 |
+
targets_patches,
|
| 759 |
+
pred_mask_patches,
|
| 760 |
+
pred_ids,
|
| 761 |
+
pred_token_mask,
|
| 762 |
+
)
|
| 763 |
+
|
| 764 |
+
if not with_state:
|
| 765 |
+
return loss
|
| 766 |
+
|
| 767 |
+
pred_mask = self.pred_patchify.unpatchify(pred_mask_patches)
|
| 768 |
+
pred_images = self.forward_pred_images(
|
| 769 |
+
preds,
|
| 770 |
+
pred_ids,
|
| 771 |
+
pred_token_mask=pred_token_mask,
|
| 772 |
+
img_mask=img_mask,
|
| 773 |
+
targets_stats=targets_stats,
|
| 774 |
+
)
|
| 775 |
+
|
| 776 |
+
state = {
|
| 777 |
+
"targets_patches": targets_patches,
|
| 778 |
+
"targets_stats": targets_stats,
|
| 779 |
+
"patch_embeds": patch_embeds,
|
| 780 |
+
"cls_embeds": cls_embeds,
|
| 781 |
+
"reg_embeds": reg_embeds,
|
| 782 |
+
"visible_mask": visible_mask,
|
| 783 |
+
"visible_ids": visible_ids,
|
| 784 |
+
"visible_token_mask": visible_token_mask,
|
| 785 |
+
"pred_mask": pred_mask,
|
| 786 |
+
"pred_ids": pred_ids,
|
| 787 |
+
"pred_token_mask": pred_token_mask,
|
| 788 |
+
"preds": preds,
|
| 789 |
+
"pred_images": pred_images,
|
| 790 |
+
}
|
| 791 |
+
return loss, state
|
| 792 |
+
|
| 793 |
+
def forward_embedding(
|
| 794 |
+
self,
|
| 795 |
+
x: Tensor,
|
| 796 |
+
mask: Tensor | None = None,
|
| 797 |
+
mask_ratio: float | None = None,
|
| 798 |
+
):
|
| 799 |
+
return self.encoder.forward_embedding(x, mask=mask, mask_ratio=mask_ratio)
|
| 800 |
+
|
| 801 |
+
|
| 802 |
+
class MaskedViT(MaskedEncoder, PyTorchModelHubMixin):
|
| 803 |
+
def __init__(
|
| 804 |
+
self,
|
| 805 |
+
img_size: int | tuple[int, int, int] = (208, 240, 208),
|
| 806 |
+
in_chans: int = 1,
|
| 807 |
+
patch_size: int | tuple[int, int, int] = (16, 16, 16),
|
| 808 |
+
depth: int = 12,
|
| 809 |
+
embed_dim: int = 768,
|
| 810 |
+
num_heads: int = 12,
|
| 811 |
+
qkv_bias: bool = True,
|
| 812 |
+
proj_bias: bool = True,
|
| 813 |
+
mlp_ratio: int | float = 4,
|
| 814 |
+
class_token: bool = True,
|
| 815 |
+
reg_tokens: int = 0,
|
| 816 |
+
no_embed_class: bool = False,
|
| 817 |
+
final_norm: bool = True,
|
| 818 |
+
drop_path_rate: float = 0.0,
|
| 819 |
+
mask_drop_scale: bool = False,
|
| 820 |
+
pos_embed: Literal["abs", "sep", "sincos"] = "sincos",
|
| 821 |
+
):
|
| 822 |
+
img_size = _to_3d_tuple(img_size, "img_size")
|
| 823 |
+
patch_size = _to_3d_tuple(patch_size, "patch_size")
|
| 824 |
+
|
| 825 |
+
patchify = Patchify3D(img_size, patch_size, in_chans=in_chans)
|
| 826 |
+
patch_embed = nn.Linear(patchify.patch_dim, embed_dim)
|
| 827 |
+
if pos_embed == "sincos":
|
| 828 |
+
pos_embed_layer = SinCosPosEmbed3D
|
| 829 |
+
else:
|
| 830 |
+
pos_embed_layer = {"abs": AbsolutePosEmbed, "sep": SeparablePosEmbed}[pos_embed]
|
| 831 |
+
pos_embed = pos_embed_layer(embed_dim, patchify.grid_size)
|
| 832 |
+
|
| 833 |
+
super().__init__(
|
| 834 |
+
patchify=patchify,
|
| 835 |
+
patch_embed=patch_embed,
|
| 836 |
+
pos_embed=pos_embed,
|
| 837 |
+
depth=depth,
|
| 838 |
+
embed_dim=embed_dim,
|
| 839 |
+
num_heads=num_heads,
|
| 840 |
+
qkv_bias=qkv_bias,
|
| 841 |
+
proj_bias=proj_bias,
|
| 842 |
+
mlp_ratio=mlp_ratio,
|
| 843 |
+
class_token=class_token,
|
| 844 |
+
reg_tokens=reg_tokens,
|
| 845 |
+
no_embed_class=no_embed_class,
|
| 846 |
+
final_norm=final_norm,
|
| 847 |
+
drop_path_rate=drop_path_rate,
|
| 848 |
+
mask_drop_scale=mask_drop_scale,
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
self.init_weights()
|
| 852 |
+
|
| 853 |
+
def init_weights(self):
|
| 854 |
+
self.apply(_init_weights)
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
def _to_3d_tuple(value: int | Sequence[int], name: str) -> tuple[int, int, int]:
|
| 858 |
+
if isinstance(value, int):
|
| 859 |
+
return (value, value, value)
|
| 860 |
+
if len(value) != 3:
|
| 861 |
+
raise ValueError(f"{name} must have exactly 3 spatial dimensions, got {tuple(value)}")
|
| 862 |
+
return tuple(int(item) for item in value)
|
| 863 |
+
|
| 864 |
+
|
| 865 |
+
# JAX ViT xavier uniform init
|
| 866 |
+
# https://github.com/facebookresearch/capi/blob/main/model.py
|
| 867 |
+
def _init_weights(m: nn.Module) -> None:
|
| 868 |
+
if isinstance(m, nn.Linear):
|
| 869 |
+
nn.init.xavier_uniform_(m.weight)
|
| 870 |
+
if m.bias is not None:
|
| 871 |
+
nn.init.constant_(m.bias, 0)
|
| 872 |
+
elif isinstance(m, nn.LayerNorm) and m.elementwise_affine:
|
| 873 |
+
nn.init.constant_(m.weight, 1.0)
|
| 874 |
+
if m.bias is not None:
|
| 875 |
+
nn.init.constant_(m.bias, 0)
|
| 876 |
+
|
| 877 |
+
|
| 878 |
+
def _create_vit(**kwargs):
|
| 879 |
+
model = MaskedViT(**kwargs)
|
| 880 |
+
return model
|
| 881 |
+
|
| 882 |
+
|
| 883 |
+
def _create_mae_vit(**kwargs):
|
| 884 |
+
model = MaskedAutoencoderViT(**kwargs)
|
| 885 |
+
return model
|
| 886 |
+
|
| 887 |
+
|
| 888 |
+
def mae_vit_small(**kwargs):
|
| 889 |
+
model_args = dict(embed_dim=384, depth=12, num_heads=6)
|
| 890 |
+
return _create_mae_vit(**model_args, **kwargs)
|
| 891 |
+
|
| 892 |
+
|
| 893 |
+
def mae_vit_base(**kwargs):
|
| 894 |
+
model_args = dict(embed_dim=768, depth=12, num_heads=12)
|
| 895 |
+
return _create_mae_vit(**model_args, **kwargs)
|
| 896 |
+
|
| 897 |
+
|
| 898 |
+
def mae_vit_large(**kwargs):
|
| 899 |
+
model_args = dict(embed_dim=1024, depth=24, num_heads=16)
|
| 900 |
+
return _create_mae_vit(**model_args, **kwargs)
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
def mae_vit_huge(**kwargs):
|
| 904 |
+
model_args = dict(embed_dim=1280, depth=32, num_heads=16)
|
| 905 |
+
return _create_mae_vit(**model_args, **kwargs)
|
| 906 |
+
|
| 907 |
+
|
| 908 |
+
# "patch embed" baseline model, depth 0 ViT (hah)
|
| 909 |
+
def patch_embed_small(**kwargs):
|
| 910 |
+
model_args = dict(embed_dim=384, depth=0)
|
| 911 |
+
return _create_vit(**model_args, **kwargs)
|
| 912 |
+
|
| 913 |
+
|
| 914 |
+
def patch_embed_base(**kwargs):
|
| 915 |
+
model_args = dict(embed_dim=768, depth=0)
|
| 916 |
+
return _create_vit(**model_args, **kwargs)
|
finetune/fomo_tune_baseline/output/task3/config.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task: task3
|
| 2 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 3 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 4 |
+
name: task3
|
| 5 |
+
device: cuda
|
| 6 |
+
seed: 4466
|
finetune/fomo_tune_baseline/output/task3/log.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
16:34:12 run task3 (git 1df2e5d)
|
| 2 |
+
16:34:12 config:
|
| 3 |
+
task: task3
|
| 4 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 5 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 6 |
+
name: task3
|
| 7 |
+
device: cuda
|
| 8 |
+
seed: 4466
|
| 9 |
+
16:35:16 dataset: 494 subjects, age 19-80 mean 45.2
|
| 10 |
+
16:38:39 fold 1/20 n=25 mae=3.91 (190s)
|
| 11 |
+
16:38:51 fold 2/20 n=25 mae=4.50 (202s)
|
| 12 |
+
16:38:57 fold 3/20 n=25 mae=3.74 (208s)
|
| 13 |
+
16:39:03 fold 4/20 n=25 mae=3.58 (214s)
|
| 14 |
+
16:39:09 fold 5/20 n=25 mae=3.57 (220s)
|
| 15 |
+
16:39:14 fold 6/20 n=25 mae=3.12 (226s)
|
| 16 |
+
16:39:20 fold 7/20 n=25 mae=3.42 (232s)
|
| 17 |
+
16:39:26 fold 8/20 n=25 mae=3.12 (238s)
|
| 18 |
+
16:39:32 fold 9/20 n=25 mae=3.74 (244s)
|
| 19 |
+
16:39:38 fold 10/20 n=25 mae=4.20 (249s)
|
| 20 |
+
16:39:44 fold 11/20 n=25 mae=3.22 (255s)
|
| 21 |
+
16:39:50 fold 12/20 n=25 mae=3.50 (261s)
|
| 22 |
+
16:39:55 fold 13/20 n=25 mae=3.39 (267s)
|
| 23 |
+
16:40:01 fold 14/20 n=25 mae=4.17 (273s)
|
| 24 |
+
16:40:07 fold 15/20 n=24 mae=4.36 (278s)
|
| 25 |
+
16:40:12 fold 16/20 n=24 mae=4.15 (284s)
|
| 26 |
+
16:40:18 fold 17/20 n=24 mae=3.66 (289s)
|
| 27 |
+
16:40:23 fold 18/20 n=24 mae=3.54 (295s)
|
| 28 |
+
16:40:29 fold 19/20 n=24 mae=3.42 (300s)
|
| 29 |
+
16:40:34 fold 20/20 n=24 mae=3.53 (306s)
|
| 30 |
+
16:40:35 result: pearson_r=0.9631 pearson_r_ci_low=0.9567 pearson_r_ci_high=0.9687 mae=3.6910 mae_ci_low=3.4464 mae_ci_high=3.9466 (306s)
|
finetune/fomo_tune_baseline/output/task3/metrics.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"name": "task3", "pearson_r": 0.9630771532969529, "pearson_r_ci_low": 0.9567144985968425, "pearson_r_ci_high": 0.9687044710309238, "mae": 3.6910024558001684, "mae_ci_low": 3.446356968454986, "mae_ci_high": 3.946628273643462, "run_time": 306.1}
|
finetune/fomo_tune_baseline/output/task3/model/config.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task: task3
|
| 2 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 3 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 4 |
+
name: task3
|
| 5 |
+
device: cuda
|
| 6 |
+
seed: 4466
|
finetune/fomo_tune_baseline/output/task5/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/task5/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/task5/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/task5/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/task5/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/task5/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()
|
finetune/fomo_tune_baseline/output/task5/build/fomo_tune/main_task5.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FOMO task 5: polymicrogyria classification, scored by AUROC as the challenge scores it.
|
| 2 |
+
|
| 3 |
+
`Task5Method` 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 48 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 probability out. Both go through `Task5Method.predict`, so every fold exercises
|
| 9 |
+
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
|
| 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.model_selection import KFold
|
| 27 |
+
from sklearn.pipeline import make_pipeline
|
| 28 |
+
from sklearn.preprocessing import StandardScaler
|
| 29 |
+
|
| 30 |
+
from fomo_tune.backbone import load_backbone
|
| 31 |
+
from fomo_tune.utils import git_sha, set_seed, setup_logging
|
| 32 |
+
|
| 33 |
+
logger = logging.getLogger("fomo_tune")
|
| 34 |
+
|
| 35 |
+
Images = dict[str, nib.Nifti1Image]
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class Config:
|
| 40 |
+
task: str = "task5"
|
| 41 |
+
ckpt_path: str = (
|
| 42 |
+
"/data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth"
|
| 43 |
+
)
|
| 44 |
+
output_root: str = "output/fomo_tune"
|
| 45 |
+
name: str = "task5"
|
| 46 |
+
device: str = "cuda"
|
| 47 |
+
seed: int = 4466
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ---- method: the part we tune -----------------------------------------------------------
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class Task5Method:
|
| 54 |
+
"""Frozen sMRI MAE, mean-pooled tokens over the t1w, 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.cache: dict[str, np.ndarray] = {}
|
| 62 |
+
self.head = None
|
| 63 |
+
|
| 64 |
+
@torch.inference_mode()
|
| 65 |
+
def features(self, images: Images) -> np.ndarray:
|
| 66 |
+
"""(D,) per subject. A pure function of the images, so training and inference agree."""
|
| 67 |
+
sample = self.transform(images["t1w"])
|
| 68 |
+
batch = {key: value[None].to(self.device) for key, value in sample.items()}
|
| 69 |
+
|
| 70 |
+
with torch.autocast("cuda", torch.bfloat16, enabled=self.device.type == "cuda"):
|
| 71 |
+
out = self.backbone(batch)
|
| 72 |
+
|
| 73 |
+
patch_embeds = out["patch_embeds"]
|
| 74 |
+
token_mask = out["token_mask"].bool().unsqueeze(-1)
|
| 75 |
+
embed = (patch_embeds * token_mask).sum(dim=1) / token_mask.sum(dim=1)
|
| 76 |
+
return embed[0].float().cpu().numpy()
|
| 77 |
+
|
| 78 |
+
def cached_features(self, row: dict) -> np.ndarray:
|
| 79 |
+
if row["subject"] not in self.cache:
|
| 80 |
+
self.cache[row["subject"]] = self.features(row)
|
| 81 |
+
return self.cache[row["subject"]]
|
| 82 |
+
|
| 83 |
+
def fit(self, rows: list[dict]) -> None:
|
| 84 |
+
X = np.stack([self.cached_features(row) for row in rows])
|
| 85 |
+
y = np.array([row["label"] for row in rows])
|
| 86 |
+
|
| 87 |
+
clf = LogisticRegressionCV(
|
| 88 |
+
Cs=10,
|
| 89 |
+
class_weight="balanced",
|
| 90 |
+
scoring="roc_auc",
|
| 91 |
+
max_iter=1000,
|
| 92 |
+
l1_ratios=(0,),
|
| 93 |
+
use_legacy_attributes=False,
|
| 94 |
+
)
|
| 95 |
+
self.head = make_pipeline(StandardScaler(), clf)
|
| 96 |
+
self.head.fit(X, y)
|
| 97 |
+
self.positive = list(self.head.classes_).index(1)
|
| 98 |
+
|
| 99 |
+
def predict(self, images: Images) -> float:
|
| 100 |
+
"""Positive-class probability. Indexes `classes_` rather than assuming column 1, which
|
| 101 |
+
would silently score the wrong class if the label order differed."""
|
| 102 |
+
X = self.features(images)[None]
|
| 103 |
+
probs = self.head.predict_proba(X)[0]
|
| 104 |
+
return float(probs[self.positive])
|
| 105 |
+
|
| 106 |
+
def save(self, model_dir: Path) -> None:
|
| 107 |
+
"""Everything `load` needs but the backbone weights, which stay wherever `ckpt_path`
|
| 108 |
+
points -- a few hundred KB, so a run saves one without copying a 3.7G checkpoint."""
|
| 109 |
+
model_dir.mkdir(parents=True, exist_ok=True)
|
| 110 |
+
OmegaConf.save(self.cfg, model_dir / "config.yaml")
|
| 111 |
+
joblib.dump({"head": self.head, "positive": self.positive}, model_dir / "head.joblib")
|
| 112 |
+
|
| 113 |
+
@classmethod
|
| 114 |
+
def load(cls, model_dir: Path, **overrides) -> "Task5Method":
|
| 115 |
+
"""Rebuild a fitted method from `save`. Overrides are Config fields, for what differs
|
| 116 |
+
between here and the container -- the backbone path, the device."""
|
| 117 |
+
cfg = OmegaConf.merge(
|
| 118 |
+
OmegaConf.structured(Config), OmegaConf.load(model_dir / "config.yaml"), overrides
|
| 119 |
+
)
|
| 120 |
+
method = cls(cfg)
|
| 121 |
+
state = joblib.load(model_dir / "head.joblib")
|
| 122 |
+
method.head, method.positive = state["head"], state["positive"]
|
| 123 |
+
return method
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# ---- protocol: the part we hold fixed ---------------------------------------------------
|
| 127 |
+
|
| 128 |
+
# Every image the task ships. The method picks which of them it wants, as at inference, where the
|
| 129 |
+
# challenge hands over the modalities whether or not a model uses them.
|
| 130 |
+
IMAGE_COLS = ("t1w",)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def cross_validate(
|
| 134 |
+
rows: list[dict], method: Task5Method, seed: int = 0, n_folds: int = 20
|
| 135 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 136 |
+
"""Out-of-fold score for every subject, each predicted by a head fit on the other folds."""
|
| 137 |
+
y = np.array([row["label"] for row in rows])
|
| 138 |
+
oof = np.zeros(len(rows), dtype=float)
|
| 139 |
+
folds = KFold(n_splits=n_folds, shuffle=True, random_state=seed)
|
| 140 |
+
start = time.perf_counter()
|
| 141 |
+
for fold, (train, test) in enumerate(folds.split(rows)):
|
| 142 |
+
method.fit([rows[i] for i in train])
|
| 143 |
+
for i in test:
|
| 144 |
+
oof[i] = method.predict({key: rows[i][key] for key in IMAGE_COLS})
|
| 145 |
+
logger.info(
|
| 146 |
+
f"fold {fold + 1}/{n_folds} n={len(test)} y={y[test]} "
|
| 147 |
+
f"p={np.round(oof[test], 3)} ({time.perf_counter() - start:.0f}s)"
|
| 148 |
+
)
|
| 149 |
+
return y, oof
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def score(
|
| 153 |
+
y: np.ndarray, oof: np.ndarray, seed: int = 0, n_boot: int = 2000, alpha: float = 0.05
|
| 154 |
+
) -> dict:
|
| 155 |
+
"""AUROC, the challenge metric, plus a percentile CI resampling subjects with replacement."""
|
| 156 |
+
rng = np.random.default_rng(seed)
|
| 157 |
+
samples = []
|
| 158 |
+
for _ in range(n_boot):
|
| 159 |
+
rows = rng.integers(0, len(y), size=len(y))
|
| 160 |
+
if len(np.unique(y[rows])) < 2:
|
| 161 |
+
continue
|
| 162 |
+
samples.append(roc_auc_score(y[rows], oof[rows]))
|
| 163 |
+
|
| 164 |
+
low, high = np.percentile(samples, [100 * alpha / 2, 100 * (1 - alpha / 2)])
|
| 165 |
+
return {
|
| 166 |
+
"auroc": float(roc_auc_score(y, oof)),
|
| 167 |
+
"auroc_ci_low": float(low),
|
| 168 |
+
"auroc_ci_high": float(high),
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
# ---- entrypoints ------------------------------------------------------------------------
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def train(args: argparse.Namespace) -> None:
|
| 176 |
+
# imported here, not at the top, so the container needs no dataset stack to run `predict`
|
| 177 |
+
from fomo_tune.datasets import load_fomo_task5
|
| 178 |
+
|
| 179 |
+
cfg = OmegaConf.merge(OmegaConf.structured(Config), OmegaConf.from_dotlist(args.overrides))
|
| 180 |
+
run_dir = Path(cfg.output_root) / cfg.name
|
| 181 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 182 |
+
|
| 183 |
+
setup_logging(run_dir)
|
| 184 |
+
set_seed(cfg.seed)
|
| 185 |
+
logger.info(f"run {cfg.name} (git {git_sha()})")
|
| 186 |
+
logger.info(f"config:\n{OmegaConf.to_yaml(cfg).rstrip()}")
|
| 187 |
+
OmegaConf.save(cfg, run_dir / "config.yaml")
|
| 188 |
+
|
| 189 |
+
rows = list(load_fomo_task5())
|
| 190 |
+
logger.info(f"dataset: {len(rows)} subjects, {sum(r['label'] for r in rows)} positive")
|
| 191 |
+
|
| 192 |
+
method = Task5Method(cfg)
|
| 193 |
+
start = time.perf_counter()
|
| 194 |
+
y, oof = cross_validate(rows, method)
|
| 195 |
+
run_time = time.perf_counter() - start
|
| 196 |
+
summary = score(y, oof)
|
| 197 |
+
|
| 198 |
+
# the shipped head sees all n subjects, so it is not any of the models scored above
|
| 199 |
+
method.fit(rows)
|
| 200 |
+
method.save(run_dir / "model")
|
| 201 |
+
|
| 202 |
+
record = {"name": cfg.name, **summary, "run_time": round(run_time, 1)}
|
| 203 |
+
(run_dir / "metrics.json").write_text(json.dumps(record) + "\n")
|
| 204 |
+
scores = " ".join(f"{k}={v:.4f}" for k, v in summary.items())
|
| 205 |
+
logger.info(f"result: {scores} ({run_time:.0f}s)")
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def predict(args: argparse.Namespace) -> None:
|
| 209 |
+
"""The challenge contract: a t1 path in, one probability written to `--output`.
|
| 210 |
+
|
| 211 |
+
`/app/predict.py` in the container is a shim over this, so what scores the submission is the
|
| 212 |
+
code cross-validation already ran, not something generated at build time.
|
| 213 |
+
"""
|
| 214 |
+
overrides = {"device": args.device}
|
| 215 |
+
if args.ckpt_path:
|
| 216 |
+
overrides["ckpt_path"] = args.ckpt_path
|
| 217 |
+
method = Task5Method.load(args.model_dir, **overrides)
|
| 218 |
+
|
| 219 |
+
probability = method.predict({"t1w": nib.load(args.t1)})
|
| 220 |
+
|
| 221 |
+
args.output.write_text(f"{probability:.6f}\n")
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def main() -> None:
|
| 225 |
+
parser = argparse.ArgumentParser()
|
| 226 |
+
modes = parser.add_subparsers(required=True)
|
| 227 |
+
|
| 228 |
+
train_parser = modes.add_parser("train", help="cross-validate over the task, then fit and save")
|
| 229 |
+
train_parser.add_argument("overrides", nargs="*", help="config overrides, e.g. device=cpu")
|
| 230 |
+
train_parser.set_defaults(run=train)
|
| 231 |
+
|
| 232 |
+
predict_parser = modes.add_parser("predict", help="one subject, one probability")
|
| 233 |
+
predict_parser.add_argument("--t1", type=Path, required=True)
|
| 234 |
+
predict_parser.add_argument("--output", type=Path, required=True)
|
| 235 |
+
predict_parser.add_argument("--model-dir", type=Path, default=Path("/app/model"))
|
| 236 |
+
predict_parser.add_argument("--ckpt-path", help="overrides the trained config's backbone path")
|
| 237 |
+
predict_parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
|
| 238 |
+
predict_parser.set_defaults(run=predict)
|
| 239 |
+
|
| 240 |
+
args = parser.parse_args()
|
| 241 |
+
args.run(args)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
if __name__ == "__main__":
|
| 245 |
+
main()
|
finetune/fomo_tune_baseline/output/task5/build/fomo_tune/utils.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import random
|
| 3 |
+
import subprocess
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger("fomo_tune")
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def set_seed(seed: int) -> None:
|
| 14 |
+
random.seed(seed)
|
| 15 |
+
np.random.seed(seed)
|
| 16 |
+
torch.manual_seed(seed)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def git_sha() -> str:
|
| 20 |
+
kwargs = dict(cwd=Path(__file__).parent, capture_output=True, text=True, check=True)
|
| 21 |
+
sha = subprocess.run(["git", "rev-parse", "--short", "HEAD"], **kwargs).stdout.strip()
|
| 22 |
+
dirty = subprocess.run(["git", "status", "--porcelain", "-uno"], **kwargs).stdout.strip()
|
| 23 |
+
return f"{sha}-dirty" if dirty else sha
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def setup_logging(run_dir: Path) -> None:
|
| 27 |
+
handlers = [logging.StreamHandler(sys.stdout), logging.FileHandler(run_dir / "log.txt")]
|
| 28 |
+
logger.setLevel(logging.INFO)
|
| 29 |
+
logger.handlers.clear()
|
| 30 |
+
for handler in handlers:
|
| 31 |
+
handler.setFormatter(logging.Formatter("%(asctime)s %(message)s", datefmt="%H:%M:%S"))
|
| 32 |
+
logger.addHandler(handler)
|
| 33 |
+
logger.propagate = False
|
finetune/fomo_tune_baseline/output/task5/build/model/config.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task: task5
|
| 2 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 3 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 4 |
+
name: task5
|
| 5 |
+
device: cuda
|
| 6 |
+
seed: 4466
|
finetune/fomo_tune_baseline/output/task5/build/predict.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
from fomo_tune.main_task5 import main
|
| 5 |
+
|
| 6 |
+
sys.argv = [
|
| 7 |
+
sys.argv[0],
|
| 8 |
+
"predict",
|
| 9 |
+
*sys.argv[1:],
|
| 10 |
+
"--model-dir",
|
| 11 |
+
"/app/model",
|
| 12 |
+
"--ckpt-path",
|
| 13 |
+
"/app/model/backbone.pth",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
main()
|
finetune/fomo_tune_baseline/output/task5/build/smri_mae/config/default_pretrain.yaml
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Name of the run. Used for output directory suffix and wandb.
|
| 2 |
+
name: pretrain
|
| 3 |
+
|
| 4 |
+
# Description of the run. Goes in wandb notes.
|
| 5 |
+
notes: null
|
| 6 |
+
|
| 7 |
+
# Root output directory.
|
| 8 |
+
# The run writes to checkpoints/<name> when name is set.
|
| 9 |
+
output_dir: checkpoints
|
| 10 |
+
|
| 11 |
+
# Standard 3D structural MRI volume size.
|
| 12 |
+
img_size: [208, 240, 208]
|
| 13 |
+
patch_size: 8
|
| 14 |
+
|
| 15 |
+
# Masking.
|
| 16 |
+
mask_ratio: 0.80
|
| 17 |
+
pred_mask_ratio: null
|
| 18 |
+
pad_to_multiple: 32
|
| 19 |
+
|
| 20 |
+
# Model.
|
| 21 |
+
model: mae_vit_large
|
| 22 |
+
model_kwargs:
|
| 23 |
+
# target normalization: null/none, global, slice, or patch.
|
| 24 |
+
target_norm: none
|
| 25 |
+
|
| 26 |
+
no_decode_pos: false
|
| 27 |
+
mask_drop_scale: false
|
| 28 |
+
|
| 29 |
+
class_token: true
|
| 30 |
+
reg_tokens: 0
|
| 31 |
+
no_embed_class: false
|
| 32 |
+
|
| 33 |
+
decoder_depth: 4
|
| 34 |
+
drop_path_rate: 0.0
|
| 35 |
+
|
| 36 |
+
# Datasets.
|
| 37 |
+
datasets:
|
| 38 |
+
fomo_train:
|
| 39 |
+
url: datasets/FOMO_with_dwi/shard.{000000..001620}.tar
|
| 40 |
+
samples_per_epoch: 243200
|
| 41 |
+
shuffle: true
|
| 42 |
+
buffer_size: 8000
|
| 43 |
+
drop_last: true
|
| 44 |
+
|
| 45 |
+
fomo_val:
|
| 46 |
+
url: datasets/FOMO_with_dwi/shard.{001621..001800}.tar
|
| 47 |
+
samples_per_epoch: 26880
|
| 48 |
+
shuffle: false
|
| 49 |
+
buffer_size: 1000
|
| 50 |
+
drop_last: true
|
| 51 |
+
|
| 52 |
+
train_dataset: fomo_train
|
| 53 |
+
eval_datasets:
|
| 54 |
+
- fomo_val
|
| 55 |
+
|
| 56 |
+
# Data loader.
|
| 57 |
+
num_workers: 4
|
| 58 |
+
prefetch_factor: 2
|
| 59 |
+
presend_cuda: true
|
| 60 |
+
|
| 61 |
+
# Optimization.
|
| 62 |
+
epochs: 100
|
| 63 |
+
batch_size: 64
|
| 64 |
+
accum_iter: 1
|
| 65 |
+
|
| 66 |
+
base_lr: 0.001
|
| 67 |
+
min_lr: 1e-6
|
| 68 |
+
warmup_epochs: 10
|
| 69 |
+
weight_decay: 0.05
|
| 70 |
+
betas: [0.9, 0.95]
|
| 71 |
+
clip_grad: 1.0
|
| 72 |
+
|
| 73 |
+
amp: true
|
| 74 |
+
amp_dtype: bfloat16
|
| 75 |
+
|
| 76 |
+
# Checkpointing.
|
| 77 |
+
ckpt: null
|
| 78 |
+
resume: false
|
| 79 |
+
auto_resume: true
|
| 80 |
+
start_epoch: 0
|
| 81 |
+
checkpoint_period: 10
|
| 82 |
+
max_checkpoints: 5
|
| 83 |
+
|
| 84 |
+
# Evaluation.
|
| 85 |
+
eval_period: 10
|
| 86 |
+
|
| 87 |
+
# Sync checkpoints to an R2 bucket using the AWS CLI. Set to a URL to enable.
|
| 88 |
+
r2_sync: null
|
| 89 |
+
|
| 90 |
+
device: cuda
|
| 91 |
+
distributed: false
|
| 92 |
+
seed: 7338
|
| 93 |
+
eval_seed: 7338
|
| 94 |
+
debug: false
|
| 95 |
+
|
| 96 |
+
wandb: false
|
| 97 |
+
wandb_entity: null
|
| 98 |
+
wandb_project: smri-fm
|
finetune/fomo_tune_baseline/output/task5/build/smri_mae/main_pretrain.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Sophont, Inc
|
| 2 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 3 |
+
#
|
| 4 |
+
# References:
|
| 5 |
+
# deit: https://github.com/facebookresearch/deit/blob/main/main.py
|
| 6 |
+
# capi: https://github.com/facebookresearch/capi/blob/main/train_capi.py
|
| 7 |
+
|
| 8 |
+
import argparse
|
| 9 |
+
import datetime
|
| 10 |
+
import json
|
| 11 |
+
import math
|
| 12 |
+
import random
|
| 13 |
+
import subprocess
|
| 14 |
+
import time
|
| 15 |
+
from contextlib import nullcontext
|
| 16 |
+
from functools import partial
|
| 17 |
+
from itertools import islice
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Iterable, Sequence
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn as nn
|
| 23 |
+
import wandb
|
| 24 |
+
import webdataset as wds
|
| 25 |
+
from omegaconf import DictConfig, OmegaConf
|
| 26 |
+
from PIL import Image
|
| 27 |
+
|
| 28 |
+
from matplotlib import pyplot as plt
|
| 29 |
+
from torch import Tensor
|
| 30 |
+
|
| 31 |
+
import data.mri_data as mri_data
|
| 32 |
+
import smri_mae.model_mae as models_mae
|
| 33 |
+
import smri_mae.utils as ut
|
| 34 |
+
import smri_mae.visualization as vis
|
| 35 |
+
|
| 36 |
+
DEFAULT_CONFIG = Path(__file__).parent / "config/default_pretrain.yaml"
|
| 37 |
+
|
| 38 |
+
MODELS_DICT = models_mae.__dict__
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def main(args: DictConfig):
|
| 42 |
+
# setup
|
| 43 |
+
ut.init_distributed_mode(args)
|
| 44 |
+
global_rank = ut.get_rank()
|
| 45 |
+
is_master = global_rank == 0
|
| 46 |
+
world_size = ut.get_world_size()
|
| 47 |
+
device = torch.device(args.device)
|
| 48 |
+
ut.configure_flash_sdpa()
|
| 49 |
+
ut.random_seed(args.seed, rank=global_rank)
|
| 50 |
+
|
| 51 |
+
if args.name and not args.output_dir.endswith(args.name):
|
| 52 |
+
args.output_dir = f"{args.output_dir}/{args.name}"
|
| 53 |
+
output_dir = Path(args.output_dir)
|
| 54 |
+
|
| 55 |
+
if is_master:
|
| 56 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 57 |
+
out_cfg_path = output_dir / "config.yaml"
|
| 58 |
+
if out_cfg_path.exists():
|
| 59 |
+
prev_cfg = OmegaConf.load(out_cfg_path)
|
| 60 |
+
assert args == prev_cfg, "current config doesn't match previous config"
|
| 61 |
+
else:
|
| 62 |
+
OmegaConf.save(args, out_cfg_path)
|
| 63 |
+
|
| 64 |
+
if args.wandb:
|
| 65 |
+
wandb.init(
|
| 66 |
+
entity=args.wandb_entity,
|
| 67 |
+
project=args.wandb_project,
|
| 68 |
+
name=args.name,
|
| 69 |
+
notes=args.notes,
|
| 70 |
+
config=OmegaConf.to_container(args),
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
ut.setup_for_distributed(log_path=output_dir / "log.txt")
|
| 74 |
+
|
| 75 |
+
print("pretraining 3D ViTMAE")
|
| 76 |
+
print(f"start: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 77 |
+
print(f"cwd: {Path.cwd()}")
|
| 78 |
+
print(ut.get_sha())
|
| 79 |
+
print("config:", OmegaConf.to_yaml(args), sep="\n")
|
| 80 |
+
|
| 81 |
+
# data loaders
|
| 82 |
+
train_loader, eval_loaders = create_data_loaders(args)
|
| 83 |
+
|
| 84 |
+
# model
|
| 85 |
+
model = MODELS_DICT[args.model](
|
| 86 |
+
img_size=args.img_size,
|
| 87 |
+
in_chans=args.get("in_chans", 1),
|
| 88 |
+
patch_size=args.patch_size,
|
| 89 |
+
**(args.get("model_kwargs") or {}),
|
| 90 |
+
)
|
| 91 |
+
model.to(device)
|
| 92 |
+
print("model:", model, sep="\n")
|
| 93 |
+
num_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 94 |
+
print(f"num params: {num_params / 1e6:.1f}M")
|
| 95 |
+
|
| 96 |
+
model_without_ddp = model
|
| 97 |
+
if args.distributed:
|
| 98 |
+
model = torch.nn.parallel.DistributedDataParallel(
|
| 99 |
+
model,
|
| 100 |
+
device_ids=[args.gpu],
|
| 101 |
+
gradient_as_bucket_view=True,
|
| 102 |
+
)
|
| 103 |
+
model_without_ddp = model.module
|
| 104 |
+
|
| 105 |
+
# optimizer
|
| 106 |
+
total_batch_size = args.batch_size * args.accum_iter * world_size
|
| 107 |
+
print(
|
| 108 |
+
f"total batch size: {total_batch_size} = "
|
| 109 |
+
f"{args.batch_size} bs per gpu x {args.accum_iter} accum x {world_size} gpus"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
if not args.get("lr"):
|
| 113 |
+
args.lr = args.base_lr * total_batch_size / 256
|
| 114 |
+
print(f"lr: {args.lr:.2e} = {args.base_lr:.2e} x {total_batch_size} / 256")
|
| 115 |
+
else:
|
| 116 |
+
print(f"lr: {args.lr:.2e}")
|
| 117 |
+
|
| 118 |
+
param_groups = ut.get_param_groups(model)
|
| 119 |
+
ut.update_lr(param_groups, args.lr)
|
| 120 |
+
ut.update_wd(param_groups, args.weight_decay)
|
| 121 |
+
# cast or else it corrupts the checkpoint
|
| 122 |
+
betas = tuple(args.betas) if args.betas is not None else None
|
| 123 |
+
optimizer = torch.optim.AdamW(param_groups, betas=betas, fused=True)
|
| 124 |
+
|
| 125 |
+
epoch_num_batches = len(train_loader)
|
| 126 |
+
steps_per_epoch = math.ceil(epoch_num_batches / args.accum_iter)
|
| 127 |
+
total_steps = args.epochs * steps_per_epoch
|
| 128 |
+
warmup_steps = args.warmup_epochs * steps_per_epoch
|
| 129 |
+
lr_schedule = ut.WarmupThenCosine(
|
| 130 |
+
base_value=args.lr,
|
| 131 |
+
final_value=args.min_lr,
|
| 132 |
+
total_iters=total_steps,
|
| 133 |
+
warmup_iters=warmup_steps,
|
| 134 |
+
)
|
| 135 |
+
print(f"full schedule: epochs = {args.epochs} (steps = {total_steps})")
|
| 136 |
+
print(f"warmup: epochs = {args.warmup_epochs} (steps = {warmup_steps})")
|
| 137 |
+
|
| 138 |
+
# loss scaling not needed for bfloat16 (according to timm)
|
| 139 |
+
if args.amp and args.amp_dtype != "bfloat16":
|
| 140 |
+
loss_scaler = torch.GradScaler(device.type)
|
| 141 |
+
else:
|
| 142 |
+
loss_scaler = None
|
| 143 |
+
|
| 144 |
+
# load checkpoint/resume training
|
| 145 |
+
ut.load_model(args, model_without_ddp, optimizer, loss_scaler)
|
| 146 |
+
|
| 147 |
+
print(f"start training for {args.epochs} epochs")
|
| 148 |
+
start_time = time.monotonic()
|
| 149 |
+
for epoch in range(args.start_epoch, args.epochs):
|
| 150 |
+
train_stats = train_one_epoch(
|
| 151 |
+
args,
|
| 152 |
+
model,
|
| 153 |
+
train_loader,
|
| 154 |
+
optimizer,
|
| 155 |
+
loss_scaler,
|
| 156 |
+
lr_schedule,
|
| 157 |
+
epoch,
|
| 158 |
+
device,
|
| 159 |
+
)
|
| 160 |
+
eval_stats = {}
|
| 161 |
+
eval_plots = {}
|
| 162 |
+
eval_period = args.get("eval_period", 1)
|
| 163 |
+
if eval_period and (epoch % eval_period == 0 or epoch == args.epochs - 1):
|
| 164 |
+
for name, loader in eval_loaders.items():
|
| 165 |
+
stats, plots = evaluate(
|
| 166 |
+
args,
|
| 167 |
+
model,
|
| 168 |
+
loader,
|
| 169 |
+
epoch,
|
| 170 |
+
device,
|
| 171 |
+
eval_name=name,
|
| 172 |
+
)
|
| 173 |
+
eval_stats.update(stats)
|
| 174 |
+
eval_plots.update(plots)
|
| 175 |
+
|
| 176 |
+
merged_stats = {"epoch": epoch, **train_stats, **eval_stats}
|
| 177 |
+
if is_master:
|
| 178 |
+
with (output_dir / "log.json").open("a") as f:
|
| 179 |
+
print(json.dumps(merged_stats), file=f)
|
| 180 |
+
|
| 181 |
+
for plot_name, img in eval_plots.items():
|
| 182 |
+
plot_name = plot_name.replace("/", "__")
|
| 183 |
+
img.save(output_dir / f"{plot_name}__{epoch:05d}.png")
|
| 184 |
+
|
| 185 |
+
ut.save_model(args, epoch, model_without_ddp, optimizer, loss_scaler)
|
| 186 |
+
sync_checkpoints_to_r2(args, output_dir)
|
| 187 |
+
|
| 188 |
+
if args.distributed:
|
| 189 |
+
torch.distributed.destroy_process_group()
|
| 190 |
+
|
| 191 |
+
total_time = time.monotonic() - start_time
|
| 192 |
+
print(f"done! training time: {datetime.timedelta(seconds=int(total_time))}")
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def create_data_loaders(args: DictConfig):
|
| 196 |
+
data_loaders = {}
|
| 197 |
+
dataset_names = [args.train_dataset] + args.eval_datasets
|
| 198 |
+
|
| 199 |
+
for dataset_name in dataset_names:
|
| 200 |
+
dataset_config = args.datasets[dataset_name].copy()
|
| 201 |
+
drop_last = dataset_config.pop("drop_last")
|
| 202 |
+
is_train = dataset_name == args.train_dataset
|
| 203 |
+
|
| 204 |
+
print(f"loading dataset: {dataset_name}\n\n{OmegaConf.to_yaml(dataset_config)}")
|
| 205 |
+
shuffle = dataset_config["shuffle"]
|
| 206 |
+
samples_per_epoch = dataset_config.pop("samples_per_epoch")
|
| 207 |
+
dataset = mri_data.make_sparse_wds_dataset(
|
| 208 |
+
dataset_config["url"],
|
| 209 |
+
shuffle=shuffle,
|
| 210 |
+
buffer_size=dataset_config["buffer_size"],
|
| 211 |
+
)
|
| 212 |
+
num_workers = int(args.num_workers)
|
| 213 |
+
loader_kwargs = {
|
| 214 |
+
"batch_size": args.batch_size,
|
| 215 |
+
"collate_fn": partial(mri_data.collate, include_meta=not is_train),
|
| 216 |
+
"shuffle": False,
|
| 217 |
+
"num_workers": num_workers,
|
| 218 |
+
"persistent_workers": num_workers > 0,
|
| 219 |
+
"pin_memory": True,
|
| 220 |
+
"drop_last": drop_last,
|
| 221 |
+
"prefetch_factor": args.prefetch_factor,
|
| 222 |
+
}
|
| 223 |
+
loader = wds.WebLoader(dataset, **loader_kwargs)
|
| 224 |
+
num_batches = samples_per_epoch // (ut.get_world_size() * args.batch_size)
|
| 225 |
+
loader = loader.with_epoch(num_batches)
|
| 226 |
+
loader = loader.with_length(num_batches, silent=True)
|
| 227 |
+
|
| 228 |
+
data_loaders[dataset_name] = loader
|
| 229 |
+
|
| 230 |
+
train_loader = data_loaders.pop(args.train_dataset)
|
| 231 |
+
return train_loader, data_loaders
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def sync_checkpoints_to_r2(args: DictConfig, output_dir: Path) -> None:
|
| 235 |
+
r2_sync_url = args.get("r2_sync")
|
| 236 |
+
if not r2_sync_url or not ut.is_main_process():
|
| 237 |
+
return
|
| 238 |
+
|
| 239 |
+
cmd = ["aws", "s3", "sync", str(output_dir), str(r2_sync_url), "--profile", "r2"]
|
| 240 |
+
print(f"syncing checkpoints to R2: {output_dir} -> {r2_sync_url}")
|
| 241 |
+
subprocess.run(cmd, check=True)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def train_one_epoch(
|
| 245 |
+
args: DictConfig,
|
| 246 |
+
model: nn.Module,
|
| 247 |
+
data_loader: Iterable,
|
| 248 |
+
optimizer: torch.optim.Optimizer,
|
| 249 |
+
loss_scaler: torch.GradScaler | None,
|
| 250 |
+
lr_schedule: Sequence[float],
|
| 251 |
+
epoch: int,
|
| 252 |
+
device: torch.device,
|
| 253 |
+
):
|
| 254 |
+
model.train()
|
| 255 |
+
|
| 256 |
+
metric_logger = ut.MetricLogger(delimiter=" ")
|
| 257 |
+
metric_logger.add_meter("lr", ut.SmoothedValue(window_size=1, fmt="{value:.6f}"))
|
| 258 |
+
metric_logger.add_meter("grad", ut.SmoothedValue())
|
| 259 |
+
header = f"Train: [{epoch}]"
|
| 260 |
+
log_wandb = args.wandb and ut.is_main_process()
|
| 261 |
+
|
| 262 |
+
epoch_num_batches = len(data_loader)
|
| 263 |
+
steps_per_epoch = math.ceil(epoch_num_batches / args.accum_iter)
|
| 264 |
+
|
| 265 |
+
print_freq = args.get("print_freq", 100) if not args.debug else 1
|
| 266 |
+
num_batches = epoch_num_batches if not args.debug else 10
|
| 267 |
+
amp_dtype = getattr(torch, args.amp_dtype)
|
| 268 |
+
use_cuda = device.type == "cuda"
|
| 269 |
+
if use_cuda and args.presend_cuda:
|
| 270 |
+
data_loader = ut.pre_send_to_cuda_wrapper(
|
| 271 |
+
data_loader, device, dtype_map={torch.float16: amp_dtype}
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
optimizer.zero_grad()
|
| 275 |
+
|
| 276 |
+
for batch_idx, batch in enumerate(
|
| 277 |
+
metric_logger.log_every(data_loader, print_freq, header, total_steps=num_batches)
|
| 278 |
+
):
|
| 279 |
+
if use_cuda and not args.presend_cuda:
|
| 280 |
+
batch = ut.send_data(batch, device, dtype_map={torch.float16: amp_dtype})
|
| 281 |
+
|
| 282 |
+
batch_step = batch_idx + 1
|
| 283 |
+
log_step = batch_step % print_freq == 0 or batch_step == num_batches
|
| 284 |
+
update_in_epoch = batch_idx // args.accum_iter
|
| 285 |
+
group_size = min(args.accum_iter, num_batches - update_in_epoch * args.accum_iter)
|
| 286 |
+
need_update = batch_step % args.accum_iter == 0 or batch_step == num_batches
|
| 287 |
+
global_step = epoch * steps_per_epoch + update_in_epoch
|
| 288 |
+
lr = lr_schedule[global_step]
|
| 289 |
+
if need_update:
|
| 290 |
+
ut.update_lr(optimizer.param_groups, lr)
|
| 291 |
+
|
| 292 |
+
images, img_mask = mri_data.densify_sparse_image_batch(
|
| 293 |
+
batch["image_values"],
|
| 294 |
+
batch["img_mask"],
|
| 295 |
+
(int(args.get("in_chans", 1)), *args.img_size),
|
| 296 |
+
dtype=amp_dtype,
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
sync_context = model.no_sync() if args.distributed and not need_update else nullcontext()
|
| 300 |
+
with sync_context:
|
| 301 |
+
with torch.autocast(device_type=device.type, dtype=amp_dtype, enabled=args.amp):
|
| 302 |
+
loss = model(
|
| 303 |
+
images,
|
| 304 |
+
img_mask=img_mask,
|
| 305 |
+
mask_ratio=args.mask_ratio,
|
| 306 |
+
pred_mask_ratio=args.pred_mask_ratio,
|
| 307 |
+
pad_to_multiple=args.pad_to_multiple,
|
| 308 |
+
with_state=False,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
loss_for_log = loss.detach()
|
| 312 |
+
torch._assert_async(torch.isfinite(loss_for_log), "non-finite loss")
|
| 313 |
+
|
| 314 |
+
grad_norm = ut.backward_step(
|
| 315 |
+
loss / group_size,
|
| 316 |
+
optimizer,
|
| 317 |
+
scaler=loss_scaler,
|
| 318 |
+
need_update=need_update,
|
| 319 |
+
max_norm=args.clip_grad,
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
if need_update and log_step:
|
| 323 |
+
loss_value = loss_for_log.item()
|
| 324 |
+
grad_norm_value = grad_norm.item()
|
| 325 |
+
metric_logger.update(loss=loss_value, lr=lr, grad=grad_norm_value)
|
| 326 |
+
if log_wandb:
|
| 327 |
+
wandb.log(
|
| 328 |
+
{
|
| 329 |
+
"train/loss": loss_value,
|
| 330 |
+
"train/lr": lr,
|
| 331 |
+
"train/grad": grad_norm_value,
|
| 332 |
+
},
|
| 333 |
+
step=int(1000 * (epoch + batch_step / epoch_num_batches)),
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
# gather the stats from all processes
|
| 337 |
+
metric_logger.synchronize_between_processes()
|
| 338 |
+
print("Averaged stats:", metric_logger)
|
| 339 |
+
return {f"train/{k}": meter.global_avg for k, meter in metric_logger.meters.items()}
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
@torch.inference_mode()
|
| 343 |
+
def evaluate(
|
| 344 |
+
args: DictConfig,
|
| 345 |
+
model: nn.Module,
|
| 346 |
+
data_loader: Iterable,
|
| 347 |
+
epoch: int,
|
| 348 |
+
device: torch.device,
|
| 349 |
+
eval_name: str,
|
| 350 |
+
):
|
| 351 |
+
model.eval()
|
| 352 |
+
|
| 353 |
+
metric_logger = ut.MetricLogger(delimiter=" ")
|
| 354 |
+
header = f"Eval ({eval_name}): [{epoch}]"
|
| 355 |
+
is_master = ut.is_main_process()
|
| 356 |
+
log_wandb = args.wandb and is_master
|
| 357 |
+
|
| 358 |
+
epoch_num_batches = len(data_loader)
|
| 359 |
+
if epoch_num_batches <= 0:
|
| 360 |
+
raise ValueError(f"eval loader {eval_name!r} has zero batches")
|
| 361 |
+
|
| 362 |
+
print_freq = args.get("print_freq", 100) if not args.debug else 1
|
| 363 |
+
num_batches = epoch_num_batches if not args.debug else 10
|
| 364 |
+
num_batches = min(num_batches, epoch_num_batches)
|
| 365 |
+
eval_seed = int(args.get("eval_seed", args.seed)) + ut.get_rank()
|
| 366 |
+
example_step = random.Random(eval_seed).randint(1, num_batches)
|
| 367 |
+
amp_dtype = getattr(torch, args.amp_dtype)
|
| 368 |
+
use_cuda = device.type == "cuda"
|
| 369 |
+
rng_state = ut.capture_rng_state()
|
| 370 |
+
torch.set_rng_state(torch.Generator().manual_seed(eval_seed).get_state())
|
| 371 |
+
if use_cuda:
|
| 372 |
+
torch.cuda.manual_seed(eval_seed)
|
| 373 |
+
if use_cuda and args.presend_cuda:
|
| 374 |
+
data_loader = ut.pre_send_to_cuda_wrapper(
|
| 375 |
+
data_loader, device, dtype_map={torch.float16: amp_dtype}
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
eval_batches = islice(data_loader, num_batches)
|
| 379 |
+
for batch_idx, batch in enumerate(
|
| 380 |
+
metric_logger.log_every(eval_batches, print_freq, header, total_steps=num_batches)
|
| 381 |
+
):
|
| 382 |
+
if use_cuda and not args.presend_cuda:
|
| 383 |
+
batch = ut.send_data(batch, device, dtype_map={torch.float16: amp_dtype})
|
| 384 |
+
|
| 385 |
+
batch_step = batch_idx + 1
|
| 386 |
+
|
| 387 |
+
images, img_mask = mri_data.densify_sparse_image_batch(
|
| 388 |
+
batch["image_values"],
|
| 389 |
+
batch["img_mask"],
|
| 390 |
+
(int(args.get("in_chans", 1)), *args.img_size),
|
| 391 |
+
dtype=amp_dtype,
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
with torch.autocast(device_type=device.type, dtype=amp_dtype, enabled=args.amp):
|
| 395 |
+
loss, state = model(
|
| 396 |
+
images,
|
| 397 |
+
img_mask=img_mask,
|
| 398 |
+
mask_ratio=args.mask_ratio,
|
| 399 |
+
pred_mask_ratio=args.pred_mask_ratio,
|
| 400 |
+
pad_to_multiple=args.pad_to_multiple,
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
loss_value = loss.detach().float().item()
|
| 404 |
+
finite = torch.tensor(int(math.isfinite(loss_value)), dtype=torch.int32, device=device)
|
| 405 |
+
if args.distributed:
|
| 406 |
+
torch.distributed.all_reduce(finite, op=torch.distributed.ReduceOp.MIN)
|
| 407 |
+
if not finite.item():
|
| 408 |
+
raise RuntimeError("non-finite validation loss detected")
|
| 409 |
+
metric_logger.meters["loss"].update(loss_value, n=int(batch["img_mask"].shape[0]))
|
| 410 |
+
|
| 411 |
+
if is_master and batch_step == example_step:
|
| 412 |
+
example_batch = {"image": images[:1], "img_mask": img_mask[:1]}
|
| 413 |
+
if "meta" in batch:
|
| 414 |
+
example_batch["meta"] = batch["meta"][:1]
|
| 415 |
+
example_state = {
|
| 416 |
+
"pred_images": state["pred_images"][:1],
|
| 417 |
+
"pred_mask": state["pred_mask"][:1],
|
| 418 |
+
}
|
| 419 |
+
example_data = {
|
| 420 |
+
"batch": ut.send_data(example_batch, "cpu"),
|
| 421 |
+
"state": ut.send_data(example_state, "cpu"),
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
# gather the stats from all processes
|
| 425 |
+
metric_logger.synchronize_between_processes()
|
| 426 |
+
print(f"Averaged stats ({eval_name}):", metric_logger)
|
| 427 |
+
stats = {f"eval/{eval_name}/{k}": meter.global_avg for k, meter in metric_logger.meters.items()}
|
| 428 |
+
|
| 429 |
+
plots = {}
|
| 430 |
+
if is_master:
|
| 431 |
+
print(f"Making plots ({eval_name}): example={example_step}")
|
| 432 |
+
plots = make_plots(args, **example_data)
|
| 433 |
+
plots = {f"eval/{eval_name}/{k}": img for k, img in plots.items()}
|
| 434 |
+
|
| 435 |
+
if log_wandb:
|
| 436 |
+
wandb.log(stats, step=1000 * (epoch + 1))
|
| 437 |
+
wandb.log(
|
| 438 |
+
{k: wandb.Image(img, caption=f"example={example_step}") for k, img in plots.items()},
|
| 439 |
+
step=1000 * (epoch + 1),
|
| 440 |
+
)
|
| 441 |
+
ut.restore_rng_state(rng_state)
|
| 442 |
+
return stats, plots
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def make_plots(
|
| 446 |
+
args: DictConfig,
|
| 447 |
+
batch: dict[str, Tensor],
|
| 448 |
+
state: dict[str, Tensor],
|
| 449 |
+
) -> dict[str, Image.Image]:
|
| 450 |
+
fig_kwargs = args.get("fig_kwargs", {})
|
| 451 |
+
|
| 452 |
+
images = batch["image"]
|
| 453 |
+
img_mask = batch.get("img_mask")
|
| 454 |
+
if img_mask is not None:
|
| 455 |
+
img_mask = img_mask.expand_as(images)
|
| 456 |
+
|
| 457 |
+
raw_mean, raw_std = vis.raw_stats_from_batch(batch)
|
| 458 |
+
|
| 459 |
+
plots = {}
|
| 460 |
+
mask_pred_fig = vis.plot_mask_pred(
|
| 461 |
+
target=images,
|
| 462 |
+
pred=state["pred_images"],
|
| 463 |
+
pred_mask=state["pred_mask"],
|
| 464 |
+
img_mask=img_mask,
|
| 465 |
+
patch_size=args.patch_size,
|
| 466 |
+
raw_mean=raw_mean,
|
| 467 |
+
raw_std=raw_std,
|
| 468 |
+
**ut.filter_kwargs(vis.plot_mask_pred, fig_kwargs),
|
| 469 |
+
)
|
| 470 |
+
plots["mask_pred"] = vis.fig2pil(mask_pred_fig)
|
| 471 |
+
plt.close(mask_pred_fig)
|
| 472 |
+
|
| 473 |
+
return plots
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
if __name__ == "__main__":
|
| 477 |
+
parser = argparse.ArgumentParser()
|
| 478 |
+
parser.add_argument("--cfg-path", type=str, default=None)
|
| 479 |
+
parser.add_argument("--overrides", type=str, default=None, nargs="+")
|
| 480 |
+
args = parser.parse_args()
|
| 481 |
+
cfg = OmegaConf.load(DEFAULT_CONFIG)
|
| 482 |
+
if args.cfg_path:
|
| 483 |
+
cfg = OmegaConf.unsafe_merge(cfg, OmegaConf.load(args.cfg_path))
|
| 484 |
+
if args.overrides:
|
| 485 |
+
cfg = OmegaConf.unsafe_merge(cfg, OmegaConf.from_dotlist(args.overrides))
|
| 486 |
+
main(cfg)
|
finetune/fomo_tune_baseline/output/task5/build/smri_mae/masking.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from jaxtyping import Float, Int
|
| 3 |
+
from torch import Tensor
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def pad_patch_mask(
|
| 7 |
+
patch_mask: Float[Tensor, "B N"],
|
| 8 |
+
mask_ratio: float,
|
| 9 |
+
shuffle: bool = False,
|
| 10 |
+
generator: torch.Generator | None = None,
|
| 11 |
+
pad_to_multiple: int | None = None,
|
| 12 |
+
) -> tuple[Float[Tensor, "B N"], Int[Tensor, "B L"], Tensor]:
|
| 13 |
+
"""
|
| 14 |
+
Select each row's own mask-ratio count, then pad ids to the batch max length.
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
- selected patch mask [B, N]
|
| 18 |
+
- padded selected patch ids [B, Lpad]
|
| 19 |
+
- token mask [B, Lpad], true for real ids and false for padding
|
| 20 |
+
"""
|
| 21 |
+
if not 0.0 <= mask_ratio <= 1.0:
|
| 22 |
+
raise ValueError(f"mask_ratio must be in [0, 1], got {mask_ratio}")
|
| 23 |
+
|
| 24 |
+
B, N = patch_mask.shape
|
| 25 |
+
device = patch_mask.device
|
| 26 |
+
patch_mask = patch_mask.to(dtype=torch.bool)
|
| 27 |
+
|
| 28 |
+
valid_counts = patch_mask.sum(dim=1)
|
| 29 |
+
num_keep = torch.floor(valid_counts.to(torch.float64) * (1.0 - mask_ratio)).to(torch.long)
|
| 30 |
+
if not shuffle:
|
| 31 |
+
selected = patch_mask & (patch_mask.cumsum(dim=1) <= num_keep.unsqueeze(1))
|
| 32 |
+
padded_ids, token_mask = patch_ids_from_mask(
|
| 33 |
+
selected,
|
| 34 |
+
pad_to_multiple=pad_to_multiple,
|
| 35 |
+
)
|
| 36 |
+
return selected, padded_ids, token_mask
|
| 37 |
+
|
| 38 |
+
# One masked sort directly produces random valid IDs. The previous
|
| 39 |
+
# shuffle/select/inverse-shuffle path required two full argsorts plus a
|
| 40 |
+
# dynamic nonzero/scatter solely to recover the same selected set.
|
| 41 |
+
noise = torch.rand(B, N, generator=generator, device=device)
|
| 42 |
+
noise.masked_fill_(~patch_mask, torch.inf)
|
| 43 |
+
shuffled_ids = torch.argsort(noise, dim=1)
|
| 44 |
+
|
| 45 |
+
max_count = int(num_keep.max().item())
|
| 46 |
+
if pad_to_multiple is not None:
|
| 47 |
+
if pad_to_multiple <= 0:
|
| 48 |
+
raise ValueError(f"pad_to_multiple must be positive, got {pad_to_multiple}")
|
| 49 |
+
max_count = (max_count + pad_to_multiple - 1) // pad_to_multiple * pad_to_multiple
|
| 50 |
+
padded_ids = shuffled_ids[:, :max_count]
|
| 51 |
+
token_mask = torch.arange(max_count, device=device).unsqueeze(0) < num_keep.unsqueeze(1)
|
| 52 |
+
selected = torch.zeros_like(patch_mask).scatter_(1, padded_ids, token_mask)
|
| 53 |
+
return selected, padded_ids, token_mask
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def patch_ids_from_mask(
|
| 57 |
+
patch_mask: Tensor,
|
| 58 |
+
pad_to_multiple: int | None = None,
|
| 59 |
+
) -> tuple[Int[Tensor, "B L"], Tensor]:
|
| 60 |
+
"""Return optionally rounded patch IDs and their token-validity mask."""
|
| 61 |
+
if pad_to_multiple is not None and pad_to_multiple <= 0:
|
| 62 |
+
raise ValueError(f"pad_to_multiple must be positive, got {pad_to_multiple}")
|
| 63 |
+
|
| 64 |
+
patch_mask = patch_mask.to(dtype=torch.bool)
|
| 65 |
+
B, N = patch_mask.shape
|
| 66 |
+
device = patch_mask.device
|
| 67 |
+
counts = patch_mask.sum(dim=1)
|
| 68 |
+
max_count = int(counts.max().item())
|
| 69 |
+
if pad_to_multiple is not None:
|
| 70 |
+
max_count = (max_count + pad_to_multiple - 1) // pad_to_multiple * pad_to_multiple
|
| 71 |
+
|
| 72 |
+
patch_ids = torch.zeros((B, max_count), dtype=torch.long, device=device)
|
| 73 |
+
token_mask = torch.arange(max_count, device=device).unsqueeze(0) < counts.unsqueeze(1)
|
| 74 |
+
if max_count == 0:
|
| 75 |
+
return patch_ids, token_mask
|
| 76 |
+
|
| 77 |
+
batch_ids, selected_ids = patch_mask.nonzero(as_tuple=True)
|
| 78 |
+
slot_ids = patch_mask.cumsum(dim=1)[batch_ids, selected_ids].to(torch.long) - 1
|
| 79 |
+
patch_ids[batch_ids, slot_ids] = selected_ids
|
| 80 |
+
return patch_ids, token_mask
|
finetune/fomo_tune_baseline/output/task5/build/smri_mae/model_mae.py
ADDED
|
@@ -0,0 +1,916 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Sophont, Inc
|
| 2 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 3 |
+
#
|
| 4 |
+
# References:
|
| 5 |
+
# capi: https://github.com/facebookresearch/capi/blob/main/model.py
|
| 6 |
+
# timm: https://github.com/huggingface/pytorch-image-models/blob/v1.0.20/timm/models/vision_transformer.py
|
| 7 |
+
|
| 8 |
+
"""
|
| 9 |
+
From-scratch re-implementation of the original MAE model.
|
| 10 |
+
|
| 11 |
+
MaskedEncoder: standard ViT with masking
|
| 12 |
+
MaskedDecoder: standard self-attention MAE decoder
|
| 13 |
+
MaskedAutoEncoderViT: full MAE model for 3D structural MRI volumes
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from collections.abc import Sequence
|
| 17 |
+
from typing import Literal
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import torch.nn as nn
|
| 21 |
+
import torch.nn.functional as F
|
| 22 |
+
from torch import Tensor
|
| 23 |
+
from torch.utils.checkpoint import checkpoint
|
| 24 |
+
from huggingface_hub import PyTorchModelHubMixin
|
| 25 |
+
from jaxtyping import Float, Int
|
| 26 |
+
|
| 27 |
+
from .modules import (
|
| 28 |
+
AbsolutePosEmbed,
|
| 29 |
+
Block,
|
| 30 |
+
LayerNorm,
|
| 31 |
+
Normalize,
|
| 32 |
+
JaggedBatch,
|
| 33 |
+
Patchify3D,
|
| 34 |
+
SeparablePosEmbed,
|
| 35 |
+
SinCosPosEmbed3D,
|
| 36 |
+
unpack_tokens,
|
| 37 |
+
)
|
| 38 |
+
from .masking import pad_patch_mask
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class MaskedEncoder(nn.Module):
|
| 42 |
+
"""
|
| 43 |
+
Masked transformer encoder.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
patchify: nn.Module,
|
| 49 |
+
patch_embed: nn.Module,
|
| 50 |
+
pos_embed: nn.Module,
|
| 51 |
+
depth: int = 12,
|
| 52 |
+
embed_dim: int = 768,
|
| 53 |
+
num_heads: int = 12,
|
| 54 |
+
qkv_bias: bool = True,
|
| 55 |
+
proj_bias: bool = True,
|
| 56 |
+
mlp_ratio: int | float = 4,
|
| 57 |
+
class_token: bool = True,
|
| 58 |
+
reg_tokens: int = 0,
|
| 59 |
+
no_embed_class: bool = False,
|
| 60 |
+
final_norm: bool = True,
|
| 61 |
+
drop_path_rate: float = 0.0,
|
| 62 |
+
mask_drop_scale: bool = False,
|
| 63 |
+
):
|
| 64 |
+
super().__init__()
|
| 65 |
+
self.num_prefix_tokens = int(class_token) + reg_tokens
|
| 66 |
+
self.num_reg_tokens = reg_tokens
|
| 67 |
+
self.has_class_token = class_token
|
| 68 |
+
self.no_embed_class = no_embed_class
|
| 69 |
+
|
| 70 |
+
# scale inputs by 1 / observed rate (like dropout)
|
| 71 |
+
self.mask_drop_scale = mask_drop_scale
|
| 72 |
+
|
| 73 |
+
# inject tokenization modules, so that the encoder doesn't specifically need to
|
| 74 |
+
# know how the data are tokenized, while still implementing a complete
|
| 75 |
+
# self-contained model.
|
| 76 |
+
self.patchify = patchify
|
| 77 |
+
self.patch_embed = patch_embed
|
| 78 |
+
self.pos_embed = pos_embed
|
| 79 |
+
|
| 80 |
+
R = reg_tokens
|
| 81 |
+
self.cls_token = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
|
| 82 |
+
self.reg_token = nn.Parameter(torch.empty(1, R, embed_dim)) if reg_tokens else None
|
| 83 |
+
|
| 84 |
+
if not no_embed_class:
|
| 85 |
+
self.cls_token_pos = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
|
| 86 |
+
self.reg_token_pos = nn.Parameter(torch.empty(1, R, embed_dim)) if reg_tokens else None
|
| 87 |
+
else:
|
| 88 |
+
self.cls_token_pos = self.reg_token_pos = None
|
| 89 |
+
|
| 90 |
+
# stochastic depth decay rule
|
| 91 |
+
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]
|
| 92 |
+
|
| 93 |
+
self.blocks = nn.ModuleList(
|
| 94 |
+
[
|
| 95 |
+
Block(
|
| 96 |
+
dim=embed_dim,
|
| 97 |
+
num_heads=num_heads,
|
| 98 |
+
qkv_bias=qkv_bias,
|
| 99 |
+
proj_bias=proj_bias,
|
| 100 |
+
mlp_ratio=mlp_ratio,
|
| 101 |
+
drop_path=dpr[ii],
|
| 102 |
+
)
|
| 103 |
+
for ii in range(depth)
|
| 104 |
+
]
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
self.norm = LayerNorm(embed_dim) if final_norm else nn.Identity()
|
| 108 |
+
|
| 109 |
+
self.reset_parameters()
|
| 110 |
+
|
| 111 |
+
def extra_repr(self):
|
| 112 |
+
return (
|
| 113 |
+
f"class_token={self.has_class_token}, reg_tokens={self.num_reg_tokens}, "
|
| 114 |
+
f"no_embed_class={self.no_embed_class}, mask_drop_scale={self.mask_drop_scale}"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
def reset_parameters(self) -> None:
|
| 118 |
+
for p in [self.cls_token, self.cls_token_pos, self.reg_token, self.reg_token_pos]:
|
| 119 |
+
if p is not None:
|
| 120 |
+
nn.init.trunc_normal_(p, std=0.02)
|
| 121 |
+
|
| 122 |
+
def cat_tokens(self, x: Tensor) -> Tensor:
|
| 123 |
+
# prepend cls and reg tokens with optional learned position embedding
|
| 124 |
+
# the cls and reg pos embedding is ofc redundant, but included in many other
|
| 125 |
+
# implementations.
|
| 126 |
+
B, _, _ = x.shape
|
| 127 |
+
|
| 128 |
+
to_cat = []
|
| 129 |
+
if self.has_class_token:
|
| 130 |
+
cls_token = self.cls_token
|
| 131 |
+
if not self.no_embed_class:
|
| 132 |
+
cls_token = cls_token + self.cls_token_pos
|
| 133 |
+
to_cat.append(cls_token.expand(B, -1, -1))
|
| 134 |
+
|
| 135 |
+
if self.num_reg_tokens:
|
| 136 |
+
reg_token = self.reg_token
|
| 137 |
+
if not self.no_embed_class:
|
| 138 |
+
reg_token = reg_token + self.reg_token_pos
|
| 139 |
+
to_cat.append(reg_token.expand(B, -1, -1))
|
| 140 |
+
|
| 141 |
+
if to_cat:
|
| 142 |
+
x = torch.cat(to_cat + [x], dim=1)
|
| 143 |
+
return x
|
| 144 |
+
|
| 145 |
+
def cat_token_mask(self, token_mask: Tensor, batch_size: int) -> Tensor:
|
| 146 |
+
if self.num_prefix_tokens:
|
| 147 |
+
prefix_mask = torch.ones(
|
| 148 |
+
(batch_size, self.num_prefix_tokens),
|
| 149 |
+
dtype=torch.bool,
|
| 150 |
+
device=token_mask.device,
|
| 151 |
+
)
|
| 152 |
+
token_mask = torch.cat([prefix_mask, token_mask], dim=1)
|
| 153 |
+
return token_mask
|
| 154 |
+
|
| 155 |
+
def chunk_tokens(self, x: Tensor) -> tuple[Tensor | None, Tensor | None, Tensor]:
|
| 156 |
+
cls_offset = int(self.has_class_token)
|
| 157 |
+
cls = x[:, :cls_offset] if self.has_class_token else None
|
| 158 |
+
if self.num_reg_tokens:
|
| 159 |
+
reg = x[:, cls_offset : self.num_prefix_tokens, :]
|
| 160 |
+
else:
|
| 161 |
+
reg = None
|
| 162 |
+
patch = x[:, self.num_prefix_tokens :, :]
|
| 163 |
+
return cls, reg, patch
|
| 164 |
+
|
| 165 |
+
def forward(
|
| 166 |
+
self,
|
| 167 |
+
x: Tensor,
|
| 168 |
+
mask: Tensor | None = None,
|
| 169 |
+
mask_ratio: float | None = None,
|
| 170 |
+
pad_to_multiple: int | None = None,
|
| 171 |
+
) -> tuple[
|
| 172 |
+
Float[Tensor, "B 1 D"] | None,
|
| 173 |
+
Float[Tensor, "B R D"] | None,
|
| 174 |
+
Float[Tensor, "B L D"],
|
| 175 |
+
Tensor | None,
|
| 176 |
+
Int[Tensor, "B L"] | None,
|
| 177 |
+
Tensor | None,
|
| 178 |
+
]:
|
| 179 |
+
"""
|
| 180 |
+
x: input data shape [B, C, D, H, W]
|
| 181 |
+
mask: visible mask, 1 = visible, 0 = invisible. broadcastable shape
|
| 182 |
+
mask_ratio: mask ratio for uniform random masking
|
| 183 |
+
|
| 184 |
+
returns:
|
| 185 |
+
- cls_embeds: [B, 1, D]
|
| 186 |
+
- reg_embeds: [B, R, D]
|
| 187 |
+
- patch_embeds: [B, L, D], where L is the number of visible patches
|
| 188 |
+
- mask: observed mask, 1 = observed, 0 = unobserved. same shape as input
|
| 189 |
+
- mask_ids: indices of visible patches [B L]
|
| 190 |
+
- token_mask: valid token mask for padded per-sample masking [B L]
|
| 191 |
+
"""
|
| 192 |
+
# apply mask to the input
|
| 193 |
+
if mask is not None:
|
| 194 |
+
mask = mask.to(device=x.device, dtype=torch.bool).expand_as(x)
|
| 195 |
+
x = x.masked_fill(~mask, 0)
|
| 196 |
+
|
| 197 |
+
# patchify input
|
| 198 |
+
x = self.patchify(x)
|
| 199 |
+
B, N, P = x.shape
|
| 200 |
+
|
| 201 |
+
# patchify mask and apply dropout style scaling
|
| 202 |
+
if mask is not None:
|
| 203 |
+
mask_patches = self.patchify(mask)
|
| 204 |
+
patch_num_obs = mask_patches.sum(dim=-1)
|
| 205 |
+
patch_mask = patch_num_obs > 0
|
| 206 |
+
if self.mask_drop_scale:
|
| 207 |
+
patch_num_obs = patch_num_obs.to(x.dtype)
|
| 208 |
+
x = x * (P / patch_num_obs.unsqueeze(-1).clamp(min=1.0))
|
| 209 |
+
elif mask_ratio is not None:
|
| 210 |
+
patch_mask = torch.ones((B, N), dtype=torch.bool, device=x.device)
|
| 211 |
+
mask_patches = patch_mask.unsqueeze(-1).expand(-1, -1, P)
|
| 212 |
+
else:
|
| 213 |
+
patch_mask = mask_patches = None
|
| 214 |
+
|
| 215 |
+
# patch and position embed
|
| 216 |
+
x = self.patch_embed(x)
|
| 217 |
+
x = self.pos_embed(x)
|
| 218 |
+
|
| 219 |
+
if mask is not None or mask_ratio is not None:
|
| 220 |
+
mask_ratio = 0.0 if mask_ratio is None else mask_ratio
|
| 221 |
+
patch_mask, mask_ids, token_mask = pad_patch_mask(
|
| 222 |
+
patch_mask,
|
| 223 |
+
mask_ratio=mask_ratio,
|
| 224 |
+
shuffle=mask_ratio > 0,
|
| 225 |
+
pad_to_multiple=pad_to_multiple,
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
mask_patches = mask_patches & patch_mask.unsqueeze(-1)
|
| 229 |
+
mask = self.patchify.unpatchify(mask_patches)
|
| 230 |
+
x = x.gather(1, mask_ids.unsqueeze(-1).expand(-1, -1, x.shape[-1]))
|
| 231 |
+
else:
|
| 232 |
+
mask_ids = None
|
| 233 |
+
token_mask = None
|
| 234 |
+
|
| 235 |
+
cls_embeds, reg_embeds, patch_embeds = self.forward_patch_embeds(
|
| 236 |
+
x,
|
| 237 |
+
token_mask=token_mask,
|
| 238 |
+
)
|
| 239 |
+
return cls_embeds, reg_embeds, patch_embeds, mask, mask_ids, token_mask
|
| 240 |
+
|
| 241 |
+
def forward_patch_embeds(
|
| 242 |
+
self,
|
| 243 |
+
x: Float[Tensor, "B L D"],
|
| 244 |
+
token_mask: Tensor | None = None,
|
| 245 |
+
) -> tuple[
|
| 246 |
+
Float[Tensor, "B 1 D"] | None,
|
| 247 |
+
Float[Tensor, "B R D"] | None,
|
| 248 |
+
Float[Tensor, "B L D"],
|
| 249 |
+
]:
|
| 250 |
+
B = x.shape[0]
|
| 251 |
+
if token_mask is None:
|
| 252 |
+
token_mask = torch.ones(x.shape[:2], dtype=torch.bool, device=x.device)
|
| 253 |
+
x = self.cat_tokens(x)
|
| 254 |
+
token_mask = self.cat_token_mask(token_mask, B)
|
| 255 |
+
jagged_batch = JaggedBatch.from_mask(token_mask)
|
| 256 |
+
x = x[token_mask]
|
| 257 |
+
for block in self.blocks:
|
| 258 |
+
x = block(x, jagged_batch=jagged_batch)
|
| 259 |
+
x = self.norm(x)
|
| 260 |
+
x = unpack_tokens(x, token_mask)
|
| 261 |
+
|
| 262 |
+
cls_embeds, reg_embeds, patch_embeds = self.chunk_tokens(x)
|
| 263 |
+
return cls_embeds, reg_embeds, patch_embeds
|
| 264 |
+
|
| 265 |
+
def forward_visible_ids(
|
| 266 |
+
self,
|
| 267 |
+
x: Tensor,
|
| 268 |
+
visible_ids: Int[Tensor, "B L"],
|
| 269 |
+
img_mask: Tensor | None = None,
|
| 270 |
+
) -> tuple[
|
| 271 |
+
Float[Tensor, "B 1 D"] | None,
|
| 272 |
+
Float[Tensor, "B R D"] | None,
|
| 273 |
+
Float[Tensor, "B L D"],
|
| 274 |
+
]:
|
| 275 |
+
if img_mask is not None:
|
| 276 |
+
img_mask = img_mask.to(device=x.device, dtype=torch.bool).expand_as(x)
|
| 277 |
+
x = x.masked_fill(~img_mask, 0)
|
| 278 |
+
|
| 279 |
+
x = self.patchify(x)
|
| 280 |
+
if self.mask_drop_scale and img_mask is not None:
|
| 281 |
+
mask_patches = self.patchify(img_mask)
|
| 282 |
+
patch_num_obs = mask_patches.sum(dim=-1).to(x.dtype)
|
| 283 |
+
x = x * (self.patchify.patch_dim / patch_num_obs.unsqueeze(-1).clamp(min=1.0))
|
| 284 |
+
x = self.patch_embed(x)
|
| 285 |
+
x = self.pos_embed(x)
|
| 286 |
+
visible_ids = visible_ids.to(device=x.device)
|
| 287 |
+
x = x.gather(1, visible_ids.unsqueeze(-1).expand(-1, -1, x.shape[-1]))
|
| 288 |
+
return self.forward_patch_embeds(x)
|
| 289 |
+
|
| 290 |
+
def forward_embedding(
|
| 291 |
+
self,
|
| 292 |
+
x: Tensor,
|
| 293 |
+
mask: Tensor | None = None,
|
| 294 |
+
mask_ratio: float | None = None,
|
| 295 |
+
):
|
| 296 |
+
cls_embeds, reg_embeds, patch_embeds, *_ = self.forward(
|
| 297 |
+
x,
|
| 298 |
+
mask=mask,
|
| 299 |
+
mask_ratio=mask_ratio,
|
| 300 |
+
)
|
| 301 |
+
return cls_embeds, reg_embeds, patch_embeds
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
class MaskedDecoder(nn.Module):
|
| 305 |
+
"""Self-attention MAE decoder supporting sparse subset decoding via pred_ids."""
|
| 306 |
+
|
| 307 |
+
def __init__(
|
| 308 |
+
self,
|
| 309 |
+
pos_embed: nn.Module,
|
| 310 |
+
head: nn.Module | None = None,
|
| 311 |
+
input_dim: int | None = None,
|
| 312 |
+
depth: int = 12,
|
| 313 |
+
embed_dim: int = 768,
|
| 314 |
+
num_heads: int = 12,
|
| 315 |
+
qkv_bias: bool = True,
|
| 316 |
+
proj_bias: bool = True,
|
| 317 |
+
mlp_ratio: int | float = 4,
|
| 318 |
+
class_token: bool = True,
|
| 319 |
+
no_embed_class: bool = False,
|
| 320 |
+
final_norm: bool = True,
|
| 321 |
+
):
|
| 322 |
+
super().__init__()
|
| 323 |
+
input_dim = embed_dim if input_dim is None else input_dim
|
| 324 |
+
self.has_class_token = class_token
|
| 325 |
+
self.no_embed_class = no_embed_class
|
| 326 |
+
|
| 327 |
+
self.cls_token = nn.Parameter(torch.empty(1, 1, embed_dim)) if class_token else None
|
| 328 |
+
self.cls_token_pos = (
|
| 329 |
+
nn.Parameter(torch.empty(1, 1, embed_dim))
|
| 330 |
+
if class_token and not no_embed_class
|
| 331 |
+
else None
|
| 332 |
+
)
|
| 333 |
+
self.mask_token = nn.Parameter(torch.empty(1, 1, embed_dim))
|
| 334 |
+
|
| 335 |
+
# decoder position embedding, encodes query position information into masks
|
| 336 |
+
self.pos_embed = pos_embed
|
| 337 |
+
|
| 338 |
+
self.proj = nn.Identity() if input_dim == embed_dim else nn.Linear(input_dim, embed_dim)
|
| 339 |
+
|
| 340 |
+
self.blocks = nn.ModuleList(
|
| 341 |
+
[
|
| 342 |
+
Block(
|
| 343 |
+
dim=embed_dim,
|
| 344 |
+
num_heads=num_heads,
|
| 345 |
+
qkv_bias=qkv_bias,
|
| 346 |
+
proj_bias=proj_bias,
|
| 347 |
+
mlp_ratio=mlp_ratio,
|
| 348 |
+
)
|
| 349 |
+
for _ in range(depth)
|
| 350 |
+
]
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
self.norm = LayerNorm(embed_dim) if final_norm else nn.Identity()
|
| 354 |
+
|
| 355 |
+
# optional injected prediction head
|
| 356 |
+
self.head = nn.Identity() if head is None else head
|
| 357 |
+
|
| 358 |
+
self.reset_parameters()
|
| 359 |
+
|
| 360 |
+
def extra_repr(self):
|
| 361 |
+
return f"class_token={self.has_class_token}, no_embed_class={self.no_embed_class}"
|
| 362 |
+
|
| 363 |
+
def reset_parameters(self) -> None:
|
| 364 |
+
# official mae initializes decoder cls token to zeros
|
| 365 |
+
# although perhaps this was an oversight
|
| 366 |
+
if self.cls_token is not None:
|
| 367 |
+
nn.init.zeros_(self.cls_token)
|
| 368 |
+
if self.cls_token_pos is not None:
|
| 369 |
+
nn.init.trunc_normal_(self.cls_token_pos, std=0.02)
|
| 370 |
+
nn.init.trunc_normal_(self.mask_token, std=0.02)
|
| 371 |
+
|
| 372 |
+
def cat_tokens(self, x: Tensor) -> Tensor:
|
| 373 |
+
if not self.has_class_token:
|
| 374 |
+
return x
|
| 375 |
+
cls_token = self.cls_token
|
| 376 |
+
if not self.no_embed_class:
|
| 377 |
+
cls_token = cls_token + self.cls_token_pos
|
| 378 |
+
return torch.cat([cls_token.expand(x.shape[0], -1, -1), x], dim=1)
|
| 379 |
+
|
| 380 |
+
def cat_token_mask(self, token_mask: Tensor, batch_size: int) -> Tensor:
|
| 381 |
+
if self.has_class_token:
|
| 382 |
+
cls_mask = torch.ones(
|
| 383 |
+
(batch_size, 1),
|
| 384 |
+
dtype=torch.bool,
|
| 385 |
+
device=token_mask.device,
|
| 386 |
+
)
|
| 387 |
+
token_mask = torch.cat([cls_mask, token_mask], dim=1)
|
| 388 |
+
return token_mask
|
| 389 |
+
|
| 390 |
+
def chunk_tokens(self, x: Tensor) -> tuple[Tensor | None, Tensor]:
|
| 391 |
+
cls_offset = int(self.has_class_token)
|
| 392 |
+
cls = x[:, :cls_offset] if self.has_class_token else None
|
| 393 |
+
patch = x[:, cls_offset:, :]
|
| 394 |
+
return cls, patch
|
| 395 |
+
|
| 396 |
+
def forward(
|
| 397 |
+
self,
|
| 398 |
+
embeds: Float[Tensor, "B L D"],
|
| 399 |
+
embed_ids: Int[Tensor, "B L"] | None = None,
|
| 400 |
+
pred_ids: Int[Tensor, "B Q"] | None = None,
|
| 401 |
+
embed_token_mask: Tensor | None = None,
|
| 402 |
+
pred_token_mask: Tensor | None = None,
|
| 403 |
+
packed_output: bool = False,
|
| 404 |
+
) -> Float[Tensor, "B Q P"] | Float[Tensor, "T P"]:
|
| 405 |
+
"""
|
| 406 |
+
embeds: input patch embeddings.
|
| 407 |
+
embed_ids: optional patch indices for input embeddings. If not provided, no
|
| 408 |
+
position will be added to the embeddings.
|
| 409 |
+
pred_ids: patch indices of query mask positions. If None, decode *all* patches.
|
| 410 |
+
|
| 411 |
+
returns:
|
| 412 |
+
- pred [B, Q, P] where Q is the number of prediction patches and P is the output
|
| 413 |
+
dimension
|
| 414 |
+
"""
|
| 415 |
+
B, L, _ = embeds.shape
|
| 416 |
+
|
| 417 |
+
Q = self.pos_embed.num_patches if pred_ids is None else pred_ids.shape[1]
|
| 418 |
+
mask = self.mask_token.expand(B, Q, -1)
|
| 419 |
+
mask = self.pos_embed(mask, pos_ids=pred_ids)
|
| 420 |
+
|
| 421 |
+
embeds = self.proj(embeds)
|
| 422 |
+
|
| 423 |
+
if embed_ids is not None:
|
| 424 |
+
embeds = self.pos_embed(embeds, pos_ids=embed_ids)
|
| 425 |
+
if embed_token_mask is None:
|
| 426 |
+
embed_token_mask = torch.ones((B, L), dtype=torch.bool, device=embeds.device)
|
| 427 |
+
if pred_token_mask is None:
|
| 428 |
+
pred_token_mask = torch.ones((B, Q), dtype=torch.bool, device=embeds.device)
|
| 429 |
+
x = torch.cat([embeds, mask], dim=1)
|
| 430 |
+
token_mask = torch.cat([embed_token_mask, pred_token_mask], dim=1)
|
| 431 |
+
|
| 432 |
+
x = self.cat_tokens(x)
|
| 433 |
+
token_mask = self.cat_token_mask(token_mask, B)
|
| 434 |
+
jagged_batch = JaggedBatch.from_mask(token_mask)
|
| 435 |
+
x = x[token_mask]
|
| 436 |
+
# Keep headroom for rare maximum-length PSP batches.
|
| 437 |
+
checkpoint_start = max(0, len(self.blocks) - 2)
|
| 438 |
+
for block_index, block in enumerate(self.blocks):
|
| 439 |
+
if self.training and torch.is_grad_enabled() and block_index >= checkpoint_start:
|
| 440 |
+
x = checkpoint(block, x, jagged_batch, use_reentrant=False)
|
| 441 |
+
else:
|
| 442 |
+
x = block(x, jagged_batch=jagged_batch)
|
| 443 |
+
|
| 444 |
+
x = self.norm(x)
|
| 445 |
+
if packed_output:
|
| 446 |
+
pred_offset = int(self.has_class_token) + L
|
| 447 |
+
prediction_mask = F.pad(pred_token_mask, (pred_offset, 0))
|
| 448 |
+
return self.head(x[prediction_mask[token_mask]])
|
| 449 |
+
|
| 450 |
+
x = unpack_tokens(x, token_mask)
|
| 451 |
+
_, x = self.chunk_tokens(x)
|
| 452 |
+
|
| 453 |
+
pred = x[:, L:]
|
| 454 |
+
pred = pred.masked_fill(~pred_token_mask.unsqueeze(-1), 0)
|
| 455 |
+
pred = self.head(pred)
|
| 456 |
+
return pred
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
class MaskedAutoencoderViT(nn.Module, PyTorchModelHubMixin):
|
| 460 |
+
def __init__(
|
| 461 |
+
self,
|
| 462 |
+
img_size: int | tuple[int, int, int] = (208, 240, 208),
|
| 463 |
+
patch_size: int | tuple[int, int, int] = (16, 16, 16),
|
| 464 |
+
in_chans: int = 1,
|
| 465 |
+
depth: int = 12,
|
| 466 |
+
embed_dim: int = 768,
|
| 467 |
+
num_heads: int = 12,
|
| 468 |
+
decoder_depth: int = 4,
|
| 469 |
+
decoder_embed_dim: int | None = 512,
|
| 470 |
+
decoder_num_heads: int | None = 16, # default from mae, head dim = 32
|
| 471 |
+
qkv_bias: bool = True,
|
| 472 |
+
proj_bias: bool = True,
|
| 473 |
+
mlp_ratio: int | float = 4,
|
| 474 |
+
class_token: bool = True,
|
| 475 |
+
reg_tokens: int = 0,
|
| 476 |
+
no_embed_class: bool = False,
|
| 477 |
+
drop_path_rate: float = 0.0,
|
| 478 |
+
mask_drop_scale: bool = False,
|
| 479 |
+
no_decode_pos: bool = False,
|
| 480 |
+
pos_embed: Literal["abs", "sep", "sincos"] = "sincos",
|
| 481 |
+
target_norm: Literal["none", "global", "slice", "patch"] | None = None,
|
| 482 |
+
):
|
| 483 |
+
super().__init__()
|
| 484 |
+
img_size = _to_3d_tuple(img_size, "img_size")
|
| 485 |
+
patch_size = _to_3d_tuple(patch_size, "patch_size")
|
| 486 |
+
|
| 487 |
+
self.no_decode_pos = no_decode_pos # don't pos encode embeddings in decoder
|
| 488 |
+
|
| 489 |
+
# patchify reshapes input into sequence of flattened patches, shape [B, N, P]
|
| 490 |
+
ndim = 3
|
| 491 |
+
patchify = Patchify3D(img_size, patch_size, in_chans=in_chans)
|
| 492 |
+
|
| 493 |
+
# linear patch embedding P -> D
|
| 494 |
+
patch_embed = nn.Linear(patchify.patch_dim, embed_dim)
|
| 495 |
+
|
| 496 |
+
# position embedding
|
| 497 |
+
# separable position embedding decouples the first spatial axis from the
|
| 498 |
+
# others. Fixed sin/cos embeddings are the default for sMRI volumes.
|
| 499 |
+
if pos_embed == "sincos":
|
| 500 |
+
pos_embed_layer = SinCosPosEmbed3D
|
| 501 |
+
else:
|
| 502 |
+
pos_embed_layer = {"abs": AbsolutePosEmbed, "sep": SeparablePosEmbed}[pos_embed]
|
| 503 |
+
pos_embed = pos_embed_layer(embed_dim, patchify.grid_size)
|
| 504 |
+
|
| 505 |
+
# encoder. for inference, this model can be extracted and used like a regular vit
|
| 506 |
+
self.encoder = MaskedEncoder(
|
| 507 |
+
patchify=patchify,
|
| 508 |
+
patch_embed=patch_embed,
|
| 509 |
+
pos_embed=pos_embed,
|
| 510 |
+
depth=depth,
|
| 511 |
+
embed_dim=embed_dim,
|
| 512 |
+
num_heads=num_heads,
|
| 513 |
+
qkv_bias=qkv_bias,
|
| 514 |
+
proj_bias=proj_bias,
|
| 515 |
+
mlp_ratio=mlp_ratio,
|
| 516 |
+
class_token=class_token,
|
| 517 |
+
reg_tokens=reg_tokens,
|
| 518 |
+
no_embed_class=no_embed_class,
|
| 519 |
+
drop_path_rate=drop_path_rate,
|
| 520 |
+
mask_drop_scale=mask_drop_scale,
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
self.pred_patchify = patchify
|
| 524 |
+
|
| 525 |
+
# fall back to encoder architecture width
|
| 526 |
+
decoder_embed_dim = decoder_embed_dim or embed_dim
|
| 527 |
+
decoder_num_heads = decoder_num_heads or num_heads
|
| 528 |
+
|
| 529 |
+
decoder_pos_embed = pos_embed_layer(decoder_embed_dim, self.pred_patchify.grid_size)
|
| 530 |
+
# we might want to try tying the weights of the prediction head to the patch
|
| 531 |
+
# embedding at some point.
|
| 532 |
+
decoder_head = nn.Linear(decoder_embed_dim, self.pred_patchify.patch_dim)
|
| 533 |
+
|
| 534 |
+
self.decoder = MaskedDecoder(
|
| 535 |
+
pos_embed=decoder_pos_embed,
|
| 536 |
+
head=decoder_head,
|
| 537 |
+
input_dim=embed_dim,
|
| 538 |
+
depth=decoder_depth,
|
| 539 |
+
embed_dim=decoder_embed_dim,
|
| 540 |
+
num_heads=decoder_num_heads,
|
| 541 |
+
qkv_bias=qkv_bias,
|
| 542 |
+
proj_bias=proj_bias,
|
| 543 |
+
mlp_ratio=mlp_ratio,
|
| 544 |
+
class_token=class_token,
|
| 545 |
+
no_embed_class=no_embed_class,
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
# mae style target normalization
|
| 549 |
+
# dim is relative to an unflattened embedding tensor of shape [B, *grid_size, D]
|
| 550 |
+
if target_norm not in {"none", None}:
|
| 551 |
+
norm_dim = {
|
| 552 |
+
"global": tuple(range(1, ndim + 2)), # full sequence
|
| 553 |
+
"slice": tuple(range(2, ndim + 2)), # each depth slice along first dim
|
| 554 |
+
"patch": -1, # normalize each patch independently (mae pix norm loss)
|
| 555 |
+
}[target_norm]
|
| 556 |
+
self.target_norm = Normalize(self.pred_patchify.grid_size, dim=norm_dim)
|
| 557 |
+
else:
|
| 558 |
+
self.target_norm = None
|
| 559 |
+
|
| 560 |
+
self.init_weights()
|
| 561 |
+
|
| 562 |
+
def extra_repr(self):
|
| 563 |
+
return f"no_decode_pos={self.no_decode_pos}"
|
| 564 |
+
|
| 565 |
+
def init_weights(self):
|
| 566 |
+
self.apply(_init_weights)
|
| 567 |
+
|
| 568 |
+
def prepare_targets(self, images: Tensor, img_mask: Tensor | None):
|
| 569 |
+
"""
|
| 570 |
+
images: [B, C, D, H, W]
|
| 571 |
+
img_mask: mask of valid data. only used for computing correct normalization
|
| 572 |
+
stats. same shape as images.
|
| 573 |
+
"""
|
| 574 |
+
targets_patches = self.pred_patchify(images) # [B, N, P]
|
| 575 |
+
|
| 576 |
+
# target normalization
|
| 577 |
+
if self.target_norm is not None:
|
| 578 |
+
# full image data mask used for normalization stats only
|
| 579 |
+
if img_mask is not None:
|
| 580 |
+
img_mask_patches = self.pred_patchify(img_mask)
|
| 581 |
+
else:
|
| 582 |
+
img_mask_patches = None
|
| 583 |
+
targets_patches, *targets_stats = self.target_norm(targets_patches, img_mask_patches)
|
| 584 |
+
else:
|
| 585 |
+
targets_stats = None
|
| 586 |
+
|
| 587 |
+
return targets_patches, targets_stats
|
| 588 |
+
|
| 589 |
+
def prepare_masks(
|
| 590 |
+
self,
|
| 591 |
+
img_mask: Tensor,
|
| 592 |
+
visible_mask: Tensor | None,
|
| 593 |
+
pred_mask: Tensor | None,
|
| 594 |
+
device: torch.device,
|
| 595 |
+
):
|
| 596 |
+
img_mask = img_mask.to(device=device, dtype=torch.bool)
|
| 597 |
+
|
| 598 |
+
if visible_mask is None:
|
| 599 |
+
visible_mask = img_mask
|
| 600 |
+
else:
|
| 601 |
+
visible_mask = img_mask & visible_mask.to(device=device, dtype=torch.bool)
|
| 602 |
+
|
| 603 |
+
if pred_mask is None:
|
| 604 |
+
pred_mask = img_mask
|
| 605 |
+
else:
|
| 606 |
+
pred_mask = img_mask & pred_mask.to(device=device, dtype=torch.bool)
|
| 607 |
+
|
| 608 |
+
return img_mask, visible_mask, pred_mask
|
| 609 |
+
|
| 610 |
+
def prepare_pred_mask(
|
| 611 |
+
self,
|
| 612 |
+
visible_mask: Tensor,
|
| 613 |
+
pred_mask: Tensor | None = None,
|
| 614 |
+
pred_mask_ratio: float | None = None,
|
| 615 |
+
pad_to_multiple: int | None = None,
|
| 616 |
+
):
|
| 617 |
+
"""
|
| 618 |
+
prepare prediction mask by removing visible content
|
| 619 |
+
visible_mask: [B, C, D, H, W], 1 = visible, 0 = invisible
|
| 620 |
+
pred_mask: same shape, 1 = predict, 0 = don't predict
|
| 621 |
+
"""
|
| 622 |
+
if pred_mask is None:
|
| 623 |
+
pred_mask = torch.ones_like(visible_mask)
|
| 624 |
+
|
| 625 |
+
pred_mask = pred_mask & ~visible_mask
|
| 626 |
+
|
| 627 |
+
pred_mask_patches = self.pred_patchify(pred_mask)
|
| 628 |
+
pred_patch_mask = pred_mask_patches.any(dim=-1)
|
| 629 |
+
# Optionally subsample the prediction candidates.
|
| 630 |
+
mask_ratio = 0.0 if pred_mask_ratio is None else pred_mask_ratio
|
| 631 |
+
pred_patch_mask, pred_ids, pred_token_mask = pad_patch_mask(
|
| 632 |
+
pred_patch_mask,
|
| 633 |
+
mask_ratio=mask_ratio,
|
| 634 |
+
# With per-sample padding every candidate is retained when the ratio
|
| 635 |
+
# is zero, so randomizing their order is pure overhead.
|
| 636 |
+
shuffle=mask_ratio > 0,
|
| 637 |
+
pad_to_multiple=pad_to_multiple,
|
| 638 |
+
)
|
| 639 |
+
pred_mask_patches = pred_mask_patches & pred_patch_mask.unsqueeze(-1)
|
| 640 |
+
return pred_mask_patches, pred_ids, pred_token_mask
|
| 641 |
+
|
| 642 |
+
def forward_decoder(
|
| 643 |
+
self,
|
| 644 |
+
patch_embeds: Float[Tensor, "B L D"],
|
| 645 |
+
visible_ids: Int[Tensor, "B L"],
|
| 646 |
+
pred_ids: Int[Tensor, "B Q"] | None,
|
| 647 |
+
visible_token_mask: Tensor | None = None,
|
| 648 |
+
pred_token_mask: Tensor | None = None,
|
| 649 |
+
packed_output: bool = False,
|
| 650 |
+
) -> Float[Tensor, "B Q P"] | Float[Tensor, "T P"]:
|
| 651 |
+
return self.decoder(
|
| 652 |
+
patch_embeds,
|
| 653 |
+
embed_ids=None if self.no_decode_pos else visible_ids,
|
| 654 |
+
pred_ids=pred_ids,
|
| 655 |
+
embed_token_mask=visible_token_mask,
|
| 656 |
+
pred_token_mask=pred_token_mask,
|
| 657 |
+
packed_output=packed_output,
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
def forward_loss(
|
| 661 |
+
self,
|
| 662 |
+
preds: Float[Tensor, "T P"],
|
| 663 |
+
targets_patches: Float[Tensor, "B N P"],
|
| 664 |
+
pred_mask_patches: Float[Tensor, "B N P"],
|
| 665 |
+
pred_ids: Int[Tensor, "B Q"],
|
| 666 |
+
pred_token_mask: Tensor,
|
| 667 |
+
) -> Tensor:
|
| 668 |
+
"""Average valid-voxel MSE within each scan, then average across scans."""
|
| 669 |
+
batch_ids, slot_ids = pred_token_mask.nonzero(as_tuple=True)
|
| 670 |
+
patch_ids = pred_ids[batch_ids, slot_ids]
|
| 671 |
+
targets = targets_patches[batch_ids, patch_ids]
|
| 672 |
+
voxel_mask = pred_mask_patches[batch_ids, patch_ids]
|
| 673 |
+
|
| 674 |
+
patch_errors = ((preds - targets) ** 2 * voxel_mask).sum(dim=1)
|
| 675 |
+
patch_voxels = voxel_mask.sum(dim=1).to(dtype=patch_errors.dtype)
|
| 676 |
+
batch_size = targets_patches.shape[0]
|
| 677 |
+
scan_errors = patch_errors.new_zeros(batch_size).scatter_add_(0, batch_ids, patch_errors)
|
| 678 |
+
scan_voxels = patch_voxels.new_zeros(batch_size).scatter_add_(0, batch_ids, patch_voxels)
|
| 679 |
+
return (scan_errors / scan_voxels).mean()
|
| 680 |
+
|
| 681 |
+
@torch.no_grad()
|
| 682 |
+
def forward_pred_images(
|
| 683 |
+
self,
|
| 684 |
+
preds: Float[Tensor, "B Q P"],
|
| 685 |
+
pred_ids: Int[Tensor, "B Q"],
|
| 686 |
+
pred_token_mask: Tensor | None = None,
|
| 687 |
+
img_mask: Tensor | None = None,
|
| 688 |
+
targets_stats: tuple[Tensor, Tensor] | None = None,
|
| 689 |
+
) -> Tensor:
|
| 690 |
+
B, _, P = preds.shape
|
| 691 |
+
N = self.pred_patchify.num_patches
|
| 692 |
+
if pred_token_mask is not None:
|
| 693 |
+
preds = preds.masked_fill(~pred_token_mask.unsqueeze(-1), 0)
|
| 694 |
+
|
| 695 |
+
preds = torch.zeros((B, N, P), dtype=preds.dtype, device=preds.device).scatter_add_(
|
| 696 |
+
1, pred_ids.unsqueeze(-1).expand(-1, -1, P), preds
|
| 697 |
+
)
|
| 698 |
+
|
| 699 |
+
if targets_stats is not None:
|
| 700 |
+
targets_mean, targets_std = targets_stats
|
| 701 |
+
preds = preds * targets_std + targets_mean
|
| 702 |
+
|
| 703 |
+
pred_images = self.pred_patchify.unpatchify(preds)
|
| 704 |
+
if img_mask is not None:
|
| 705 |
+
pred_images = pred_images.masked_fill(~img_mask, 0)
|
| 706 |
+
return pred_images
|
| 707 |
+
|
| 708 |
+
def forward(
|
| 709 |
+
self,
|
| 710 |
+
images: Tensor,
|
| 711 |
+
img_mask: Tensor,
|
| 712 |
+
mask_ratio: float,
|
| 713 |
+
pred_mask_ratio: float | None = None,
|
| 714 |
+
pad_to_multiple: int | None = None,
|
| 715 |
+
with_state: bool = True,
|
| 716 |
+
) -> Tensor | tuple[Tensor, dict]:
|
| 717 |
+
img_mask, visible_mask, pred_mask = self.prepare_masks(
|
| 718 |
+
img_mask,
|
| 719 |
+
None,
|
| 720 |
+
None,
|
| 721 |
+
device=images.device,
|
| 722 |
+
)
|
| 723 |
+
targets_patches, targets_stats = self.prepare_targets(images, img_mask)
|
| 724 |
+
|
| 725 |
+
(
|
| 726 |
+
cls_embeds,
|
| 727 |
+
reg_embeds,
|
| 728 |
+
patch_embeds,
|
| 729 |
+
visible_mask,
|
| 730 |
+
visible_ids,
|
| 731 |
+
visible_token_mask,
|
| 732 |
+
) = self.encoder(
|
| 733 |
+
images,
|
| 734 |
+
mask=visible_mask,
|
| 735 |
+
mask_ratio=mask_ratio,
|
| 736 |
+
pad_to_multiple=pad_to_multiple,
|
| 737 |
+
)
|
| 738 |
+
|
| 739 |
+
pred_mask_patches, pred_ids, pred_token_mask = self.prepare_pred_mask(
|
| 740 |
+
visible_mask,
|
| 741 |
+
pred_mask=pred_mask,
|
| 742 |
+
pred_mask_ratio=pred_mask_ratio,
|
| 743 |
+
pad_to_multiple=pad_to_multiple,
|
| 744 |
+
)
|
| 745 |
+
|
| 746 |
+
preds = self.forward_decoder(
|
| 747 |
+
patch_embeds,
|
| 748 |
+
visible_ids,
|
| 749 |
+
pred_ids,
|
| 750 |
+
visible_token_mask=visible_token_mask,
|
| 751 |
+
pred_token_mask=pred_token_mask,
|
| 752 |
+
packed_output=not with_state,
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
+
loss_preds = preds if not with_state else preds[pred_token_mask]
|
| 756 |
+
loss = self.forward_loss(
|
| 757 |
+
loss_preds,
|
| 758 |
+
targets_patches,
|
| 759 |
+
pred_mask_patches,
|
| 760 |
+
pred_ids,
|
| 761 |
+
pred_token_mask,
|
| 762 |
+
)
|
| 763 |
+
|
| 764 |
+
if not with_state:
|
| 765 |
+
return loss
|
| 766 |
+
|
| 767 |
+
pred_mask = self.pred_patchify.unpatchify(pred_mask_patches)
|
| 768 |
+
pred_images = self.forward_pred_images(
|
| 769 |
+
preds,
|
| 770 |
+
pred_ids,
|
| 771 |
+
pred_token_mask=pred_token_mask,
|
| 772 |
+
img_mask=img_mask,
|
| 773 |
+
targets_stats=targets_stats,
|
| 774 |
+
)
|
| 775 |
+
|
| 776 |
+
state = {
|
| 777 |
+
"targets_patches": targets_patches,
|
| 778 |
+
"targets_stats": targets_stats,
|
| 779 |
+
"patch_embeds": patch_embeds,
|
| 780 |
+
"cls_embeds": cls_embeds,
|
| 781 |
+
"reg_embeds": reg_embeds,
|
| 782 |
+
"visible_mask": visible_mask,
|
| 783 |
+
"visible_ids": visible_ids,
|
| 784 |
+
"visible_token_mask": visible_token_mask,
|
| 785 |
+
"pred_mask": pred_mask,
|
| 786 |
+
"pred_ids": pred_ids,
|
| 787 |
+
"pred_token_mask": pred_token_mask,
|
| 788 |
+
"preds": preds,
|
| 789 |
+
"pred_images": pred_images,
|
| 790 |
+
}
|
| 791 |
+
return loss, state
|
| 792 |
+
|
| 793 |
+
def forward_embedding(
|
| 794 |
+
self,
|
| 795 |
+
x: Tensor,
|
| 796 |
+
mask: Tensor | None = None,
|
| 797 |
+
mask_ratio: float | None = None,
|
| 798 |
+
):
|
| 799 |
+
return self.encoder.forward_embedding(x, mask=mask, mask_ratio=mask_ratio)
|
| 800 |
+
|
| 801 |
+
|
| 802 |
+
class MaskedViT(MaskedEncoder, PyTorchModelHubMixin):
|
| 803 |
+
def __init__(
|
| 804 |
+
self,
|
| 805 |
+
img_size: int | tuple[int, int, int] = (208, 240, 208),
|
| 806 |
+
in_chans: int = 1,
|
| 807 |
+
patch_size: int | tuple[int, int, int] = (16, 16, 16),
|
| 808 |
+
depth: int = 12,
|
| 809 |
+
embed_dim: int = 768,
|
| 810 |
+
num_heads: int = 12,
|
| 811 |
+
qkv_bias: bool = True,
|
| 812 |
+
proj_bias: bool = True,
|
| 813 |
+
mlp_ratio: int | float = 4,
|
| 814 |
+
class_token: bool = True,
|
| 815 |
+
reg_tokens: int = 0,
|
| 816 |
+
no_embed_class: bool = False,
|
| 817 |
+
final_norm: bool = True,
|
| 818 |
+
drop_path_rate: float = 0.0,
|
| 819 |
+
mask_drop_scale: bool = False,
|
| 820 |
+
pos_embed: Literal["abs", "sep", "sincos"] = "sincos",
|
| 821 |
+
):
|
| 822 |
+
img_size = _to_3d_tuple(img_size, "img_size")
|
| 823 |
+
patch_size = _to_3d_tuple(patch_size, "patch_size")
|
| 824 |
+
|
| 825 |
+
patchify = Patchify3D(img_size, patch_size, in_chans=in_chans)
|
| 826 |
+
patch_embed = nn.Linear(patchify.patch_dim, embed_dim)
|
| 827 |
+
if pos_embed == "sincos":
|
| 828 |
+
pos_embed_layer = SinCosPosEmbed3D
|
| 829 |
+
else:
|
| 830 |
+
pos_embed_layer = {"abs": AbsolutePosEmbed, "sep": SeparablePosEmbed}[pos_embed]
|
| 831 |
+
pos_embed = pos_embed_layer(embed_dim, patchify.grid_size)
|
| 832 |
+
|
| 833 |
+
super().__init__(
|
| 834 |
+
patchify=patchify,
|
| 835 |
+
patch_embed=patch_embed,
|
| 836 |
+
pos_embed=pos_embed,
|
| 837 |
+
depth=depth,
|
| 838 |
+
embed_dim=embed_dim,
|
| 839 |
+
num_heads=num_heads,
|
| 840 |
+
qkv_bias=qkv_bias,
|
| 841 |
+
proj_bias=proj_bias,
|
| 842 |
+
mlp_ratio=mlp_ratio,
|
| 843 |
+
class_token=class_token,
|
| 844 |
+
reg_tokens=reg_tokens,
|
| 845 |
+
no_embed_class=no_embed_class,
|
| 846 |
+
final_norm=final_norm,
|
| 847 |
+
drop_path_rate=drop_path_rate,
|
| 848 |
+
mask_drop_scale=mask_drop_scale,
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
self.init_weights()
|
| 852 |
+
|
| 853 |
+
def init_weights(self):
|
| 854 |
+
self.apply(_init_weights)
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
def _to_3d_tuple(value: int | Sequence[int], name: str) -> tuple[int, int, int]:
|
| 858 |
+
if isinstance(value, int):
|
| 859 |
+
return (value, value, value)
|
| 860 |
+
if len(value) != 3:
|
| 861 |
+
raise ValueError(f"{name} must have exactly 3 spatial dimensions, got {tuple(value)}")
|
| 862 |
+
return tuple(int(item) for item in value)
|
| 863 |
+
|
| 864 |
+
|
| 865 |
+
# JAX ViT xavier uniform init
|
| 866 |
+
# https://github.com/facebookresearch/capi/blob/main/model.py
|
| 867 |
+
def _init_weights(m: nn.Module) -> None:
|
| 868 |
+
if isinstance(m, nn.Linear):
|
| 869 |
+
nn.init.xavier_uniform_(m.weight)
|
| 870 |
+
if m.bias is not None:
|
| 871 |
+
nn.init.constant_(m.bias, 0)
|
| 872 |
+
elif isinstance(m, nn.LayerNorm) and m.elementwise_affine:
|
| 873 |
+
nn.init.constant_(m.weight, 1.0)
|
| 874 |
+
if m.bias is not None:
|
| 875 |
+
nn.init.constant_(m.bias, 0)
|
| 876 |
+
|
| 877 |
+
|
| 878 |
+
def _create_vit(**kwargs):
|
| 879 |
+
model = MaskedViT(**kwargs)
|
| 880 |
+
return model
|
| 881 |
+
|
| 882 |
+
|
| 883 |
+
def _create_mae_vit(**kwargs):
|
| 884 |
+
model = MaskedAutoencoderViT(**kwargs)
|
| 885 |
+
return model
|
| 886 |
+
|
| 887 |
+
|
| 888 |
+
def mae_vit_small(**kwargs):
|
| 889 |
+
model_args = dict(embed_dim=384, depth=12, num_heads=6)
|
| 890 |
+
return _create_mae_vit(**model_args, **kwargs)
|
| 891 |
+
|
| 892 |
+
|
| 893 |
+
def mae_vit_base(**kwargs):
|
| 894 |
+
model_args = dict(embed_dim=768, depth=12, num_heads=12)
|
| 895 |
+
return _create_mae_vit(**model_args, **kwargs)
|
| 896 |
+
|
| 897 |
+
|
| 898 |
+
def mae_vit_large(**kwargs):
|
| 899 |
+
model_args = dict(embed_dim=1024, depth=24, num_heads=16)
|
| 900 |
+
return _create_mae_vit(**model_args, **kwargs)
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
def mae_vit_huge(**kwargs):
|
| 904 |
+
model_args = dict(embed_dim=1280, depth=32, num_heads=16)
|
| 905 |
+
return _create_mae_vit(**model_args, **kwargs)
|
| 906 |
+
|
| 907 |
+
|
| 908 |
+
# "patch embed" baseline model, depth 0 ViT (hah)
|
| 909 |
+
def patch_embed_small(**kwargs):
|
| 910 |
+
model_args = dict(embed_dim=384, depth=0)
|
| 911 |
+
return _create_vit(**model_args, **kwargs)
|
| 912 |
+
|
| 913 |
+
|
| 914 |
+
def patch_embed_base(**kwargs):
|
| 915 |
+
model_args = dict(embed_dim=768, depth=0)
|
| 916 |
+
return _create_vit(**model_args, **kwargs)
|
finetune/fomo_tune_baseline/output/task5/build/smri_mae/modules.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 2 |
+
#
|
| 3 |
+
# References:
|
| 4 |
+
# capi: https://github.com/facebookresearch/capi/blob/main/model.py
|
| 5 |
+
# timm: https://github.com/huggingface/pytorch-image-models/blob/v1.0.20/timm/models/vision_transformer.py
|
| 6 |
+
# vjepa2: https://github.com/facebookresearch/vjepa2/blob/main/src/models/utils/pos_embs.py
|
| 7 |
+
|
| 8 |
+
import math
|
| 9 |
+
from functools import partial
|
| 10 |
+
from typing import NamedTuple, Type
|
| 11 |
+
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
from torch import Tensor
|
| 17 |
+
from einops import rearrange
|
| 18 |
+
from jaxtyping import Float, Int
|
| 19 |
+
from timm.layers import DropPath, to_3tuple
|
| 20 |
+
|
| 21 |
+
Layer = Type[nn.Module]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class JaggedBatch(NamedTuple):
|
| 25 |
+
"""Sequence boundaries and cached launch metadata for jagged attention."""
|
| 26 |
+
|
| 27 |
+
offsets: Tensor
|
| 28 |
+
max_seqlen: int
|
| 29 |
+
|
| 30 |
+
@classmethod
|
| 31 |
+
def from_mask(cls, mask: Tensor) -> "JaggedBatch":
|
| 32 |
+
mask = mask.to(dtype=torch.bool)
|
| 33 |
+
counts = mask.sum(dim=1)
|
| 34 |
+
return cls(
|
| 35 |
+
offsets=F.pad(counts.cumsum(dim=0), (1, 0)),
|
| 36 |
+
max_seqlen=mask.shape[1],
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
def as_nested(self, tokens: Tensor) -> Tensor:
|
| 40 |
+
# Cached conservative bounds avoid min/max reductions and GPU-to-CPU
|
| 41 |
+
# synchronization when Flash SDPA inspects the jagged sequence lengths.
|
| 42 |
+
return torch.nested.nested_tensor_from_jagged(
|
| 43 |
+
tokens,
|
| 44 |
+
self.offsets,
|
| 45 |
+
min_seqlen=1,
|
| 46 |
+
max_seqlen=self.max_seqlen,
|
| 47 |
+
).transpose(1, 2)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def unpack_tokens(tokens: Tensor, token_mask: Tensor) -> Tensor:
|
| 51 |
+
"""Restore packed values to a padded batch, filling invalid slots with zero."""
|
| 52 |
+
output = tokens.new_zeros((*token_mask.shape, *tokens.shape[1:]))
|
| 53 |
+
return output.index_put((token_mask,), tokens)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def jagged_scaled_dot_product_attention(
|
| 57 |
+
query: Tensor,
|
| 58 |
+
key: Tensor,
|
| 59 |
+
value: Tensor,
|
| 60 |
+
jagged_batch: JaggedBatch,
|
| 61 |
+
) -> Tensor:
|
| 62 |
+
"""Run SDPA on a packed batch of variable-length sequences."""
|
| 63 |
+
output_jagged = F.scaled_dot_product_attention(
|
| 64 |
+
jagged_batch.as_nested(query),
|
| 65 |
+
jagged_batch.as_nested(key),
|
| 66 |
+
jagged_batch.as_nested(value),
|
| 67 |
+
)
|
| 68 |
+
return output_jagged.transpose(1, 2).values()
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
class Attention(nn.Module):
|
| 72 |
+
def __init__(
|
| 73 |
+
self,
|
| 74 |
+
dim: int,
|
| 75 |
+
num_heads: int,
|
| 76 |
+
qkv_bias: bool = False,
|
| 77 |
+
proj_bias: bool = False,
|
| 78 |
+
) -> None:
|
| 79 |
+
super().__init__()
|
| 80 |
+
self.num_heads = num_heads
|
| 81 |
+
self.head_dim = dim // num_heads
|
| 82 |
+
self.qkv = nn.Linear(dim, 3 * dim, bias=qkv_bias)
|
| 83 |
+
self.proj = nn.Linear(dim, dim, bias=proj_bias)
|
| 84 |
+
|
| 85 |
+
def extra_repr(self):
|
| 86 |
+
return f"num_heads={self.num_heads}"
|
| 87 |
+
|
| 88 |
+
def forward(
|
| 89 |
+
self,
|
| 90 |
+
x: Float[Tensor, "L D"],
|
| 91 |
+
jagged_batch: JaggedBatch,
|
| 92 |
+
) -> Float[Tensor, "L D"]:
|
| 93 |
+
L, D = x.shape
|
| 94 |
+
h, dh = self.num_heads, self.head_dim
|
| 95 |
+
|
| 96 |
+
qkv = self.qkv(x).reshape(L, 3, h, dh)
|
| 97 |
+
q, k, v = qkv.unbind(1)
|
| 98 |
+
|
| 99 |
+
x = jagged_scaled_dot_product_attention(
|
| 100 |
+
q,
|
| 101 |
+
k,
|
| 102 |
+
v,
|
| 103 |
+
jagged_batch=jagged_batch,
|
| 104 |
+
)
|
| 105 |
+
x = x.reshape(L, D)
|
| 106 |
+
x = self.proj(x)
|
| 107 |
+
return x
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class Mlp(nn.Module):
|
| 111 |
+
def __init__(
|
| 112 |
+
self,
|
| 113 |
+
dim: int,
|
| 114 |
+
mlp_ratio: int | float = 4,
|
| 115 |
+
bias: bool = False,
|
| 116 |
+
) -> None:
|
| 117 |
+
super().__init__()
|
| 118 |
+
hidden_features = int(dim * mlp_ratio)
|
| 119 |
+
self.fc1 = nn.Linear(dim, hidden_features, bias=bias)
|
| 120 |
+
self.act = nn.GELU()
|
| 121 |
+
self.fc2 = nn.Linear(hidden_features, dim, bias=bias)
|
| 122 |
+
|
| 123 |
+
def forward(self, x: Float[Tensor, "... D"]) -> Float[Tensor, "... D"]:
|
| 124 |
+
x = self.fc1(x)
|
| 125 |
+
x = self.act(x)
|
| 126 |
+
x = self.fc2(x)
|
| 127 |
+
return x
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# timm default eps=1e-6
|
| 131 |
+
LayerNorm = partial(nn.LayerNorm, eps=1e-6)
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class Block(nn.Module):
|
| 135 |
+
def __init__(
|
| 136 |
+
self,
|
| 137 |
+
dim: int,
|
| 138 |
+
num_heads: int,
|
| 139 |
+
qkv_bias: bool = False,
|
| 140 |
+
proj_bias: bool = False,
|
| 141 |
+
mlp_ratio: int | float = 4,
|
| 142 |
+
drop_path: float = 0.0,
|
| 143 |
+
norm_layer: Layer = LayerNorm,
|
| 144 |
+
) -> None:
|
| 145 |
+
super().__init__()
|
| 146 |
+
self.norm1 = norm_layer(dim)
|
| 147 |
+
self.attn = Attention(
|
| 148 |
+
dim=dim,
|
| 149 |
+
num_heads=num_heads,
|
| 150 |
+
qkv_bias=qkv_bias,
|
| 151 |
+
proj_bias=proj_bias,
|
| 152 |
+
)
|
| 153 |
+
self.drop_path1 = DropPath(drop_path) if drop_path > 0 else nn.Identity()
|
| 154 |
+
|
| 155 |
+
self.norm2 = norm_layer(dim)
|
| 156 |
+
self.mlp = Mlp(
|
| 157 |
+
dim=dim,
|
| 158 |
+
mlp_ratio=mlp_ratio,
|
| 159 |
+
bias=proj_bias,
|
| 160 |
+
)
|
| 161 |
+
self.drop_path2 = DropPath(drop_path) if drop_path > 0 else nn.Identity()
|
| 162 |
+
|
| 163 |
+
def forward(
|
| 164 |
+
self,
|
| 165 |
+
x: Float[Tensor, "L D"],
|
| 166 |
+
jagged_batch: JaggedBatch,
|
| 167 |
+
) -> Float[Tensor, "L D"]:
|
| 168 |
+
x = x + self.drop_path1(
|
| 169 |
+
self.attn(
|
| 170 |
+
self.norm1(x),
|
| 171 |
+
jagged_batch=jagged_batch,
|
| 172 |
+
)
|
| 173 |
+
)
|
| 174 |
+
x = x + self.drop_path2(self.mlp(self.norm2(x)))
|
| 175 |
+
return x
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
# Patching and position embedding modules
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class Patchify3D(nn.Module):
|
| 182 |
+
def __init__(
|
| 183 |
+
self,
|
| 184 |
+
img_size: int | tuple[int, int, int],
|
| 185 |
+
patch_size: int | tuple[int, int, int],
|
| 186 |
+
in_chans: int = 3,
|
| 187 |
+
) -> None:
|
| 188 |
+
super().__init__()
|
| 189 |
+
self.img_size = to_3tuple(img_size)
|
| 190 |
+
self.patch_size = to_3tuple(patch_size)
|
| 191 |
+
self.in_chans = in_chans
|
| 192 |
+
|
| 193 |
+
T, H, W = self.img_size
|
| 194 |
+
p_t, p_h, p_w = self.patch_size
|
| 195 |
+
if T % p_t or H % p_h or W % p_w:
|
| 196 |
+
raise ValueError(
|
| 197 |
+
f"img_size {self.img_size} must be divisible by patch_size {self.patch_size}"
|
| 198 |
+
)
|
| 199 |
+
self.grid_size = (T // p_t, H // p_h, W // p_w)
|
| 200 |
+
self.num_patches = math.prod(self.grid_size)
|
| 201 |
+
self.patch_dim = in_chans * math.prod(self.patch_size)
|
| 202 |
+
|
| 203 |
+
def forward(self, x: Float[Tensor, "B C T H W"]) -> Float[Tensor, "B N P"]:
|
| 204 |
+
x = patchify3d(x, self.patch_size)
|
| 205 |
+
return x
|
| 206 |
+
|
| 207 |
+
def unpatchify(self, x: Float[Tensor, "B N P"]) -> Float[Tensor, "B C T H W"]:
|
| 208 |
+
x = unpatchify3d(x, patch_size=self.patch_size, img_size=self.img_size)
|
| 209 |
+
return x
|
| 210 |
+
|
| 211 |
+
def extra_repr(self):
|
| 212 |
+
return f"{self.img_size}, {self.patch_size}, in_chans={self.in_chans}"
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def patchify3d(x: Tensor, patch_size: tuple[int, int, int]) -> Tensor:
|
| 216 |
+
p_t, p_h, p_w = to_3tuple(patch_size)
|
| 217 |
+
B, C, T, H, W = x.shape
|
| 218 |
+
x = rearrange(x, "b c (t u) (h p) (w q) -> b (t h w) (c u p q)", u=p_t, p=p_h, q=p_w)
|
| 219 |
+
return x
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def unpatchify3d(
|
| 223 |
+
x: Tensor,
|
| 224 |
+
patch_size: tuple[int, int, int],
|
| 225 |
+
img_size: tuple[int, int, int],
|
| 226 |
+
) -> Tensor:
|
| 227 |
+
B, N, P = x.shape
|
| 228 |
+
p_t, p_h, p_w = to_3tuple(patch_size)
|
| 229 |
+
T, H, W = to_3tuple(img_size)
|
| 230 |
+
x = rearrange(
|
| 231 |
+
x,
|
| 232 |
+
"b (t h w) (c u p q) -> b c (t u) (h p) (w q)",
|
| 233 |
+
t=T // p_t,
|
| 234 |
+
h=H // p_h,
|
| 235 |
+
w=W // p_w,
|
| 236 |
+
u=p_t,
|
| 237 |
+
p=p_h,
|
| 238 |
+
q=p_w,
|
| 239 |
+
)
|
| 240 |
+
return x
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
class AbsolutePosEmbed(nn.Module):
|
| 244 |
+
def __init__(self, embed_dim: int, grid_size: tuple[int, ...]) -> None:
|
| 245 |
+
super().__init__()
|
| 246 |
+
self.embed_dim = embed_dim
|
| 247 |
+
self.grid_size = grid_size
|
| 248 |
+
self.num_patches = math.prod(grid_size)
|
| 249 |
+
|
| 250 |
+
self.weight = nn.Parameter(torch.empty(self.num_patches, embed_dim))
|
| 251 |
+
self.reset_parameters()
|
| 252 |
+
|
| 253 |
+
def reset_parameters(self):
|
| 254 |
+
nn.init.trunc_normal_(self.weight, std=0.02)
|
| 255 |
+
|
| 256 |
+
def forward(
|
| 257 |
+
self,
|
| 258 |
+
x: Float[Tensor, "B L D"],
|
| 259 |
+
pos_ids: Int[Tensor, "B L"] | None = None,
|
| 260 |
+
) -> Float[Tensor, "B L D"]:
|
| 261 |
+
x = apply_pos_embed(x, self.weight, pos_ids=pos_ids)
|
| 262 |
+
return x
|
| 263 |
+
|
| 264 |
+
def extra_repr(self):
|
| 265 |
+
return f"{self.embed_dim}, {self.grid_size}"
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
class SeparablePosEmbed(nn.Module):
|
| 269 |
+
def __init__(self, embed_dim: int, grid_size: tuple[int, ...]) -> None:
|
| 270 |
+
super().__init__()
|
| 271 |
+
self.embed_dim = embed_dim
|
| 272 |
+
self.grid_size = grid_size
|
| 273 |
+
self.num_patches = math.prod(grid_size)
|
| 274 |
+
|
| 275 |
+
N_t, *grid_size_spatial = grid_size
|
| 276 |
+
N_s = math.prod(grid_size_spatial)
|
| 277 |
+
self.weight_spatial = nn.Parameter(torch.empty(1, N_s, embed_dim))
|
| 278 |
+
self.weight_temporal = nn.Parameter(torch.empty(N_t, 1, embed_dim))
|
| 279 |
+
self.reset_parameters()
|
| 280 |
+
|
| 281 |
+
def reset_parameters(self):
|
| 282 |
+
nn.init.trunc_normal_(self.weight_spatial, std=0.02)
|
| 283 |
+
nn.init.trunc_normal_(self.weight_temporal, std=0.02)
|
| 284 |
+
|
| 285 |
+
def forward(
|
| 286 |
+
self,
|
| 287 |
+
x: Float[Tensor, "B L D"],
|
| 288 |
+
pos_ids: Int[Tensor, "B L"] | None = None,
|
| 289 |
+
) -> Float[Tensor, "B L D"]:
|
| 290 |
+
B, N, D = x.shape
|
| 291 |
+
weight = (self.weight_temporal + self.weight_spatial).flatten(0, 1) # [N, D]
|
| 292 |
+
x = apply_pos_embed(x, weight, pos_ids=pos_ids)
|
| 293 |
+
return x
|
| 294 |
+
|
| 295 |
+
def extra_repr(self):
|
| 296 |
+
return f"{self.embed_dim}, {self.grid_size}"
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
class SinCosPosEmbed3D(nn.Module):
|
| 300 |
+
def __init__(self, embed_dim: int, grid_size: tuple[int, int, int]) -> None:
|
| 301 |
+
super().__init__()
|
| 302 |
+
self.embed_dim = embed_dim
|
| 303 |
+
self.grid_size = grid_size
|
| 304 |
+
self.num_patches = math.prod(grid_size)
|
| 305 |
+
|
| 306 |
+
N_t, N_h, N_w = grid_size
|
| 307 |
+
weight = get_3d_sincos_pos_embed(
|
| 308 |
+
embed_dim=embed_dim,
|
| 309 |
+
grid_size=(N_h, N_w),
|
| 310 |
+
grid_depth=N_t,
|
| 311 |
+
uniform_power=True,
|
| 312 |
+
)
|
| 313 |
+
self.weight = nn.Parameter(torch.from_numpy(weight).float(), requires_grad=False)
|
| 314 |
+
|
| 315 |
+
def forward(
|
| 316 |
+
self,
|
| 317 |
+
x: Float[Tensor, "B L D"],
|
| 318 |
+
pos_ids: Int[Tensor, "B L"] | None = None,
|
| 319 |
+
) -> Float[Tensor, "B L D"]:
|
| 320 |
+
x = apply_pos_embed(x, self.weight, pos_ids=pos_ids)
|
| 321 |
+
return x
|
| 322 |
+
|
| 323 |
+
def extra_repr(self):
|
| 324 |
+
return f"{self.embed_dim}, {self.grid_size}"
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
# sincos pos embed utils from vjepa2, but fixed the confusing meshgrid indexing
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def get_3d_sincos_pos_embed(embed_dim, grid_size, grid_depth, cls_token=False, uniform_power=False):
|
| 331 |
+
"""
|
| 332 |
+
grid_size: tuple of int of the grid height and width
|
| 333 |
+
grid_depth: int of the grid depth
|
| 334 |
+
returns:
|
| 335 |
+
pos_embed: [grid_depth*grid_height*grid_width, embed_dim] (w/o cls_token)
|
| 336 |
+
or [1+grid_depth*grid_height*grid_width, embed_dim] (w/ cls_token)
|
| 337 |
+
"""
|
| 338 |
+
grid_d = np.arange(grid_depth, dtype=float)
|
| 339 |
+
grid_h = np.arange(grid_size[0], dtype=float)
|
| 340 |
+
grid_w = np.arange(grid_size[1], dtype=float)
|
| 341 |
+
grid_d, grid_h, grid_w = np.meshgrid(grid_d, grid_h, grid_w, indexing="ij")
|
| 342 |
+
|
| 343 |
+
if not uniform_power:
|
| 344 |
+
h_embed_dim = embed_dim // 4
|
| 345 |
+
w_embed_dim = embed_dim // 4
|
| 346 |
+
d_embed_dim = embed_dim // 2
|
| 347 |
+
else:
|
| 348 |
+
h_embed_dim = w_embed_dim = d_embed_dim = int(np.ceil(embed_dim / 6) * 2)
|
| 349 |
+
|
| 350 |
+
emb_h = get_1d_sincos_pos_embed_from_grid(h_embed_dim, grid_h) # (T*H*W, D1)
|
| 351 |
+
emb_w = get_1d_sincos_pos_embed_from_grid(w_embed_dim, grid_w) # (T*H*W, D2)
|
| 352 |
+
emb_d = get_1d_sincos_pos_embed_from_grid(d_embed_dim, grid_d) # (T*H*W, D3)
|
| 353 |
+
pos_embed = np.concatenate([emb_d, emb_h, emb_w], axis=1)
|
| 354 |
+
pos_embed = pos_embed[:, :embed_dim]
|
| 355 |
+
if cls_token:
|
| 356 |
+
pos_embed = np.concatenate([np.zeros([1, embed_dim]), pos_embed], axis=0)
|
| 357 |
+
return pos_embed
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
|
| 361 |
+
"""
|
| 362 |
+
embed_dim: output dimension for each position
|
| 363 |
+
pos: a list of positions to be encoded: size (M,)
|
| 364 |
+
returns: (M, D)
|
| 365 |
+
"""
|
| 366 |
+
assert embed_dim % 2 == 0
|
| 367 |
+
omega = np.arange(embed_dim // 2, dtype=float)
|
| 368 |
+
omega /= embed_dim / 2.0
|
| 369 |
+
omega = 1.0 / 10000**omega # (D/2,)
|
| 370 |
+
|
| 371 |
+
pos = pos.reshape(-1) # (M,)
|
| 372 |
+
out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product
|
| 373 |
+
|
| 374 |
+
emb_sin = np.sin(out) # (M, D/2)
|
| 375 |
+
emb_cos = np.cos(out) # (M, D/2)
|
| 376 |
+
|
| 377 |
+
emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
|
| 378 |
+
return emb
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def apply_pos_embed(
|
| 382 |
+
x: Float[Tensor, "B L D"],
|
| 383 |
+
weight: Float[Tensor, "N D"],
|
| 384 |
+
pos_ids: Int[Tensor, "B L"] | None = None,
|
| 385 |
+
) -> Float[Tensor, "B L D"]:
|
| 386 |
+
B, L, D = x.shape
|
| 387 |
+
weight = weight.expand(B, -1, -1)
|
| 388 |
+
if pos_ids is not None:
|
| 389 |
+
weight = weight.gather(1, pos_ids.unsqueeze(-1).expand(-1, -1, D))
|
| 390 |
+
x = x + weight
|
| 391 |
+
return x
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
# (masked) normalization used for MAE target normalization
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
class Normalize(nn.Module):
|
| 398 |
+
def __init__(
|
| 399 |
+
self,
|
| 400 |
+
grid_size: tuple[int, ...],
|
| 401 |
+
dim: int | tuple[int, ...] | None = -1,
|
| 402 |
+
eps: float = 1e-6,
|
| 403 |
+
) -> None:
|
| 404 |
+
super().__init__()
|
| 405 |
+
self.grid_size = grid_size
|
| 406 |
+
self.dim = dim
|
| 407 |
+
self.eps = eps
|
| 408 |
+
|
| 409 |
+
def forward(self, x: Tensor, mask: Tensor | None = None) -> tuple[Tensor, Tensor, Tensor]:
|
| 410 |
+
"""
|
| 411 |
+
Normalize input sequence along dim(s) after reshaping to grid.
|
| 412 |
+
Returns tuple of (x, mean, std).
|
| 413 |
+
"""
|
| 414 |
+
B, N, D = x.shape
|
| 415 |
+
x = x.reshape((B, *self.grid_size, D))
|
| 416 |
+
if mask is not None:
|
| 417 |
+
mask = mask.reshape((B, *self.grid_size, D))
|
| 418 |
+
x, mean, std = masked_normalize(x, mask, dim=self.dim, eps=self.eps)
|
| 419 |
+
else:
|
| 420 |
+
x, mean, std = normalize(x, dim=self.dim, eps=self.eps)
|
| 421 |
+
mean = mean.expand_as(x).reshape(B, N, D)
|
| 422 |
+
std = std.expand_as(x).reshape(B, N, D)
|
| 423 |
+
x = x.reshape(B, N, D)
|
| 424 |
+
return x, mean, std
|
| 425 |
+
|
| 426 |
+
def extra_repr(self):
|
| 427 |
+
return f"{self.grid_size}, dim={self.dim}"
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
def masked_normalize(
|
| 431 |
+
x: Tensor,
|
| 432 |
+
mask: Tensor,
|
| 433 |
+
dim: int | tuple[int, ...] | None = -1,
|
| 434 |
+
eps: float = 1e-6,
|
| 435 |
+
) -> tuple[Tensor, Tensor, Tensor]:
|
| 436 |
+
num_obs = mask.sum(dim=dim, keepdim=True).clamp(min=1)
|
| 437 |
+
mean = (mask * x).sum(dim=dim, keepdim=True) / num_obs
|
| 438 |
+
var = (mask * (x - mean) ** 2).sum(dim=dim, keepdim=True) / num_obs
|
| 439 |
+
std = (var + eps) ** 0.5
|
| 440 |
+
x = mask * (x - mean) / std
|
| 441 |
+
return x, mean, std
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def normalize(
|
| 445 |
+
x: Tensor,
|
| 446 |
+
dim: int | tuple[int, ...] | None = -1,
|
| 447 |
+
eps: float = 1e-6,
|
| 448 |
+
) -> tuple[Tensor, Tensor, Tensor]:
|
| 449 |
+
mean = x.mean(dim=dim, keepdim=True)
|
| 450 |
+
var = torch.var(x, dim=dim, keepdim=True, unbiased=False)
|
| 451 |
+
std = (var + eps) ** 0.5
|
| 452 |
+
x = (x - mean) / std
|
| 453 |
+
return x, mean, std
|
finetune/fomo_tune_baseline/output/task5/build/smri_mae/utils.py
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Sophont, Inc
|
| 2 |
+
# This source code is licensed under the Apache License, Version 2.0
|
| 3 |
+
#
|
| 4 |
+
# References:
|
| 5 |
+
# deit: https://github.com/facebookresearch/deit/blob/main/utils.py
|
| 6 |
+
# beit3: https://github.com/microsoft/unilm/blob/master/beit3/utils.py
|
| 7 |
+
# capi: https://github.com/facebookresearch/capi/blob/main/utils.py
|
| 8 |
+
# dinov2: https://github.com/facebookresearch/dinov2/blob/main/dinov2/utils/param_groups.py
|
| 9 |
+
# timm: https://github.com/huggingface/pytorch-image-models/blob/main/timm/utils/cuda.py
|
| 10 |
+
# dino: https://github.com/facebookresearch/dino/blob/main/utils.py
|
| 11 |
+
|
| 12 |
+
import datetime
|
| 13 |
+
import inspect
|
| 14 |
+
import math
|
| 15 |
+
import os
|
| 16 |
+
import random
|
| 17 |
+
import subprocess
|
| 18 |
+
import time
|
| 19 |
+
from collections import defaultdict, deque
|
| 20 |
+
from omegaconf import OmegaConf
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
import numpy as np
|
| 24 |
+
import torch
|
| 25 |
+
import torch.distributed as dist
|
| 26 |
+
import torch.nn as nn
|
| 27 |
+
from torch import Tensor
|
| 28 |
+
from torch.amp import GradScaler
|
| 29 |
+
from torch.optim import Optimizer
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# these very useful utils copied from deit with only minor changes
|
| 33 |
+
# thanks to the original authors, wherever you are
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def configure_flash_sdpa() -> None:
|
| 37 |
+
"""Use Flash Attention exclusively for CUDA SDPA."""
|
| 38 |
+
torch.backends.cuda.enable_flash_sdp(True)
|
| 39 |
+
torch.backends.cuda.enable_mem_efficient_sdp(False)
|
| 40 |
+
torch.backends.cuda.enable_math_sdp(False)
|
| 41 |
+
torch.backends.cuda.enable_cudnn_sdp(False)
|
| 42 |
+
print("SDPA backend: flash")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class SmoothedValue:
|
| 46 |
+
"""Track a series of values and provide access to smoothed values over a
|
| 47 |
+
window or the global series average.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def __init__(self, window_size=20, fmt=None):
|
| 51 |
+
if fmt is None:
|
| 52 |
+
fmt = "{median:.4f} ({global_avg:.4f})"
|
| 53 |
+
self.deque = deque(maxlen=window_size)
|
| 54 |
+
self.total = 0.0
|
| 55 |
+
self.count = 0
|
| 56 |
+
self.fmt = fmt
|
| 57 |
+
|
| 58 |
+
def update(self, value, n=1):
|
| 59 |
+
value = float(value)
|
| 60 |
+
if math.isfinite(value):
|
| 61 |
+
self.deque.append(value)
|
| 62 |
+
self.count += n
|
| 63 |
+
self.total += value * n
|
| 64 |
+
|
| 65 |
+
def synchronize_between_processes(self):
|
| 66 |
+
"""
|
| 67 |
+
Warning: does not synchronize the deque!
|
| 68 |
+
"""
|
| 69 |
+
if not is_dist_avail_and_initialized():
|
| 70 |
+
return
|
| 71 |
+
t = torch.tensor([self.count, self.total], dtype=torch.float64, device="cuda")
|
| 72 |
+
dist.barrier()
|
| 73 |
+
dist.all_reduce(t)
|
| 74 |
+
t = t.tolist()
|
| 75 |
+
self.count = int(t[0])
|
| 76 |
+
self.total = t[1]
|
| 77 |
+
|
| 78 |
+
@property
|
| 79 |
+
def median(self):
|
| 80 |
+
if not self.count:
|
| 81 |
+
return float("nan")
|
| 82 |
+
d = torch.tensor(list(self.deque))
|
| 83 |
+
return d.median().item()
|
| 84 |
+
|
| 85 |
+
@property
|
| 86 |
+
def avg(self):
|
| 87 |
+
if not self.count:
|
| 88 |
+
return float("nan")
|
| 89 |
+
d = torch.tensor(list(self.deque), dtype=torch.float32)
|
| 90 |
+
return d.mean().item()
|
| 91 |
+
|
| 92 |
+
@property
|
| 93 |
+
def global_avg(self):
|
| 94 |
+
if not self.count:
|
| 95 |
+
return float("nan")
|
| 96 |
+
return self.total / self.count
|
| 97 |
+
|
| 98 |
+
@property
|
| 99 |
+
def max(self):
|
| 100 |
+
if not self.count:
|
| 101 |
+
return float("nan")
|
| 102 |
+
return max(self.deque)
|
| 103 |
+
|
| 104 |
+
@property
|
| 105 |
+
def value(self):
|
| 106 |
+
if not self.count:
|
| 107 |
+
return float("nan")
|
| 108 |
+
return self.deque[-1]
|
| 109 |
+
|
| 110 |
+
def __str__(self):
|
| 111 |
+
return self.fmt.format(
|
| 112 |
+
median=self.median,
|
| 113 |
+
avg=self.avg,
|
| 114 |
+
global_avg=self.global_avg,
|
| 115 |
+
max=self.max,
|
| 116 |
+
value=self.value,
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class MetricLogger:
|
| 121 |
+
def __init__(self, delimiter="\t"):
|
| 122 |
+
self.meters = defaultdict(SmoothedValue)
|
| 123 |
+
self.delimiter = delimiter
|
| 124 |
+
|
| 125 |
+
def update(self, **kwargs):
|
| 126 |
+
for k, v in kwargs.items():
|
| 127 |
+
if v is None:
|
| 128 |
+
continue
|
| 129 |
+
if isinstance(v, (torch.Tensor, np.generic)):
|
| 130 |
+
v = v.item()
|
| 131 |
+
assert isinstance(v, (float, int))
|
| 132 |
+
self.meters[k].update(v)
|
| 133 |
+
|
| 134 |
+
def __getattr__(self, attr):
|
| 135 |
+
if attr in self.meters:
|
| 136 |
+
return self.meters[attr]
|
| 137 |
+
if attr in self.__dict__:
|
| 138 |
+
return self.__dict__[attr]
|
| 139 |
+
raise AttributeError("'{}' object has no attribute '{}'".format(type(self).__name__, attr))
|
| 140 |
+
|
| 141 |
+
def __str__(self):
|
| 142 |
+
loss_str = []
|
| 143 |
+
for name, meter in self.meters.items():
|
| 144 |
+
loss_str.append("{}: {}".format(name, str(meter)))
|
| 145 |
+
return self.delimiter.join(loss_str)
|
| 146 |
+
|
| 147 |
+
def synchronize_between_processes(self):
|
| 148 |
+
for meter in self.meters.values():
|
| 149 |
+
meter.synchronize_between_processes()
|
| 150 |
+
|
| 151 |
+
def add_meter(self, name, meter):
|
| 152 |
+
self.meters[name] = meter
|
| 153 |
+
|
| 154 |
+
def log_every(self, iterable, print_freq, header=None, total_steps=None):
|
| 155 |
+
i = 0
|
| 156 |
+
total_steps = total_steps or len(iterable)
|
| 157 |
+
if not header:
|
| 158 |
+
header = ""
|
| 159 |
+
start_time = time.time()
|
| 160 |
+
end = time.time()
|
| 161 |
+
iter_time = SmoothedValue(fmt="{avg:.4f}")
|
| 162 |
+
data_time = SmoothedValue(fmt="{avg:.4f}")
|
| 163 |
+
space_fmt = ":" + str(len(str(total_steps))) + "d"
|
| 164 |
+
log_msg = [
|
| 165 |
+
header,
|
| 166 |
+
"[{0" + space_fmt + "}/{1}]",
|
| 167 |
+
"eta: {eta}",
|
| 168 |
+
"{meters}",
|
| 169 |
+
"time: {time}",
|
| 170 |
+
"data: {data}",
|
| 171 |
+
]
|
| 172 |
+
if torch.cuda.is_available():
|
| 173 |
+
log_msg.append("max mem: {memory:.0f}")
|
| 174 |
+
log_msg = self.delimiter.join(log_msg)
|
| 175 |
+
MB = 1024.0 * 1024.0
|
| 176 |
+
for obj in iterable:
|
| 177 |
+
if i >= total_steps:
|
| 178 |
+
break
|
| 179 |
+
data_time.update(time.time() - end)
|
| 180 |
+
yield obj
|
| 181 |
+
iter_time.update(time.time() - end)
|
| 182 |
+
if i % print_freq == 0 or i == total_steps - 1:
|
| 183 |
+
eta_seconds = iter_time.global_avg * (total_steps - i)
|
| 184 |
+
eta_string = str(datetime.timedelta(seconds=int(eta_seconds)))
|
| 185 |
+
if torch.cuda.is_available():
|
| 186 |
+
print(
|
| 187 |
+
log_msg.format(
|
| 188 |
+
i,
|
| 189 |
+
total_steps,
|
| 190 |
+
eta=eta_string,
|
| 191 |
+
meters=str(self),
|
| 192 |
+
time=str(iter_time),
|
| 193 |
+
data=str(data_time),
|
| 194 |
+
memory=torch.cuda.max_memory_allocated() / MB,
|
| 195 |
+
)
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
else:
|
| 199 |
+
print(
|
| 200 |
+
log_msg.format(
|
| 201 |
+
i,
|
| 202 |
+
total_steps,
|
| 203 |
+
eta=eta_string,
|
| 204 |
+
meters=str(self),
|
| 205 |
+
time=str(iter_time),
|
| 206 |
+
data=str(data_time),
|
| 207 |
+
)
|
| 208 |
+
)
|
| 209 |
+
i += 1
|
| 210 |
+
end = time.time()
|
| 211 |
+
total_time = time.time() - start_time
|
| 212 |
+
total_time_str = str(datetime.timedelta(seconds=int(total_time)))
|
| 213 |
+
print(
|
| 214 |
+
"{} Total time: {} ({:.4f} s / it)".format(
|
| 215 |
+
header, total_time_str, total_time / total_steps
|
| 216 |
+
)
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def setup_for_distributed(log_path=None):
|
| 221 |
+
"""
|
| 222 |
+
This function disables printing when not in master process
|
| 223 |
+
"""
|
| 224 |
+
import builtins as __builtin__
|
| 225 |
+
|
| 226 |
+
builtin_print = __builtin__.print
|
| 227 |
+
|
| 228 |
+
is_master = is_main_process()
|
| 229 |
+
|
| 230 |
+
def print(*args, **kwargs):
|
| 231 |
+
force = kwargs.pop("force", False)
|
| 232 |
+
if is_master or force:
|
| 233 |
+
builtin_print(*args, **kwargs)
|
| 234 |
+
# tee to log file
|
| 235 |
+
if log_path and "file" not in kwargs:
|
| 236 |
+
with open(log_path, "a") as f:
|
| 237 |
+
builtin_print(*args, file=f, **kwargs)
|
| 238 |
+
|
| 239 |
+
__builtin__.print = print
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def is_dist_avail_and_initialized():
|
| 243 |
+
if not dist.is_available():
|
| 244 |
+
return False
|
| 245 |
+
if not dist.is_initialized():
|
| 246 |
+
return False
|
| 247 |
+
return True
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def get_world_size():
|
| 251 |
+
if not is_dist_avail_and_initialized():
|
| 252 |
+
return 1
|
| 253 |
+
return dist.get_world_size()
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def get_rank():
|
| 257 |
+
if not is_dist_avail_and_initialized():
|
| 258 |
+
return 0
|
| 259 |
+
return dist.get_rank()
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
def is_main_process():
|
| 263 |
+
return get_rank() == 0
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def save_on_master(obj, path, *args, **kwargs):
|
| 267 |
+
if is_main_process():
|
| 268 |
+
path = Path(path)
|
| 269 |
+
tmp_path = path.with_name(f".{path.name}.tmp-{os.getpid()}")
|
| 270 |
+
try:
|
| 271 |
+
torch.save(obj, tmp_path, *args, **kwargs)
|
| 272 |
+
os.replace(tmp_path, path)
|
| 273 |
+
except Exception:
|
| 274 |
+
tmp_path.unlink(missing_ok=True)
|
| 275 |
+
raise
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def init_distributed_mode(args):
|
| 279 |
+
# removed slurm block, can add if we use slurm
|
| 280 |
+
if "RANK" in os.environ and "WORLD_SIZE" in os.environ:
|
| 281 |
+
args.rank = int(os.environ["RANK"])
|
| 282 |
+
args.world_size = int(os.environ["WORLD_SIZE"])
|
| 283 |
+
args.gpu = int(os.environ["LOCAL_RANK"])
|
| 284 |
+
else:
|
| 285 |
+
args.distributed = False
|
| 286 |
+
return
|
| 287 |
+
|
| 288 |
+
args.distributed = True
|
| 289 |
+
|
| 290 |
+
torch.cuda.set_device(args.gpu)
|
| 291 |
+
args.dist_backend = "nccl"
|
| 292 |
+
print(f"| distributed init (rank {args.rank})")
|
| 293 |
+
torch.distributed.init_process_group(
|
| 294 |
+
backend=args.dist_backend,
|
| 295 |
+
world_size=args.world_size,
|
| 296 |
+
rank=args.rank,
|
| 297 |
+
device_id=args.gpu,
|
| 298 |
+
)
|
| 299 |
+
torch.distributed.barrier()
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
# checkpoint saving utils adapted from beit3
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def capture_rng_state() -> dict:
|
| 306 |
+
state = {"torch": torch.get_rng_state()}
|
| 307 |
+
if torch.cuda.is_available():
|
| 308 |
+
state["cuda"] = torch.cuda.get_rng_state()
|
| 309 |
+
return state
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def restore_rng_state(state: dict) -> None:
|
| 313 |
+
torch.set_rng_state(state["torch"])
|
| 314 |
+
if "cuda" in state and torch.cuda.is_available():
|
| 315 |
+
torch.cuda.set_rng_state(state["cuda"])
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def _all_rank_rng_states() -> list[dict]:
|
| 319 |
+
local_state = capture_rng_state()
|
| 320 |
+
if not is_dist_avail_and_initialized():
|
| 321 |
+
return [local_state]
|
| 322 |
+
states = [None] * get_world_size()
|
| 323 |
+
dist.all_gather_object(states, local_state)
|
| 324 |
+
return states
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def save_model(args, epoch, model_without_ddp, optimizer, loss_scaler):
|
| 328 |
+
output_dir = Path(args.output_dir)
|
| 329 |
+
checkpoint_path = output_dir / f"checkpoint-{epoch:05d}.pth"
|
| 330 |
+
last_checkpoint_path = output_dir / "checkpoint-last.pth"
|
| 331 |
+
if epoch % args.checkpoint_period != 0 and epoch != args.epochs - 1:
|
| 332 |
+
return
|
| 333 |
+
|
| 334 |
+
to_save = {
|
| 335 |
+
"model": model_without_ddp.state_dict(),
|
| 336 |
+
"optimizer": optimizer.state_dict(),
|
| 337 |
+
"epoch": epoch,
|
| 338 |
+
"scaler": None if loss_scaler is None else loss_scaler.state_dict(),
|
| 339 |
+
"args": OmegaConf.to_container(args),
|
| 340 |
+
"rng_states": _all_rank_rng_states(),
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
print(f"saving checkpoint {last_checkpoint_path}")
|
| 344 |
+
save_on_master(to_save, last_checkpoint_path)
|
| 345 |
+
print(f"saving checkpoint {checkpoint_path}")
|
| 346 |
+
save_on_master(to_save, checkpoint_path)
|
| 347 |
+
|
| 348 |
+
if args.max_checkpoints and is_main_process():
|
| 349 |
+
all_checkpoints = sorted(output_dir.glob("checkpoint-[0-9]*.pth"))
|
| 350 |
+
del_count = max(0, len(all_checkpoints) - args.max_checkpoints)
|
| 351 |
+
for checkpoint_path in all_checkpoints[:del_count]:
|
| 352 |
+
print(f"removing checkpoint {checkpoint_path}")
|
| 353 |
+
checkpoint_path.unlink()
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
def load_model(args, model_without_ddp, optimizer, loss_scaler):
|
| 357 |
+
auto_resume = getattr(args, "auto_resume", True)
|
| 358 |
+
output_dir = Path(args.output_dir)
|
| 359 |
+
|
| 360 |
+
last_checkpoint_path = output_dir / "checkpoint-last.pth"
|
| 361 |
+
if auto_resume and last_checkpoint_path.exists():
|
| 362 |
+
args.ckpt = str(last_checkpoint_path)
|
| 363 |
+
args.resume = True
|
| 364 |
+
|
| 365 |
+
if args.ckpt:
|
| 366 |
+
ckpt = torch.load(args.ckpt, map_location="cpu", weights_only=True)
|
| 367 |
+
model_without_ddp.load_state_dict(ckpt["model"])
|
| 368 |
+
print(f"loaded model state from checkpoint {args.ckpt}")
|
| 369 |
+
|
| 370 |
+
if args.resume:
|
| 371 |
+
optimizer.load_state_dict(ckpt["optimizer"])
|
| 372 |
+
if loss_scaler is not None:
|
| 373 |
+
loss_scaler.load_state_dict(ckpt["scaler"])
|
| 374 |
+
args.start_epoch = ckpt["epoch"] + 1
|
| 375 |
+
rng_states = ckpt.get("rng_states")
|
| 376 |
+
if rng_states is not None:
|
| 377 |
+
if len(rng_states) != get_world_size():
|
| 378 |
+
raise ValueError(
|
| 379 |
+
"checkpoint RNG state world size does not match current world size: "
|
| 380 |
+
f"{len(rng_states)} != {get_world_size()}"
|
| 381 |
+
)
|
| 382 |
+
restore_rng_state(rng_states[get_rank()])
|
| 383 |
+
print(f"restored RNG state for rank {get_rank()}")
|
| 384 |
+
print(f"loaded optimizer state, resuming training from {args.start_epoch}")
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
# optimization utils
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
# from capi
|
| 391 |
+
class WarmupThenCosine:
|
| 392 |
+
def __init__(
|
| 393 |
+
self,
|
| 394 |
+
base_value: float,
|
| 395 |
+
final_value: float,
|
| 396 |
+
total_iters: int,
|
| 397 |
+
warmup_iters: int = 0,
|
| 398 |
+
start_warmup_value: float = 0.0,
|
| 399 |
+
freeze_iters: int = 0,
|
| 400 |
+
truncate_cos: float = 1.0,
|
| 401 |
+
):
|
| 402 |
+
super().__init__()
|
| 403 |
+
self.final_value = final_value
|
| 404 |
+
self.total_iters = total_iters
|
| 405 |
+
|
| 406 |
+
freeze_schedule = np.zeros(freeze_iters)
|
| 407 |
+
|
| 408 |
+
warmup_schedule = np.linspace(start_warmup_value, base_value, warmup_iters)
|
| 409 |
+
|
| 410 |
+
iters = np.arange(total_iters - warmup_iters - freeze_iters)
|
| 411 |
+
schedule = final_value + 0.5 * (base_value - final_value) * (
|
| 412 |
+
1 + np.cos(np.pi * truncate_cos * iters / len(iters))
|
| 413 |
+
)
|
| 414 |
+
self.schedule = np.concatenate((freeze_schedule, warmup_schedule, schedule))
|
| 415 |
+
assert len(self.schedule) == self.total_iters
|
| 416 |
+
|
| 417 |
+
def __getitem__(self, it: int) -> float:
|
| 418 |
+
if it >= self.total_iters:
|
| 419 |
+
return self.final_value
|
| 420 |
+
# cast to float or else it can corrupt the checkpoint
|
| 421 |
+
return float(self.schedule[it])
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
# adapted from timm backward logic
|
| 425 |
+
# https://github.com/huggingface/pytorch-image-models/blob/main/timm/utils/cuda.py
|
| 426 |
+
def backward_step(
|
| 427 |
+
loss: Tensor,
|
| 428 |
+
optimizer: Optimizer,
|
| 429 |
+
scaler: GradScaler = None,
|
| 430 |
+
need_update: bool = True,
|
| 431 |
+
max_norm: float | None = None,
|
| 432 |
+
) -> Tensor | None:
|
| 433 |
+
if scaler is not None:
|
| 434 |
+
scaler.scale(loss).backward()
|
| 435 |
+
else:
|
| 436 |
+
loss.backward()
|
| 437 |
+
|
| 438 |
+
if need_update:
|
| 439 |
+
if scaler is not None:
|
| 440 |
+
scaler.unscale_(optimizer)
|
| 441 |
+
|
| 442 |
+
total_norm = clip_grad(optimizer, max_norm)
|
| 443 |
+
|
| 444 |
+
if scaler is not None:
|
| 445 |
+
scaler.step(optimizer)
|
| 446 |
+
scaler.update()
|
| 447 |
+
else:
|
| 448 |
+
optimizer.step()
|
| 449 |
+
optimizer.zero_grad()
|
| 450 |
+
else:
|
| 451 |
+
total_norm = None
|
| 452 |
+
return total_norm
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
def clip_grad(optimizer: Optimizer, max_norm: float | None = None) -> Tensor:
|
| 456 |
+
params = [p for group in optimizer.param_groups for p in group["params"]]
|
| 457 |
+
if max_norm:
|
| 458 |
+
total_norm = nn.utils.clip_grad_norm_(params, max_norm, error_if_nonfinite=False)
|
| 459 |
+
else:
|
| 460 |
+
grads = [p.grad for p in params if p.grad is not None]
|
| 461 |
+
total_norm = nn.utils.get_total_norm(grads, error_if_nonfinite=False)
|
| 462 |
+
torch._assert_async(torch.isfinite(total_norm), "non-finite gradient norm")
|
| 463 |
+
return total_norm
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
# from dinov2 with some minor changes
|
| 467 |
+
def get_param_groups(model, patch_embed_lr_mult=1.0):
|
| 468 |
+
# no lr decay, we could add this later if needed
|
| 469 |
+
all_params = []
|
| 470 |
+
|
| 471 |
+
for name, param in model.named_parameters():
|
| 472 |
+
if not param.requires_grad:
|
| 473 |
+
continue
|
| 474 |
+
d = {"param": param, "lr_multiplier": 1.0, "wd_multiplier": 1.0, "name": name}
|
| 475 |
+
|
| 476 |
+
if name.endswith(".bias") or "norm" in name or "gamma" in name:
|
| 477 |
+
d["wd_multiplier"] = 0.0
|
| 478 |
+
|
| 479 |
+
if "patch_embed" in name:
|
| 480 |
+
d["lr_multiplier"] = d["lr_multiplier"] * patch_embed_lr_mult
|
| 481 |
+
|
| 482 |
+
all_params.append(d)
|
| 483 |
+
|
| 484 |
+
param_groups = _fuse_param_groups(all_params)
|
| 485 |
+
return param_groups
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
def _fuse_param_groups(all_param_groups):
|
| 489 |
+
fused_param_groups = defaultdict(lambda: {"params": []})
|
| 490 |
+
for d in all_param_groups:
|
| 491 |
+
keys = sorted(set(d.keys()) - {"param", "name"})
|
| 492 |
+
identifier = "_".join(f"{k}{d[k]}" for k in keys)
|
| 493 |
+
for k in keys:
|
| 494 |
+
fused_param_groups[identifier][k] = d[k]
|
| 495 |
+
fused_param_groups[identifier]["params"].append(d["param"])
|
| 496 |
+
|
| 497 |
+
param_groups = list(fused_param_groups.values())
|
| 498 |
+
return param_groups
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
def update_lr(param_groups, lr: float):
|
| 502 |
+
for group in param_groups:
|
| 503 |
+
group["lr"] = lr * group["lr_multiplier"]
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
def update_wd(param_groups, weight_decay: float | None = None):
|
| 507 |
+
for group in param_groups:
|
| 508 |
+
group["weight_decay"] = weight_decay * group["wd_multiplier"]
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
# moving data to cuda utils copied from capi
|
| 512 |
+
# added device argument
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
def send_data(x, device=None, dtype_map=None):
|
| 516 |
+
if device is None:
|
| 517 |
+
device = torch.device("cuda")
|
| 518 |
+
else:
|
| 519 |
+
device = torch.device(device)
|
| 520 |
+
|
| 521 |
+
if isinstance(x, torch.Tensor):
|
| 522 |
+
dtype = dtype_map.get(x.dtype) if dtype_map else None
|
| 523 |
+
return x.to(device=device, dtype=dtype, non_blocking=True)
|
| 524 |
+
if isinstance(x, dict):
|
| 525 |
+
return {k: send_data(v, device=device, dtype_map=dtype_map) for k, v in x.items()}
|
| 526 |
+
if isinstance(x, list):
|
| 527 |
+
return [send_data(v, device=device, dtype_map=dtype_map) for v in x]
|
| 528 |
+
return x
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
def pre_send_to_cuda_wrapper(generator, device=None, dtype_map=None):
|
| 532 |
+
"""From apex"""
|
| 533 |
+
data = None
|
| 534 |
+
stream = torch.cuda.Stream(device)
|
| 535 |
+
for next_data in generator:
|
| 536 |
+
with torch.cuda.stream(stream):
|
| 537 |
+
next_data = send_data(next_data, device=device, dtype_map=dtype_map)
|
| 538 |
+
if data is not None:
|
| 539 |
+
yield data
|
| 540 |
+
torch.cuda.current_stream(device).wait_stream(stream)
|
| 541 |
+
data = next_data
|
| 542 |
+
if data is not None:
|
| 543 |
+
yield data
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
# other misc utils
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
# from dino
|
| 550 |
+
def get_sha():
|
| 551 |
+
cwd = os.path.dirname(os.path.abspath(__file__))
|
| 552 |
+
|
| 553 |
+
def _run(command):
|
| 554 |
+
return subprocess.check_output(command, cwd=cwd).decode("ascii").strip()
|
| 555 |
+
|
| 556 |
+
sha = "N/A"
|
| 557 |
+
diff = "clean"
|
| 558 |
+
branch = "N/A"
|
| 559 |
+
try:
|
| 560 |
+
sha = _run(["git", "rev-parse", "HEAD"])
|
| 561 |
+
diff = _run(["git", "diff-index", "HEAD"])
|
| 562 |
+
diff = "has uncommitted changes" if diff else "clean"
|
| 563 |
+
branch = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"])
|
| 564 |
+
except Exception:
|
| 565 |
+
pass
|
| 566 |
+
message = f"sha: {sha}, status: {diff}, branch: {branch}"
|
| 567 |
+
return message
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
# from timm
|
| 571 |
+
def random_seed(seed=42, rank=0):
|
| 572 |
+
torch.manual_seed(seed + rank)
|
| 573 |
+
np.random.seed(seed + rank)
|
| 574 |
+
random.seed(seed + rank)
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
# mine :)
|
| 578 |
+
def filter_kwargs(func, kwargs):
|
| 579 |
+
sigature = inspect.signature(func)
|
| 580 |
+
kwargs = {k: v for k, v in kwargs.items() if k in sigature.parameters}
|
| 581 |
+
return kwargs
|
finetune/fomo_tune_baseline/output/task5/build/smri_mae/visualization.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections.abc import Mapping
|
| 2 |
+
from io import BytesIO
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
from matplotlib import patches
|
| 7 |
+
from matplotlib import pyplot as plt
|
| 8 |
+
from PIL import Image
|
| 9 |
+
from torch import Tensor
|
| 10 |
+
|
| 11 |
+
VIEW_NAMES = {
|
| 12 |
+
"sagittal": "Sagittal",
|
| 13 |
+
"saggital": "Sagittal",
|
| 14 |
+
"axial": "Axial",
|
| 15 |
+
"coronal": "Coronal",
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def fig2pil(fig) -> Image.Image:
|
| 20 |
+
buffer = BytesIO()
|
| 21 |
+
fig.savefig(buffer, format="png", dpi=fig.dpi, facecolor=fig.get_facecolor())
|
| 22 |
+
buffer.seek(0)
|
| 23 |
+
image = Image.open(buffer).convert("RGB")
|
| 24 |
+
buffer.close()
|
| 25 |
+
return image
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def raw_stats_from_batch(batch: dict) -> tuple[Tensor | None, Tensor | None]:
|
| 29 |
+
metas = batch.get("meta")
|
| 30 |
+
if not metas:
|
| 31 |
+
return None, None
|
| 32 |
+
|
| 33 |
+
means = []
|
| 34 |
+
stds = []
|
| 35 |
+
for meta in metas:
|
| 36 |
+
try:
|
| 37 |
+
mean = meta["raw_mean"]
|
| 38 |
+
std = meta["raw_std"]
|
| 39 |
+
except (KeyError, TypeError):
|
| 40 |
+
return None, None
|
| 41 |
+
if mean in ("", None) or std in ("", None):
|
| 42 |
+
return None, None
|
| 43 |
+
means.append(float(mean))
|
| 44 |
+
stds.append(float(std))
|
| 45 |
+
return torch.tensor(means), torch.tensor(stds)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def plot_mask_pred(
|
| 49 |
+
target: Tensor,
|
| 50 |
+
pred: Tensor,
|
| 51 |
+
pred_mask: Tensor | None = None,
|
| 52 |
+
img_mask: Tensor | None = None,
|
| 53 |
+
sample_idx: int = 0,
|
| 54 |
+
channel_idx: int = 0,
|
| 55 |
+
slice_idx: int | Mapping[str, int] | None = None,
|
| 56 |
+
patch_size: int | tuple[int, int, int] = 16,
|
| 57 |
+
views: tuple[str, ...] = ("sagittal", "axial", "coronal"),
|
| 58 |
+
cmap: str = "gray",
|
| 59 |
+
figsize: tuple[float, float] | None = None,
|
| 60 |
+
mask_style: str = "blank",
|
| 61 |
+
raw_mean: float | Tensor | None = None,
|
| 62 |
+
raw_std: float | Tensor | None = None,
|
| 63 |
+
):
|
| 64 |
+
target_vol = _select_volume(target, sample_idx=sample_idx, channel_idx=channel_idx)
|
| 65 |
+
pred_vol = _select_volume(pred, sample_idx=sample_idx, channel_idx=channel_idx)
|
| 66 |
+
if raw_mean is not None and raw_std is not None:
|
| 67 |
+
raw_mean = _select_scalar(raw_mean, sample_idx=sample_idx)
|
| 68 |
+
raw_std = _select_scalar(raw_std, sample_idx=sample_idx)
|
| 69 |
+
target_vol = target_vol * raw_std + raw_mean
|
| 70 |
+
pred_vol = pred_vol * raw_std + raw_mean
|
| 71 |
+
pred_mask_vol = (
|
| 72 |
+
torch.zeros_like(target_vol)
|
| 73 |
+
if pred_mask is None
|
| 74 |
+
else _select_volume(pred_mask, sample_idx=sample_idx, channel_idx=channel_idx) > 0
|
| 75 |
+
)
|
| 76 |
+
img_mask_vol = None
|
| 77 |
+
if img_mask is not None:
|
| 78 |
+
img_mask_vol = _select_volume(img_mask, sample_idx=sample_idx, channel_idx=channel_idx) > 0
|
| 79 |
+
|
| 80 |
+
composite_vol = _prediction_composite(target_vol, pred_vol, pred_mask_vol)
|
| 81 |
+
vmin, vmax = _intensity_limits(target_vol, img_mask_vol)
|
| 82 |
+
|
| 83 |
+
patch_size = _as_3tuple(patch_size)
|
| 84 |
+
view_items = []
|
| 85 |
+
for view in views:
|
| 86 |
+
view_key = view.lower()
|
| 87 |
+
if view_key not in VIEW_NAMES:
|
| 88 |
+
raise ValueError(f"unknown MRI view {view!r}; expected one of {tuple(VIEW_NAMES)}")
|
| 89 |
+
target_slice = _extract_view_slice(target_vol, view_key, slice_idx)
|
| 90 |
+
composite_slice = _extract_view_slice(composite_vol, view_key, slice_idx)
|
| 91 |
+
mask_slice = _extract_view_slice(pred_mask_vol.float(), view_key, slice_idx) > 0
|
| 92 |
+
img_mask_slice = None
|
| 93 |
+
if img_mask_vol is not None:
|
| 94 |
+
img_mask_slice = _extract_view_slice(img_mask_vol.float(), view_key, slice_idx) > 0
|
| 95 |
+
view_items.append(
|
| 96 |
+
{
|
| 97 |
+
"key": view_key,
|
| 98 |
+
"title": VIEW_NAMES[view_key],
|
| 99 |
+
"target": _masked_input_display(target_slice, mask_slice, img_mask_slice, vmin),
|
| 100 |
+
"composite": _apply_display_mask(composite_slice, img_mask_slice, vmin),
|
| 101 |
+
"actual": _apply_display_mask(target_slice, img_mask_slice, vmin),
|
| 102 |
+
"mask": mask_slice,
|
| 103 |
+
"img_mask": img_mask_slice,
|
| 104 |
+
"patch_rc": _view_patch_size(view_key, patch_size),
|
| 105 |
+
}
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
_crop_view_items(view_items)
|
| 109 |
+
|
| 110 |
+
fig, axes, layout = _make_figure_canvas(view_items, figsize=figsize)
|
| 111 |
+
for item, x in zip(view_items, layout["col_centers"]):
|
| 112 |
+
fig.text(
|
| 113 |
+
x,
|
| 114 |
+
layout["title_y"],
|
| 115 |
+
item["title"],
|
| 116 |
+
ha="center",
|
| 117 |
+
va="center",
|
| 118 |
+
color="#f8fafc",
|
| 119 |
+
fontsize=7,
|
| 120 |
+
)
|
| 121 |
+
for label, y in zip(("Masked", "Pred", "Actual"), layout["row_centers"]):
|
| 122 |
+
fig.text(layout["label_x"], y, label, ha="right", va="center", color="#cbd5e1", fontsize=6)
|
| 123 |
+
|
| 124 |
+
for item, top_ax, middle_ax, bottom_ax in zip(view_items, axes[0], axes[1], axes[2]):
|
| 125 |
+
top_ax.imshow(
|
| 126 |
+
item["target"],
|
| 127 |
+
cmap=cmap,
|
| 128 |
+
vmin=vmin,
|
| 129 |
+
vmax=vmax,
|
| 130 |
+
interpolation="nearest",
|
| 131 |
+
origin="upper",
|
| 132 |
+
)
|
| 133 |
+
if mask_style == "boxes":
|
| 134 |
+
_draw_patch_boxes(top_ax, item["mask"], item["patch_rc"])
|
| 135 |
+
elif mask_style != "blank":
|
| 136 |
+
raise ValueError("mask_style must be 'blank' or 'boxes'")
|
| 137 |
+
_style_axis(top_ax)
|
| 138 |
+
|
| 139 |
+
middle_ax.imshow(
|
| 140 |
+
item["composite"],
|
| 141 |
+
cmap=cmap,
|
| 142 |
+
vmin=vmin,
|
| 143 |
+
vmax=vmax,
|
| 144 |
+
interpolation="nearest",
|
| 145 |
+
origin="upper",
|
| 146 |
+
)
|
| 147 |
+
_style_axis(middle_ax)
|
| 148 |
+
|
| 149 |
+
bottom_ax.imshow(
|
| 150 |
+
item["actual"],
|
| 151 |
+
cmap=cmap,
|
| 152 |
+
vmin=vmin,
|
| 153 |
+
vmax=vmax,
|
| 154 |
+
interpolation="nearest",
|
| 155 |
+
origin="upper",
|
| 156 |
+
)
|
| 157 |
+
_style_axis(bottom_ax)
|
| 158 |
+
return fig
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _select_volume(
|
| 162 |
+
x: Tensor,
|
| 163 |
+
sample_idx: int = 0,
|
| 164 |
+
channel_idx: int = 0,
|
| 165 |
+
) -> Tensor:
|
| 166 |
+
x = x.detach().float().cpu()
|
| 167 |
+
if x.ndim == 5:
|
| 168 |
+
return x[sample_idx, channel_idx]
|
| 169 |
+
if x.ndim == 4:
|
| 170 |
+
return x[sample_idx]
|
| 171 |
+
if x.ndim == 3:
|
| 172 |
+
return x
|
| 173 |
+
raise ValueError(f"expected a 3D volume tensor, got shape {tuple(x.shape)}")
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _select_scalar(value: float | Tensor, sample_idx: int = 0) -> float:
|
| 177 |
+
if isinstance(value, Tensor):
|
| 178 |
+
value = value.detach().float().cpu()
|
| 179 |
+
if value.ndim > 0:
|
| 180 |
+
value = value.reshape(-1)[sample_idx]
|
| 181 |
+
return float(value)
|
| 182 |
+
return float(value)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _prediction_composite(target: Tensor, pred: Tensor, pred_mask: Tensor) -> Tensor:
|
| 186 |
+
pred_mask = pred_mask.to(dtype=target.dtype)
|
| 187 |
+
return target * (1 - pred_mask) + pred * pred_mask
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _extract_view_slice(
|
| 191 |
+
volume: Tensor,
|
| 192 |
+
view: str,
|
| 193 |
+
slice_idx: int | Mapping[str, int] | None = None,
|
| 194 |
+
) -> Tensor:
|
| 195 |
+
if isinstance(slice_idx, Mapping):
|
| 196 |
+
slice_idx = slice_idx.get(view)
|
| 197 |
+
|
| 198 |
+
if view in ("sagittal", "saggital"):
|
| 199 |
+
idx = _resolve_slice_idx(volume.shape[0], slice_idx)
|
| 200 |
+
return volume[idx, :, :].transpose(0, 1).flip(0)
|
| 201 |
+
if view == "axial":
|
| 202 |
+
idx = _resolve_slice_idx(volume.shape[2], slice_idx)
|
| 203 |
+
return volume[:, :, idx].transpose(0, 1).flip(0)
|
| 204 |
+
if view == "coronal":
|
| 205 |
+
idx = _resolve_slice_idx(volume.shape[1], slice_idx)
|
| 206 |
+
return volume[:, idx, :].transpose(0, 1).flip(0)
|
| 207 |
+
raise ValueError(f"unknown MRI view {view!r}")
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def _resolve_slice_idx(size: int, slice_idx: int | None) -> int:
|
| 211 |
+
idx = size // 2 if slice_idx is None else int(slice_idx)
|
| 212 |
+
if idx < 0:
|
| 213 |
+
idx += size
|
| 214 |
+
if idx < 0 or idx >= size:
|
| 215 |
+
raise IndexError(f"slice index {idx} is out of bounds for axis with size {size}")
|
| 216 |
+
return idx
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def _intensity_limits(volume: Tensor, mask: Tensor | None = None) -> tuple[float, float]:
|
| 220 |
+
values = volume[mask] if mask is not None and mask.any() else volume.flatten()
|
| 221 |
+
values = values[torch.isfinite(values)]
|
| 222 |
+
if values.numel() == 0:
|
| 223 |
+
return 0.0, 1.0
|
| 224 |
+
if values.numel() < 32:
|
| 225 |
+
vmin = values.min()
|
| 226 |
+
vmax = values.max()
|
| 227 |
+
else:
|
| 228 |
+
vmin, vmax = torch.quantile(values, torch.tensor([0.005, 0.995]))
|
| 229 |
+
if torch.isclose(vmin, vmax):
|
| 230 |
+
delta = max(abs(float(vmin)) * 0.05, 1.0)
|
| 231 |
+
return float(vmin) - delta, float(vmax) + delta
|
| 232 |
+
return float(vmin), float(vmax)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def _apply_display_mask(image: Tensor, mask: Tensor | None, fill_value: float) -> Tensor:
|
| 236 |
+
if mask is None:
|
| 237 |
+
return image
|
| 238 |
+
return torch.where(mask, image, torch.full_like(image, fill_value))
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _masked_input_display(
|
| 242 |
+
image: Tensor,
|
| 243 |
+
pred_mask: Tensor,
|
| 244 |
+
img_mask: Tensor | None,
|
| 245 |
+
fill_value: float,
|
| 246 |
+
) -> Tensor:
|
| 247 |
+
display = torch.where(pred_mask, torch.full_like(image, fill_value), image)
|
| 248 |
+
return _apply_display_mask(display, img_mask, fill_value)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def _crop_view_items(view_items: list[dict]) -> None:
|
| 252 |
+
for item in view_items:
|
| 253 |
+
mask = item["img_mask"]
|
| 254 |
+
if mask is None:
|
| 255 |
+
mask = item["actual"] != item["actual"].min()
|
| 256 |
+
row_slice, col_slice = _content_crop(mask, item["patch_rc"])
|
| 257 |
+
for key in ("target", "composite", "actual", "mask"):
|
| 258 |
+
item[key] = item[key][row_slice, col_slice]
|
| 259 |
+
if item["img_mask"] is not None:
|
| 260 |
+
item["img_mask"] = item["img_mask"][row_slice, col_slice]
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def _content_crop(mask: Tensor, patch_size: tuple[int, int]) -> tuple[slice, slice]:
|
| 264 |
+
mask = mask.detach().cpu() > 0
|
| 265 |
+
if not mask.any():
|
| 266 |
+
return slice(None), slice(None)
|
| 267 |
+
|
| 268 |
+
rows, cols = mask.nonzero(as_tuple=True)
|
| 269 |
+
patch_h, patch_w = patch_size
|
| 270 |
+
height, width = mask.shape
|
| 271 |
+
row0 = max((int(rows.min()) // patch_h - 1) * patch_h, 0)
|
| 272 |
+
row1 = min((int(rows.max()) // patch_h + 2) * patch_h, height)
|
| 273 |
+
col0 = max((int(cols.min()) // patch_w - 1) * patch_w, 0)
|
| 274 |
+
col1 = min((int(cols.max()) // patch_w + 2) * patch_w, width)
|
| 275 |
+
return slice(row0, row1), slice(col0, col1)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def _as_3tuple(value: int | tuple[int, int, int]) -> tuple[int, int, int]:
|
| 279 |
+
if isinstance(value, int):
|
| 280 |
+
return (value, value, value)
|
| 281 |
+
if len(value) != 3:
|
| 282 |
+
raise ValueError(f"expected a 3-tuple patch size, got {value!r}")
|
| 283 |
+
return tuple(int(v) for v in value)
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def _view_patch_size(view: str, patch_size: tuple[int, int, int]) -> tuple[int, int]:
|
| 287 |
+
p_x, p_y, p_z = patch_size
|
| 288 |
+
if view in ("sagittal", "saggital"):
|
| 289 |
+
return p_z, p_y
|
| 290 |
+
if view == "axial":
|
| 291 |
+
return p_y, p_x
|
| 292 |
+
if view == "coronal":
|
| 293 |
+
return p_z, p_x
|
| 294 |
+
raise ValueError(f"unknown MRI view {view!r}")
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def _make_figure_canvas(
|
| 298 |
+
view_items: list[dict[str, Tensor | str | tuple[int, int]]],
|
| 299 |
+
figsize: tuple[float, float] | None = None,
|
| 300 |
+
):
|
| 301 |
+
dpi = 160
|
| 302 |
+
left = 58
|
| 303 |
+
right = 6
|
| 304 |
+
top = 18
|
| 305 |
+
bottom = 8
|
| 306 |
+
row_gap = 14
|
| 307 |
+
col_gap = 8
|
| 308 |
+
widths = [int(item["target"].shape[1]) for item in view_items]
|
| 309 |
+
heights = [int(item["target"].shape[0]) for item in view_items]
|
| 310 |
+
row_h = max(heights)
|
| 311 |
+
num_rows = 3
|
| 312 |
+
fig_w = left + right + sum(widths) + col_gap * (len(widths) - 1)
|
| 313 |
+
fig_h = top + bottom + row_h * num_rows + row_gap * (num_rows - 1)
|
| 314 |
+
|
| 315 |
+
scale = 1.35
|
| 316 |
+
if figsize is not None:
|
| 317 |
+
requested_w = figsize[0] * dpi
|
| 318 |
+
requested_h = figsize[1] * dpi
|
| 319 |
+
scale = max(requested_w / fig_w, requested_h / fig_h)
|
| 320 |
+
figsize = (fig_w * scale / dpi, fig_h * scale / dpi)
|
| 321 |
+
fig = plt.figure(figsize=figsize, dpi=dpi, facecolor="#0b0f14")
|
| 322 |
+
|
| 323 |
+
axes = [[] for _ in range(num_rows)]
|
| 324 |
+
col_centers = []
|
| 325 |
+
x = left
|
| 326 |
+
for width, height in zip(widths, heights):
|
| 327 |
+
col_centers.append((x + width / 2) / fig_w)
|
| 328 |
+
ys = [
|
| 329 |
+
bottom + (num_rows - row - 1) * (row_h + row_gap) + (row_h - height) / 2
|
| 330 |
+
for row in range(num_rows)
|
| 331 |
+
]
|
| 332 |
+
for row, y in enumerate(ys):
|
| 333 |
+
axes[row].append(
|
| 334 |
+
fig.add_axes(
|
| 335 |
+
[
|
| 336 |
+
x / fig_w,
|
| 337 |
+
y / fig_h,
|
| 338 |
+
width / fig_w,
|
| 339 |
+
height / fig_h,
|
| 340 |
+
],
|
| 341 |
+
facecolor="black",
|
| 342 |
+
)
|
| 343 |
+
)
|
| 344 |
+
x += width + col_gap
|
| 345 |
+
row_centers = [
|
| 346 |
+
(bottom + (num_rows - row - 1) * (row_h + row_gap) + row_h / 2) / fig_h
|
| 347 |
+
for row in range(num_rows)
|
| 348 |
+
]
|
| 349 |
+
layout = {
|
| 350 |
+
"col_centers": col_centers,
|
| 351 |
+
"row_centers": row_centers,
|
| 352 |
+
"label_x": (left - 8) / fig_w,
|
| 353 |
+
"title_y": (fig_h - top / 2) / fig_h,
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
return fig, axes, layout
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def _style_axis(ax) -> None:
|
| 360 |
+
ax.set_xticks([])
|
| 361 |
+
ax.set_yticks([])
|
| 362 |
+
for spine in ax.spines.values():
|
| 363 |
+
spine.set_visible(False)
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def _draw_patch_boxes(
|
| 367 |
+
ax,
|
| 368 |
+
mask: Tensor,
|
| 369 |
+
patch_size: tuple[int, int],
|
| 370 |
+
color: str = "#facc15",
|
| 371 |
+
) -> None:
|
| 372 |
+
for col, row, width, height in _patch_rectangles(mask, patch_size):
|
| 373 |
+
ax.add_patch(
|
| 374 |
+
patches.Rectangle(
|
| 375 |
+
(col - 0.5, row - 0.5),
|
| 376 |
+
width,
|
| 377 |
+
height,
|
| 378 |
+
fill=False,
|
| 379 |
+
edgecolor=color,
|
| 380 |
+
linewidth=0.75,
|
| 381 |
+
alpha=0.95,
|
| 382 |
+
)
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def _patch_rectangles(
|
| 387 |
+
mask: Tensor,
|
| 388 |
+
patch_size: tuple[int, int],
|
| 389 |
+
) -> list[tuple[int, int, int, int]]:
|
| 390 |
+
mask = mask.detach().cpu() > 0
|
| 391 |
+
patch_h, patch_w = patch_size
|
| 392 |
+
height, width = mask.shape
|
| 393 |
+
rectangles = []
|
| 394 |
+
for row in range(0, height, patch_h):
|
| 395 |
+
box_h = min(patch_h, height - row)
|
| 396 |
+
for col in range(0, width, patch_w):
|
| 397 |
+
box_w = min(patch_w, width - col)
|
| 398 |
+
if mask[row : row + box_h, col : col + box_w].any():
|
| 399 |
+
rectangles.append((col, row, box_w, box_h))
|
| 400 |
+
return rectangles
|
finetune/fomo_tune_baseline/output/task5/config.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task: task5
|
| 2 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 3 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 4 |
+
name: task5
|
| 5 |
+
device: cuda
|
| 6 |
+
seed: 4466
|
finetune/fomo_tune_baseline/output/task5/log.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
16:32:19 run task5 (git 1df2e5d)
|
| 2 |
+
16:32:19 config:
|
| 3 |
+
task: task5
|
| 4 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 5 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 6 |
+
name: task5
|
| 7 |
+
device: cuda
|
| 8 |
+
seed: 4466
|
| 9 |
+
16:32:42 dataset: 48 subjects, 24 positive
|
| 10 |
+
16:33:21 fold 1/20 n=3 y=[0 1 1] p=[0.013 0.998 0.974] (35s)
|
| 11 |
+
16:33:24 fold 2/20 n=3 y=[1 1 1] p=[0.796 0.719 0.999] (37s)
|
| 12 |
+
16:33:26 fold 3/20 n=3 y=[0 1 1] p=[0.155 0.998 0.905] (39s)
|
| 13 |
+
16:33:28 fold 4/20 n=3 y=[0 0 1] p=[0.024 0.032 0.811] (42s)
|
| 14 |
+
16:33:29 fold 5/20 n=3 y=[1 1 1] p=[0.988 0.652 0.552] (43s)
|
| 15 |
+
16:33:31 fold 6/20 n=3 y=[0 0 1] p=[0.46 0.744 0.994] (44s)
|
| 16 |
+
16:33:33 fold 7/20 n=3 y=[0 0 1] p=[0.134 0.148 0.996] (47s)
|
| 17 |
+
16:33:36 fold 8/20 n=3 y=[0 0 1] p=[0.037 0.012 0.915] (50s)
|
| 18 |
+
16:33:37 fold 9/20 n=2 y=[0 1] p=[0.137 0.997] (51s)
|
| 19 |
+
16:33:38 fold 10/20 n=2 y=[0 1] p=[0.223 0.993] (51s)
|
| 20 |
+
16:33:39 fold 11/20 n=2 y=[0 0] p=[0.118 0.32 ] (53s)
|
| 21 |
+
16:33:42 fold 12/20 n=2 y=[0 1] p=[0.122 0.849] (55s)
|
| 22 |
+
16:33:43 fold 13/20 n=2 y=[0 1] p=[0.207 0.282] (57s)
|
| 23 |
+
16:33:45 fold 14/20 n=2 y=[0 1] p=[0.177 0.319] (58s)
|
| 24 |
+
16:33:47 fold 15/20 n=2 y=[0 1] p=[0.112 0.943] (60s)
|
| 25 |
+
16:33:48 fold 16/20 n=2 y=[0 1] p=[0.034 0.998] (61s)
|
| 26 |
+
16:33:50 fold 17/20 n=2 y=[0 0] p=[0.018 0.021] (63s)
|
| 27 |
+
16:33:51 fold 18/20 n=2 y=[0 1] p=[0.02 0.937] (64s)
|
| 28 |
+
16:33:52 fold 19/20 n=2 y=[0 1] p=[0.004 0.96 ] (65s)
|
| 29 |
+
16:33:54 fold 20/20 n=2 y=[0 1] p=[0.075 0.929] (68s)
|
| 30 |
+
16:33:56 result: auroc=0.9844 auroc_ci_low=0.9528 auroc_ci_high=1.0000 (68s)
|
finetune/fomo_tune_baseline/output/task5/metrics.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"name": "task5", "auroc": 0.984375, "auroc_ci_low": 0.9527867965367964, "auroc_ci_high": 1.0, "run_time": 67.5}
|
finetune/fomo_tune_baseline/output/task5/model/config.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
task: task5
|
| 2 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 3 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 4 |
+
name: task5
|
| 5 |
+
device: cuda
|
| 6 |
+
seed: 4466
|
finetune/fomo_tune_baseline/slurms/build.log
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
=== task1 ===
|
| 2 |
+
INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
|
| 3 |
+
INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
|
| 4 |
+
INFO: Starting build...
|
| 5 |
+
INFO: Fetching OCI image...
|
| 6 |
+
INFO: Extracting OCI image...
|
| 7 |
+
INFO: Inserting Apptainer configuration...
|
| 8 |
+
INFO: Copying fomo_tune to /app/fomo_tune
|
| 9 |
+
INFO: Copying smri_mae to /app/smri_mae
|
| 10 |
+
INFO: Copying model to /app/model
|
| 11 |
+
INFO: Copying predict.py to /app/predict.py
|
| 12 |
+
INFO: Running post scriptlet
|
| 13 |
+
+ pip install --no-cache-dir torch==2.8.0 numpy==2.4.6 nibabel==5.4.2 einops==0.8.2 jaxtyping==0.3.10 timm==1.0.27 huggingface-hub==0.36.2 scikit-learn==1.8.0 joblib==1.5.3 omegaconf==2.3.0
|
| 14 |
+
Collecting torch==2.8.0
|
| 15 |
+
Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (30 kB)
|
| 16 |
+
Collecting numpy==2.4.6
|
| 17 |
+
Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB)
|
| 18 |
+
Collecting nibabel==5.4.2
|
| 19 |
+
Downloading nibabel-5.4.2-py3-none-any.whl.metadata (8.9 kB)
|
| 20 |
+
Collecting einops==0.8.2
|
| 21 |
+
Downloading einops-0.8.2-py3-none-any.whl.metadata (13 kB)
|
| 22 |
+
Collecting jaxtyping==0.3.10
|
| 23 |
+
Downloading jaxtyping-0.3.10-py3-none-any.whl.metadata (6.2 kB)
|
| 24 |
+
Collecting timm==1.0.27
|
| 25 |
+
Downloading timm-1.0.27-py3-none-any.whl.metadata (40 kB)
|
| 26 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40.2/40.2 kB 115.4 MB/s eta 0:00:00
|
| 27 |
+
Collecting huggingface-hub==0.36.2
|
| 28 |
+
Downloading huggingface_hub-0.36.2-py3-none-any.whl.metadata (15 kB)
|
| 29 |
+
Collecting scikit-learn==1.8.0
|
| 30 |
+
Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (11 kB)
|
| 31 |
+
Collecting joblib==1.5.3
|
| 32 |
+
Downloading joblib-1.5.3-py3-none-any.whl.metadata (5.5 kB)
|
| 33 |
+
Collecting omegaconf==2.3.0
|
| 34 |
+
Downloading omegaconf-2.3.0-py3-none-any.whl.metadata (3.9 kB)
|
| 35 |
+
Collecting filelock (from torch==2.8.0)
|
| 36 |
+
Downloading filelock-3.32.2-py3-none-any.whl.metadata (2.0 kB)
|
| 37 |
+
Collecting typing-extensions>=4.10.0 (from torch==2.8.0)
|
| 38 |
+
Downloading typing_extensions-4.16.0-py3-none-any.whl.metadata (3.3 kB)
|
| 39 |
+
Collecting sympy>=1.13.3 (from torch==2.8.0)
|
| 40 |
+
Downloading sympy-1.14.0-py3-none-any.whl.metadata (12 kB)
|
| 41 |
+
Collecting networkx (from torch==2.8.0)
|
| 42 |
+
Downloading networkx-3.6.1-py3-none-any.whl.metadata (6.8 kB)
|
| 43 |
+
Collecting jinja2 (from torch==2.8.0)
|
| 44 |
+
Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
|
| 45 |
+
Collecting fsspec (from torch==2.8.0)
|
| 46 |
+
Downloading fsspec-2026.7.0-py3-none-any.whl.metadata (10 kB)
|
| 47 |
+
Collecting nvidia-cuda-nvrtc-cu12==12.8.93 (from torch==2.8.0)
|
| 48 |
+
Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
|
| 49 |
+
Collecting nvidia-cuda-runtime-cu12==12.8.90 (from torch==2.8.0)
|
| 50 |
+
Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 51 |
+
Collecting nvidia-cuda-cupti-cu12==12.8.90 (from torch==2.8.0)
|
| 52 |
+
Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 53 |
+
Collecting nvidia-cudnn-cu12==9.10.2.21 (from torch==2.8.0)
|
| 54 |
+
Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
|
| 55 |
+
Collecting nvidia-cublas-cu12==12.8.4.1 (from torch==2.8.0)
|
| 56 |
+
Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
|
| 57 |
+
Collecting nvidia-cufft-cu12==11.3.3.83 (from torch==2.8.0)
|
| 58 |
+
Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 59 |
+
Collecting nvidia-curand-cu12==10.3.9.90 (from torch==2.8.0)
|
| 60 |
+
Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
|
| 61 |
+
Collecting nvidia-cusolver-cu12==11.7.3.90 (from torch==2.8.0)
|
| 62 |
+
Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
|
| 63 |
+
Collecting nvidia-cusparse-cu12==12.5.8.93 (from torch==2.8.0)
|
| 64 |
+
Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
|
| 65 |
+
Collecting nvidia-cusparselt-cu12==0.7.1 (from torch==2.8.0)
|
| 66 |
+
Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl.metadata (7.0 kB)
|
| 67 |
+
Collecting nvidia-nccl-cu12==2.27.3 (from torch==2.8.0)
|
| 68 |
+
Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (2.0 kB)
|
| 69 |
+
Collecting nvidia-nvtx-cu12==12.8.90 (from torch==2.8.0)
|
| 70 |
+
Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
|
| 71 |
+
Collecting nvidia-nvjitlink-cu12==12.8.93 (from torch==2.8.0)
|
| 72 |
+
Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
|
| 73 |
+
Collecting nvidia-cufile-cu12==1.13.1.3 (from torch==2.8.0)
|
| 74 |
+
Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 75 |
+
Collecting triton==3.4.0 (from torch==2.8.0)
|
| 76 |
+
Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (1.7 kB)
|
| 77 |
+
Collecting importlib-resources>=5.12 (from nibabel==5.4.2)
|
| 78 |
+
Downloading importlib_resources-7.1.0-py3-none-any.whl.metadata (4.0 kB)
|
| 79 |
+
Requirement already satisfied: packaging>=20 in /usr/local/lib/python3.11/site-packages (from nibabel==5.4.2) (26.3)
|
| 80 |
+
Collecting wadler-lindig>=0.1.3 (from jaxtyping==0.3.10)
|
| 81 |
+
Downloading wadler_lindig-0.1.7-py3-none-any.whl.metadata (17 kB)
|
| 82 |
+
Collecting torchvision (from timm==1.0.27)
|
| 83 |
+
Downloading torchvision-0.28.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.6 kB)
|
| 84 |
+
Collecting pyyaml (from timm==1.0.27)
|
| 85 |
+
Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.4 kB)
|
| 86 |
+
Collecting safetensors (from timm==1.0.27)
|
| 87 |
+
Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.2 kB)
|
| 88 |
+
Collecting hf-xet<2.0.0,>=1.1.3 (from huggingface-hub==0.36.2)
|
| 89 |
+
Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (4.9 kB)
|
| 90 |
+
Collecting requests (from huggingface-hub==0.36.2)
|
| 91 |
+
Downloading requests-2.34.2-py3-none-any.whl.metadata (4.8 kB)
|
| 92 |
+
Collecting tqdm>=4.42.1 (from huggingface-hub==0.36.2)
|
| 93 |
+
Downloading tqdm-4.70.0-py3-none-any.whl.metadata (57 kB)
|
| 94 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.3/57.3 kB 49.5 MB/s eta 0:00:00
|
| 95 |
+
Collecting scipy>=1.10.0 (from scikit-learn==1.8.0)
|
| 96 |
+
Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (62 kB)
|
| 97 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.1/62.1 kB 59.8 MB/s eta 0:00:00
|
| 98 |
+
Collecting threadpoolctl>=3.2.0 (from scikit-learn==1.8.0)
|
| 99 |
+
Downloading threadpoolctl-3.6.0-py3-none-any.whl.metadata (13 kB)
|
| 100 |
+
Collecting antlr4-python3-runtime==4.9.* (from omegaconf==2.3.0)
|
| 101 |
+
Downloading antlr4-python3-runtime-4.9.3.tar.gz (117 kB)
|
| 102 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 117.0/117.0 kB 53.3 MB/s eta 0:00:00
|
| 103 |
+
Preparing metadata (setup.py): started
|
| 104 |
+
Preparing metadata (setup.py): finished with status 'done'
|
| 105 |
+
Requirement already satisfied: setuptools>=40.8.0 in /usr/local/lib/python3.11/site-packages (from triton==3.4.0->torch==2.8.0) (79.0.1)
|
| 106 |
+
Collecting mpmath<1.4,>=1.1.0 (from sympy>=1.13.3->torch==2.8.0)
|
| 107 |
+
Downloading mpmath-1.3.0-py3-none-any.whl.metadata (8.6 kB)
|
| 108 |
+
Collecting MarkupSafe>=2.0 (from jinja2->torch==2.8.0)
|
| 109 |
+
Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.7 kB)
|
| 110 |
+
Collecting charset_normalizer<4,>=2 (from requests->huggingface-hub==0.36.2)
|
| 111 |
+
Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (41 kB)
|
| 112 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.7/41.7 kB 322.5 MB/s eta 0:00:00
|
| 113 |
+
Collecting idna<4,>=2.5 (from requests->huggingface-hub==0.36.2)
|
| 114 |
+
Downloading idna-3.18-py3-none-any.whl.metadata (6.1 kB)
|
| 115 |
+
Collecting urllib3<3,>=1.26 (from requests->huggingface-hub==0.36.2)
|
| 116 |
+
Downloading urllib3-2.7.0-py3-none-any.whl.metadata (6.9 kB)
|
| 117 |
+
Collecting certifi>=2023.5.7 (from requests->huggingface-hub==0.36.2)
|
| 118 |
+
Downloading certifi-2026.7.22-py3-none-any.whl.metadata (2.5 kB)
|
| 119 |
+
INFO: pip is looking at multiple versions of torchvision to determine which version is compatible with other requirements. This could take a while.
|
| 120 |
+
Collecting torchvision (from timm==1.0.27)
|
| 121 |
+
Downloading torchvision-0.27.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
|
| 122 |
+
Downloading torchvision-0.27.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
|
| 123 |
+
Downloading torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
|
| 124 |
+
Downloading torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.4 kB)
|
| 125 |
+
Downloading torchvision-0.24.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
|
| 126 |
+
Downloading torchvision-0.24.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
|
| 127 |
+
Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (6.1 kB)
|
| 128 |
+
Collecting pillow!=8.3.*,>=5.3.0 (from torchvision->timm==1.0.27)
|
| 129 |
+
Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (9.1 kB)
|
| 130 |
+
Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl (888.1 MB)
|
| 131 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 888.1/888.1 MB 227.6 MB/s eta 0:00:00
|
| 132 |
+
Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (16.9 MB)
|
| 133 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.9/16.9 MB 286.0 MB/s eta 0:00:00
|
| 134 |
+
Downloading nibabel-5.4.2-py3-none-any.whl (3.3 MB)
|
| 135 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.3/3.3 MB 288.9 MB/s eta 0:00:00
|
| 136 |
+
Downloading einops-0.8.2-py3-none-any.whl (65 kB)
|
| 137 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.6/65.6 kB 290.5 MB/s eta 0:00:00
|
| 138 |
+
Downloading jaxtyping-0.3.10-py3-none-any.whl (56 kB)
|
| 139 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 56.4/56.4 kB 356.7 MB/s eta 0:00:00
|
| 140 |
+
Downloading timm-1.0.27-py3-none-any.whl (2.6 MB)
|
| 141 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.6/2.6 MB 215.7 MB/s eta 0:00:00
|
| 142 |
+
Downloading huggingface_hub-0.36.2-py3-none-any.whl (566 kB)
|
| 143 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 566.4/566.4 kB 221.4 MB/s eta 0:00:00
|
| 144 |
+
Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (9.1 MB)
|
| 145 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.1/9.1 MB 169.5 MB/s eta 0:00:00
|
| 146 |
+
Downloading joblib-1.5.3-py3-none-any.whl (309 kB)
|
| 147 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 309.1/309.1 kB 112.6 MB/s eta 0:00:00
|
| 148 |
+
Downloading omegaconf-2.3.0-py3-none-any.whl (79 kB)
|
| 149 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 79.5/79.5 kB 406.4 MB/s eta 0:00:00
|
| 150 |
+
Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl (594.3 MB)
|
| 151 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 594.3/594.3 MB 213.7 MB/s eta 0:00:00
|
| 152 |
+
Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (10.2 MB)
|
| 153 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.2/10.2 MB 218.2 MB/s eta 0:00:00
|
| 154 |
+
Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (88.0 MB)
|
| 155 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 88.0/88.0 MB 301.5 MB/s eta 0:00:00
|
| 156 |
+
Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (954 kB)
|
| 157 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 954.8/954.8 kB 223.9 MB/s eta 0:00:00
|
| 158 |
+
Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl (706.8 MB)
|
| 159 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 706.8/706.8 MB 302.6 MB/s eta 0:00:00
|
| 160 |
+
Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (193.1 MB)
|
| 161 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 193.1/193.1 MB 305.5 MB/s eta 0:00:00
|
| 162 |
+
Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (1.2 MB)
|
| 163 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 228.2 MB/s eta 0:00:00
|
| 164 |
+
Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl (63.6 MB)
|
| 165 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 63.6/63.6 MB 312.1 MB/s eta 0:00:00
|
| 166 |
+
Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl (267.5 MB)
|
| 167 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 267.5/267.5 MB 301.3 MB/s eta 0:00:00
|
| 168 |
+
Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (288.2 MB)
|
| 169 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 288.2/288.2 MB 299.6 MB/s eta 0:00:00
|
| 170 |
+
Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl (287.2 MB)
|
| 171 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 287.2/287.2 MB 304.3 MB/s eta 0:00:00
|
| 172 |
+
Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (322.4 MB)
|
| 173 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 322.4/322.4 MB 275.0 MB/s eta 0:00:00
|
| 174 |
+
Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (39.3 MB)
|
| 175 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━���━━━ 39.3/39.3 MB 255.2 MB/s eta 0:00:00
|
| 176 |
+
Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (89 kB)
|
| 177 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 90.0/90.0 kB 393.8 MB/s eta 0:00:00
|
| 178 |
+
Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (155.5 MB)
|
| 179 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 155.5/155.5 MB 302.6 MB/s eta 0:00:00
|
| 180 |
+
Downloading fsspec-2026.7.0-py3-none-any.whl (206 kB)
|
| 181 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 206.6/206.6 kB 464.9 MB/s eta 0:00:00
|
| 182 |
+
Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (4.5 MB)
|
| 183 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 338.8 MB/s eta 0:00:00
|
| 184 |
+
Downloading importlib_resources-7.1.0-py3-none-any.whl (37 kB)
|
| 185 |
+
Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (806 kB)
|
| 186 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 806.6/806.6 kB 427.8 MB/s eta 0:00:00
|
| 187 |
+
Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (35.3 MB)
|
| 188 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 35.3/35.3 MB 268.4 MB/s eta 0:00:00
|
| 189 |
+
Downloading sympy-1.14.0-py3-none-any.whl (6.3 MB)
|
| 190 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 331.0 MB/s eta 0:00:00
|
| 191 |
+
Downloading threadpoolctl-3.6.0-py3-none-any.whl (18 kB)
|
| 192 |
+
Downloading tqdm-4.70.0-py3-none-any.whl (80 kB)
|
| 193 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 80.2/80.2 kB 319.2 MB/s eta 0:00:00
|
| 194 |
+
Downloading typing_extensions-4.16.0-py3-none-any.whl (45 kB)
|
| 195 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.6/45.6 kB 289.4 MB/s eta 0:00:00
|
| 196 |
+
Downloading wadler_lindig-0.1.7-py3-none-any.whl (20 kB)
|
| 197 |
+
Downloading filelock-3.32.2-py3-none-any.whl (98 kB)
|
| 198 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 98.8/98.8 kB 317.4 MB/s eta 0:00:00
|
| 199 |
+
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
|
| 200 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 134.9/134.9 kB 406.2 MB/s eta 0:00:00
|
| 201 |
+
Downloading networkx-3.6.1-py3-none-any.whl (2.1 MB)
|
| 202 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 220.8 MB/s eta 0:00:00
|
| 203 |
+
Downloading requests-2.34.2-py3-none-any.whl (73 kB)
|
| 204 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 73.1/73.1 kB 367.4 MB/s eta 0:00:00
|
| 205 |
+
Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (516 kB)
|
| 206 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 516.0/516.0 kB 227.7 MB/s eta 0:00:00
|
| 207 |
+
Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl (8.6 MB)
|
| 208 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.6/8.6 MB 168.8 MB/s eta 0:00:00
|
| 209 |
+
Downloading certifi-2026.7.22-py3-none-any.whl (136 kB)
|
| 210 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 137.0/137.0 kB 412.0 MB/s eta 0:00:00
|
| 211 |
+
Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (221 kB)
|
| 212 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 221.3/221.3 kB 455.8 MB/s eta 0:00:00
|
| 213 |
+
Downloading idna-3.18-py3-none-any.whl (65 kB)
|
| 214 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.5/65.5 kB 258.9 MB/s eta 0:00:00
|
| 215 |
+
Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (22 kB)
|
| 216 |
+
Downloading mpmath-1.3.0-py3-none-any.whl (536 kB)
|
| 217 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 536.2/536.2 kB 241.6 MB/s eta 0:00:00
|
| 218 |
+
Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (6.9 MB)
|
| 219 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 176.0 MB/s eta 0:00:00
|
| 220 |
+
Downloading urllib3-2.7.0-py3-none-any.whl (131 kB)
|
| 221 |
+
━━━━━━���━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 131.1/131.1 kB 349.6 MB/s eta 0:00:00
|
| 222 |
+
Building wheels for collected packages: antlr4-python3-runtime
|
| 223 |
+
Building wheel for antlr4-python3-runtime (setup.py): started
|
| 224 |
+
Building wheel for antlr4-python3-runtime (setup.py): finished with status 'done'
|
| 225 |
+
Created wheel for antlr4-python3-runtime: filename=antlr4_python3_runtime-4.9.3-py3-none-any.whl size=144590 sha256=7049f12da321fd7083d3bdaa9af713223bfc3702787beff8fbc0980b4dfdc5a8
|
| 226 |
+
Stored in directory: /tmp/pip-ephem-wheel-cache-1binwsnn/wheels/1a/97/32/461f837398029ad76911109f07047fde1d7b661a147c7c56d1
|
| 227 |
+
Successfully built antlr4-python3-runtime
|
| 228 |
+
Installing collected packages: nvidia-cusparselt-cu12, mpmath, antlr4-python3-runtime, wadler-lindig, urllib3, typing-extensions, triton, tqdm, threadpoolctl, sympy, safetensors, pyyaml, pillow, nvidia-nvtx-cu12, nvidia-nvjitlink-cu12, nvidia-nccl-cu12, nvidia-curand-cu12, nvidia-cufile-cu12, nvidia-cuda-runtime-cu12, nvidia-cuda-nvrtc-cu12, nvidia-cuda-cupti-cu12, nvidia-cublas-cu12, numpy, networkx, MarkupSafe, joblib, importlib-resources, idna, hf-xet, fsspec, filelock, einops, charset_normalizer, certifi, scipy, requests, omegaconf, nvidia-cusparse-cu12, nvidia-cufft-cu12, nvidia-cudnn-cu12, nibabel, jinja2, jaxtyping, scikit-learn, nvidia-cusolver-cu12, huggingface-hub, torch, torchvision, timm
|
| 229 |
+
Successfully installed MarkupSafe-3.0.3 antlr4-python3-runtime-4.9.3 certifi-2026.7.22 charset_normalizer-3.4.9 einops-0.8.2 filelock-3.32.2 fsspec-2026.7.0 hf-xet-1.6.0 huggingface-hub-0.36.2 idna-3.18 importlib-resources-7.1.0 jaxtyping-0.3.10 jinja2-3.1.6 joblib-1.5.3 mpmath-1.3.0 networkx-3.6.1 nibabel-5.4.2 numpy-2.4.6 nvidia-cublas-cu12-12.8.4.1 nvidia-cuda-cupti-cu12-12.8.90 nvidia-cuda-nvrtc-cu12-12.8.93 nvidia-cuda-runtime-cu12-12.8.90 nvidia-cudnn-cu12-9.10.2.21 nvidia-cufft-cu12-11.3.3.83 nvidia-cufile-cu12-1.13.1.3 nvidia-curand-cu12-10.3.9.90 nvidia-cusolver-cu12-11.7.3.90 nvidia-cusparse-cu12-12.5.8.93 nvidia-cusparselt-cu12-0.7.1 nvidia-nccl-cu12-2.27.3 nvidia-nvjitlink-cu12-12.8.93 nvidia-nvtx-cu12-12.8.90 omegaconf-2.3.0 pillow-12.3.0 pyyaml-6.0.3 requests-2.34.2 safetensors-0.8.0 scikit-learn-1.8.0 scipy-1.17.1 sympy-1.14.0 threadpoolctl-3.6.0 timm-1.0.27 torch-2.8.0 torchvision-0.23.0 tqdm-4.70.0 triton-3.4.0 typing-extensions-4.16.0 urllib3-2.7.0 wadler-lindig-0.1.7
|
| 230 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
|
| 231 |
+
|
| 232 |
+
[notice] A new release of pip is available: 24.0 -> 26.2.1
|
| 233 |
+
[notice] To update, run: pip install --upgrade pip
|
| 234 |
+
INFO: Adding runscript
|
| 235 |
+
INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
|
| 236 |
+
INFO: Creating SIF file...
|
| 237 |
+
INFO: Build complete: /data/connor/nanobrain.1/experiments/fomo_tune_baseline/output/task1/task1.sif
|
| 238 |
+
built experiments/fomo_tune_baseline/output/task1/task1.sif
|
| 239 |
+
=== task5 ===
|
| 240 |
+
INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
|
| 241 |
+
INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
|
| 242 |
+
INFO: Starting build...
|
| 243 |
+
INFO: Fetching OCI image...
|
| 244 |
+
INFO: Extracting OCI image...
|
| 245 |
+
INFO: Inserting Apptainer configuration...
|
| 246 |
+
INFO: Copying fomo_tune to /app/fomo_tune
|
| 247 |
+
INFO: Copying smri_mae to /app/smri_mae
|
| 248 |
+
INFO: Copying model to /app/model
|
| 249 |
+
INFO: Copying predict.py to /app/predict.py
|
| 250 |
+
INFO: Running post scriptlet
|
| 251 |
+
+ pip install --no-cache-dir torch==2.8.0 numpy==2.4.6 nibabel==5.4.2 einops==0.8.2 jaxtyping==0.3.10 timm==1.0.27 huggingface-hub==0.36.2 scikit-learn==1.8.0 joblib==1.5.3 omegaconf==2.3.0
|
| 252 |
+
Collecting torch==2.8.0
|
| 253 |
+
Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (30 kB)
|
| 254 |
+
Collecting numpy==2.4.6
|
| 255 |
+
Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB)
|
| 256 |
+
Collecting nibabel==5.4.2
|
| 257 |
+
Downloading nibabel-5.4.2-py3-none-any.whl.metadata (8.9 kB)
|
| 258 |
+
Collecting einops==0.8.2
|
| 259 |
+
Downloading einops-0.8.2-py3-none-any.whl.metadata (13 kB)
|
| 260 |
+
Collecting jaxtyping==0.3.10
|
| 261 |
+
Downloading jaxtyping-0.3.10-py3-none-any.whl.metadata (6.2 kB)
|
| 262 |
+
Collecting timm==1.0.27
|
| 263 |
+
Downloading timm-1.0.27-py3-none-any.whl.metadata (40 kB)
|
| 264 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40.2/40.2 kB 20.8 MB/s eta 0:00:00
|
| 265 |
+
Collecting huggingface-hub==0.36.2
|
| 266 |
+
Downloading huggingface_hub-0.36.2-py3-none-any.whl.metadata (15 kB)
|
| 267 |
+
Collecting scikit-learn==1.8.0
|
| 268 |
+
Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (11 kB)
|
| 269 |
+
Collecting joblib==1.5.3
|
| 270 |
+
Downloading joblib-1.5.3-py3-none-any.whl.metadata (5.5 kB)
|
| 271 |
+
Collecting omegaconf==2.3.0
|
| 272 |
+
Downloading omegaconf-2.3.0-py3-none-any.whl.metadata (3.9 kB)
|
| 273 |
+
Collecting filelock (from torch==2.8.0)
|
| 274 |
+
Downloading filelock-3.32.2-py3-none-any.whl.metadata (2.0 kB)
|
| 275 |
+
Collecting typing-extensions>=4.10.0 (from torch==2.8.0)
|
| 276 |
+
Downloading typing_extensions-4.16.0-py3-none-any.whl.metadata (3.3 kB)
|
| 277 |
+
Collecting sympy>=1.13.3 (from torch==2.8.0)
|
| 278 |
+
Downloading sympy-1.14.0-py3-none-any.whl.metadata (12 kB)
|
| 279 |
+
Collecting networkx (from torch==2.8.0)
|
| 280 |
+
Downloading networkx-3.6.1-py3-none-any.whl.metadata (6.8 kB)
|
| 281 |
+
Collecting jinja2 (from torch==2.8.0)
|
| 282 |
+
Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
|
| 283 |
+
Collecting fsspec (from torch==2.8.0)
|
| 284 |
+
Downloading fsspec-2026.7.0-py3-none-any.whl.metadata (10 kB)
|
| 285 |
+
Collecting nvidia-cuda-nvrtc-cu12==12.8.93 (from torch==2.8.0)
|
| 286 |
+
Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
|
| 287 |
+
Collecting nvidia-cuda-runtime-cu12==12.8.90 (from torch==2.8.0)
|
| 288 |
+
Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 289 |
+
Collecting nvidia-cuda-cupti-cu12==12.8.90 (from torch==2.8.0)
|
| 290 |
+
Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 291 |
+
Collecting nvidia-cudnn-cu12==9.10.2.21 (from torch==2.8.0)
|
| 292 |
+
Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
|
| 293 |
+
Collecting nvidia-cublas-cu12==12.8.4.1 (from torch==2.8.0)
|
| 294 |
+
Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
|
| 295 |
+
Collecting nvidia-cufft-cu12==11.3.3.83 (from torch==2.8.0)
|
| 296 |
+
Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 297 |
+
Collecting nvidia-curand-cu12==10.3.9.90 (from torch==2.8.0)
|
| 298 |
+
Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
|
| 299 |
+
Collecting nvidia-cusolver-cu12==11.7.3.90 (from torch==2.8.0)
|
| 300 |
+
Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
|
| 301 |
+
Collecting nvidia-cusparse-cu12==12.5.8.93 (from torch==2.8.0)
|
| 302 |
+
Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
|
| 303 |
+
Collecting nvidia-cusparselt-cu12==0.7.1 (from torch==2.8.0)
|
| 304 |
+
Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl.metadata (7.0 kB)
|
| 305 |
+
Collecting nvidia-nccl-cu12==2.27.3 (from torch==2.8.0)
|
| 306 |
+
Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (2.0 kB)
|
| 307 |
+
Collecting nvidia-nvtx-cu12==12.8.90 (from torch==2.8.0)
|
| 308 |
+
Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
|
| 309 |
+
Collecting nvidia-nvjitlink-cu12==12.8.93 (from torch==2.8.0)
|
| 310 |
+
Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
|
| 311 |
+
Collecting nvidia-cufile-cu12==1.13.1.3 (from torch==2.8.0)
|
| 312 |
+
Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 313 |
+
Collecting triton==3.4.0 (from torch==2.8.0)
|
| 314 |
+
Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (1.7 kB)
|
| 315 |
+
Collecting importlib-resources>=5.12 (from nibabel==5.4.2)
|
| 316 |
+
Downloading importlib_resources-7.1.0-py3-none-any.whl.metadata (4.0 kB)
|
| 317 |
+
Requirement already satisfied: packaging>=20 in /usr/local/lib/python3.11/site-packages (from nibabel==5.4.2) (26.3)
|
| 318 |
+
Collecting wadler-lindig>=0.1.3 (from jaxtyping==0.3.10)
|
| 319 |
+
Downloading wadler_lindig-0.1.7-py3-none-any.whl.metadata (17 kB)
|
| 320 |
+
Collecting torchvision (from timm==1.0.27)
|
| 321 |
+
Downloading torchvision-0.28.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.6 kB)
|
| 322 |
+
Collecting pyyaml (from timm==1.0.27)
|
| 323 |
+
Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.4 kB)
|
| 324 |
+
Collecting safetensors (from timm==1.0.27)
|
| 325 |
+
Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.2 kB)
|
| 326 |
+
Collecting hf-xet<2.0.0,>=1.1.3 (from huggingface-hub==0.36.2)
|
| 327 |
+
Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (4.9 kB)
|
| 328 |
+
Collecting requests (from huggingface-hub==0.36.2)
|
| 329 |
+
Downloading requests-2.34.2-py3-none-any.whl.metadata (4.8 kB)
|
| 330 |
+
Collecting tqdm>=4.42.1 (from huggingface-hub==0.36.2)
|
| 331 |
+
Downloading tqdm-4.70.0-py3-none-any.whl.metadata (57 kB)
|
| 332 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.3/57.3 kB 39.7 MB/s eta 0:00:00
|
| 333 |
+
Collecting scipy>=1.10.0 (from scikit-learn==1.8.0)
|
| 334 |
+
Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (62 kB)
|
| 335 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.1/62.1 kB 67.7 MB/s eta 0:00:00
|
| 336 |
+
Collecting threadpoolctl>=3.2.0 (from scikit-learn==1.8.0)
|
| 337 |
+
Downloading threadpoolctl-3.6.0-py3-none-any.whl.metadata (13 kB)
|
| 338 |
+
Collecting antlr4-python3-runtime==4.9.* (from omegaconf==2.3.0)
|
| 339 |
+
Downloading antlr4-python3-runtime-4.9.3.tar.gz (117 kB)
|
| 340 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 117.0/117.0 kB 86.0 MB/s eta 0:00:00
|
| 341 |
+
Preparing metadata (setup.py): started
|
| 342 |
+
Preparing metadata (setup.py): finished with status 'done'
|
| 343 |
+
Requirement already satisfied: setuptools>=40.8.0 in /usr/local/lib/python3.11/site-packages (from triton==3.4.0->torch==2.8.0) (79.0.1)
|
| 344 |
+
Collecting mpmath<1.4,>=1.1.0 (from sympy>=1.13.3->torch==2.8.0)
|
| 345 |
+
Downloading mpmath-1.3.0-py3-none-any.whl.metadata (8.6 kB)
|
| 346 |
+
Collecting MarkupSafe>=2.0 (from jinja2->torch==2.8.0)
|
| 347 |
+
Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.7 kB)
|
| 348 |
+
Collecting charset_normalizer<4,>=2 (from requests->huggingface-hub==0.36.2)
|
| 349 |
+
Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (41 kB)
|
| 350 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.7/41.7 kB 295.5 MB/s eta 0:00:00
|
| 351 |
+
Collecting idna<4,>=2.5 (from requests->huggingface-hub==0.36.2)
|
| 352 |
+
Downloading idna-3.18-py3-none-any.whl.metadata (6.1 kB)
|
| 353 |
+
Collecting urllib3<3,>=1.26 (from requests->huggingface-hub==0.36.2)
|
| 354 |
+
Downloading urllib3-2.7.0-py3-none-any.whl.metadata (6.9 kB)
|
| 355 |
+
Collecting certifi>=2023.5.7 (from requests->huggingface-hub==0.36.2)
|
| 356 |
+
Downloading certifi-2026.7.22-py3-none-any.whl.metadata (2.5 kB)
|
| 357 |
+
INFO: pip is looking at multiple versions of torchvision to determine which version is compatible with other requirements. This could take a while.
|
| 358 |
+
Collecting torchvision (from timm==1.0.27)
|
| 359 |
+
Downloading torchvision-0.27.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
|
| 360 |
+
Downloading torchvision-0.27.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
|
| 361 |
+
Downloading torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
|
| 362 |
+
Downloading torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.4 kB)
|
| 363 |
+
Downloading torchvision-0.24.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
|
| 364 |
+
Downloading torchvision-0.24.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
|
| 365 |
+
Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (6.1 kB)
|
| 366 |
+
Collecting pillow!=8.3.*,>=5.3.0 (from torchvision->timm==1.0.27)
|
| 367 |
+
Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (9.1 kB)
|
| 368 |
+
Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl (888.1 MB)
|
| 369 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 888.1/888.1 MB 212.0 MB/s eta 0:00:00
|
| 370 |
+
Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (16.9 MB)
|
| 371 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.9/16.9 MB 311.3 MB/s eta 0:00:00
|
| 372 |
+
Downloading nibabel-5.4.2-py3-none-any.whl (3.3 MB)
|
| 373 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.3/3.3 MB 210.5 MB/s eta 0:00:00
|
| 374 |
+
Downloading einops-0.8.2-py3-none-any.whl (65 kB)
|
| 375 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.6/65.6 kB 354.1 MB/s eta 0:00:00
|
| 376 |
+
Downloading jaxtyping-0.3.10-py3-none-any.whl (56 kB)
|
| 377 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 56.4/56.4 kB 336.3 MB/s eta 0:00:00
|
| 378 |
+
Downloading timm-1.0.27-py3-none-any.whl (2.6 MB)
|
| 379 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.6/2.6 MB 175.2 MB/s eta 0:00:00
|
| 380 |
+
Downloading huggingface_hub-0.36.2-py3-none-any.whl (566 kB)
|
| 381 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 566.4/566.4 kB 129.1 MB/s eta 0:00:00
|
| 382 |
+
Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (9.1 MB)
|
| 383 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.1/9.1 MB 308.6 MB/s eta 0:00:00
|
| 384 |
+
Downloading joblib-1.5.3-py3-none-any.whl (309 kB)
|
| 385 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 309.1/309.1 kB 431.8 MB/s eta 0:00:00
|
| 386 |
+
Downloading omegaconf-2.3.0-py3-none-any.whl (79 kB)
|
| 387 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 79.5/79.5 kB 388.1 MB/s eta 0:00:00
|
| 388 |
+
Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl (594.3 MB)
|
| 389 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 594.3/594.3 MB 139.4 MB/s eta 0:00:00
|
| 390 |
+
Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (10.2 MB)
|
| 391 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.2/10.2 MB 174.8 MB/s eta 0:00:00
|
| 392 |
+
Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (88.0 MB)
|
| 393 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 88.0/88.0 MB 244.3 MB/s eta 0:00:00
|
| 394 |
+
Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (954 kB)
|
| 395 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 954.8/954.8 kB 289.0 MB/s eta 0:00:00
|
| 396 |
+
Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl (706.8 MB)
|
| 397 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 706.8/706.8 MB 232.5 MB/s eta 0:00:00
|
| 398 |
+
Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (193.1 MB)
|
| 399 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 193.1/193.1 MB 155.1 MB/s eta 0:00:00
|
| 400 |
+
Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (1.2 MB)
|
| 401 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 173.3 MB/s eta 0:00:00
|
| 402 |
+
Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl (63.6 MB)
|
| 403 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 63.6/63.6 MB 280.8 MB/s eta 0:00:00
|
| 404 |
+
Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl (267.5 MB)
|
| 405 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 267.5/267.5 MB 206.9 MB/s eta 0:00:00
|
| 406 |
+
Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (288.2 MB)
|
| 407 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 288.2/288.2 MB 178.9 MB/s eta 0:00:00
|
| 408 |
+
Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl (287.2 MB)
|
| 409 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 287.2/287.2 MB 219.2 MB/s eta 0:00:00
|
| 410 |
+
Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (322.4 MB)
|
| 411 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 322.4/322.4 MB 296.8 MB/s eta 0:00:00
|
| 412 |
+
Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (39.3 MB)
|
| 413 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 39.3/39.3 MB 207.8 MB/s eta 0:00:00
|
| 414 |
+
Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (89 kB)
|
| 415 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 90.0/90.0 kB 401.1 MB/s eta 0:00:00
|
| 416 |
+
Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (155.5 MB)
|
| 417 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 155.5/155.5 MB 299.9 MB/s eta 0:00:00
|
| 418 |
+
Downloading fsspec-2026.7.0-py3-none-any.whl (206 kB)
|
| 419 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 206.6/206.6 kB 461.6 MB/s eta 0:00:00
|
| 420 |
+
Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (4.5 MB)
|
| 421 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 305.4 MB/s eta 0:00:00
|
| 422 |
+
Downloading importlib_resources-7.1.0-py3-none-any.whl (37 kB)
|
| 423 |
+
Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (806 kB)
|
| 424 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 806.6/806.6 kB 461.9 MB/s eta 0:00:00
|
| 425 |
+
Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (35.3 MB)
|
| 426 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 35.3/35.3 MB 157.6 MB/s eta 0:00:00
|
| 427 |
+
Downloading sympy-1.14.0-py3-none-any.whl (6.3 MB)
|
| 428 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 254.2 MB/s eta 0:00:00
|
| 429 |
+
Downloading threadpoolctl-3.6.0-py3-none-any.whl (18 kB)
|
| 430 |
+
Downloading tqdm-4.70.0-py3-none-any.whl (80 kB)
|
| 431 |
+
━━━━━━━━━━━━━━━��━━━━━━━━━━━━━━━━━━━━━━━━ 80.2/80.2 kB 384.1 MB/s eta 0:00:00
|
| 432 |
+
Downloading typing_extensions-4.16.0-py3-none-any.whl (45 kB)
|
| 433 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.6/45.6 kB 314.4 MB/s eta 0:00:00
|
| 434 |
+
Downloading wadler_lindig-0.1.7-py3-none-any.whl (20 kB)
|
| 435 |
+
Downloading filelock-3.32.2-py3-none-any.whl (98 kB)
|
| 436 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 98.8/98.8 kB 308.0 MB/s eta 0:00:00
|
| 437 |
+
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
|
| 438 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 134.9/134.9 kB 357.5 MB/s eta 0:00:00
|
| 439 |
+
Downloading networkx-3.6.1-py3-none-any.whl (2.1 MB)
|
| 440 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 327.2 MB/s eta 0:00:00
|
| 441 |
+
Downloading requests-2.34.2-py3-none-any.whl (73 kB)
|
| 442 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 73.1/73.1 kB 379.7 MB/s eta 0:00:00
|
| 443 |
+
Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (516 kB)
|
| 444 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 516.0/516.0 kB 201.6 MB/s eta 0:00:00
|
| 445 |
+
Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl (8.6 MB)
|
| 446 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.6/8.6 MB 189.9 MB/s eta 0:00:00
|
| 447 |
+
Downloading certifi-2026.7.22-py3-none-any.whl (136 kB)
|
| 448 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 137.0/137.0 kB 390.2 MB/s eta 0:00:00
|
| 449 |
+
Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (221 kB)
|
| 450 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 221.3/221.3 kB 440.2 MB/s eta 0:00:00
|
| 451 |
+
Downloading idna-3.18-py3-none-any.whl (65 kB)
|
| 452 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.5/65.5 kB 278.9 MB/s eta 0:00:00
|
| 453 |
+
Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (22 kB)
|
| 454 |
+
Downloading mpmath-1.3.0-py3-none-any.whl (536 kB)
|
| 455 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 536.2/536.2 kB 470.6 MB/s eta 0:00:00
|
| 456 |
+
Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (6.9 MB)
|
| 457 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 295.5 MB/s eta 0:00:00
|
| 458 |
+
Downloading urllib3-2.7.0-py3-none-any.whl (131 kB)
|
| 459 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 131.1/131.1 kB 434.9 MB/s eta 0:00:00
|
| 460 |
+
Building wheels for collected packages: antlr4-python3-runtime
|
| 461 |
+
Building wheel for antlr4-python3-runtime (setup.py): started
|
| 462 |
+
Building wheel for antlr4-python3-runtime (setup.py): finished with status 'done'
|
| 463 |
+
Created wheel for antlr4-python3-runtime: filename=antlr4_python3_runtime-4.9.3-py3-none-any.whl size=144590 sha256=9153c9732e66786ceb0134b28a0118f6e9fedc66584880fabf6533f1989fac2e
|
| 464 |
+
Stored in directory: /tmp/pip-ephem-wheel-cache-5dznrpve/wheels/1a/97/32/461f837398029ad76911109f07047fde1d7b661a147c7c56d1
|
| 465 |
+
Successfully built antlr4-python3-runtime
|
| 466 |
+
Installing collected packages: nvidia-cusparselt-cu12, mpmath, antlr4-python3-runtime, wadler-lindig, urllib3, typing-extensions, triton, tqdm, threadpoolctl, sympy, safetensors, pyyaml, pillow, nvidia-nvtx-cu12, nvidia-nvjitlink-cu12, nvidia-nccl-cu12, nvidia-curand-cu12, nvidia-cufile-cu12, nvidia-cuda-runtime-cu12, nvidia-cuda-nvrtc-cu12, nvidia-cuda-cupti-cu12, nvidia-cublas-cu12, numpy, networkx, MarkupSafe, joblib, importlib-resources, idna, hf-xet, fsspec, filelock, einops, charset_normalizer, certifi, scipy, requests, omegaconf, nvidia-cusparse-cu12, nvidia-cufft-cu12, nvidia-cudnn-cu12, nibabel, jinja2, jaxtyping, scikit-learn, nvidia-cusolver-cu12, huggingface-hub, torch, torchvision, timm
|
| 467 |
+
Successfully installed MarkupSafe-3.0.3 antlr4-python3-runtime-4.9.3 certifi-2026.7.22 charset_normalizer-3.4.9 einops-0.8.2 filelock-3.32.2 fsspec-2026.7.0 hf-xet-1.6.0 huggingface-hub-0.36.2 idna-3.18 importlib-resources-7.1.0 jaxtyping-0.3.10 jinja2-3.1.6 joblib-1.5.3 mpmath-1.3.0 networkx-3.6.1 nibabel-5.4.2 numpy-2.4.6 nvidia-cublas-cu12-12.8.4.1 nvidia-cuda-cupti-cu12-12.8.90 nvidia-cuda-nvrtc-cu12-12.8.93 nvidia-cuda-runtime-cu12-12.8.90 nvidia-cudnn-cu12-9.10.2.21 nvidia-cufft-cu12-11.3.3.83 nvidia-cufile-cu12-1.13.1.3 nvidia-curand-cu12-10.3.9.90 nvidia-cusolver-cu12-11.7.3.90 nvidia-cusparse-cu12-12.5.8.93 nvidia-cusparselt-cu12-0.7.1 nvidia-nccl-cu12-2.27.3 nvidia-nvjitlink-cu12-12.8.93 nvidia-nvtx-cu12-12.8.90 omegaconf-2.3.0 pillow-12.3.0 pyyaml-6.0.3 requests-2.34.2 safetensors-0.8.0 scikit-learn-1.8.0 scipy-1.17.1 sympy-1.14.0 threadpoolctl-3.6.0 timm-1.0.27 torch-2.8.0 torchvision-0.23.0 tqdm-4.70.0 triton-3.4.0 typing-extensions-4.16.0 urllib3-2.7.0 wadler-lindig-0.1.7
|
| 468 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
|
| 469 |
+
|
| 470 |
+
[notice] A new release of pip is available: 24.0 -> 26.2.1
|
| 471 |
+
[notice] To update, run: pip install --upgrade pip
|
| 472 |
+
INFO: Adding runscript
|
| 473 |
+
INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
|
| 474 |
+
INFO: Creating SIF file...
|
| 475 |
+
INFO: Build complete: /data/connor/nanobrain.1/experiments/fomo_tune_baseline/output/task5/task5.sif
|
| 476 |
+
built experiments/fomo_tune_baseline/output/task5/task5.sif
|
| 477 |
+
=== task3 ===
|
| 478 |
+
INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
|
| 479 |
+
INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
|
| 480 |
+
INFO: Starting build...
|
| 481 |
+
INFO: Fetching OCI image...
|
| 482 |
+
INFO: Extracting OCI image...
|
| 483 |
+
INFO: Inserting Apptainer configuration...
|
| 484 |
+
INFO: Copying fomo_tune to /app/fomo_tune
|
| 485 |
+
INFO: Copying smri_mae to /app/smri_mae
|
| 486 |
+
INFO: Copying model to /app/model
|
| 487 |
+
INFO: Copying predict.py to /app/predict.py
|
| 488 |
+
INFO: Running post scriptlet
|
| 489 |
+
+ pip install --no-cache-dir torch==2.8.0 numpy==2.4.6 nibabel==5.4.2 einops==0.8.2 jaxtyping==0.3.10 timm==1.0.27 huggingface-hub==0.36.2 scikit-learn==1.8.0 joblib==1.5.3 omegaconf==2.3.0
|
| 490 |
+
Collecting torch==2.8.0
|
| 491 |
+
Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (30 kB)
|
| 492 |
+
Collecting numpy==2.4.6
|
| 493 |
+
Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB)
|
| 494 |
+
Collecting nibabel==5.4.2
|
| 495 |
+
Downloading nibabel-5.4.2-py3-none-any.whl.metadata (8.9 kB)
|
| 496 |
+
Collecting einops==0.8.2
|
| 497 |
+
Downloading einops-0.8.2-py3-none-any.whl.metadata (13 kB)
|
| 498 |
+
Collecting jaxtyping==0.3.10
|
| 499 |
+
Downloading jaxtyping-0.3.10-py3-none-any.whl.metadata (6.2 kB)
|
| 500 |
+
Collecting timm==1.0.27
|
| 501 |
+
Downloading timm-1.0.27-py3-none-any.whl.metadata (40 kB)
|
| 502 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 40.2/40.2 kB 330.8 MB/s eta 0:00:00
|
| 503 |
+
Collecting huggingface-hub==0.36.2
|
| 504 |
+
Downloading huggingface_hub-0.36.2-py3-none-any.whl.metadata (15 kB)
|
| 505 |
+
Collecting scikit-learn==1.8.0
|
| 506 |
+
Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (11 kB)
|
| 507 |
+
Collecting joblib==1.5.3
|
| 508 |
+
Downloading joblib-1.5.3-py3-none-any.whl.metadata (5.5 kB)
|
| 509 |
+
Collecting omegaconf==2.3.0
|
| 510 |
+
Downloading omegaconf-2.3.0-py3-none-any.whl.metadata (3.9 kB)
|
| 511 |
+
Collecting filelock (from torch==2.8.0)
|
| 512 |
+
Downloading filelock-3.32.2-py3-none-any.whl.metadata (2.0 kB)
|
| 513 |
+
Collecting typing-extensions>=4.10.0 (from torch==2.8.0)
|
| 514 |
+
Downloading typing_extensions-4.16.0-py3-none-any.whl.metadata (3.3 kB)
|
| 515 |
+
Collecting sympy>=1.13.3 (from torch==2.8.0)
|
| 516 |
+
Downloading sympy-1.14.0-py3-none-any.whl.metadata (12 kB)
|
| 517 |
+
Collecting networkx (from torch==2.8.0)
|
| 518 |
+
Downloading networkx-3.6.1-py3-none-any.whl.metadata (6.8 kB)
|
| 519 |
+
Collecting jinja2 (from torch==2.8.0)
|
| 520 |
+
Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
|
| 521 |
+
Collecting fsspec (from torch==2.8.0)
|
| 522 |
+
Downloading fsspec-2026.7.0-py3-none-any.whl.metadata (10 kB)
|
| 523 |
+
Collecting nvidia-cuda-nvrtc-cu12==12.8.93 (from torch==2.8.0)
|
| 524 |
+
Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
|
| 525 |
+
Collecting nvidia-cuda-runtime-cu12==12.8.90 (from torch==2.8.0)
|
| 526 |
+
Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 527 |
+
Collecting nvidia-cuda-cupti-cu12==12.8.90 (from torch==2.8.0)
|
| 528 |
+
Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 529 |
+
Collecting nvidia-cudnn-cu12==9.10.2.21 (from torch==2.8.0)
|
| 530 |
+
Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
|
| 531 |
+
Collecting nvidia-cublas-cu12==12.8.4.1 (from torch==2.8.0)
|
| 532 |
+
Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
|
| 533 |
+
Collecting nvidia-cufft-cu12==11.3.3.83 (from torch==2.8.0)
|
| 534 |
+
Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 535 |
+
Collecting nvidia-curand-cu12==10.3.9.90 (from torch==2.8.0)
|
| 536 |
+
Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.7 kB)
|
| 537 |
+
Collecting nvidia-cusolver-cu12==11.7.3.90 (from torch==2.8.0)
|
| 538 |
+
Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl.metadata (1.8 kB)
|
| 539 |
+
Collecting nvidia-cusparse-cu12==12.5.8.93 (from torch==2.8.0)
|
| 540 |
+
Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
|
| 541 |
+
Collecting nvidia-cusparselt-cu12==0.7.1 (from torch==2.8.0)
|
| 542 |
+
Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl.metadata (7.0 kB)
|
| 543 |
+
Collecting nvidia-nccl-cu12==2.27.3 (from torch==2.8.0)
|
| 544 |
+
Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (2.0 kB)
|
| 545 |
+
Collecting nvidia-nvtx-cu12==12.8.90 (from torch==2.8.0)
|
| 546 |
+
Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.8 kB)
|
| 547 |
+
Collecting nvidia-nvjitlink-cu12==12.8.93 (from torch==2.8.0)
|
| 548 |
+
Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl.metadata (1.7 kB)
|
| 549 |
+
Collecting nvidia-cufile-cu12==1.13.1.3 (from torch==2.8.0)
|
| 550 |
+
Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (1.7 kB)
|
| 551 |
+
Collecting triton==3.4.0 (from torch==2.8.0)
|
| 552 |
+
Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (1.7 kB)
|
| 553 |
+
Collecting importlib-resources>=5.12 (from nibabel==5.4.2)
|
| 554 |
+
Downloading importlib_resources-7.1.0-py3-none-any.whl.metadata (4.0 kB)
|
| 555 |
+
Requirement already satisfied: packaging>=20 in /usr/local/lib/python3.11/site-packages (from nibabel==5.4.2) (26.3)
|
| 556 |
+
Collecting wadler-lindig>=0.1.3 (from jaxtyping==0.3.10)
|
| 557 |
+
Downloading wadler_lindig-0.1.7-py3-none-any.whl.metadata (17 kB)
|
| 558 |
+
Collecting torchvision (from timm==1.0.27)
|
| 559 |
+
Downloading torchvision-0.28.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.6 kB)
|
| 560 |
+
Collecting pyyaml (from timm==1.0.27)
|
| 561 |
+
Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.4 kB)
|
| 562 |
+
Collecting safetensors (from timm==1.0.27)
|
| 563 |
+
Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.2 kB)
|
| 564 |
+
Collecting hf-xet<2.0.0,>=1.1.3 (from huggingface-hub==0.36.2)
|
| 565 |
+
Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (4.9 kB)
|
| 566 |
+
Collecting requests (from huggingface-hub==0.36.2)
|
| 567 |
+
Downloading requests-2.34.2-py3-none-any.whl.metadata (4.8 kB)
|
| 568 |
+
Collecting tqdm>=4.42.1 (from huggingface-hub==0.36.2)
|
| 569 |
+
Downloading tqdm-4.70.0-py3-none-any.whl.metadata (57 kB)
|
| 570 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 57.3/57.3 kB 362.2 MB/s eta 0:00:00
|
| 571 |
+
Collecting scipy>=1.10.0 (from scikit-learn==1.8.0)
|
| 572 |
+
Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (62 kB)
|
| 573 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.1/62.1 kB 394.1 MB/s eta 0:00:00
|
| 574 |
+
Collecting threadpoolctl>=3.2.0 (from scikit-learn==1.8.0)
|
| 575 |
+
Downloading threadpoolctl-3.6.0-py3-none-any.whl.metadata (13 kB)
|
| 576 |
+
Collecting antlr4-python3-runtime==4.9.* (from omegaconf==2.3.0)
|
| 577 |
+
Downloading antlr4-python3-runtime-4.9.3.tar.gz (117 kB)
|
| 578 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 117.0/117.0 kB 424.4 MB/s eta 0:00:00
|
| 579 |
+
Preparing metadata (setup.py): started
|
| 580 |
+
Preparing metadata (setup.py): finished with status 'done'
|
| 581 |
+
Requirement already satisfied: setuptools>=40.8.0 in /usr/local/lib/python3.11/site-packages (from triton==3.4.0->torch==2.8.0) (79.0.1)
|
| 582 |
+
Collecting mpmath<1.4,>=1.1.0 (from sympy>=1.13.3->torch==2.8.0)
|
| 583 |
+
Downloading mpmath-1.3.0-py3-none-any.whl.metadata (8.6 kB)
|
| 584 |
+
Collecting MarkupSafe>=2.0 (from jinja2->torch==2.8.0)
|
| 585 |
+
Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.7 kB)
|
| 586 |
+
Collecting charset_normalizer<4,>=2 (from requests->huggingface-hub==0.36.2)
|
| 587 |
+
Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (41 kB)
|
| 588 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.7/41.7 kB 330.0 MB/s eta 0:00:00
|
| 589 |
+
Collecting idna<4,>=2.5 (from requests->huggingface-hub==0.36.2)
|
| 590 |
+
Downloading idna-3.18-py3-none-any.whl.metadata (6.1 kB)
|
| 591 |
+
Collecting urllib3<3,>=1.26 (from requests->huggingface-hub==0.36.2)
|
| 592 |
+
Downloading urllib3-2.7.0-py3-none-any.whl.metadata (6.9 kB)
|
| 593 |
+
Collecting certifi>=2023.5.7 (from requests->huggingface-hub==0.36.2)
|
| 594 |
+
Downloading certifi-2026.7.22-py3-none-any.whl.metadata (2.5 kB)
|
| 595 |
+
INFO: pip is looking at multiple versions of torchvision to determine which version is compatible with other requirements. This could take a while.
|
| 596 |
+
Collecting torchvision (from timm==1.0.27)
|
| 597 |
+
Downloading torchvision-0.27.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
|
| 598 |
+
Downloading torchvision-0.27.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
|
| 599 |
+
Downloading torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.5 kB)
|
| 600 |
+
Downloading torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.4 kB)
|
| 601 |
+
Downloading torchvision-0.24.1-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
|
| 602 |
+
Downloading torchvision-0.24.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (5.9 kB)
|
| 603 |
+
Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (6.1 kB)
|
| 604 |
+
Collecting pillow!=8.3.*,>=5.3.0 (from torchvision->timm==1.0.27)
|
| 605 |
+
Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (9.1 kB)
|
| 606 |
+
Downloading torch-2.8.0-cp311-cp311-manylinux_2_28_x86_64.whl (888.1 MB)
|
| 607 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 888.1/888.1 MB 183.5 MB/s eta 0:00:00
|
| 608 |
+
Downloading numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (16.9 MB)
|
| 609 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.9/16.9 MB 258.7 MB/s eta 0:00:00
|
| 610 |
+
Downloading nibabel-5.4.2-py3-none-any.whl (3.3 MB)
|
| 611 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.3/3.3 MB 227.2 MB/s eta 0:00:00
|
| 612 |
+
Downloading einops-0.8.2-py3-none-any.whl (65 kB)
|
| 613 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.6/65.6 kB 281.2 MB/s eta 0:00:00
|
| 614 |
+
Downloading jaxtyping-0.3.10-py3-none-any.whl (56 kB)
|
| 615 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 56.4/56.4 kB 341.7 MB/s eta 0:00:00
|
| 616 |
+
Downloading timm-1.0.27-py3-none-any.whl (2.6 MB)
|
| 617 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.6/2.6 MB 220.8 MB/s eta 0:00:00
|
| 618 |
+
Downloading huggingface_hub-0.36.2-py3-none-any.whl (566 kB)
|
| 619 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 566.4/566.4 kB 451.6 MB/s eta 0:00:00
|
| 620 |
+
Downloading scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (9.1 MB)
|
| 621 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.1/9.1 MB 264.0 MB/s eta 0:00:00
|
| 622 |
+
Downloading joblib-1.5.3-py3-none-any.whl (309 kB)
|
| 623 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 309.1/309.1 kB 480.7 MB/s eta 0:00:00
|
| 624 |
+
Downloading omegaconf-2.3.0-py3-none-any.whl (79 kB)
|
| 625 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 79.5/79.5 kB 377.4 MB/s eta 0:00:00
|
| 626 |
+
Downloading nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl (594.3 MB)
|
| 627 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 594.3/594.3 MB 261.8 MB/s eta 0:00:00
|
| 628 |
+
Downloading nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (10.2 MB)
|
| 629 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 10.2/10.2 MB 212.3 MB/s eta 0:00:00
|
| 630 |
+
Downloading nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (88.0 MB)
|
| 631 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 88.0/88.0 MB 224.5 MB/s eta 0:00:00
|
| 632 |
+
Downloading nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (954 kB)
|
| 633 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 954.8/954.8 kB 309.2 MB/s eta 0:00:00
|
| 634 |
+
Downloading nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl (706.8 MB)
|
| 635 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 706.8/706.8 MB 361.1 MB/s eta 0:00:00
|
| 636 |
+
Downloading nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (193.1 MB)
|
| 637 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 193.1/193.1 MB 210.5 MB/s eta 0:00:00
|
| 638 |
+
Downloading nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (1.2 MB)
|
| 639 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 406.4 MB/s eta 0:00:00
|
| 640 |
+
Downloading nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl (63.6 MB)
|
| 641 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 63.6/63.6 MB 339.4 MB/s eta 0:00:00
|
| 642 |
+
Downloading nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl (267.5 MB)
|
| 643 |
+
━━━━━━━━━━━━━━━━��━━━━━━━━━━━━━━━━━━━━━━ 267.5/267.5 MB 292.8 MB/s eta 0:00:00
|
| 644 |
+
Downloading nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (288.2 MB)
|
| 645 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 288.2/288.2 MB 291.6 MB/s eta 0:00:00
|
| 646 |
+
Downloading nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl (287.2 MB)
|
| 647 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 287.2/287.2 MB 301.2 MB/s eta 0:00:00
|
| 648 |
+
Downloading nvidia_nccl_cu12-2.27.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (322.4 MB)
|
| 649 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 322.4/322.4 MB 221.2 MB/s eta 0:00:00
|
| 650 |
+
Downloading nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl (39.3 MB)
|
| 651 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 39.3/39.3 MB 343.4 MB/s eta 0:00:00
|
| 652 |
+
Downloading nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (89 kB)
|
| 653 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 90.0/90.0 kB 389.6 MB/s eta 0:00:00
|
| 654 |
+
Downloading triton-3.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (155.5 MB)
|
| 655 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 155.5/155.5 MB 206.4 MB/s eta 0:00:00
|
| 656 |
+
Downloading fsspec-2026.7.0-py3-none-any.whl (206 kB)
|
| 657 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 206.6/206.6 kB 369.1 MB/s eta 0:00:00
|
| 658 |
+
Downloading hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (4.5 MB)
|
| 659 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 376.2 MB/s eta 0:00:00
|
| 660 |
+
Downloading importlib_resources-7.1.0-py3-none-any.whl (37 kB)
|
| 661 |
+
Downloading pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (806 kB)
|
| 662 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 806.6/806.6 kB 457.9 MB/s eta 0:00:00
|
| 663 |
+
Downloading scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (35.3 MB)
|
| 664 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 35.3/35.3 MB 210.6 MB/s eta 0:00:00
|
| 665 |
+
Downloading sympy-1.14.0-py3-none-any.whl (6.3 MB)
|
| 666 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 365.8 MB/s eta 0:00:00
|
| 667 |
+
Downloading threadpoolctl-3.6.0-py3-none-any.whl (18 kB)
|
| 668 |
+
Downloading tqdm-4.70.0-py3-none-any.whl (80 kB)
|
| 669 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 80.2/80.2 kB 406.0 MB/s eta 0:00:00
|
| 670 |
+
Downloading typing_extensions-4.16.0-py3-none-any.whl (45 kB)
|
| 671 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.6/45.6 kB 218.5 MB/s eta 0:00:00
|
| 672 |
+
Downloading wadler_lindig-0.1.7-py3-none-any.whl (20 kB)
|
| 673 |
+
Downloading filelock-3.32.2-py3-none-any.whl (98 kB)
|
| 674 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 98.8/98.8 kB 321.0 MB/s eta 0:00:00
|
| 675 |
+
Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
|
| 676 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 134.9/134.9 kB 418.1 MB/s eta 0:00:00
|
| 677 |
+
Downloading networkx-3.6.1-py3-none-any.whl (2.1 MB)
|
| 678 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 377.2 MB/s eta 0:00:00
|
| 679 |
+
Downloading requests-2.34.2-py3-none-any.whl (73 kB)
|
| 680 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 73.1/73.1 kB 298.4 MB/s eta 0:00:00
|
| 681 |
+
Downloading safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (516 kB)
|
| 682 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 516.0/516.0 kB 453.2 MB/s eta 0:00:00
|
| 683 |
+
Downloading torchvision-0.23.0-cp311-cp311-manylinux_2_28_x86_64.whl (8.6 MB)
|
| 684 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.6/8.6 MB 168.6 MB/s eta 0:00:00
|
| 685 |
+
Downloading certifi-2026.7.22-py3-none-any.whl (136 kB)
|
| 686 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 137.0/137.0 kB 368.6 MB/s eta 0:00:00
|
| 687 |
+
Downloading charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (221 kB)
|
| 688 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 221.3/221.3 kB 471.0 MB/s eta 0:00:00
|
| 689 |
+
Downloading idna-3.18-py3-none-any.whl (65 kB)
|
| 690 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 65.5/65.5 kB 392.2 MB/s eta 0:00:00
|
| 691 |
+
Downloading markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (22 kB)
|
| 692 |
+
Downloading mpmath-1.3.0-py3-none-any.whl (536 kB)
|
| 693 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 536.2/536.2 kB 478.8 MB/s eta 0:00:00
|
| 694 |
+
Downloading pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (6.9 MB)
|
| 695 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 369.2 MB/s eta 0:00:00
|
| 696 |
+
Downloading urllib3-2.7.0-py3-none-any.whl (131 kB)
|
| 697 |
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 131.1/131.1 kB 438.3 MB/s eta 0:00:00
|
| 698 |
+
Building wheels for collected packages: antlr4-python3-runtime
|
| 699 |
+
Building wheel for antlr4-python3-runtime (setup.py): started
|
| 700 |
+
Building wheel for antlr4-python3-runtime (setup.py): finished with status 'done'
|
| 701 |
+
Created wheel for antlr4-python3-runtime: filename=antlr4_python3_runtime-4.9.3-py3-none-any.whl size=144590 sha256=5aa799b6efbe9e55190ab92e61a9ecd9b458a6b893b508bf688fa3754a709a14
|
| 702 |
+
Stored in directory: /tmp/pip-ephem-wheel-cache-pem62r8t/wheels/1a/97/32/461f837398029ad76911109f07047fde1d7b661a147c7c56d1
|
| 703 |
+
Successfully built antlr4-python3-runtime
|
| 704 |
+
Installing collected packages: nvidia-cusparselt-cu12, mpmath, antlr4-python3-runtime, wadler-lindig, urllib3, typing-extensions, triton, tqdm, threadpoolctl, sympy, safetensors, pyyaml, pillow, nvidia-nvtx-cu12, nvidia-nvjitlink-cu12, nvidia-nccl-cu12, nvidia-curand-cu12, nvidia-cufile-cu12, nvidia-cuda-runtime-cu12, nvidia-cuda-nvrtc-cu12, nvidia-cuda-cupti-cu12, nvidia-cublas-cu12, numpy, networkx, MarkupSafe, joblib, importlib-resources, idna, hf-xet, fsspec, filelock, einops, charset_normalizer, certifi, scipy, requests, omegaconf, nvidia-cusparse-cu12, nvidia-cufft-cu12, nvidia-cudnn-cu12, nibabel, jinja2, jaxtyping, scikit-learn, nvidia-cusolver-cu12, huggingface-hub, torch, torchvision, timm
|
| 705 |
+
Successfully installed MarkupSafe-3.0.3 antlr4-python3-runtime-4.9.3 certifi-2026.7.22 charset_normalizer-3.4.9 einops-0.8.2 filelock-3.32.2 fsspec-2026.7.0 hf-xet-1.6.0 huggingface-hub-0.36.2 idna-3.18 importlib-resources-7.1.0 jaxtyping-0.3.10 jinja2-3.1.6 joblib-1.5.3 mpmath-1.3.0 networkx-3.6.1 nibabel-5.4.2 numpy-2.4.6 nvidia-cublas-cu12-12.8.4.1 nvidia-cuda-cupti-cu12-12.8.90 nvidia-cuda-nvrtc-cu12-12.8.93 nvidia-cuda-runtime-cu12-12.8.90 nvidia-cudnn-cu12-9.10.2.21 nvidia-cufft-cu12-11.3.3.83 nvidia-cufile-cu12-1.13.1.3 nvidia-curand-cu12-10.3.9.90 nvidia-cusolver-cu12-11.7.3.90 nvidia-cusparse-cu12-12.5.8.93 nvidia-cusparselt-cu12-0.7.1 nvidia-nccl-cu12-2.27.3 nvidia-nvjitlink-cu12-12.8.93 nvidia-nvtx-cu12-12.8.90 omegaconf-2.3.0 pillow-12.3.0 pyyaml-6.0.3 requests-2.34.2 safetensors-0.8.0 scikit-learn-1.8.0 scipy-1.17.1 sympy-1.14.0 threadpoolctl-3.6.0 timm-1.0.27 torch-2.8.0 torchvision-0.23.0 tqdm-4.70.0 triton-3.4.0 typing-extensions-4.16.0 urllib3-2.7.0 wadler-lindig-0.1.7
|
| 706 |
+
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
|
| 707 |
+
|
| 708 |
+
[notice] A new release of pip is available: 24.0 -> 26.2.1
|
| 709 |
+
[notice] To update, run: pip install --upgrade pip
|
| 710 |
+
INFO: Adding runscript
|
| 711 |
+
INFO: /etc/singularity/ exists; cleanup by system administrator is not complete (see https://apptainer.org/docs/admin/latest/singularity_migration.html)
|
| 712 |
+
INFO: Creating SIF file...
|
| 713 |
+
INFO: Build complete: /data/connor/nanobrain.1/experiments/fomo_tune_baseline/output/task3/task3.sif
|
| 714 |
+
built experiments/fomo_tune_baseline/output/task3/task3.sif
|
| 715 |
+
=== sifs ===
|
| 716 |
+
-rwxr-xr-x 1 connor hpcroot 5.0G Aug 11 16:48 experiments/fomo_tune_baseline/output/task1/task1.sif
|
| 717 |
+
-rwxr-xr-x 1 connor hpcroot 5.0G Aug 11 16:58 experiments/fomo_tune_baseline/output/task3/task3.sif
|
| 718 |
+
-rwxr-xr-x 1 connor hpcroot 5.0G Aug 11 16:53 experiments/fomo_tune_baseline/output/task5/task5.sif
|
finetune/fomo_tune_baseline/slurms/slurm-378508.out
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
=== task1 ===
|
| 2 |
+
16:31:38 run task1 (git 1df2e5d)
|
| 3 |
+
16:31:38 config:
|
| 4 |
+
task: task1
|
| 5 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 6 |
+
modalities:
|
| 7 |
+
- dwi_b1000
|
| 8 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 9 |
+
name: task1
|
| 10 |
+
device: cuda
|
| 11 |
+
seed: 4466
|
| 12 |
+
16:31:40 dataset: 21 subjects, 13 positive
|
| 13 |
+
16:31:48 fold 1/21 sub-01 y=1 p=0.512 (4s)
|
| 14 |
+
16:31:49 fold 2/21 sub-02 y=1 p=0.514 (4s)
|
| 15 |
+
16:31:49 fold 3/21 sub-03 y=0 p=0.486 (5s)
|
| 16 |
+
16:31:49 fold 4/21 sub-04 y=1 p=0.499 (5s)
|
| 17 |
+
16:31:50 fold 5/21 sub-05 y=1 p=0.507 (5s)
|
| 18 |
+
16:31:50 fold 6/21 sub-06 y=1 p=0.493 (6s)
|
| 19 |
+
16:31:50 fold 7/21 sub-07 y=0 p=0.489 (6s)
|
| 20 |
+
16:31:51 fold 8/21 sub-08 y=0 p=0.490 (6s)
|
| 21 |
+
16:31:51 fold 9/21 sub-09 y=1 p=0.523 (7s)
|
| 22 |
+
16:31:52 fold 10/21 sub-10 y=1 p=0.525 (7s)
|
| 23 |
+
16:31:52 fold 11/21 sub-11 y=0 p=0.479 (7s)
|
| 24 |
+
16:31:52 fold 12/21 sub-12 y=0 p=0.474 (8s)
|
| 25 |
+
16:31:53 fold 13/21 sub-13 y=1 p=0.516 (8s)
|
| 26 |
+
16:31:53 fold 14/21 sub-14 y=1 p=0.536 (9s)
|
| 27 |
+
16:31:53 fold 15/21 sub-15 y=0 p=0.486 (9s)
|
| 28 |
+
16:31:54 fold 16/21 sub-16 y=0 p=0.482 (9s)
|
| 29 |
+
16:31:54 fold 17/21 sub-17 y=1 p=0.520 (10s)
|
| 30 |
+
16:31:54 fold 18/21 sub-18 y=1 p=0.518 (10s)
|
| 31 |
+
16:31:55 fold 19/21 sub-19 y=1 p=0.520 (10s)
|
| 32 |
+
16:31:55 fold 20/21 sub-20 y=1 p=0.519 (11s)
|
| 33 |
+
16:31:56 fold 21/21 sub-21 y=0 p=0.498 (11s)
|
| 34 |
+
16:31:57 result: auroc=0.9904 auroc_ci_low=0.9444 auroc_ci_high=1.0000 (11s)
|
| 35 |
+
=== task5 ===
|
| 36 |
+
16:32:19 run task5 (git 1df2e5d)
|
| 37 |
+
16:32:19 config:
|
| 38 |
+
task: task5
|
| 39 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 40 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 41 |
+
name: task5
|
| 42 |
+
device: cuda
|
| 43 |
+
seed: 4466
|
| 44 |
+
16:32:42 dataset: 48 subjects, 24 positive
|
| 45 |
+
16:33:21 fold 1/20 n=3 y=[0 1 1] p=[0.013 0.998 0.974] (35s)
|
| 46 |
+
16:33:24 fold 2/20 n=3 y=[1 1 1] p=[0.796 0.719 0.999] (37s)
|
| 47 |
+
16:33:26 fold 3/20 n=3 y=[0 1 1] p=[0.155 0.998 0.905] (39s)
|
| 48 |
+
16:33:28 fold 4/20 n=3 y=[0 0 1] p=[0.024 0.032 0.811] (42s)
|
| 49 |
+
16:33:29 fold 5/20 n=3 y=[1 1 1] p=[0.988 0.652 0.552] (43s)
|
| 50 |
+
16:33:31 fold 6/20 n=3 y=[0 0 1] p=[0.46 0.744 0.994] (44s)
|
| 51 |
+
16:33:33 fold 7/20 n=3 y=[0 0 1] p=[0.134 0.148 0.996] (47s)
|
| 52 |
+
16:33:36 fold 8/20 n=3 y=[0 0 1] p=[0.037 0.012 0.915] (50s)
|
| 53 |
+
16:33:37 fold 9/20 n=2 y=[0 1] p=[0.137 0.997] (51s)
|
| 54 |
+
16:33:38 fold 10/20 n=2 y=[0 1] p=[0.223 0.993] (51s)
|
| 55 |
+
16:33:39 fold 11/20 n=2 y=[0 0] p=[0.118 0.32 ] (53s)
|
| 56 |
+
16:33:42 fold 12/20 n=2 y=[0 1] p=[0.122 0.849] (55s)
|
| 57 |
+
16:33:43 fold 13/20 n=2 y=[0 1] p=[0.207 0.282] (57s)
|
| 58 |
+
16:33:45 fold 14/20 n=2 y=[0 1] p=[0.177 0.319] (58s)
|
| 59 |
+
16:33:47 fold 15/20 n=2 y=[0 1] p=[0.112 0.943] (60s)
|
| 60 |
+
16:33:48 fold 16/20 n=2 y=[0 1] p=[0.034 0.998] (61s)
|
| 61 |
+
16:33:50 fold 17/20 n=2 y=[0 0] p=[0.018 0.021] (63s)
|
| 62 |
+
16:33:51 fold 18/20 n=2 y=[0 1] p=[0.02 0.937] (64s)
|
| 63 |
+
16:33:52 fold 19/20 n=2 y=[0 1] p=[0.004 0.96 ] (65s)
|
| 64 |
+
16:33:54 fold 20/20 n=2 y=[0 1] p=[0.075 0.929] (68s)
|
| 65 |
+
16:33:56 result: auroc=0.9844 auroc_ci_low=0.9528 auroc_ci_high=1.0000 (68s)
|
| 66 |
+
=== task3 ===
|
| 67 |
+
16:34:12 run task3 (git 1df2e5d)
|
| 68 |
+
16:34:12 config:
|
| 69 |
+
task: task3
|
| 70 |
+
ckpt_path: /data/mihir-stuff/smri-pretrained/pretrain_full_90_10_h100/checkpoint-last.pth
|
| 71 |
+
output_root: experiments/fomo_tune_baseline/output
|
| 72 |
+
name: task3
|
| 73 |
+
device: cuda
|
| 74 |
+
seed: 4466
|
| 75 |
+
16:35:16 dataset: 494 subjects, age 19-80 mean 45.2
|
| 76 |
+
16:38:39 fold 1/20 n=25 mae=3.91 (190s)
|
| 77 |
+
16:38:51 fold 2/20 n=25 mae=4.50 (202s)
|
| 78 |
+
16:38:57 fold 3/20 n=25 mae=3.74 (208s)
|
| 79 |
+
16:39:03 fold 4/20 n=25 mae=3.58 (214s)
|
| 80 |
+
16:39:09 fold 5/20 n=25 mae=3.57 (220s)
|
| 81 |
+
16:39:14 fold 6/20 n=25 mae=3.12 (226s)
|
| 82 |
+
16:39:20 fold 7/20 n=25 mae=3.42 (232s)
|
| 83 |
+
16:39:26 fold 8/20 n=25 mae=3.12 (238s)
|
| 84 |
+
16:39:32 fold 9/20 n=25 mae=3.74 (244s)
|
| 85 |
+
16:39:38 fold 10/20 n=25 mae=4.20 (249s)
|
| 86 |
+
16:39:44 fold 11/20 n=25 mae=3.22 (255s)
|
| 87 |
+
16:39:50 fold 12/20 n=25 mae=3.50 (261s)
|
| 88 |
+
16:39:55 fold 13/20 n=25 mae=3.39 (267s)
|
| 89 |
+
16:40:01 fold 14/20 n=25 mae=4.17 (273s)
|
| 90 |
+
16:40:07 fold 15/20 n=24 mae=4.36 (278s)
|
| 91 |
+
16:40:12 fold 16/20 n=24 mae=4.15 (284s)
|
| 92 |
+
16:40:18 fold 17/20 n=24 mae=3.66 (289s)
|
| 93 |
+
16:40:23 fold 18/20 n=24 mae=3.54 (295s)
|
| 94 |
+
16:40:29 fold 19/20 n=24 mae=3.42 (300s)
|
| 95 |
+
16:40:34 fold 20/20 n=24 mae=3.53 (306s)
|
| 96 |
+
16:40:35 result: pearson_r=0.9631 pearson_r_ci_low=0.9567 pearson_r_ci_high=0.9687 mae=3.6910 mae_ci_low=3.4464 mae_ci_high=3.9466 (306s)
|
| 97 |
+
=== results ===
|
| 98 |
+
{"name": "task1", "auroc": 0.9903846153846154, "auroc_ci_low": 0.9444444444444445, "auroc_ci_high": 1.0, "run_time": 11.2}
|
| 99 |
+
{"name": "task3", "pearson_r": 0.9630771532969529, "pearson_r_ci_low": 0.9567144985968425, "pearson_r_ci_high": 0.9687044710309238, "mae": 3.6910024558001684, "mae_ci_low": 3.446356968454986, "mae_ci_high": 3.946628273643462, "run_time": 306.1}
|
| 100 |
+
{"name": "task5", "auroc": 0.984375, "auroc_ci_low": 0.9527867965367964, "auroc_ci_high": 1.0, "run_time": 67.5}
|