From e16923453b15a502613c37d94593852bf40cf068 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Mon, 3 Nov 2025 12:46:38 +1000 Subject: [PATCH 01/31] scaffolding project files --- recognition/adni_convnext_47280647/README.md | 0 recognition/adni_convnext_47280647/dataset.py | 0 recognition/adni_convnext_47280647/modules.py | 0 recognition/adni_convnext_47280647/predict.py | 0 recognition/adni_convnext_47280647/train.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 recognition/adni_convnext_47280647/README.md create mode 100644 recognition/adni_convnext_47280647/dataset.py create mode 100644 recognition/adni_convnext_47280647/modules.py create mode 100644 recognition/adni_convnext_47280647/predict.py create mode 100644 recognition/adni_convnext_47280647/train.py diff --git a/recognition/adni_convnext_47280647/README.md b/recognition/adni_convnext_47280647/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/recognition/adni_convnext_47280647/dataset.py b/recognition/adni_convnext_47280647/dataset.py new file mode 100644 index 000000000..e69de29bb diff --git a/recognition/adni_convnext_47280647/modules.py b/recognition/adni_convnext_47280647/modules.py new file mode 100644 index 000000000..e69de29bb diff --git a/recognition/adni_convnext_47280647/predict.py b/recognition/adni_convnext_47280647/predict.py new file mode 100644 index 000000000..e69de29bb diff --git a/recognition/adni_convnext_47280647/train.py b/recognition/adni_convnext_47280647/train.py new file mode 100644 index 000000000..e69de29bb From 637744ffac6ae0ac6fafa149d1c23af84d230841 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 11:09:59 +1000 Subject: [PATCH 02/31] start with TinyCNN and param counter --- recognition/adni_convnext_47280647/modules.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/recognition/adni_convnext_47280647/modules.py b/recognition/adni_convnext_47280647/modules.py index e69de29bb..9e88b1d37 100644 --- a/recognition/adni_convnext_47280647/modules.py +++ b/recognition/adni_convnext_47280647/modules.py @@ -0,0 +1,30 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +class TinyCNN(nn.Module): + """Minimal 2D CNN for binary classification on 1×224×224 inputs.""" + def __init__(self, in_chans: int = 1, num_classes: int = 2, head_dropout: float = 0.0): + super().__init__() + self.features = nn.Sequential( + nn.Conv2d(in_chans, 16, 3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 112×112 + nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 56×56 + nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(inplace=True), + nn.AdaptiveAvgPool2d((1, 1)) # 64×1×1 + ) + self.dropout = nn.Dropout(p=head_dropout) if head_dropout > 0 else nn.Identity() + self.head = nn.Linear(64, num_classes) + + def forward(self, x): + x = self.features(x) # (B,64,1,1) + x = x.flatten(1) # (B,64) + x = self.dropout(x) + logits = self.head(x) # (B,2) + probs = F.softmax(logits, dim=1) + return {"logits": logits, "probs": probs} + +def build_model(in_chans: int = 1, num_classes: int = 2, head_dropout: float = 0.0) -> nn.Module: + return TinyCNN(in_chans=in_chans, num_classes=num_classes, head_dropout=head_dropout) + +def count_params(model: nn.Module) -> int: + return sum(p.numel() for p in model.parameters() if p.requires_grad) From 7c009c6f021040dd5ded000fc47ca10dc11fd47c Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 11:12:27 +1000 Subject: [PATCH 03/31] add RandSliceDataset and loaders for a smoke test --- recognition/adni_convnext_47280647/dataset.py | 26 +++++++++++++++++++ recognition/adni_convnext_47280647/modules.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/recognition/adni_convnext_47280647/dataset.py b/recognition/adni_convnext_47280647/dataset.py index e69de29bb..630e75681 100644 --- a/recognition/adni_convnext_47280647/dataset.py +++ b/recognition/adni_convnext_47280647/dataset.py @@ -0,0 +1,26 @@ +from typing import Tuple +import torch +from torch.utils.data import Dataset, DataLoader, random_split + +class RandomSliceDataset(Dataset): + """Deterministic random 'images' for smoke testing the pipeline.""" + def __init__(self, n: int = 192, image_size: Tuple[int, int, int] = (1, 224, 224), num_classes: int = 2, seed: int = 42): + super().__init__() + g = torch.Generator().manual_seed(seed) + self.x = torch.rand((n, *image_size), generator=g) + self.y = torch.randint(low=0, high=num_classes, size=(n,), generator=g) + + def __len__(self): + return self.x.shape[0] + + def __getitem__(self, i): + return self.x[i], int(self.y[i]) + +def build_loaders(batch_size: int = 16, seed: int = 42): + full = RandomSliceDataset(n=192, seed=seed) + val_size = 64 + train_size = len(full) - val_size + train_set, val_set = random_split(full, [train_size, val_size], generator=torch.Generator().manual_seed(seed)) + train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=0) + val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False, num_workers=0) + return train_loader, val_loader diff --git a/recognition/adni_convnext_47280647/modules.py b/recognition/adni_convnext_47280647/modules.py index 9e88b1d37..14c8d64bb 100644 --- a/recognition/adni_convnext_47280647/modules.py +++ b/recognition/adni_convnext_47280647/modules.py @@ -3,7 +3,7 @@ import torch.nn.functional as F class TinyCNN(nn.Module): - """Minimal 2D CNN for binary classification on 1×224×224 inputs.""" + """Minimal 2D CNN for binary classification on 1x224x224 inputs.""" def __init__(self, in_chans: int = 1, num_classes: int = 2, head_dropout: float = 0.0): super().__init__() self.features = nn.Sequential( From 8e792f7746b00dfbc08309911c07f36c1c19cfc9 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 11:13:11 +1000 Subject: [PATCH 04/31] added minimal CE+Adam loop with accuracy metric for the smoke test --- recognition/adni_convnext_47280647/train.py | 102 ++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/recognition/adni_convnext_47280647/train.py b/recognition/adni_convnext_47280647/train.py index e69de29bb..ac30352bc 100644 --- a/recognition/adni_convnext_47280647/train.py +++ b/recognition/adni_convnext_47280647/train.py @@ -0,0 +1,102 @@ +# train.py +import argparse, os, json, random +import numpy as np +import torch +import torch.nn as nn +from torch.utils.data import DataLoader +import matplotlib.pyplot as plt +from modules import build_model, count_params +from dataset import build_loaders + +def set_seed(seed: int = 42): + random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed); torch.backends.cudnn.deterministic = True; torch.backends.cudnn.benchmark = False + +def accuracy_from_logits(logits: torch.Tensor, targets: torch.Tensor) -> float: + preds = logits.argmax(dim=1) + return (preds == targets).float().mean().item() + +def plot_curves(history, save_dir): + os.makedirs(save_dir, exist_ok=True) + # Loss + plt.figure() + plt.plot(history["train_loss"], label="train") + plt.plot(history["val_loss"], label="val") + plt.xlabel("epoch"); plt.ylabel("loss"); plt.legend(); plt.tight_layout() + plt.savefig(os.path.join(save_dir, "loss_curve.png")); plt.close() + # Acc + plt.figure() + plt.plot(history["train_acc"], label="train") + plt.plot(history["val_acc"], label="val") + plt.xlabel("epoch"); plt.ylabel("accuracy"); plt.legend(); plt.tight_layout() + plt.savefig(os.path.join(save_dir, "acc_curve.png")); plt.close() + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--epochs", type=int, default=3) + ap.add_argument("--batch_size", type=int, default=16) + ap.add_argument("--lr", type=float, default=1e-3) + ap.add_argument("--seed", type=int, default=42) + ap.add_argument("--head_dropout", type=float, default=0.0) + ap.add_argument("--save_dir", type=str, default="results/min1") + args = ap.parse_args() + + set_seed(args.seed) + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + os.makedirs(args.save_dir, exist_ok=True) + with open(os.path.join(args.save_dir, "config.json"), "w") as f: + json.dump(vars(args), f, indent=2) + + train_loader, val_loader = build_loaders(batch_size=args.batch_size, seed=args.seed) + + model = build_model(in_chans=1, num_classes=2, head_dropout=args.head_dropout).to(device) + print(f"Model params: {count_params(model):,}") + optim = torch.optim.Adam(model.parameters(), lr=args.lr) + criterion = nn.CrossEntropyLoss() + + best_val_acc, best_state = -1.0, None + history = {"train_loss": [], "val_loss": [], "train_acc": [], "val_acc": []} + + for epoch in range(1, args.epochs + 1): + # train + model.train() + tr_loss, tr_acc, n_tr = 0.0, 0.0, 0 + for x, y in train_loader: + x, y = x.to(device), y.to(device) + out = model(x)["logits"] + loss = criterion(out, y) + optim.zero_grad(); loss.backward(); optim.step() + bs = y.size(0) + tr_loss += loss.item() * bs + tr_acc += accuracy_from_logits(out, y) * bs + n_tr += bs + + # val + model.eval() + va_loss, va_acc, n_va = 0.0, 0.0, 0 + with torch.no_grad(): + for x, y in val_loader: + x, y = x.to(device), y.to(device) + out = model(x)["logits"] + loss = criterion(out, y) + bs = y.size(0) + va_loss += loss.item() * bs + va_acc += accuracy_from_logits(out, y) * bs + n_va += bs + + tr_loss /= n_tr; tr_acc /= n_tr + va_loss /= n_va; va_acc /= n_va + history["train_loss"].append(tr_loss); history["val_loss"].append(va_loss) + history["train_acc"].append(tr_acc); history["val_acc"].append(va_acc) + print(f"Epoch {epoch:02d} | train_loss={tr_loss:.4f} val_loss={va_loss:.4f} | train_acc={tr_acc:.3f} val_acc={va_acc:.3f}") + + if va_acc > best_val_acc: + best_val_acc = va_acc + best_state = {"state_dict": model.state_dict(), "epoch": epoch} + torch.save(best_state, os.path.join(args.save_dir, "best.pt")) + + plot_curves(history, args.save_dir) + print(f"Best val_acc: {best_val_acc:.3f} (checkpoint saved)") + +if __name__ == "__main__": + main() From c196d18b1179ee8f6420085222d123a309997eca Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 11:13:47 +1000 Subject: [PATCH 05/31] cleaning up code --- recognition/adni_convnext_47280647/train.py | 1 - 1 file changed, 1 deletion(-) diff --git a/recognition/adni_convnext_47280647/train.py b/recognition/adni_convnext_47280647/train.py index ac30352bc..c23a8371f 100644 --- a/recognition/adni_convnext_47280647/train.py +++ b/recognition/adni_convnext_47280647/train.py @@ -1,4 +1,3 @@ -# train.py import argparse, os, json, random import numpy as np import torch From 66f05ee76734bd58719a52b72fb27f3ea7d0bc1a Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 11:15:15 +1000 Subject: [PATCH 06/31] predict.py - load checkpoints and report validation accuracy --- recognition/adni_convnext_47280647/predict.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/recognition/adni_convnext_47280647/predict.py b/recognition/adni_convnext_47280647/predict.py index e69de29bb..0f8d2639c 100644 --- a/recognition/adni_convnext_47280647/predict.py +++ b/recognition/adni_convnext_47280647/predict.py @@ -0,0 +1,31 @@ +import argparse, os, torch +from modules import build_model +from dataset import build_loaders + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--checkpoint", type=str, default="results/min1/best.pt") + ap.add_argument("--batch_size", type=int, default=32) + args = ap.parse_args() + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + _, val_loader = build_loaders(batch_size=args.batch_size) + ckpt = torch.load(args.checkpoint, map_location="cpu") + + model = build_model().to(device) + model.load_state_dict(ckpt["state_dict"]) + model.eval() + + correct, total = 0, 0 + with torch.no_grad(): + for x, y in val_loader: + x, y = x.to(device), y.to(device) + logits = model(x)["logits"] + preds = logits.argmax(1) + correct += (preds == y).sum().item() + total += y.size(0) + + print(f"Validation accuracy (using saved checkpoint): {correct/total:.3f}") + +if __name__ == "__main__": + main() From d41e2fc5d237cd6e239fcda2f1951475e3809515 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 11:23:17 +1000 Subject: [PATCH 07/31] added gitignore and ran the first smoke test --- recognition/adni_convnext_47280647/.gitignore | 4 ++++ .../__pycache__/dataset.cpython-313.pyc | Bin 0 -> 2569 bytes .../__pycache__/modules.cpython-313.pyc | Bin 0 -> 3251 bytes 3 files changed, 4 insertions(+) create mode 100644 recognition/adni_convnext_47280647/.gitignore create mode 100644 recognition/adni_convnext_47280647/__pycache__/dataset.cpython-313.pyc create mode 100644 recognition/adni_convnext_47280647/__pycache__/modules.cpython-313.pyc diff --git a/recognition/adni_convnext_47280647/.gitignore b/recognition/adni_convnext_47280647/.gitignore new file mode 100644 index 000000000..05c22a0c2 --- /dev/null +++ b/recognition/adni_convnext_47280647/.gitignore @@ -0,0 +1,4 @@ +results/ +checkpoints/ +*.pt +*.pth diff --git a/recognition/adni_convnext_47280647/__pycache__/dataset.cpython-313.pyc b/recognition/adni_convnext_47280647/__pycache__/dataset.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2c27f4ece562a927085ddfc389769ce367a3b67a GIT binary patch literal 2569 zcmZ`*O>7fK6rR~1d;OD!H2j2=fD1uns}KVU|}VqgtC$n&Ppjkk$TA~;S^<&0w+foQADPmI8iB^dRpea@FC6B z9NqPz$Q-k^yw1Eh)2zH*Qk}ACxVb=WKqJk?au%3Rz|t&XR3Ts;WxT>ML5V4RR#b#+ zC@bP$ibvPLJ`rpZQ&NVcs1gR7B;Tqq+Q%d%0yLsTSu{iTQ5Neba@>1)+_yq8jhx=J zauJoC-*-rNbyhMg!*PvVA}}_w%_wOF-Px9ywpqd{*{Ah{tAl`5NVr8kQ8voDX;}J@ zM=dYv%c_oXUiV^FrKILe&2e|(Bs$43Nc_elMGGL3^zvigfgN+hq(+f z+uvJp_oZj=;R@=7o7|bAaE~7r;GWcyDG*sm-=JQkpj$fAT$?ct&Sb&kP5Z1T6pca= z;sYULt8hQ!iB6>q)gq}Xq{3BIPlQXkq8B;R(%{8PnpM$E6~pQYm?@8*^TNm&mg~`3 zIgBwkry2UmVo2ghI)Hez;6>CRH>&DjCKCRrdMA@byQC+bqA`=KSVmIWwSCu)UAvP9 zkAFD&`ktLTlB1dnHDbMEY38hBIKe5?b9MntHEb)XnAW=f*fnJvS7J6%9?rrGuPutQR&QfkmyX%arxO%E3=1 zSOO2is%q*MWVH8%?)zt$A!jv^DtXY^eQ9QH=F)|^3$=IVKCep~*=9J%tuzHK@Qoh@ z=KqY(_=ZlQoIk+Dc1N}L2vk2RYjsPC1IH*}S>O{7b;UNoxy<=)dDL*wjzrV7n5q_Z z*KqX`xVQ0z?zPXc&Yf3*v|YTc&CPzlrI!C;`r7pT*frz3p?c>)T^jKHg)d@QgRcvV zw}F@WMGWiWpFr3Ua%juqYd`?upmQv(q=O+*RV~^1iivz&RnI`rG*3EJb=qK#i`Zq^ za16Q0+c{Nb1E>>2I%yzJz97E{M;>y5I7(|B$hCRF8E`zKf@=vsWv>0w*jvufuPue@Nq<-=SGIMv@60eUx5UcKbpeo z_s8U@0#BO~9u}@G7Ti z^t9Q0o6g$ow9cIO9@>wgKWr_Mbx3-UpwDu|R}tW+FkLZCPntmh#-}doGR?38 zKGNjj=@0t?z{G<+Gx#qcirWG2;CmHdFVFxZI!;y$Gv5+mhe4tnevSn);JZKRKifarchl`-29-KzXbD*J-HY8m2RZIL{DU{HO)t^y_~B_t2ReB&gQ}@SgN`(4PC* z2rRK69sJG`PbvM4^!!TVe~|qRvj33~r*duGBLdUI4Pn|}%PkR@uIK*2$=CQV#5GC( literal 0 HcmV?d00001 diff --git a/recognition/adni_convnext_47280647/__pycache__/modules.cpython-313.pyc b/recognition/adni_convnext_47280647/__pycache__/modules.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa9e097009ce63ea83a86df2fd8c7cd265edca58 GIT binary patch literal 3251 zcmc&$O>7&-6`on{l1p-_pGbCW)vY&F)0%ZkiZo?ajjTqt8;h8)>eNtzaf z9(w73JM(7VH*em|yzdQnVzCH<@Wina%iMm;>Id;^m|F^Yy zh>ZVr-#yQs4DG?bBkg^Uwm4QmMrYrJfE4}Vn+Pd`sQXTy+shJQg>igtu2`k=c-V{eOIVFJ6Ms!}ZKX88h@ zA)*aM)wbyLEs<8QK$Tz!jRU`;DuCi@qQ~XzT2&)1FU!CmN0wd2G~I}#U9V}Tqbr6R zJY|`SnWD?)wV8q&o>i94Tb2O>{ZmDy>gbEw)MB|c#FsS1sSzMf=(Nunm(LZcA!peQ zwP@r9XLM6jh|5xPB{-M0HKRl*F$m2RgkFammVFY+vQ7CdLb>J6rxvV=ma-T0#Z=AI zQ&Q&pnUTzB>eRW}^T)>0BdPO><7mX3G8JRl)@@%)G}S7@=79VurD*E13Oh2jB}dMV zWqvSnJUf=ESjC#5*~#iMp_l0PwQ@_ceJs>5`!7&^eG6^!vDHwMKkzv}w0U6Y?butf zO`&Hk_v_pn3k{|*_0ILCkp3w1@z{rB_p{Ao)1Ssad+EVT>lZIQOto?I^>|%*r@tv2 z{pjS!XFfc0|8z5(`!w^}#Dj_TOP3%1=)dAX`B)Gi31VaahHzvj%pHuZu&>9q1$6u* zzCX1w`r|jry_|C@{A3XZ$A-Ts4fI^r9;vD zo=L9&Pl?2gGiKn@uoOr#@IbJ0Ua&))2a0+n!MH)gD(jBza#do@+le6g1}x@=N(PM? zlY9$$M5IIbRX1c?C8we+C0O!;H?%~KP<;X_nCIY_4bQ7N2?*@Ew(}$@HpH~u5%4}1q zXu#}B@gCxPOyAN3D zz-y|?aCStWuQ{3x;8=^Zs%99noWLXwV4DWeE8bZVdU9Sx1Hw*0_4TjNU)eKXFkvp$ z7}!S8yl;EWC)!uE=gD8~4|17p1eLdS-^{Lj5hNO99#icOMhfwQc|Qp7LU%&>5~cw& z9)K`m=HTF-wFQoQj!sY~>HN-JN9Sw0QKYA+8RP_LhA4!dIAn$BbM@&@W}4B<>+EIE zPnYw6YG+x%b@+7>I^8Zwc!;vC+kHUL(*7GU(2?5g^W1;NJe__FzMT?O!$|ZfJb<$GszL+5rHO|Hf6#SWexIgl}x&aRjXz?a#bNp#ZFEbmZ})`WU>RGth7a_ zw%~}@d!`@7v-O|;x%W@Kx0hG0uiaR^v9`Rj41@o+X@>aaU>d*md>X^T0lQXlBc5Fi zo&*~%b&}emm(gOb(7iU92E{ii;t*8yI(wQu(~TD&$1}}%=JR-Vqi1>}I=#+L`!P5I zsD$8hjz!c3m-m{aX%aX?B9pLY2>L)j#5-`)_ZmQU^@;YWQ^&z!?7x34cfX{(%m!qr+da130rXzoxJ1Um_5{_#VbT z!}Z~9MBDcRetW;>x9b-#Kfd@{^Wtm&^3>ZwjB~ioydD01m;!RRp;Ao_d!GCU0qv1S literal 0 HcmV?d00001 From 4ddfdd5ece165645a051c8e0db1ab7b130ea7338 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 11:42:02 +1000 Subject: [PATCH 08/31] added requirements.txt to manage dependencies --- recognition/adni_convnext_47280647/requirements.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 recognition/adni_convnext_47280647/requirements.txt diff --git a/recognition/adni_convnext_47280647/requirements.txt b/recognition/adni_convnext_47280647/requirements.txt new file mode 100644 index 000000000..64a3614b0 --- /dev/null +++ b/recognition/adni_convnext_47280647/requirements.txt @@ -0,0 +1,10 @@ +torch>=2.1 +torchvision>=0.16 +timm>=0.9 +numpy>=1.26 +pandas>=2.1 +scikit-learn>=1.3 +matplotlib>=3.8 +scipy>=1.11 +nibabel>=5.1 +tqdm>=4.66 From ca21fb19ae96f107000c6c39873666498a008553 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 12:03:32 +1000 Subject: [PATCH 09/31] moving to integrate ADNI dataset --- recognition/adni_convnext_47280647/modules.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/recognition/adni_convnext_47280647/modules.py b/recognition/adni_convnext_47280647/modules.py index 14c8d64bb..7ef81cb94 100644 --- a/recognition/adni_convnext_47280647/modules.py +++ b/recognition/adni_convnext_47280647/modules.py @@ -7,19 +7,18 @@ class TinyCNN(nn.Module): def __init__(self, in_chans: int = 1, num_classes: int = 2, head_dropout: float = 0.0): super().__init__() self.features = nn.Sequential( - nn.Conv2d(in_chans, 16, 3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 112×112 - nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 56×56 + nn.Conv2d(in_chans, 16, 3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 112x112 + nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), # 56x56 nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(inplace=True), - nn.AdaptiveAvgPool2d((1, 1)) # 64×1×1 + nn.AdaptiveAvgPool2d((1, 1)) # 64x1x1 ) self.dropout = nn.Dropout(p=head_dropout) if head_dropout > 0 else nn.Identity() self.head = nn.Linear(64, num_classes) def forward(self, x): - x = self.features(x) # (B,64,1,1) - x = x.flatten(1) # (B,64) + x = self.features(x).flatten(1) x = self.dropout(x) - logits = self.head(x) # (B,2) + logits = self.head(x) probs = F.softmax(logits, dim=1) return {"logits": logits, "probs": probs} From c633536c1dad49dc39d460f605455ae67b8eb6c8 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 12:05:09 +1000 Subject: [PATCH 10/31] modified dataset.py to incorporate ADNI dataset --- recognition/adni_convnext_47280647/dataset.py | 169 +++++++++++++++++- 1 file changed, 160 insertions(+), 9 deletions(-) diff --git a/recognition/adni_convnext_47280647/dataset.py b/recognition/adni_convnext_47280647/dataset.py index 630e75681..f12a46732 100644 --- a/recognition/adni_convnext_47280647/dataset.py +++ b/recognition/adni_convnext_47280647/dataset.py @@ -1,26 +1,177 @@ -from typing import Tuple +from __future__ import annotations +import os, glob, random +from dataclasses import dataclass +from typing import Tuple, List, Dict + +import numpy as np +import pandas as pd +import nibabel as nib import torch +import torch.nn.functional as F from torch.utils.data import Dataset, DataLoader, random_split +# ============== Random dataset (for local smoke tests) ============== class RandomSliceDataset(Dataset): - """Deterministic random 'images' for smoke testing the pipeline.""" def __init__(self, n: int = 192, image_size: Tuple[int, int, int] = (1, 224, 224), num_classes: int = 2, seed: int = 42): super().__init__() g = torch.Generator().manual_seed(seed) self.x = torch.rand((n, *image_size), generator=g) self.y = torch.randint(low=0, high=num_classes, size=(n,), generator=g) - def __len__(self): - return self.x.shape[0] - - def __getitem__(self, i): - return self.x[i], int(self.y[i]) + def __len__(self): return self.x.shape[0] + def __getitem__(self, i): return self.x[i], int(self.y[i]) -def build_loaders(batch_size: int = 16, seed: int = 42): +def build_loaders_random(batch_size: int = 16, seed: int = 42): full = RandomSliceDataset(n=192, seed=seed) val_size = 64 train_size = len(full) - val_size train_set, val_set = random_split(full, [train_size, val_size], generator=torch.Generator().manual_seed(seed)) train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=0) val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False, num_workers=0) - return train_loader, val_loader + return train_loader, val_loader, None + +# ============== ADNI dataset (Rangpur implementation) ============== +@dataclass +class ADNIArgs: + data_root: str + labels_csv: str + plane: str = "axial" # axial|sagittal|coronal + slice_mode: str = "center_k" # center_k|step_s|all + center_k: int = 32 + step_s: int = 2 + resize_hw: Tuple[int,int] = (224,224) + val_ratio: float = 0.1 + test_ratio: float = 0.1 + seed: int = 42 + batch_size: int = 16 + num_workers: int = 4 + augment: bool = False + +def _axis_for_plane(plane: str) -> int: + plane = plane.lower() + return {"axial": 2, "sagittal": 0, "coronal": 1}[plane] + +def _find_nifti(subject_dir: str) -> str: + cands = glob.glob(os.path.join(subject_dir, "**", "*.nii*"), recursive=True) + if not cands: + raise FileNotFoundError(f"No NIfTI under {subject_dir}") + return sorted(cands)[0] + +def _zscore(x: np.ndarray) -> np.ndarray: + mask = x != 0 + if mask.sum() < 10: + return x.astype(np.float32) + mu = x[mask].mean() + sd = x[mask].std() + 1e-6 + return ((x - mu) / sd).astype(np.float32) + +class ADNISliceDataset(Dataset): + """ + Returns (image, label, subject_id). Image is (1,H,W) float32. + Splits are per-subject to avoid leakage. + """ + def __init__(self, subjects: List[str], labels: Dict[str,int], args: ADNIArgs, split: str): + super().__init__() + self.subjects = subjects + self.labels = labels + self.args = args + self.split = split + self.axis = _axis_for_plane(args.plane) + self.index: List[Tuple[str,int]] = [] + self.cache: Dict[str, np.ndarray] = {} + + for sid in self.subjects: + vol = self._load_volume(sid) + D = vol.shape[self.axis] + if self.args.slice_mode == "center_k": + k = min(self.args.center_k, D); mid = D // 2 + start = max(0, mid - k//2); sl_idx = list(range(start, start + k)) + elif self.args.slice_mode == "step_s": + s = max(1, self.args.step_s); sl_idx = list(range(0, D, s)) + elif self.args.slice_mode == "all": + sl_idx = list(range(0, D)) + else: + raise ValueError(f"Unknown slice_mode={self.args.slice_mode}") + for z in sl_idx: + sl = self._get_slice(vol, z) + if np.count_nonzero(sl) > 20: + self.index.append((sid, z)) + + def _load_volume(self, subject_id: str) -> np.ndarray: + if subject_id in self.cache: return self.cache[subject_id] + path = _find_nifti(os.path.join(self.args.data_root, subject_id)) + arr = nib.load(path).get_fdata(caching="unchanged") + if arr.ndim == 4: arr = arr[..., 0] + arr = _zscore(arr) + self.cache[subject_id] = arr + return arr + + def _get_slice(self, vol: np.ndarray, idx: int) -> np.ndarray: + if self.axis == 0: return vol[idx, :, :] + if self.axis == 1: return vol[:, idx, :] + return vol[:, :, idx] + + def _to_tensor(self, sl: np.ndarray) -> torch.Tensor: + t = torch.from_numpy(sl).unsqueeze(0) # (1,H,W) + t = F.interpolate(t.unsqueeze(0), size=self.args.resize_hw, mode="bilinear", align_corners=False).squeeze(0) + if self.args.augment and self.split == "train": + if random.random() < 0.5: + t = torch.flip(t, dims=[2]) + return t.float() + + def __len__(self): return len(self.index) + + def __getitem__(self, i): + sid, z = self.index[i] + vol = self._load_volume(sid) + sl = self._get_slice(vol, z) + x = self._to_tensor(sl) + y = int(self.labels[sid]) + return x, y, sid + +def _split_subjects(all_subjects: List[str], val_ratio: float, test_ratio: float, seed: int): + rng = random.Random(seed) + subs = all_subjects[:]; rng.shuffle(subs) + n = len(subs) + n_test = int(round(n * test_ratio)) + n_val = int(round(n * val_ratio)) + test_ids = subs[:n_test] + val_ids = subs[n_test:n_test+n_val] + train_ids = subs[n_test+n_val:] + return train_ids, val_ids, test_ids + +def _read_labels(labels_csv: str) -> Dict[str,int]: + df = pd.read_csv(labels_csv) + def map_label(v): + if isinstance(v, str): + v = v.strip().upper() + if v in ("CN", "CONTROL", "NORMAL"): return 0 + if v in ("AD", "ALZHEIMERS", "ALZHEIMER'S"): return 1 + return int(v) + return {str(r["subject_id"]): map_label(r["label"]) for _, r in df.iterrows()} + +def build_loaders_adni(args: ADNIArgs): + assert os.path.isdir(args.data_root), f"data_root not found: {args.data_root}" + assert os.path.isfile(args.labels_csv), f"labels_csv not found: {args.labels_csv}" + labels = _read_labels(args.labels_csv) + subjects = sorted([d for d in os.listdir(args.data_root) if os.path.isdir(os.path.join(args.data_root, d)) and d in labels]) + if not subjects: + raise RuntimeError("No subject folders matched labels_csv; check subject_id values and directory names.") + train_ids, val_ids, test_ids = _split_subjects(subjects, args.val_ratio, args.test_ratio, args.seed) + train_set = ADNISliceDataset(train_ids, labels, args, split="train") + val_set = ADNISliceDataset(val_ids, labels, args, split="val") + test_set = ADNISliceDataset(test_ids, labels, args, split="test") + train_loader = DataLoader(train_set, batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True) + val_loader = DataLoader(val_set, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True) + test_loader = DataLoader(test_set, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True) + return train_loader, val_loader, test_loader + +# ============== Unified entry point ============== +def build_loaders(dataset: str = "random", **kwargs): + if dataset == "random": + return build_loaders_random(batch_size=kwargs.get("batch_size", 16), seed=kwargs.get("seed", 42)) + elif dataset == "adni": + a = ADNIArgs(**kwargs) + return build_loaders_adni(a) + else: + raise ValueError(f"Unknown dataset={dataset}") From 437e9ad4d21045900ec02a53779e42f655bc224d Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 12:06:55 +1000 Subject: [PATCH 11/31] added additional CLI argument handlers and spruced up training loop with ADNI dataset --- recognition/adni_convnext_47280647/train.py | 42 ++++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/recognition/adni_convnext_47280647/train.py b/recognition/adni_convnext_47280647/train.py index c23a8371f..4a3531771 100644 --- a/recognition/adni_convnext_47280647/train.py +++ b/recognition/adni_convnext_47280647/train.py @@ -2,10 +2,9 @@ import numpy as np import torch import torch.nn as nn -from torch.utils.data import DataLoader -import matplotlib.pyplot as plt from modules import build_model, count_params from dataset import build_loaders +import matplotlib.pyplot as plt def set_seed(seed: int = 42): random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) @@ -32,12 +31,25 @@ def plot_curves(history, save_dir): def main(): ap = argparse.ArgumentParser() + # core + ap.add_argument("--dataset", type=str, default="random", choices=["random","adni"]) ap.add_argument("--epochs", type=int, default=3) ap.add_argument("--batch_size", type=int, default=16) ap.add_argument("--lr", type=float, default=1e-3) ap.add_argument("--seed", type=int, default=42) ap.add_argument("--head_dropout", type=float, default=0.0) - ap.add_argument("--save_dir", type=str, default="results/min1") + ap.add_argument("--save_dir", type=str, default="results/run1") + # data (ADNI) + ap.add_argument("--data_root", type=str, default=None) + ap.add_argument("--labels_csv", type=str, default=None) + ap.add_argument("--num_workers", type=int, default=4) + ap.add_argument("--plane", type=str, default="axial") + ap.add_argument("--slice_mode", type=str, default="center_k") + ap.add_argument("--center_k", type=int, default=32) + ap.add_argument("--step_s", type=int, default=2) + ap.add_argument("--val_ratio", type=float, default=0.1) + ap.add_argument("--test_ratio", type=float, default=0.1) + ap.add_argument("--augment", action="store_true") args = ap.parse_args() set_seed(args.seed) @@ -46,7 +58,15 @@ def main(): with open(os.path.join(args.save_dir, "config.json"), "w") as f: json.dump(vars(args), f, indent=2) - train_loader, val_loader = build_loaders(batch_size=args.batch_size, seed=args.seed) + train_loader, val_loader, _ = build_loaders( + dataset=args.dataset, + data_root=args.data_root, labels_csv=args.labels_csv, + plane=args.plane, slice_mode=args.slice_mode, + center_k=args.center_k, step_s=args.step_s, + resize_hw=(224,224), val_ratio=args.val_ratio, test_ratio=args.test_ratio, + seed=args.seed, batch_size=args.batch_size, num_workers=args.num_workers, + augment=args.augment + ) model = build_model(in_chans=1, num_classes=2, head_dropout=args.head_dropout).to(device) print(f"Model params: {count_params(model):,}") @@ -57,10 +77,11 @@ def main(): history = {"train_loss": [], "val_loss": [], "train_acc": [], "val_acc": []} for epoch in range(1, args.epochs + 1): - # train + # --- train --- model.train() - tr_loss, tr_acc, n_tr = 0.0, 0.0, 0 - for x, y in train_loader: + tr_loss = tr_acc = n_tr = 0.0 + for batch in train_loader: + x, y = (batch[0], batch[1]) if isinstance(batch, (list, tuple)) else batch x, y = x.to(device), y.to(device) out = model(x)["logits"] loss = criterion(out, y) @@ -70,11 +91,12 @@ def main(): tr_acc += accuracy_from_logits(out, y) * bs n_tr += bs - # val + # --- val --- model.eval() - va_loss, va_acc, n_va = 0.0, 0.0, 0 + va_loss = va_acc = n_va = 0.0 with torch.no_grad(): - for x, y in val_loader: + for batch in val_loader: + x, y = (batch[0], batch[1]) if isinstance(batch, (list, tuple)) else batch x, y = x.to(device), y.to(device) out = model(x)["logits"] loss = criterion(out, y) From d8dfc260be4a9e46a3603a2e68b999ddfcf0fab0 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 12:08:26 +1000 Subject: [PATCH 12/31] added CLI handlers for train.py and extended checkpoint loading for ADNI dataset --- recognition/adni_convnext_47280647/predict.py | 59 ++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/recognition/adni_convnext_47280647/predict.py b/recognition/adni_convnext_47280647/predict.py index 0f8d2639c..0de576c54 100644 --- a/recognition/adni_convnext_47280647/predict.py +++ b/recognition/adni_convnext_47280647/predict.py @@ -1,31 +1,76 @@ -import argparse, os, torch +import argparse, torch, numpy as np +from collections import defaultdict from modules import build_model from dataset import build_loaders def main(): ap = argparse.ArgumentParser() - ap.add_argument("--checkpoint", type=str, default="results/min1/best.pt") + ap.add_argument("--checkpoint", type=str, default="results/run1/best.pt") + ap.add_argument("--dataset", type=str, default="random", choices=["random","adni"]) ap.add_argument("--batch_size", type=int, default=32) + # ADNI flags (only used if dataset=adni) + ap.add_argument("--data_root", type=str, default=None) + ap.add_argument("--labels_csv", type=str, default=None) + ap.add_argument("--num_workers", type=int, default=4) + ap.add_argument("--plane", type=str, default="axial") + ap.add_argument("--slice_mode", type=str, default="center_k") + ap.add_argument("--center_k", type=int, default=32) + ap.add_argument("--step_s", type=int, default=2) + ap.add_argument("--val_ratio", type=float, default=0.1) + ap.add_argument("--test_ratio", type=float, default=0.1) + ap.add_argument("--seed", type=int, default=42) args = ap.parse_args() device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - _, val_loader = build_loaders(batch_size=args.batch_size) - ckpt = torch.load(args.checkpoint, map_location="cpu") + tr, va, te = build_loaders( + dataset=args.dataset, + data_root=args.data_root, labels_csv=args.labels_csv, + plane=args.plane, slice_mode=args.slice_mode, center_k=args.center_k, step_s=args.step_s, + resize_hw=(224,224), val_ratio=args.val_ratio, test_ratio=args.test_ratio, + seed=args.seed, batch_size=args.batch_size, num_workers=args.num_workers, + augment=False + ) + loader = te if (args.dataset == "adni" and te is not None) else va + + ckpt = torch.load(args.checkpoint, map_location="cpu") model = build_model().to(device) model.load_state_dict(ckpt["state_dict"]) model.eval() - correct, total = 0, 0 + correct = total = 0 + subj_logits = defaultdict(list) + subj_labels = {} + with torch.no_grad(): - for x, y in val_loader: + for batch in loader: + if isinstance(batch, (list, tuple)) and len(batch) == 3: + x, y, sid = batch + else: + sid = None + x, y = (batch[0], batch[1]) if isinstance(batch, (list, tuple)) else batch x, y = x.to(device), y.to(device) logits = model(x)["logits"] preds = logits.argmax(1) correct += (preds == y).sum().item() total += y.size(0) - print(f"Validation accuracy (using saved checkpoint): {correct/total:.3f}") + if sid is not None: + for i, s in enumerate(sid): + subj_logits[s].append(logits[i].cpu().numpy()) + subj_labels[s] = int(y[i].cpu().item()) + + slice_acc = correct / max(total, 1) + print(f"Slice-level accuracy: {slice_acc:.3f}") + + if subj_logits: + p_correct = 0 + for s, logit_list in subj_logits.items(): + mlog = np.mean(np.stack(logit_list, axis=0), axis=0) + pred = int(np.argmax(mlog)) + p_correct += int(pred == subj_labels[s]) + patient_acc = p_correct / len(subj_logits) + print(f"Patient-level accuracy: {patient_acc:.3f} (subjects={len(subj_logits)})") if __name__ == "__main__": main() From 2040d91e8cac87553ed92da0090da97787f2b9b5 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 12:09:18 +1000 Subject: [PATCH 13/31] updated gitignore --- recognition/adni_convnext_47280647/.gitignore | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/recognition/adni_convnext_47280647/.gitignore b/recognition/adni_convnext_47280647/.gitignore index 05c22a0c2..c211bdcbb 100644 --- a/recognition/adni_convnext_47280647/.gitignore +++ b/recognition/adni_convnext_47280647/.gitignore @@ -1,4 +1,15 @@ +# python +__pycache__/ +*.pyc +.venv/ +.env + +# training artifacts results/ checkpoints/ *.pt *.pth + +# data (never commit datasets) +data/ +datasets/ From d0149c9e19dc9c4c7e2032eac1fecd2a16f92f2f Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 12:34:29 +1000 Subject: [PATCH 14/31] updated dataset.py to integrate both local testing (using csv's) and rangpur's data location pathing --- recognition/adni_convnext_47280647/dataset.py | 154 ++++++++++++++---- 1 file changed, 121 insertions(+), 33 deletions(-) diff --git a/recognition/adni_convnext_47280647/dataset.py b/recognition/adni_convnext_47280647/dataset.py index f12a46732..56d44d36a 100644 --- a/recognition/adni_convnext_47280647/dataset.py +++ b/recognition/adni_convnext_47280647/dataset.py @@ -1,7 +1,7 @@ from __future__ import annotations import os, glob, random from dataclasses import dataclass -from typing import Tuple, List, Dict +from typing import Tuple, List, Dict, Optional import numpy as np import pandas as pd @@ -10,7 +10,7 @@ import torch.nn.functional as F from torch.utils.data import Dataset, DataLoader, random_split -# ============== Random dataset (for local smoke tests) ============== +# ============== Random dataset (for local smoke testing) ============== class RandomSliceDataset(Dataset): def __init__(self, n: int = 192, image_size: Tuple[int, int, int] = (1, 224, 224), num_classes: int = 2, seed: int = 42): super().__init__() @@ -30,13 +30,13 @@ def build_loaders_random(batch_size: int = 16, seed: int = 42): val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False, num_workers=0) return train_loader, val_loader, None -# ============== ADNI dataset (Rangpur implementation) ============== +# ============== ADNI dataset (actual dataset for the assignment) ============== @dataclass class ADNIArgs: data_root: str - labels_csv: str - plane: str = "axial" # axial|sagittal|coronal - slice_mode: str = "center_k" # center_k|step_s|all + labels_csv: Optional[str] = None # if None -> folder mode + plane: str = "axial" # axial|sagittal|coronal + slice_mode: str = "center_k" # center_k|step_s|all center_k: int = 32 step_s: int = 2 resize_hw: Tuple[int,int] = (224,224) @@ -72,8 +72,8 @@ class ADNISliceDataset(Dataset): """ def __init__(self, subjects: List[str], labels: Dict[str,int], args: ADNIArgs, split: str): super().__init__() - self.subjects = subjects - self.labels = labels + self.subjects = subjects # list of subject folder paths + self.labels = labels # {basename(subject_dir): 0/1} self.args = args self.split = split self.axis = _axis_for_plane(args.plane) @@ -97,13 +97,13 @@ def __init__(self, subjects: List[str], labels: Dict[str,int], args: ADNIArgs, s if np.count_nonzero(sl) > 20: self.index.append((sid, z)) - def _load_volume(self, subject_id: str) -> np.ndarray: - if subject_id in self.cache: return self.cache[subject_id] - path = _find_nifti(os.path.join(self.args.data_root, subject_id)) + def _load_volume(self, subject_dir: str) -> np.ndarray: + if subject_dir in self.cache: return self.cache[subject_dir] + path = _find_nifti(subject_dir) arr = nib.load(path).get_fdata(caching="unchanged") if arr.ndim == 4: arr = arr[..., 0] arr = _zscore(arr) - self.cache[subject_id] = arr + self.cache[subject_dir] = arr return arr def _get_slice(self, vol: np.ndarray, idx: int) -> np.ndarray: @@ -126,8 +126,49 @@ def __getitem__(self, i): vol = self._load_volume(sid) sl = self._get_slice(vol, z) x = self._to_tensor(sl) - y = int(self.labels[sid]) - return x, y, sid + base = os.path.basename(sid) + y = int(self.labels[base]) + return x, y, base + +# ---------- Subject/label discovery ---------- + +def _read_labels_csv(labels_csv: str) -> Dict[str,int]: + df = pd.read_csv(labels_csv) + def map_label(v): + if isinstance(v, str): + v = v.strip().upper() + if v in ("CN", "NC", "CONTROL", "NORMAL"): return 0 + if v in ("AD", "ALZHEIMERS", "ALZHEIMER'S"): return 1 + return int(v) + return {str(r["subject_id"]): map_label(r["label"]) for _, r in df.iterrows()} + +def _scan_class_folders(root: str, class_map: Dict[str,int] = None): + """ + Expects root to contain AD/ and/or NC/ with subject folders inside each. + Returns (list_of_subject_paths, labels_by_subject_basename) + """ + if class_map is None: + class_map = {"AD": 1, "NC": 0} + + subjects: List[str] = [] + labels: Dict[str, int] = {} + for cls_name, cls_label in class_map.items(): + cls_dir = os.path.join(root, cls_name) + if not os.path.isdir(cls_dir): + continue + for entry in sorted(os.listdir(cls_dir)): + subj_dir = os.path.join(cls_dir, entry) + if os.path.isdir(subj_dir): + subjects.append(subj_dir) + labels[entry] = cls_label + if not subjects: + raise RuntimeError(f"No subjects found under class folders in: {root}") + return subjects, labels + +def _has_subdirs(path: str, names: List[str]) -> bool: + return all(os.path.isdir(os.path.join(path, n)) for n in names) + +# ---------- Build loaders (auto-detect train/test) ---------- def _split_subjects(all_subjects: List[str], val_ratio: float, test_ratio: float, seed: int): rng = random.Random(seed) @@ -140,27 +181,74 @@ def _split_subjects(all_subjects: List[str], val_ratio: float, test_ratio: float train_ids = subs[n_test+n_val:] return train_ids, val_ids, test_ids -def _read_labels(labels_csv: str) -> Dict[str,int]: - df = pd.read_csv(labels_csv) - def map_label(v): - if isinstance(v, str): - v = v.strip().upper() - if v in ("CN", "CONTROL", "NORMAL"): return 0 - if v in ("AD", "ALZHEIMERS", "ALZHEIMER'S"): return 1 - return int(v) - return {str(r["subject_id"]): map_label(r["label"]) for _, r in df.iterrows()} - def build_loaders_adni(args: ADNIArgs): assert os.path.isdir(args.data_root), f"data_root not found: {args.data_root}" - assert os.path.isfile(args.labels_csv), f"labels_csv not found: {args.labels_csv}" - labels = _read_labels(args.labels_csv) - subjects = sorted([d for d in os.listdir(args.data_root) if os.path.isdir(os.path.join(args.data_root, d)) and d in labels]) - if not subjects: - raise RuntimeError("No subject folders matched labels_csv; check subject_id values and directory names.") - train_ids, val_ids, test_ids = _split_subjects(subjects, args.val_ratio, args.test_ratio, args.seed) - train_set = ADNISliceDataset(train_ids, labels, args, split="train") - val_set = ADNISliceDataset(val_ids, labels, args, split="val") - test_set = ADNISliceDataset(test_ids, labels, args, split="test") + + # If CSV provided, we keep the previous CSV mode. + if args.labels_csv: + labels = _read_labels_csv(args.labels_csv) + subjects = [] + for d in sorted(os.listdir(args.data_root)): + p = os.path.join(args.data_root, d) + if os.path.isdir(p) and d in labels: + subjects.append(p) + if not subjects: + raise RuntimeError("No subject folders matched labels_csv under data_root.") + train_ids, val_ids, test_ids = _split_subjects(subjects, args.val_ratio, args.test_ratio, args.seed) + basename_labels = {k: v for k, v in labels.items()} + + else: + root = args.data_root.rstrip("/") + + # Case 1: data_root is the parent containing 'train' and 'test' + if _has_subdirs(root, ["train", "test"]): + train_root = os.path.join(root, "train") + test_root = os.path.join(root, "test") + train_subjects, train_labels = _scan_class_folders(train_root, {"AD":1, "NC":0}) + # Validation from TRAIN split + tr_ids, val_ids, _ = _split_subjects(train_subjects, args.val_ratio, 0.0, args.seed) + # Test from TEST split + test_subjects, test_labels = _scan_class_folders(test_root, {"AD":1, "NC":0}) + # Ensure consistent label map (both are {basename: label}) + basename_labels = {**train_labels, **test_labels} + train_ids, test_ids = tr_ids, test_subjects + + else: + # Case 2: data_root is .../train or .../test, try to find sibling + base = os.path.basename(root) + parent = os.path.dirname(root) + if base in ("train", "test") and _has_subdirs(parent, ["train", "test"]): + split_root = root + other_root = os.path.join(parent, "test" if base == "train" else "train") + # load the chosen split + split_subjects, split_labels = _scan_class_folders(split_root, {"AD":1, "NC":0}) + # build val from chosen split + if base == "train": + tr_ids, val_ids, _ = _split_subjects(split_subjects, args.val_ratio, 0.0, args.seed) + train_ids = tr_ids + # test is sibling test/ + test_subjects, test_labels = _scan_class_folders(other_root, {"AD":1, "NC":0}) + test_ids = test_subjects + basename_labels = {**split_labels, **test_labels} + else: + # base == "test": we will create val from sibling train, and keep test as this root + train_subjects, train_labels = _scan_class_folders(other_root, {"AD":1, "NC":0}) + tr_ids, val_ids, _ = _split_subjects(train_subjects, args.val_ratio, 0.0, args.seed) + train_ids = tr_ids + test_subjects, test_labels = _scan_class_folders(split_root, {"AD":1, "NC":0}) + test_ids = test_subjects + basename_labels = {**train_labels, **test_labels} + + else: + # Case 3: data_root itself has AD/ and NC/ (single split only) + subjects, basename_labels = _scan_class_folders(root, {"AD":1, "NC":0}) + train_ids, val_ids, test_ids = _split_subjects(subjects, args.val_ratio, args.test_ratio, args.seed) + + # Build datasets + train_set = ADNISliceDataset(train_ids, basename_labels, args, split="train") + val_set = ADNISliceDataset(val_ids, basename_labels, args, split="val") + test_set = ADNISliceDataset(test_ids, basename_labels, args, split="test") + train_loader = DataLoader(train_set, batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True) val_loader = DataLoader(val_set, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True) test_loader = DataLoader(test_set, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True) From f4e2d248b506c2844fdda3e5e16d5980f581e8a9 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 13:58:03 +1000 Subject: [PATCH 15/31] refactored dataset.py removing NIfTI/volume logic and adding support for image extensions --- recognition/adni_convnext_47280647/dataset.py | 308 ++++++------------ 1 file changed, 107 insertions(+), 201 deletions(-) diff --git a/recognition/adni_convnext_47280647/dataset.py b/recognition/adni_convnext_47280647/dataset.py index 56d44d36a..358ac85c9 100644 --- a/recognition/adni_convnext_47280647/dataset.py +++ b/recognition/adni_convnext_47280647/dataset.py @@ -4,13 +4,12 @@ from typing import Tuple, List, Dict, Optional import numpy as np -import pandas as pd -import nibabel as nib import torch import torch.nn.functional as F from torch.utils.data import Dataset, DataLoader, random_split +from PIL import Image # image loading -# ============== Random dataset (for local smoke testing) ============== +# ===== Random dataset (for local smoke tests) ===== class RandomSliceDataset(Dataset): def __init__(self, n: int = 192, image_size: Tuple[int, int, int] = (1, 224, 224), num_classes: int = 2, seed: int = 42): super().__init__() @@ -30,15 +29,15 @@ def build_loaders_random(batch_size: int = 16, seed: int = 42): val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False, num_workers=0) return train_loader, val_loader, None -# ============== ADNI dataset (actual dataset for the assignment) ============== +# ===== ADNI (for Rangpur) ===== @dataclass class ADNIArgs: data_root: str - labels_csv: Optional[str] = None # if None -> folder mode - plane: str = "axial" # axial|sagittal|coronal - slice_mode: str = "center_k" # center_k|step_s|all - center_k: int = 32 - step_s: int = 2 + labels_csv: Optional[str] = None # ignored in image-only mode; kept for CLI compatibility + plane: str = "axial" # ignored (MRI-only option) + slice_mode: str = "center_k" # ignored (MRI-only option) + center_k: int = 32 # ignored + step_s: int = 2 # ignored resize_hw: Tuple[int,int] = (224,224) val_ratio: float = 0.1 test_ratio: float = 0.1 @@ -47,214 +46,121 @@ class ADNIArgs: num_workers: int = 4 augment: bool = False -def _axis_for_plane(plane: str) -> int: - plane = plane.lower() - return {"axial": 2, "sagittal": 0, "coronal": 1}[plane] +_IMG_EXTS = ( + "*.jpg", "*.jpeg", "*.png", "*.bmp", "*.tif", "*.tiff", + "*.JPG", "*.JPEG", "*.PNG", "*.BMP", "*.TIF", "*.TIFF" +) -def _find_nifti(subject_dir: str) -> str: - cands = glob.glob(os.path.join(subject_dir, "**", "*.nii*"), recursive=True) - if not cands: - raise FileNotFoundError(f"No NIfTI under {subject_dir}") - return sorted(cands)[0] - -def _zscore(x: np.ndarray) -> np.ndarray: - mask = x != 0 - if mask.sum() < 10: - return x.astype(np.float32) - mu = x[mask].mean() - sd = x[mask].std() + 1e-6 - return ((x - mu) / sd).astype(np.float32) - -class ADNISliceDataset(Dataset): - """ - Returns (image, label, subject_id). Image is (1,H,W) float32. - Splits are per-subject to avoid leakage. - """ - def __init__(self, subjects: List[str], labels: Dict[str,int], args: ADNIArgs, split: str): - super().__init__() - self.subjects = subjects # list of subject folder paths - self.labels = labels # {basename(subject_dir): 0/1} - self.args = args - self.split = split - self.axis = _axis_for_plane(args.plane) - self.index: List[Tuple[str,int]] = [] - self.cache: Dict[str, np.ndarray] = {} +def _has_subdirs(path: str, names: List[str]) -> bool: + return all(os.path.isdir(os.path.join(path, n)) for n in names) - for sid in self.subjects: - vol = self._load_volume(sid) - D = vol.shape[self.axis] - if self.args.slice_mode == "center_k": - k = min(self.args.center_k, D); mid = D // 2 - start = max(0, mid - k//2); sl_idx = list(range(start, start + k)) - elif self.args.slice_mode == "step_s": - s = max(1, self.args.step_s); sl_idx = list(range(0, D, s)) - elif self.args.slice_mode == "all": - sl_idx = list(range(0, D)) - else: - raise ValueError(f"Unknown slice_mode={self.args.slice_mode}") - for z in sl_idx: - sl = self._get_slice(vol, z) - if np.count_nonzero(sl) > 20: - self.index.append((sid, z)) +def _list_images_under(root: str, class_map: Dict[str,int]) -> List[Tuple[str,int]]: + items: List[Tuple[str,int]] = [] + for cls_name, label in class_map.items(): + cls_dir = os.path.join(root, cls_name) + if not os.path.isdir(cls_dir): + continue + for pat in _IMG_EXTS: + for p in glob.glob(os.path.join(cls_dir, "**", pat), recursive=True): + if os.path.isfile(p): + items.append((p, label)) + return items - def _load_volume(self, subject_dir: str) -> np.ndarray: - if subject_dir in self.cache: return self.cache[subject_dir] - path = _find_nifti(subject_dir) - arr = nib.load(path).get_fdata(caching="unchanged") - if arr.ndim == 4: arr = arr[..., 0] - arr = _zscore(arr) - self.cache[subject_dir] = arr - return arr +class ADNIImageDataset(Dataset): + """Each image file is one sample -> returns (x, y).""" + def __init__(self, items: List[Tuple[str,int]], args: ADNIArgs, split: str): + self.items = items + self.args = args + self.split = split - def _get_slice(self, vol: np.ndarray, idx: int) -> np.ndarray: - if self.axis == 0: return vol[idx, :, :] - if self.axis == 1: return vol[:, idx, :] - return vol[:, :, idx] + def __len__(self): return len(self.items) - def _to_tensor(self, sl: np.ndarray) -> torch.Tensor: - t = torch.from_numpy(sl).unsqueeze(0) # (1,H,W) + def __getitem__(self, i): + path, y = self.items[i] + # load grayscale and normalize to [0,1] + img = Image.open(path).convert("L") + arr = np.asarray(img, dtype=np.float32) / 255.0 # (H, W) + t = torch.from_numpy(arr).unsqueeze(0) # (1, H, W) + # resize to model input t = F.interpolate(t.unsqueeze(0), size=self.args.resize_hw, mode="bilinear", align_corners=False).squeeze(0) + # simple augmentation (optional) if self.args.augment and self.split == "train": if random.random() < 0.5: - t = torch.flip(t, dims=[2]) - return t.float() - - def __len__(self): return len(self.index) - - def __getitem__(self, i): - sid, z = self.index[i] - vol = self._load_volume(sid) - sl = self._get_slice(vol, z) - x = self._to_tensor(sl) - base = os.path.basename(sid) - y = int(self.labels[base]) - return x, y, base - -# ---------- Subject/label discovery ---------- - -def _read_labels_csv(labels_csv: str) -> Dict[str,int]: - df = pd.read_csv(labels_csv) - def map_label(v): - if isinstance(v, str): - v = v.strip().upper() - if v in ("CN", "NC", "CONTROL", "NORMAL"): return 0 - if v in ("AD", "ALZHEIMERS", "ALZHEIMER'S"): return 1 - return int(v) - return {str(r["subject_id"]): map_label(r["label"]) for _, r in df.iterrows()} - -def _scan_class_folders(root: str, class_map: Dict[str,int] = None): - """ - Expects root to contain AD/ and/or NC/ with subject folders inside each. - Returns (list_of_subject_paths, labels_by_subject_basename) - """ - if class_map is None: - class_map = {"AD": 1, "NC": 0} - - subjects: List[str] = [] - labels: Dict[str, int] = {} - for cls_name, cls_label in class_map.items(): - cls_dir = os.path.join(root, cls_name) - if not os.path.isdir(cls_dir): - continue - for entry in sorted(os.listdir(cls_dir)): - subj_dir = os.path.join(cls_dir, entry) - if os.path.isdir(subj_dir): - subjects.append(subj_dir) - labels[entry] = cls_label - if not subjects: - raise RuntimeError(f"No subjects found under class folders in: {root}") - return subjects, labels - -def _has_subdirs(path: str, names: List[str]) -> bool: - return all(os.path.isdir(os.path.join(path, n)) for n in names) + t = torch.flip(t, dims=[2]) # horizontal flip + return t.float(), int(y) -# ---------- Build loaders (auto-detect train/test) ---------- - -def _split_subjects(all_subjects: List[str], val_ratio: float, test_ratio: float, seed: int): +def _split_items(items: List, val_ratio: float, test_ratio: float, seed: int): rng = random.Random(seed) - subs = all_subjects[:]; rng.shuffle(subs) - n = len(subs) + arr = items[:]; rng.shuffle(arr) + n = len(arr) n_test = int(round(n * test_ratio)) n_val = int(round(n * val_ratio)) - test_ids = subs[:n_test] - val_ids = subs[n_test:n_test+n_val] - train_ids = subs[n_test+n_val:] - return train_ids, val_ids, test_ids + test_items = arr[:n_test] + val_items = arr[n_test:n_test+n_val] + train_items = arr[n_test+n_val:] + return train_items, val_items, test_items def build_loaders_adni(args: ADNIArgs): assert os.path.isdir(args.data_root), f"data_root not found: {args.data_root}" - - # If CSV provided, we keep the previous CSV mode. - if args.labels_csv: - labels = _read_labels_csv(args.labels_csv) - subjects = [] - for d in sorted(os.listdir(args.data_root)): - p = os.path.join(args.data_root, d) - if os.path.isdir(p) and d in labels: - subjects.append(p) - if not subjects: - raise RuntimeError("No subject folders matched labels_csv under data_root.") - train_ids, val_ids, test_ids = _split_subjects(subjects, args.val_ratio, args.test_ratio, args.seed) - basename_labels = {k: v for k, v in labels.items()} - - else: - root = args.data_root.rstrip("/") - - # Case 1: data_root is the parent containing 'train' and 'test' - if _has_subdirs(root, ["train", "test"]): - train_root = os.path.join(root, "train") - test_root = os.path.join(root, "test") - train_subjects, train_labels = _scan_class_folders(train_root, {"AD":1, "NC":0}) - # Validation from TRAIN split - tr_ids, val_ids, _ = _split_subjects(train_subjects, args.val_ratio, 0.0, args.seed) - # Test from TEST split - test_subjects, test_labels = _scan_class_folders(test_root, {"AD":1, "NC":0}) - # Ensure consistent label map (both are {basename: label}) - basename_labels = {**train_labels, **test_labels} - train_ids, test_ids = tr_ids, test_subjects - + class_map = {"AD": 1, "NC": 0} + root = args.data_root.rstrip("/") + + # Case A: parent contains train/ and test/ + if _has_subdirs(root, ["train", "test"]): + train_root = os.path.join(root, "train") + test_root = os.path.join(root, "test") + tr_items = _list_images_under(train_root, class_map) + te_items = _list_images_under(test_root, class_map) + if not tr_items: raise RuntimeError(f"No images found under: {train_root}") + if not te_items: raise RuntimeError(f"No images found under: {test_root}") + tr_split, va_split, _ = _split_items(tr_items, args.val_ratio, 0.0, args.seed) + print(f"[ADNI IMAGES] train={len(tr_split)} val={len(va_split)} test={len(te_items)}") + return ( + DataLoader(ADNIImageDataset(tr_split, args, split="train"), batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True), + DataLoader(ADNIImageDataset(va_split, args, split="val"), batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True), + DataLoader(ADNIImageDataset(te_items, args, split="test"), batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True), + ) + + # Case B: root is .../train or .../test (use sibling as the other split) + base = os.path.basename(root) + parent = os.path.dirname(root) + if base in ("train", "test") and _has_subdirs(parent, ["train", "test"]): + split_root = root + other_root = os.path.join(parent, "test" if base == "train" else "train") + sp_items = _list_images_under(split_root, class_map) + ot_items = _list_images_under(other_root, class_map) + if not sp_items: raise RuntimeError(f"No images found under: {split_root}") + if not ot_items: raise RuntimeError(f"No images found under: {other_root}") + if base == "train": + tr_split, va_split, _ = _split_items(sp_items, args.val_ratio, 0.0, args.seed) + print(f"[ADNI IMAGES] train={len(tr_split)} val={len(va_split)} test={len(ot_items)}") + return ( + DataLoader(ADNIImageDataset(tr_split, args, split="train"), batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True), + DataLoader(ADNIImageDataset(va_split, args, split="val"), batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True), + DataLoader(ADNIImageDataset(ot_items, args, split="test"), batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True), + ) else: - # Case 2: data_root is .../train or .../test, try to find sibling - base = os.path.basename(root) - parent = os.path.dirname(root) - if base in ("train", "test") and _has_subdirs(parent, ["train", "test"]): - split_root = root - other_root = os.path.join(parent, "test" if base == "train" else "train") - # load the chosen split - split_subjects, split_labels = _scan_class_folders(split_root, {"AD":1, "NC":0}) - # build val from chosen split - if base == "train": - tr_ids, val_ids, _ = _split_subjects(split_subjects, args.val_ratio, 0.0, args.seed) - train_ids = tr_ids - # test is sibling test/ - test_subjects, test_labels = _scan_class_folders(other_root, {"AD":1, "NC":0}) - test_ids = test_subjects - basename_labels = {**split_labels, **test_labels} - else: - # base == "test": we will create val from sibling train, and keep test as this root - train_subjects, train_labels = _scan_class_folders(other_root, {"AD":1, "NC":0}) - tr_ids, val_ids, _ = _split_subjects(train_subjects, args.val_ratio, 0.0, args.seed) - train_ids = tr_ids - test_subjects, test_labels = _scan_class_folders(split_root, {"AD":1, "NC":0}) - test_ids = test_subjects - basename_labels = {**train_labels, **test_labels} - - else: - # Case 3: data_root itself has AD/ and NC/ (single split only) - subjects, basename_labels = _scan_class_folders(root, {"AD":1, "NC":0}) - train_ids, val_ids, test_ids = _split_subjects(subjects, args.val_ratio, args.test_ratio, args.seed) - - # Build datasets - train_set = ADNISliceDataset(train_ids, basename_labels, args, split="train") - val_set = ADNISliceDataset(val_ids, basename_labels, args, split="val") - test_set = ADNISliceDataset(test_ids, basename_labels, args, split="test") - - train_loader = DataLoader(train_set, batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True) - val_loader = DataLoader(val_set, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True) - test_loader = DataLoader(test_set, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True) - return train_loader, val_loader, test_loader - -# ============== Unified entry point ============== + tr_split, va_split, _ = _split_items(ot_items, args.val_ratio, 0.0, args.seed) + print(f"[ADNI IMAGES] train={len(tr_split)} val={len(va_split)} test={len(sp_items)}") + return ( + DataLoader(ADNIImageDataset(tr_split, args, split="train"), batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True), + DataLoader(ADNIImageDataset(va_split, args, split="val"), batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True), + DataLoader(ADNIImageDataset(sp_items, args, split="test"), batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True), + ) + + # Case C: single pool (root has AD/ and NC/ only) + items = _list_images_under(root, class_map) + if not items: + raise RuntimeError(f"No AD/NC images found under: {root}") + tr_items, va_items, te_items = _split_items(items, args.val_ratio, args.test_ratio, args.seed) + print(f"[ADNI IMAGES] train={len(tr_items)} val={len(va_items)} test={len(te_items)}") + return ( + DataLoader(ADNIImageDataset(tr_items, args, split="train"), batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True), + DataLoader(ADNIImageDataset(va_items, args, split="val"), batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True), + DataLoader(ADNIImageDataset(te_items, args, split="test"), batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True), + ) + +# ===== Unified entry point ===== def build_loaders(dataset: str = "random", **kwargs): if dataset == "random": return build_loaders_random(batch_size=kwargs.get("batch_size", 16), seed=kwargs.get("seed", 42)) From 636937af27022e1159b16f010233f1a97462f3ff Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 17:42:36 +1000 Subject: [PATCH 16/31] updated requirements.txt to remove torch/torchvision and add pillow for image processing --- .../adni_convnext_47280647/requirements.txt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/recognition/adni_convnext_47280647/requirements.txt b/recognition/adni_convnext_47280647/requirements.txt index 64a3614b0..7066dbe9a 100644 --- a/recognition/adni_convnext_47280647/requirements.txt +++ b/recognition/adni_convnext_47280647/requirements.txt @@ -1,10 +1,7 @@ -torch>=2.1 -torchvision>=0.16 -timm>=0.9 -numpy>=1.26 -pandas>=2.1 -scikit-learn>=1.3 -matplotlib>=3.8 -scipy>=1.11 -nibabel>=5.1 -tqdm>=4.66 +#! remember this does not include torch/torchvision dependencies +pillow==10.4.0 +numpy==1.26.4 +matplotlib==3.8.4 +tqdm==4.66.4 +scikit-learn==1.3.2 +scipy==1.11.4 From df1ea3021904c7ac6cbcae089067744f250788d4 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Tue, 4 Nov 2025 18:54:41 +1000 Subject: [PATCH 17/31] updated gitignore for ADNI dataset for testing locally --- recognition/adni_convnext_47280647/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/recognition/adni_convnext_47280647/.gitignore b/recognition/adni_convnext_47280647/.gitignore index c211bdcbb..952f6c476 100644 --- a/recognition/adni_convnext_47280647/.gitignore +++ b/recognition/adni_convnext_47280647/.gitignore @@ -13,3 +13,4 @@ checkpoints/ # data (never commit datasets) data/ datasets/ +ADNI/ \ No newline at end of file From 7a57b283643a97d8e4b5efb99b84e1d4d48a46e9 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Wed, 5 Nov 2025 00:22:04 +1000 Subject: [PATCH 18/31] added .python-version for better venv management --- recognition/adni_convnext_47280647/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/recognition/adni_convnext_47280647/.gitignore b/recognition/adni_convnext_47280647/.gitignore index 952f6c476..f72ab2d69 100644 --- a/recognition/adni_convnext_47280647/.gitignore +++ b/recognition/adni_convnext_47280647/.gitignore @@ -3,6 +3,7 @@ __pycache__/ *.pyc .venv/ .env +.python-version # training artifacts results/ From 4c98346a262652401e2b13f819d00203fdbbfa3d Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Wed, 5 Nov 2025 12:09:28 +1000 Subject: [PATCH 19/31] completed convnext-lite model and added arg config to train.py --- recognition/adni_convnext_47280647/modules.py | 180 +++++++++++++++++- recognition/adni_convnext_47280647/train.py | 6 +- 2 files changed, 183 insertions(+), 3 deletions(-) diff --git a/recognition/adni_convnext_47280647/modules.py b/recognition/adni_convnext_47280647/modules.py index 7ef81cb94..fba36bb9f 100644 --- a/recognition/adni_convnext_47280647/modules.py +++ b/recognition/adni_convnext_47280647/modules.py @@ -1,7 +1,11 @@ import torch import torch.nn as nn import torch.nn.functional as F +from typing import List +# ------------------------------ +# 1) Minimal CNN +# ------------------------------ class TinyCNN(nn.Module): """Minimal 2D CNN for binary classification on 1x224x224 inputs.""" def __init__(self, in_chans: int = 1, num_classes: int = 2, head_dropout: float = 0.0): @@ -22,8 +26,180 @@ def forward(self, x): probs = F.softmax(logits, dim=1) return {"logits": logits, "probs": probs} -def build_model(in_chans: int = 1, num_classes: int = 2, head_dropout: float = 0.0) -> nn.Module: - return TinyCNN(in_chans=in_chans, num_classes=num_classes, head_dropout=head_dropout) +# ------------------------------ +# 2) ConvNeXt-Lite +# Differences vs “canonical” ConvNeXt: +# - depthwise kernel 5 (not 7) +# - SiLU activation (not GELU) +# - GroupNorm(1,C) as NCHW LayerNorm +# - two-step stem downsampling (3x3 s=2 twice) +# - depths=[2,4,8,2], dims=[80,160,320,640] +# ------------------------------ + +class StochasticDepth(nn.Module): + """Per-sample DropPath (stochastic depth).""" + def __init__(self, p: float = 0.0): + super().__init__() + self.p = float(p) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if self.p == 0.0 or not self.training: + return x + keep = 1.0 - self.p + # shape: [N, 1, 1, 1] so each sample is dropped/kept whole + mask = torch.empty(x.shape[0], 1, 1, 1, device=x.device, dtype=x.dtype).bernoulli_(keep) + return x * mask / keep + +class ChannelNorm(nn.Module): + """ + LayerNorm for NCHW via GroupNorm(1, C): normalizes each channel with affine params. + This is a standard trick to avoid permute for channels-last LN. + """ + def __init__(self, num_channels: int, eps: float = 1e-6): + super().__init__() + self.norm = nn.GroupNorm(1, num_channels, eps=eps, affine=True) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.norm(x) + +class NeXtLiteBlock(nn.Module): + """ + ConvNeXt-inspired block (distinct variant): + depthwise 5x5 -> ChannelNorm -> 1x1 (4x) -> SiLU -> 1x1 (proj) -> LayerScale -> StochasticDepth -> +res + """ + def __init__(self, dim: int, drop_path: float = 0.0, layer_scale_init: float = 1e-6): + super().__init__() + self.dw = nn.Conv2d(dim, dim, kernel_size=5, padding=2, groups=dim) # K=5 + self.norm = ChannelNorm(dim) + self.pw_expand = nn.Conv2d(dim, 4 * dim, kernel_size=1) + self.act = nn.SiLU() # different from GELU + self.pw_proj = nn.Conv2d(4 * dim, dim, kernel_size=1) + # per-channel layerscale (optional) + self.alpha = nn.Parameter(torch.full((dim,), layer_scale_init)) if layer_scale_init > 0 else None + self.drop = StochasticDepth(drop_path) if drop_path > 0 else nn.Identity() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + residual = x + x = self.dw(x) + x = self.norm(x) + x = self.pw_expand(x) + x = self.act(x) + x = self.pw_proj(x) + if self.alpha is not None: + x = x * self.alpha[:, None, None] + x = self.drop(x) + return x + residual + +class ConvNeXtLite(nn.Module): + """ + A compact, convnext-inspired classifier for 1x224x224 inputs. + From-scratch, no external backbones, distinct config (dims/depths/kernels/norm/act). + """ + def __init__( + self, + in_chans: int = 1, + num_classes: int = 2, + depths: List[int] = [2, 4, 8, 2], + dims: List[int] = [80, 160, 320, 640], + drop_path_rate: float = 0.15, + head_dropout: float = 0.2, + layer_scale_init: float = 1e-6, + ): + super().__init__() + assert len(depths) == 4 and len(dims) == 4 + + # Stem: two 3x3 stride-2 downsamples (224 -> 56) + self.stem = nn.Sequential( + nn.Conv2d(in_chans, dims[0]//2, kernel_size=3, stride=2, padding=1), # 224->112 + ChannelNorm(dims[0]//2), + nn.SiLU(), + nn.Conv2d(dims[0]//2, dims[0], kernel_size=3, stride=2, padding=1), # 112->56 + ChannelNorm(dims[0]), + ) + + # Downsample layers between stages: 2x2 stride-2 convs + self.downsamples = nn.ModuleList([ + nn.Sequential(ChannelNorm(dims[0]), nn.Conv2d(dims[0], dims[1], 2, 2)), + nn.Sequential(ChannelNorm(dims[1]), nn.Conv2d(dims[1], dims[2], 2, 2)), + nn.Sequential(ChannelNorm(dims[2]), nn.Conv2d(dims[2], dims[3], 2, 2)), + ]) + + # Stages with progressive stochastic depth + total_blocks = sum(depths) + dp_rates = torch.linspace(0, drop_path_rate, total_blocks).tolist() + cursor = 0 + self.stages = nn.ModuleList() + for stage_idx in range(4): + blocks = [] + width = dims[stage_idx] + for _ in range(depths[stage_idx]): + blocks.append(NeXtLiteBlock(width, drop_path=dp_rates[cursor], layer_scale_init=layer_scale_init)) + cursor += 1 + self.stages.append(nn.Sequential(*blocks)) + + # Head: global avg pool -> LN (via GroupNorm) -> dropout -> linear + self.head_norm = ChannelNorm(dims[-1]) + self.head_drop = nn.Dropout(head_dropout) if head_dropout > 0 else nn.Identity() + self.head_fc = nn.Linear(dims[-1], num_classes) + + self.apply(self._init_weights) + + @staticmethod + def _init_weights(m: nn.Module): + if isinstance(m, (nn.Conv2d, nn.Linear)): + nn.init.trunc_normal_(m.weight, std=0.02) + if m.bias is not None: + nn.init.zeros_(m.bias) + + def forward_features(self, x: torch.Tensor) -> torch.Tensor: + x = self.stem(x) + x = self.stages[0](x) + x = self.downsamples[0](x) + x = self.stages[1](x) + x = self.downsamples[1](x) + x = self.stages[2](x) + x = self.downsamples[2](x) + x = self.stages[3](x) + # global average pooling + x = x.mean(dim=(2, 3)) + return x + + def forward(self, x: torch.Tensor): + feats = self.forward_features(x) + feats = self.head_norm(feats[:, :, None, None]).squeeze(-1).squeeze(-1) # norm in NCHW path + feats = self.head_drop(feats) + logits = self.head_fc(feats) + probs = F.softmax(logits, dim=1) + return {"logits": logits, "probs": probs} + +# ------------------------------ +# 3) Factory & utility +# ------------------------------ +def build_model( + in_chans: int = 1, + num_classes: int = 2, + head_dropout: float = 0.0, + model: str = "tiny", # "tiny" or "nextlite_tiny" +) -> nn.Module: + """ + Backward-compatible factory. + - model="tiny" -> TinyCNN (baseline) + - model="nextlite_tiny" -> ConvNeXtLite + """ + if model == "tiny": + return TinyCNN(in_chans=in_chans, num_classes=num_classes, head_dropout=head_dropout) + elif model == "nextlite_tiny": + return ConvNeXtLite( + in_chans=in_chans, + num_classes=num_classes, + depths=[2, 4, 8, 2], + dims=[80, 160, 320, 640], + drop_path_rate=0.15, + head_dropout=max(head_dropout, 0.2), + layer_scale_init=1e-6, + ) + else: + raise ValueError(f"Unknown model '{model}' (use 'tiny' or 'nextlite_tiny').") def count_params(model: nn.Module) -> int: return sum(p.numel() for p in model.parameters() if p.requires_grad) diff --git a/recognition/adni_convnext_47280647/train.py b/recognition/adni_convnext_47280647/train.py index 4a3531771..8d8a5b0c5 100644 --- a/recognition/adni_convnext_47280647/train.py +++ b/recognition/adni_convnext_47280647/train.py @@ -50,6 +50,10 @@ def main(): ap.add_argument("--val_ratio", type=float, default=0.1) ap.add_argument("--test_ratio", type=float, default=0.1) ap.add_argument("--augment", action="store_true") + ap.add_argument("--model", type=str, default="tiny", + choices=["tiny","nextlite_tiny","nextlite_small"], + help="Which model to build") + args = ap.parse_args() set_seed(args.seed) @@ -68,7 +72,7 @@ def main(): augment=args.augment ) - model = build_model(in_chans=1, num_classes=2, head_dropout=args.head_dropout).to(device) + model = build_model(name=args.model, in_chans=1, num_classes=2, head_dropout=args.head_dropout).to(device) print(f"Model params: {count_params(model):,}") optim = torch.optim.Adam(model.parameters(), lr=args.lr) criterion = nn.CrossEntropyLoss() From aa453f82d42d8f2d1ce4702b22da31cd98828401 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Wed, 5 Nov 2025 12:11:40 +1000 Subject: [PATCH 20/31] adding helper print statements --- recognition/adni_convnext_47280647/train.py | 1 + 1 file changed, 1 insertion(+) diff --git a/recognition/adni_convnext_47280647/train.py b/recognition/adni_convnext_47280647/train.py index 8d8a5b0c5..79494e911 100644 --- a/recognition/adni_convnext_47280647/train.py +++ b/recognition/adni_convnext_47280647/train.py @@ -58,6 +58,7 @@ def main(): set_seed(args.seed) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Using device: {device}") os.makedirs(args.save_dir, exist_ok=True) with open(os.path.join(args.save_dir, "config.json"), "w") as f: json.dump(vars(args), f, indent=2) From 07bcc3f3f052acb51a153fbcbb340d1677614f3e Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Wed, 5 Nov 2025 12:25:14 +1000 Subject: [PATCH 21/31] fixed model naming bug --- recognition/adni_convnext_47280647/modules.py | 12 ++++++------ recognition/adni_convnext_47280647/train.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/recognition/adni_convnext_47280647/modules.py b/recognition/adni_convnext_47280647/modules.py index fba36bb9f..4b1295773 100644 --- a/recognition/adni_convnext_47280647/modules.py +++ b/recognition/adni_convnext_47280647/modules.py @@ -176,19 +176,19 @@ def forward(self, x: torch.Tensor): # 3) Factory & utility # ------------------------------ def build_model( + name: str = "tiny", in_chans: int = 1, num_classes: int = 2, head_dropout: float = 0.0, - model: str = "tiny", # "tiny" or "nextlite_tiny" ) -> nn.Module: """ Backward-compatible factory. - - model="tiny" -> TinyCNN (baseline) - - model="nextlite_tiny" -> ConvNeXtLite + - name="tiny" -> TinyCNN (baseline) + - name="nextlite_tiny" -> ConvNeXtLite """ - if model == "tiny": + if name == "tiny": return TinyCNN(in_chans=in_chans, num_classes=num_classes, head_dropout=head_dropout) - elif model == "nextlite_tiny": + elif name == "nextlite_tiny": return ConvNeXtLite( in_chans=in_chans, num_classes=num_classes, @@ -199,7 +199,7 @@ def build_model( layer_scale_init=1e-6, ) else: - raise ValueError(f"Unknown model '{model}' (use 'tiny' or 'nextlite_tiny').") + raise ValueError(f"Unknown model '{name}' (use 'tiny' or 'nextlite_tiny').") def count_params(model: nn.Module) -> int: return sum(p.numel() for p in model.parameters() if p.requires_grad) diff --git a/recognition/adni_convnext_47280647/train.py b/recognition/adni_convnext_47280647/train.py index 79494e911..027b9cc42 100644 --- a/recognition/adni_convnext_47280647/train.py +++ b/recognition/adni_convnext_47280647/train.py @@ -51,7 +51,7 @@ def main(): ap.add_argument("--test_ratio", type=float, default=0.1) ap.add_argument("--augment", action="store_true") ap.add_argument("--model", type=str, default="tiny", - choices=["tiny","nextlite_tiny","nextlite_small"], + choices=["tiny","nextlite_tiny"], help="Which model to build") args = ap.parse_args() From ef7a82263ec1e45ebfe8d89b82881330f40202b5 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Wed, 5 Nov 2025 18:55:00 +1000 Subject: [PATCH 22/31] fixed dataset bug resulting in extremely accurate results --- .../__pycache__/dataset.cpython-313.pyc | Bin 2569 -> 12935 bytes recognition/adni_convnext_47280647/dataset.py | 19 ++++++++++-------- recognition/adni_convnext_47280647/modules.py | 11 ++++++---- recognition/adni_convnext_47280647/predict.py | 13 +++++++++++- recognition/adni_convnext_47280647/train.py | 11 +++++++++- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/recognition/adni_convnext_47280647/__pycache__/dataset.cpython-313.pyc b/recognition/adni_convnext_47280647/__pycache__/dataset.cpython-313.pyc index 2c27f4ece562a927085ddfc389769ce367a3b67a..c744232ca6511f0af83167b57a9d755c0737e4bc 100644 GIT binary patch literal 12935 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 ChannelNorm(dims[0]), + nn.SiLU(), ) # Downsample layers between stages: 2x2 stride-2 convs @@ -166,7 +167,7 @@ def forward_features(self, x: torch.Tensor) -> torch.Tensor: def forward(self, x: torch.Tensor): feats = self.forward_features(x) - feats = self.head_norm(feats[:, :, None, None]).squeeze(-1).squeeze(-1) # norm in NCHW path + feats = self.head_norm(feats[:, :, None, None]).squeeze(-1).squeeze(-1) feats = self.head_drop(feats) logits = self.head_fc(feats) probs = F.softmax(logits, dim=1) @@ -180,6 +181,8 @@ def build_model( in_chans: int = 1, num_classes: int = 2, head_dropout: float = 0.0, + drop_path_rate: float = 0.15, + layer_scale_init: float = 1e-6, ) -> nn.Module: """ Backward-compatible factory. @@ -194,9 +197,9 @@ def build_model( num_classes=num_classes, depths=[2, 4, 8, 2], dims=[80, 160, 320, 640], - drop_path_rate=0.15, - head_dropout=max(head_dropout, 0.2), - layer_scale_init=1e-6, + drop_path_rate=drop_path_rate, + head_dropout=head_dropout, + layer_scale_init=layer_scale_init, ) else: raise ValueError(f"Unknown model '{name}' (use 'tiny' or 'nextlite_tiny').") diff --git a/recognition/adni_convnext_47280647/predict.py b/recognition/adni_convnext_47280647/predict.py index 0de576c54..d1c0c174c 100644 --- a/recognition/adni_convnext_47280647/predict.py +++ b/recognition/adni_convnext_47280647/predict.py @@ -8,6 +8,10 @@ def main(): ap.add_argument("--checkpoint", type=str, default="results/run1/best.pt") ap.add_argument("--dataset", type=str, default="random", choices=["random","adni"]) ap.add_argument("--batch_size", type=int, default=32) + ap.add_argument("--model", type=str, default="tiny", choices=["tiny","nextlite_tiny"]) + ap.add_argument("--head_dropout", type=float, default=0.0) + ap.add_argument("--drop_path_rate", type=float, default=0.15) + ap.add_argument("--layer_scale_init", type=float, default=1e-6) # ADNI flags (only used if dataset=adni) ap.add_argument("--data_root", type=str, default=None) ap.add_argument("--labels_csv", type=str, default=None) @@ -34,7 +38,14 @@ def main(): loader = te if (args.dataset == "adni" and te is not None) else va ckpt = torch.load(args.checkpoint, map_location="cpu") - model = build_model().to(device) + model = build_model( + name=args.model, + in_chans=1, + num_classes=2, + head_dropout=args.head_dropout, + drop_path_rate=args.drop_path_rate, + layer_scale_init=args.layer_scale_init, + ).to(device) model.load_state_dict(ckpt["state_dict"]) model.eval() diff --git a/recognition/adni_convnext_47280647/train.py b/recognition/adni_convnext_47280647/train.py index 027b9cc42..154028f54 100644 --- a/recognition/adni_convnext_47280647/train.py +++ b/recognition/adni_convnext_47280647/train.py @@ -38,6 +38,8 @@ def main(): ap.add_argument("--lr", type=float, default=1e-3) ap.add_argument("--seed", type=int, default=42) ap.add_argument("--head_dropout", type=float, default=0.0) + ap.add_argument("--drop_path_rate", type=float, default=0.15) + ap.add_argument("--layer_scale_init", type=float, default=1e-6) ap.add_argument("--save_dir", type=str, default="results/run1") # data (ADNI) ap.add_argument("--data_root", type=str, default=None) @@ -73,7 +75,14 @@ def main(): augment=args.augment ) - model = build_model(name=args.model, in_chans=1, num_classes=2, head_dropout=args.head_dropout).to(device) + model = build_model( + name=args.model, + in_chans=1, + num_classes=2, + head_dropout=args.head_dropout, + drop_path_rate=args.drop_path_rate, + layer_scale_init=args.layer_scale_init, + ).to(device) print(f"Model params: {count_params(model):,}") optim = torch.optim.Adam(model.parameters(), lr=args.lr) criterion = nn.CrossEntropyLoss() From f6a70bfba045a4989d877e1e2bbdd86586a47f92 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Wed, 5 Nov 2025 22:32:38 +1000 Subject: [PATCH 23/31] fixing dataset: using subject-wise split to stop leakage during training --- recognition/adni_convnext_47280647/dataset.py | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/recognition/adni_convnext_47280647/dataset.py b/recognition/adni_convnext_47280647/dataset.py index 599429a26..c3cc94b4d 100644 --- a/recognition/adni_convnext_47280647/dataset.py +++ b/recognition/adni_convnext_47280647/dataset.py @@ -66,6 +66,38 @@ def _list_images_under(root: str, class_map: Dict[str,int]) -> List[Tuple[str,in items.append((p, label)) return items +def _extract_subject_id(path: str) -> str: + """Heuristic subject ID from path: prefer folder just under AD/ or NC/; fallback to filename stem prefix before '_'""" + cls_names = {"AD", "NC"} + parts = os.path.normpath(path).split(os.sep) + for i, part in enumerate(parts): + if part in cls_names: + if i + 1 < len(parts) - 1: + return parts[i + 1] + break + stem = os.path.splitext(os.path.basename(path))[0] + return stem.split("_")[0] + +def _split_items_by_subject(items: List[Tuple[str,int]], val_ratio: float, seed: int): + from collections import defaultdict + subj_to_items: Dict[str, List[Tuple[str,int]]] = defaultdict(list) + for p, y in items: + s = _extract_subject_id(p) + subj_to_items[s].append((p, y)) + subjects = list(subj_to_items.keys()) + rng = random.Random(seed) + rng.shuffle(subjects) + n_val = int(round(len(subjects) * val_ratio)) + val_subjects = set(subjects[:n_val]) + train_subjects = set(subjects[n_val:]) + tr_items: List[Tuple[str,int]] = [] + va_items: List[Tuple[str,int]] = [] + for s in train_subjects: + tr_items.extend(subj_to_items[s]) + for s in val_subjects: + va_items.extend(subj_to_items[s]) + return tr_items, va_items + class ADNIImageDataset(Dataset): """Each image file is one sample -> returns (x, y).""" def __init__(self, items: List[Tuple[str,int]], args: ADNIArgs, split: str): @@ -90,7 +122,8 @@ def __getitem__(self, i): if self.args.augment and self.split == "train": if random.random() < 0.5: t = torch.flip(t, dims=[2]) - return t.float(), int(y) + sid = _extract_subject_id(path) + return t.float(), int(y), sid def _split_items(items: List, val_ratio: float, test_ratio: float, seed: int): rng = random.Random(seed) @@ -116,7 +149,7 @@ def build_loaders_adni(args: ADNIArgs): te_items = _list_images_under(test_root, class_map) if not tr_items: raise RuntimeError(f"No images found under: {train_root}") if not te_items: raise RuntimeError(f"No images found under: {test_root}") - tr_split, va_split, _ = _split_items(tr_items, args.val_ratio, 0.0, args.seed) + tr_split, va_split = _split_items_by_subject(tr_items, args.val_ratio, args.seed) print(f"[ADNI IMAGES] train={len(tr_split)} val={len(va_split)} test={len(te_items)}") return ( DataLoader(ADNIImageDataset(tr_split, args, split="train"), batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True), @@ -135,7 +168,7 @@ def build_loaders_adni(args: ADNIArgs): if not sp_items: raise RuntimeError(f"No images found under: {split_root}") if not ot_items: raise RuntimeError(f"No images found under: {other_root}") if base == "train": - tr_split, va_split, _ = _split_items(sp_items, args.val_ratio, 0.0, args.seed) + tr_split, va_split = _split_items_by_subject(sp_items, args.val_ratio, args.seed) print(f"[ADNI IMAGES] train={len(tr_split)} val={len(va_split)} test={len(ot_items)}") return ( DataLoader(ADNIImageDataset(tr_split, args, split="train"), batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True), @@ -143,7 +176,7 @@ def build_loaders_adni(args: ADNIArgs): DataLoader(ADNIImageDataset(ot_items, args, split="test"), batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True), ) else: - tr_split, va_split, _ = _split_items(ot_items, args.val_ratio, 0.0, args.seed) + tr_split, va_split = _split_items_by_subject(ot_items, args.val_ratio, args.seed) print(f"[ADNI IMAGES] train={len(tr_split)} val={len(va_split)} test={len(sp_items)}") return ( DataLoader(ADNIImageDataset(tr_split, args, split="train"), batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True), From 08ad1bac2b6c20b3ed0983cadaa502a03624e791 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Thu, 6 Nov 2025 09:34:55 +1000 Subject: [PATCH 24/31] added basic report structure to README.md mentioning key results and insights --- recognition/adni_convnext_47280647/README.md | 111 +++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/recognition/adni_convnext_47280647/README.md b/recognition/adni_convnext_47280647/README.md index e69de29bb..34a7d3341 100644 --- a/recognition/adni_convnext_47280647/README.md +++ b/recognition/adni_convnext_47280647/README.md @@ -0,0 +1,111 @@ +# ADNI ConvNeXtLite Classifier (Problem 8) + +This subproject trains a compact ConvNeXt-like CNN to classify AD vs NC on ADNI MRI slices. It includes a subject-wise validation split to prevent leakage and uses a held-out test/ folder for final evaluation. + +## Project layout +- `modules.py`: TinyCNN (baseline) and ConvNeXtLite model +- `dataset.py`: ADNI loaders, preprocessing, augmentation, subject-wise val split; returns `(x, y, subject_id)` +- `train.py`: training loop, validation, checkpointing (best.pt), plots +- `predict.py`: evaluation on test (slice-level and patient-level) +- `results/`: run outputs (config.json, curves, best.pt) + +## Data expectations +Directory with separate train/ and test/: +``` +AD_NC/ + train/AD/**, train/NC/** + test/AD/**, test/NC/** +``` + +## Preprocessing and split +- Grayscale → [0,1] → resize 224×224 → standardize `(x - 0.5)/0.25` +- Augmentation (train only): horizontal flip p=0.5 +- Validation split: subject-wise from the train pool (prevents leakage) +- Test set: entire `test/` folder, never used in training/validation + +## Typical training config +ConvNeXtLite, grayscale (1×224×224): +``` +python train.py \ + --dataset adni \ + --data_root /home/groups/comp3710/ADNI/AD_NC \ + --model nextlite_tiny \ + --epochs 25 \ + --batch_size 32 --num_workers 1 \ + --lr 3e-4 \ + --head_dropout 0.2 \ + --drop_path_rate 0.15 \ + --layer_scale_init 1e-6 \ + --augment \ + --save_dir results/nextlite_example +``` + +## Evaluation on test +``` +python predict.py \ + --checkpoint results/nextlite_example/best.pt \ + --dataset adni \ + --data_root /home/groups/comp3710/ADNI/AD_NC \ + --batch_size 64 --num_workers 1 \ + --model nextlite_tiny \ + --head_dropout 0.2 --drop_path_rate 0.15 --layer_scale_init 1e-6 +``` +Outputs slice-level accuracy and patient-level accuracy (subject aggregation). + +## Experimental Results + +### Key Finding: Validation Leakage Fix +Initial runs used image-wise validation splitting, causing the same subjects to appear in both train and val, inflating validation accuracy to ~0.98–0.99 while test accuracy remained ~0.65–0.70. After switching to subject-wise splitting, validation accuracy dropped to realistic levels (~0.80) and aligned with test performance. + +### Detailed Run Results + +#### Run 1: `a100_nextlite_p8_v1` (10 epochs, seed=42, lr=3e-4, head_dropout=0.2, drop_path=0.15) +- **Issue**: Image-wise val split (leakage) +- **Best val accuracy**: 0.861 (epoch 10) +- **Test slice accuracy**: ~0.698 +- **Observation**: Large gap (16+ pts) indicates leakage + +#### Run 2: `a100_nextlite_p8_v1_seed123` (25 epochs, seed=123, same config) +- **Issue**: Image-wise val split (leakage) +- **Best val accuracy**: 0.989 (epoch 25) +- **Test slice accuracy**: 0.645 +- **Observation**: Extreme gap (34+ pts) confirms leakage + +#### Run 3: `a100_nextlite_subjectsplit_v1` (25 epochs, seed=123, lr=3e-4, head_dropout=0.2, drop_path=0.15) +- **Fix**: Subject-wise val split implemented +- **Best val accuracy**: 0.797 (epoch 19) +- **Test slice accuracy**: 0.653 +- **Test patient accuracy**: 0.667 (450 subjects) +- **Observation**: Gap reduced to ~14 pts; plausible given domain shift + +#### Run 4: `a100_nextlite_lr1e-4_hd0.3_dp0.2_s42` (25 epochs, seed=42, lr=1e-4, head_dropout=0.3, drop_path=0.2) +- **Best val accuracy**: 0.806 (epoch 21) +- **Test slice accuracy**: 0.653 +- **Test patient accuracy**: 0.689 (450 subjects) +- **Observation**: Stronger regularization (higher dropout/drop_path) improved patient-level accuracy slightly + +#### Run 5: `a100_nextlite_fix_impl` (10 epochs, seed=42, lr=3e-3, all reg disabled) +- **Issue**: Initial broken config (no regularization, high LR) +- **Result**: Stuck at ~0.693 loss, ~0.50 accuracy (not learning) +- **Fix**: Standardized inputs and restored head normalization enabled learning + +### Summary +- **Test slice accuracy**: ~0.65 consistently across runs (subject-wise split) +- **Test patient accuracy**: ~0.67–0.69 (aggregation helps) +- **Validation accuracy**: ~0.80 (realistic after leakage fix) +- **Gap explanation**: ~10–15 pts between val and test likely due to: + 1. Domain shift between train/ and test/ folders (different sites/scanners) + 2. Per-slice label noise (patient aggregation improves results) + 3. Overfitting to train distribution after first val peak + +**Recommendation**: Always report test set accuracy as the primary metric. Validation is for model selection only. + +## Knobs to reach ~80% target +- Try sweeps: `lr ∈ {1e-4, 2e-4}`, `head_dropout=0.3`, `drop_path_rate ∈ {0.2, 0.25}` +- Optionally add weight decay (e.g., 1e-4) and mild rotations/brightness jitter +- Train 35–50 epochs; rely on best.pt (early stopping by val) + +## Reproducibility +- Seeds set for Python/NumPy/Torch; cuDNN deterministic +- All CLI args saved to `results//config.json` + From a7b21b1c614593883b7fba96417c4b363f2b45a5 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Thu, 6 Nov 2025 11:43:07 +1000 Subject: [PATCH 25/31] updating gitignore for logs and some QOL stuff --- recognition/adni_convnext_47280647/.gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/recognition/adni_convnext_47280647/.gitignore b/recognition/adni_convnext_47280647/.gitignore index f72ab2d69..f578dab0a 100644 --- a/recognition/adni_convnext_47280647/.gitignore +++ b/recognition/adni_convnext_47280647/.gitignore @@ -14,4 +14,8 @@ checkpoints/ # data (never commit datasets) data/ datasets/ -ADNI/ \ No newline at end of file +ADNI/ + +# logs and QOL stuff +logs/ +COMP3710_Report_v1.64_Final.pdf \ No newline at end of file From 2d971ac1c7dc24e0146d68436f82bd0224049917 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Thu, 6 Nov 2025 12:27:18 +1000 Subject: [PATCH 26/31] revised structure for the final report --- recognition/adni_convnext_47280647/README.md | 191 +++++++++---------- 1 file changed, 87 insertions(+), 104 deletions(-) diff --git a/recognition/adni_convnext_47280647/README.md b/recognition/adni_convnext_47280647/README.md index 34a7d3341..cf34e3c49 100644 --- a/recognition/adni_convnext_47280647/README.md +++ b/recognition/adni_convnext_47280647/README.md @@ -1,111 +1,94 @@ -# ADNI ConvNeXtLite Classifier (Problem 8) +# ADNI ConvNeXtLite Classifier – Problem 8 (COMP3710) -This subproject trains a compact ConvNeXt-like CNN to classify AD vs NC on ADNI MRI slices. It includes a subject-wise validation split to prevent leakage and uses a held-out test/ folder for final evaluation. +Author: Shivam Garg +Student Number: 47280647 -## Project layout +## Table of Contents +1. [Executive Summary](#1-executive-summary) +2. [Problem Definition](#2-problem-definition) + 1. [Problem Statement](#21-problem-statement) + 2. [Dataset Overview](#22-dataset-overview) +3. [Methodology Overview](#3-methodology-overview) +4. [Data Pipeline](#4-data-pipeline) + 1. [Ingestion & Directory Layout](#41-ingestion--directory-layout) + 2. [Pre-processing](#42-pre-processing) + 3. [Augmentation](#43-augmentation) +5. [Model Architecture](#5-model-architecture) + 1. [TinyCNN Baseline](#51-tinycnn-baseline) + 2. [ConvNeXtLite Classifier](#52-convnextlite-classifier) +6. [Training Configuration & Implementation](#6-training-configuration--implementation) +7. [Evaluation Protocol](#7-evaluation-protocol) +8. [Experiments & Results](#8-experiments--results) + 1. [Training Curves](#81-training-curves) + 2. [Validation & Test Metrics](#82-validation--test-metrics) + 3. [Ablations & Comparisons](#83-ablations--comparisons) +9. [Discussion](#9-discussion) +10. [Usage Guide](#10-usage-guide) + 1. [Environment Setup](#101-environment-setup) + 2. [Training Commands](#102-training-commands) + 3. [Evaluation Commands](#103-evaluation-commands) +11. [Reproducibility Checklist](#11-reproducibility-checklist) +12. [Dependencies](#12-dependencies) +13. [References](#13-references) + +--- + +## 1. Executive Summary + + +## 2. Problem Definition + +### 2.1 Problem Statement + +### 2.2 Dataset Overview + +## 3. Methodology Overview + +## 4. Data Pipeline + +### 4.1 Ingestion & Directory Layout + +### 4.2 Pre-processing + +### 4.3 Augmentation + +## 5. Model Architecture + +### 5.1 TinyCNN Baseline + +### 5.2 ConvNeXtLite Classifier + +## 6. Training Configuration & Implementation + +## 7. Evaluation Protocol + +## 8. Experiments & Results + +### 8.1 Training Curves + +### 8.2 Validation & Test Metrics + +### 8.3 Ablations & Comparisons + +## 9. Discussion + +## 10. Usage Guide + +### 10.1 Environment Setup + +### 10.2 Training Commands + +### 10.3 Evaluation Commands + +## 11. Reproducibility Checklist + +## 12. Dependencies + +## 13. References + +## Current Implementation Notes (for reference) - `modules.py`: TinyCNN (baseline) and ConvNeXtLite model - `dataset.py`: ADNI loaders, preprocessing, augmentation, subject-wise val split; returns `(x, y, subject_id)` - `train.py`: training loop, validation, checkpointing (best.pt), plots - `predict.py`: evaluation on test (slice-level and patient-level) - `results/`: run outputs (config.json, curves, best.pt) - -## Data expectations -Directory with separate train/ and test/: -``` -AD_NC/ - train/AD/**, train/NC/** - test/AD/**, test/NC/** -``` - -## Preprocessing and split -- Grayscale → [0,1] → resize 224×224 → standardize `(x - 0.5)/0.25` -- Augmentation (train only): horizontal flip p=0.5 -- Validation split: subject-wise from the train pool (prevents leakage) -- Test set: entire `test/` folder, never used in training/validation - -## Typical training config -ConvNeXtLite, grayscale (1×224×224): -``` -python train.py \ - --dataset adni \ - --data_root /home/groups/comp3710/ADNI/AD_NC \ - --model nextlite_tiny \ - --epochs 25 \ - --batch_size 32 --num_workers 1 \ - --lr 3e-4 \ - --head_dropout 0.2 \ - --drop_path_rate 0.15 \ - --layer_scale_init 1e-6 \ - --augment \ - --save_dir results/nextlite_example -``` - -## Evaluation on test -``` -python predict.py \ - --checkpoint results/nextlite_example/best.pt \ - --dataset adni \ - --data_root /home/groups/comp3710/ADNI/AD_NC \ - --batch_size 64 --num_workers 1 \ - --model nextlite_tiny \ - --head_dropout 0.2 --drop_path_rate 0.15 --layer_scale_init 1e-6 -``` -Outputs slice-level accuracy and patient-level accuracy (subject aggregation). - -## Experimental Results - -### Key Finding: Validation Leakage Fix -Initial runs used image-wise validation splitting, causing the same subjects to appear in both train and val, inflating validation accuracy to ~0.98–0.99 while test accuracy remained ~0.65–0.70. After switching to subject-wise splitting, validation accuracy dropped to realistic levels (~0.80) and aligned with test performance. - -### Detailed Run Results - -#### Run 1: `a100_nextlite_p8_v1` (10 epochs, seed=42, lr=3e-4, head_dropout=0.2, drop_path=0.15) -- **Issue**: Image-wise val split (leakage) -- **Best val accuracy**: 0.861 (epoch 10) -- **Test slice accuracy**: ~0.698 -- **Observation**: Large gap (16+ pts) indicates leakage - -#### Run 2: `a100_nextlite_p8_v1_seed123` (25 epochs, seed=123, same config) -- **Issue**: Image-wise val split (leakage) -- **Best val accuracy**: 0.989 (epoch 25) -- **Test slice accuracy**: 0.645 -- **Observation**: Extreme gap (34+ pts) confirms leakage - -#### Run 3: `a100_nextlite_subjectsplit_v1` (25 epochs, seed=123, lr=3e-4, head_dropout=0.2, drop_path=0.15) -- **Fix**: Subject-wise val split implemented -- **Best val accuracy**: 0.797 (epoch 19) -- **Test slice accuracy**: 0.653 -- **Test patient accuracy**: 0.667 (450 subjects) -- **Observation**: Gap reduced to ~14 pts; plausible given domain shift - -#### Run 4: `a100_nextlite_lr1e-4_hd0.3_dp0.2_s42` (25 epochs, seed=42, lr=1e-4, head_dropout=0.3, drop_path=0.2) -- **Best val accuracy**: 0.806 (epoch 21) -- **Test slice accuracy**: 0.653 -- **Test patient accuracy**: 0.689 (450 subjects) -- **Observation**: Stronger regularization (higher dropout/drop_path) improved patient-level accuracy slightly - -#### Run 5: `a100_nextlite_fix_impl` (10 epochs, seed=42, lr=3e-3, all reg disabled) -- **Issue**: Initial broken config (no regularization, high LR) -- **Result**: Stuck at ~0.693 loss, ~0.50 accuracy (not learning) -- **Fix**: Standardized inputs and restored head normalization enabled learning - -### Summary -- **Test slice accuracy**: ~0.65 consistently across runs (subject-wise split) -- **Test patient accuracy**: ~0.67–0.69 (aggregation helps) -- **Validation accuracy**: ~0.80 (realistic after leakage fix) -- **Gap explanation**: ~10–15 pts between val and test likely due to: - 1. Domain shift between train/ and test/ folders (different sites/scanners) - 2. Per-slice label noise (patient aggregation improves results) - 3. Overfitting to train distribution after first val peak - -**Recommendation**: Always report test set accuracy as the primary metric. Validation is for model selection only. - -## Knobs to reach ~80% target -- Try sweeps: `lr ∈ {1e-4, 2e-4}`, `head_dropout=0.3`, `drop_path_rate ∈ {0.2, 0.25}` -- Optionally add weight decay (e.g., 1e-4) and mild rotations/brightness jitter -- Train 35–50 epochs; rely on best.pt (early stopping by val) - -## Reproducibility -- Seeds set for Python/NumPy/Torch; cuDNN deterministic -- All CLI args saved to `results//config.json` - From 1e01ccd70add4500d8d7475910116c8e8a3379e9 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Thu, 6 Nov 2025 13:14:53 +1000 Subject: [PATCH 27/31] finalised the report structure and completed the executive summary --- recognition/adni_convnext_47280647/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/recognition/adni_convnext_47280647/README.md b/recognition/adni_convnext_47280647/README.md index cf34e3c49..57338a761 100644 --- a/recognition/adni_convnext_47280647/README.md +++ b/recognition/adni_convnext_47280647/README.md @@ -34,12 +34,15 @@ Student Number: 47280647 --- ## 1. Executive Summary +This project tackles **binary classification of Alzheimer's Disease (abbreviated as AD) vs Cognitively Normal (abbreviated as CN)** from **ADNI MRI 2D slices** data, targeting ≥ **80%** test accuracy on a strictly **patient-wise held-out** dataset. This implementation follows a leakage-safe pipeline including grayscale conversion, 224x224 resizing, normalisation (x-0.5)/0.25, and ligt MRI-appropriate augmentation, paired with strict **subject-wise** splits to prevent data leakage (patient overlap) across training, validation, and testing set. +Two models are implemented to bracket performance and guide design choices. A compact **TinyCNN** provides a clear, reproducible baseline. A **ConvNeXtLite** classifier then scales representational capacity using modern CNN components (eg., depthwise convolutions, LayerNorm, larger kernels) to better capture subtle brain textures. Training is implemented in PyTorch with **Adam**, checkpointing, seeded runs, and automatic curve exports. ## 2. Problem Definition ### 2.1 Problem Statement + ### 2.2 Dataset Overview ## 3. Methodology Overview From a345db56462fef03fc0cc92bcf95b4105d5950a3 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Thu, 6 Nov 2025 13:29:21 +1000 Subject: [PATCH 28/31] completed problem definition and methodology overview --- recognition/adni_convnext_47280647/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/recognition/adni_convnext_47280647/README.md b/recognition/adni_convnext_47280647/README.md index 57338a761..68c28985c 100644 --- a/recognition/adni_convnext_47280647/README.md +++ b/recognition/adni_convnext_47280647/README.md @@ -41,11 +41,15 @@ Two models are implemented to bracket performance and guide design choices. A co ## 2. Problem Definition ### 2.1 Problem Statement - +The task is **binary classification** of brain MRI slices into AD (Alzheimer's Disease) and CN (Cognitively Normal). Inputs are 2D axial slices derived from the ADNI scans; the output is a single class label per slice, with patient-lavel reporting obtained by aggregating slide predictions per subject. The primary objective is ≥ 0.80 accuracy on a strict patient held out test set (to prevent data leakage). ### 2.2 Dataset Overview +The data is categorised as follows - +- **Souces and Classes**: The dataset is a two-class subset of ADNI with labels AD and CN. Each subjec contributes a 3D MRI volume from which 2D axial slices are extracted for training and evaluation. +- **Data units**: Trainint operates at the slide level. Evaluation includes bnoth slice-level and patient-level (aggregated) metrics ## 3. Methodology Overview +The approach is an end-to-end pipeline that turn ADNI MRI 2D slices into patient-levl AD?/CN predictions while preventing data leakage and keeping runs easy to reproduce. It combined a transparent TinyCNN baseline with a stronger ConvNeXtLite classifier to bracket performance. ## 4. Data Pipeline From 8cefedafdc7a7847130ff27530686c98b63a8722 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Thu, 6 Nov 2025 14:24:46 +1000 Subject: [PATCH 29/31] added images folder, complete half the final report --- recognition/adni_convnext_47280647/README.md | 127 +++++++++++++++++- .../__pycache__/modules.cpython-313.pyc | Bin 3251 -> 13698 bytes .../images/leakage_acc_curve.png | Bin 0 -> 26887 bytes ...rerun_lr1e-4_hd0.3_dp0.2_s42_acc_curve.png | Bin 0 -> 27012 bytes ...erun_lr1e-4_hd0.3_dp0.2_s42_loss_curve.png | Bin 0 -> 28872 bytes ...bjectsplit_lr3e-4_hd0.2_s123_acc_curve.png | Bin 0 -> 26733 bytes ...jectsplit_lr3e-4_hd0.2_s123_loss_curve.png | Bin 0 -> 25509 bytes 7 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 recognition/adni_convnext_47280647/images/leakage_acc_curve.png create mode 100644 recognition/adni_convnext_47280647/images/rerun_lr1e-4_hd0.3_dp0.2_s42_acc_curve.png create mode 100644 recognition/adni_convnext_47280647/images/rerun_lr1e-4_hd0.3_dp0.2_s42_loss_curve.png create mode 100644 recognition/adni_convnext_47280647/images/rerun_subjectsplit_lr3e-4_hd0.2_s123_acc_curve.png create mode 100644 recognition/adni_convnext_47280647/images/rerun_subjectsplit_lr3e-4_hd0.2_s123_loss_curve.png diff --git a/recognition/adni_convnext_47280647/README.md b/recognition/adni_convnext_47280647/README.md index 68c28985c..3484965f8 100644 --- a/recognition/adni_convnext_47280647/README.md +++ b/recognition/adni_convnext_47280647/README.md @@ -22,7 +22,7 @@ Student Number: 47280647 1. [Training Curves](#81-training-curves) 2. [Validation & Test Metrics](#82-validation--test-metrics) 3. [Ablations & Comparisons](#83-ablations--comparisons) -9. [Discussion](#9-discussion) +9. [Future Improvements](#9-future-improvements) 10. [Usage Guide](#10-usage-guide) 1. [Environment Setup](#101-environment-setup) 2. [Training Commands](#102-training-commands) @@ -54,38 +54,157 @@ The approach is an end-to-end pipeline that turn ADNI MRI 2D slices into patient ## 4. Data Pipeline ### 4.1 Ingestion & Directory Layout +- **Accepted layouts**: the loader works when `DATA_ROOT` is either the parent folder `AD_NC/` or one of its child splits (`AD_NC/train` or `AD_NC/test`). Two structures are therefore supported: + 1. `AD_NC/train/{AD,NC}/**/*.jpg|png` and `AD_NC/test/{AD,NC}/**/*.jpg|png`. + 2. A single pool with only `AD_NC/{AD,NC}/...`; in this case we derive validation and test partitions via subject-wise random splits. +- **Subject IDs**: for each image path we infer the subject identifier by taking the directory immediately below `AD/` or `NC/` when available; otherwise we fall back to the filename prefix before the first underscore (e.g. `1003730_94.jpeg → subject 1003730`). This heuristic mirrors `_extract_subject_id` in `dataset.py`. +- **Split construction**: the loader first gathers all `(path, label)` pairs under the requested root, groups them by subject, and then: + - if both `train/` and `test/` trees exist, uses `train/` for training + validation (with subject-wise splitting) and leaves `test/` untouched for the held-out evaluation set; + - otherwise performs a subject-wise shuffle to carve out validation and test sets according to `val_ratio` and `test_ratio`. +Each subject therefore appears in exactly one split, eliminating patient-level leakage. ### 4.2 Pre-processing +Every slice follows the same deterministic transform chain: +1. open via PIL with `.convert("L")` to force grayscale, +2. convert to a NumPy array and scale intensities to `[0, 1]`, +3. wrap into a PyTorch tensor of shape `(1, H, W)`, +4. resize to `224×224` using `torch.nn.functional.interpolate`, +5. standardise with `(x - 0.5) / 0.25`, matching the statistics used during model development. +The dataset returns `(tensor, label, subject_id)` so downstream evaluation can aggregate logits slice-wise or subject-wise without additional bookkeeping. ### 4.3 Augmentation +Augmentation is deliberately minimal to respect anatomical structure. When `--augment` is passed and the split is `train`, we flip slices horizontally with probability 0.5. No rotations, elastic deformations, or intensity jitter are applied in this version, keeping the pipeline stable and reproducible while still injecting minor invariance to left–right orientation. ## 5. Model Architecture ### 5.1 TinyCNN Baseline +Implemented in `modules.py:9`, the baseline serves as a sanity check for the full pipeline: +- **Structure**: three 3×3 convolutions with ReLU activations and two max-pooling stages reduce the 224×224 input to a 1×1 feature map, followed by an optional dropout layer and a fully connected head. +- **Capacity**: 23,426 trainable parameters, small enough to train quickly on CPU/GPU while exposing integration bugs early. +- **Usage**: invoked via `--model tiny`; helpful for validating the subject-wise split logic and plotting scripts before expensive ConvNeXtLite runs. + +This model was implemented to verify the data pipeline structure and essentially recrate the training process without the actual intention of training. ### 5.2 ConvNeXtLite Classifier +The primary model (refer `modules.py:93`) adapts ConvNeXt ideas to single-channel medical slices: +- **Two-step stem**: consecutive 3×3 stride-2 convolutions expand the channel count to 80 while downsampling the spatial resolution to 56×56, each followed by GroupNorm-as-LayerNorm and SiLU activation. +- **Stage layout**: four stages with depths `[2, 4, 8, 2]` and channel widths `[80, 160, 320, 640]`. Each block applies a 5×5 depthwise convolution, ChannelNorm, 1×1 expansion to 4× width, SiLU, 1×1 projection, per-channel layer scaling, and stochastic depth. +- **Regularisation**: head dropout (configurable via `--head_dropout`) and a linear drop-path schedule (`--drop_path_rate`) mitigate overfitting, while layer-scale (initialised to 1e-6) stabilises training. +- **Head**: global average pooling, ChannelNorm, dropout, and a linear classifier produce slice logits; probabilities are obtained with softmax for reporting. +- **Parameter count**: 15,296,082 trainable parameters, giving significantly higher capacity than TinyCNN while remaining feasible on a single A100 with batch sizes up to 32. + +Both architectures are exposed through `modules.build_model`, enabling cli selection and consistent metric logging. ## 6. Training Configuration & Implementation +- `train.py` is entirely CLI-driven; every run writes its resolved arguments to `/config.json` (see Usage Guide for the exact commands). +- `set_seed` aligns Python/NumPy/PyTorch RNGs and cuDNN flags for reproducibility; pass `--seed` to control it. +- Models are built via `modules.build_model`, optimised with Adam + cross-entropy on the available device (`cuda` preferred); the relevant flags (`--model`, `--lr`, etc.) are listed later. +- Best validation accuracy triggers a `best.pt` checkpoint while `loss_curve.png` / `acc_curve.png` are saved alongside the config for quick inspection. ## 7. Evaluation Protocol +- Validation accuracy is recorded each epoch on the subject-wise validation loader, matching the metrics printed by `train.py`. +- `predict.py` reloads `best.pt`, rebuilds the model using the supplied hyperparameters, and reports slice-/patient-level accuracy (Usage Guide shows the command). +- Patient accuracy averages logits per subject before argmax, no additional setup required. ## 8. Experiments & Results ### 8.1 Training Curves +Fresh Rangpur reruns regenerate these plots during training; representative copies are checked into `images/`. Both configurations show steadily decreasing training loss with validation accuracy plateauing around the 0.80 mark after ~18–20 epochs, confirming the subject-wise split has removed the earlier leakage spikes. + +- ![Accuracy curve – best run](images/rerun_lr1e-4_hd0.3_dp0.2_s42_acc_curve.png) + *Figure 1: Training/validation accuracy for the strongest configuration (`lr=1e-4`, `head_dropout=0.3`, `drop_path=0.2`).* +- ![Loss curve – best run](images/rerun_lr1e-4_hd0.3_dp0.2_s42_loss_curve.png) + *Figure 2: Corresponding loss curve, showing convergence by ~epoch 20.* +- ![Accuracy curve – subject-split baseline](images/rerun_subjectsplit_lr3e-4_hd0.2_s123_acc_curve.png) + *Figure 3: Accuracy trace for the baseline rerun (`lr=3e-4`, `head_dropout=0.2`, `drop_path=0.15`).* +- ![Loss curve – subject-split baseline](images/rerun_subjectsplit_lr3e-4_hd0.2_s123_loss_curve.png) + *Figure 4: Loss curve for the baseline setting, plateauing slightly earlier.* ### 8.2 Validation & Test Metrics +| Run ID | Epochs | Val acc (slice) | Test acc (slice) | Test acc (patient) | Notes | +|------------------------------------------|:------:|:---------------:|:----------------:|:------------------:|-------| +| `rerun_subjectsplit_lr3e-4_hd0.2_s123` | 25 | 0.797 | 0.653 | 0.667 | Subject-wise split, head dropout 0.2, drop-path 0.15. | +| `rerun_lr1e-4_hd0.3_dp0.2_s42` (best) | 25 | **0.806** | 0.653 | 0.689 | Lower LR plus stronger regularisation; best held-out performance. | -### 8.3 Ablations & Comparisons +Slice metrics come directly from the validation loop / `predict.py` (averaged over slices). Patient metrics are computed by aggregating logits per subject inside `predict.py`. Validation passes the 0.80 mark while held-out test accuracy stabilises around 0.65, highlighting the residual domain gap between train and test folders. -## 9. Discussion +### 8.3 Ablations & Comparisons +- ![Leakage run accuracy](images/leakage_acc_curve.png) + *Figure 5: Example of the leakage-affected run (`a100_nextlite_p8_v1_seed123`) where validation accuracy falsely approaches 1.0 under an image-wise split.* +- **Leakage vs. subject-wise splits**: Prior runs (e.g., the figure above and `a100_nextlite_bs32_lr3e-3_hd0`) used image-wise splits and reported inflated validation accuracy (>0.98) despite ~0.65 test accuracy. Switching to subject-level grouping aligns validation with test performance; those leakage-affected artefacts are kept offline for comparison but excluded from the final metrics. +- **Regularisation sweep**: Increasing head dropout to 0.3 and ramping drop-path to 0.2 (while lowering LR to 1e-4) improves patient-level accuracy from 0.667 → 0.689, suggesting the model benefits from stronger stochastic regularisation. +- **Baseline check**: TinyCNN trains without issue but tops out around 0.58 slice accuracy (not shown); it is mainly useful for validating the pipeline before launching ConvNeXtLite experiments. + +## 9. Future Improvements +- **Reach the ≥0.80 test target**: extend training to 35–40 epochs with early stopping, and explore cosine LR decay to squeeze additional generalisation without violating the leakage-safe split. +- **Richer augmentation**: introduce small rotations (±5°), brightness/contrast jitter, or CutMix/MixUp variants while monitoring patient-level accuracy for regressions. +- **Regularisation tweaks**: add weight decay (~1e-4), experiment with label smoothing, and test moderate dropout in earlier stages to combat overfitting on the training distribution. +- **Subject-level modelling**: aggregate predictions with simple ensembling (multiple seeds) or train a lightweight per-subject classifier on pooled slice features to boost patient accuracy. +- **Data quality pass**: review misclassified subjects for slice outliers or label noise, and consider incorporating additional planes (coronal/sagittal) if ADNI derivatives permit. ## 10. Usage Guide ### 10.1 Environment Setup +- Create/activate a Python env (conda or venv). +- Install dependencies: + ```bash + pip install -r requirements.txt + pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cu121 # adjust CUDA tag if needed + ``` +- Optional: set `PYTHONPATH` to the project root for convenience. ### 10.2 Training Commands +- Baseline rerun: + ```bash + python train.py \ + --dataset adni \ + --data_root /home/groups/comp3710/ADNI/AD_NC \ + --model nextlite_tiny \ + --epochs 25 \ + --batch_size 32 \ + --lr 3e-4 \ + --seed 123 \ + --head_dropout 0.2 \ + --drop_path_rate 0.15 \ + --layer_scale_init 1e-6 \ + --augment \ + --num_workers 1 \ + --save_dir runs/rerun_subjectsplit_lr3e-4_hd0.2_s123 + ``` +- Best-performing rerun: + ```bash + python train.py \ + --dataset adni \ + --data_root /home/groups/comp3710/ADNI/AD_NC \ + --model nextlite_tiny \ + --epochs 25 \ + --batch_size 32 \ + --lr 1e-4 \ + --seed 42 \ + --head_dropout 0.3 \ + --drop_path_rate 0.2 \ + --layer_scale_init 1e-6 \ + --augment \ + --num_workers 1 \ + --save_dir runs/rerun_lr1e-4_hd0.3_dp0.2_s42 + ``` +- Tweak `--batch_size`, `--num_workers`, and `--data_root` to match your environment; each run writes `config.json`, `best.pt`, and curves into `--save_dir`. ### 10.3 Evaluation Commands +- Evaluate the best run on the held-out test split: + ```bash + python predict.py \ + --checkpoint runs/rerun_lr1e-4_hd0.3_dp0.2_s42/best.pt \ + --dataset adni \ + --data_root /home/groups/comp3710/ADNI/AD_NC \ + --model nextlite_tiny \ + --batch_size 64 \ + --num_workers 1 \ + --head_dropout 0.3 \ + --drop_path_rate 0.2 \ + --layer_scale_init 1e-6 + ``` +- Swap the checkpoint path/flags to evaluate other configs. Output prints slice accuracy and patient accuracy (if subject IDs were returned by the loader). ## 11. Reproducibility Checklist @@ -98,4 +217,4 @@ The approach is an end-to-end pipeline that turn ADNI MRI 2D slices into patient - `dataset.py`: ADNI loaders, preprocessing, augmentation, subject-wise val split; returns `(x, y, subject_id)` - `train.py`: training loop, validation, checkpointing (best.pt), plots - `predict.py`: evaluation on test (slice-level and patient-level) -- `results/`: run outputs (config.json, curves, best.pt) +- Per-run outputs (config.json, curves, best.pt) are produced alongside training but left untracked; only the illustrative figures live in `images/`. diff --git a/recognition/adni_convnext_47280647/__pycache__/modules.cpython-313.pyc b/recognition/adni_convnext_47280647/__pycache__/modules.cpython-313.pyc index aa9e097009ce63ea83a86df2fd8c7cd265edca58..1f1b4b801a22bc7f7c7451a4a84cf40d294d3f4f 100644 GIT binary patch literal 13698 zcmc&aX>c3YdAqnz0=&dSw6vl~Q?Nx+q)5q{OxFW2-J6*FA7910hf4U8ArJ3?pLfX1m%^_M ze&xmc=_uhVYMB>JFS4YRuPlxOZ9N?(egoo>vQgGoy^5RE>tkh8PgqI>^V`QT*4)e5 zQOGK;Jex3o8#hNo>50*!|3Y zmN*(1pF_5u2}h@o^!BdWq1OxhPllu6aUsIBKgmHMHxeV4PL2m?6H$^~W1+R(ZzcXPz@NOlPiyV+OUB{T2;P%hc=ZZCbIGsb~Bw&+-P|$eZ?< zbbV2t(9mcGDA~+g_OLoE*LnWZVDS*fyYy|LwX-HOY=ib*CMyOb~FJ%g*9>O;<@~%DXsS4fxL94=4uTcSv<5X*PZK~C> z{1awr_ZvEat}sD6w~}L+QO4))T}2VKinOMuNp7#SfbBnW;9f*(e<2NWg@1SDW3hJ*C7Jlr}K8y8!pvG7#uWHj8$ zw>P)%Xzy%2a_Z#iox9t1w4N3c36Vq(MTN+;6qb})A_illusC4;Rv{D(2ZFGosCYgR z=-A!<*p6KtyIaR&p~;9SwZx~%1{~d+lPxXkw&16e{t3YEUuITp?pbTvR{fr>cBQ)Z zn)|AI#Z_{t`<3pwv6LZoXmKL#YJ0o=o!xKkzTJ`D+4a-(RFlzjN%ZW4E77cXa=>{b&1ryl-jXnV%f|SUjk_>*C&Zaj6X%*S0mgxyCVL z`l>tYVs;&7Zy(BZKJ^+|xN!MGs^{IRHt6*6A3y$+{eaxsvdM$ zIt;_0#-9B}1)d+&2yfV9h#CjB^2QbeCIrTSp>DFHyq58>z=4_NZ)?Vk3!h;k?g8w$h^WWINV31CUUVBQ26ftg?cZIVX^i z>{00@SAaw$^#XXAS$26Zot!&wC-E}pjT@9(ejH_ve#fqNUo@D98Q}9w+xll7- zlWcpnZpO0WEnTo*wkID>32E=P8S`>2m-e(R+1f}Gbm=oH^Tj35NkakUZIlbYfgepi zC-QVu7#D%39f5#C;gEL)0uz%$Bv<1J1V+L{N<@GqqcNzq1_Gg25HiMaG$ETuA~7LB zd4jYPOFaSG8!o!mZQ37Olj$4CbBoYYRU`YcYOGr9T(48Vk1r#82ORWHY z|7GTvrepUEc5`d0Jj=l2rbHii#1HU|=0DhAHn(RP0CefJhafc8lB!tO2o!y^Y0+Vz zCWT2eDJ)=USc@5ZVA>Te2V)0}5HNP^V1_uT8RDd7h=)(az;cjaPo5OxiLq7uMi-h+ zi=E3AtES+=(<%R8PHW)fJmsEfr7%svwH^^CRB<_uwO5wa){e<&k)r4*#?G( z{pm041m2rLZbS__rCZbp^RxY$q{(-}8*z(y6aE245eWpPMNBN-yvNe7x+eMV7*Omv zi3;+nycs(88=67804-U1Ne5t#D%jbB1eZ)%+XoUXBu<5cqHGQ&rsJZ|M!%tKOArC{ zc63xWCt@TxCY#0aIOtllG$w#rKZYH20gxTTpcZ43kw`d@lWT;28`D`)jLW8RK{~r$ zwkhmhRY(9i`9@`-2pxlbb9yfAn%y(}c^!(GapPn)O z+Ea1qrB_}`wq4tOWp}FW%HGt(;+Dm}n-lMx|JM226F)rny>n^Lsk@$M)1GIS1_K$- z3p1u=k9YQbvSIdfOSVl4EtSJ?K9+8g&qdI5Y-`RouY~uRD5#X^~(7b(wmPbbX4O3JcsZ5}S-; z@wT?@+!5biE(&=e622fxoG1jxIP{jFEACtvB%3fY0t$qS3q%-~S{!G_!V(An0td<> z8UhW;fwBmmZUDOFvMv!Eq7?*-5E!1R% z43!=r9tvfo@k26F)e%f_=Zj|t%FxN!1N$D)aKY5hSrKQzc77VhkuAz9eP*3Ft-=bs zwpp?Xw@KwNKUC7DihRYs!F13I*b3bT(S++>z?13$6bM%^-+C_hq)Jmgw;R$G$IyVw zOIh+1G*~BMaf9e4H9+RUrd$I^2BL zEg=bFTDt(CC__6{hW@CSMsenah*42y2d0K75f~s2-l1b^m+bBp2NFHugm^d-3!Ys) z1oOGD;6SepE1JVmDIO+bh#SUgZd(YVGvR13!A%JyEJPE&y$(*XM$UyLk?TC)$u%F~ zbRQUB+s?Of+d9tsFwcj3{8|~<(=XCeDn|IA0QL^zXoe28iLmHVa0?ye5tyB9M^7*i z2a`zlMo|0&By31M&~-r;)W?*B{{T9(z!v(%L6(71^b;VIV%%fabNZm>mans+G#lZ; zb^}@lsL5&Q1>{deB@TUX1zDpYtqTsIMMTqs##2Xwm0+j`$J4A52ufIK00TT0T|cM} z3lapKs@0(D{T-$vr5WaIe-f_N*dMA ziBn7S&vpBuYvr@`uA%}ZxQ9NY>^uv`j2H<>pp6L5M7E5gx{_8=#3~jOOTw5((d8>E z5ZDAQSJ@CcN2LXocd|WxE+C$dgSL`QNa3g*3`gJ#D5z=ff^{QSnyr; zC6Cl!U(({4LiNI_93qzNOzS;QZBVT`H zF}V2jQs+~d`lIhv9$ON_(1ByQ{qbAEx0_!(mFmt#6aK#chlAfA{K0b%QcUwkb#j7}#?`D!|YQE3A!3|*$a|>CIe+?W9O8z&q^ZjMk$qyV=jgujzB_?0H8W_ZIG93AZ^1T z@be|yHLyAqdP5B=SFWeGoc`{ED(Q0oK!1Y;Z^Q7!vDssDhb|shVin!9-E+dl9_6KX zwl~?9aXqBGbwW+_O7L~v|7_#_;JaX}ft zxzDf=JUbkViqdvYGjU*mk?<(mJmXR;jkrjyitdnF(NJ%NY4}6eRc@)NT3y9e7Quk zdIB(gk%qacW(eXyYC^GKgx}gS5I}*+!ohs4X?-mQOsW`> z9n_4RVxbQ(5%Tp#gkC)}P{A80of?~=M+lJ^sYzlU)P$Sgq{mQaq3c&OB-9PS(!es)$p*Zr8Ln`qh1ScZ$sFW7i8SRpf(_H+G`U z14%X1bhCT#=(LwlFgnGD(UqXs|8;cQ%O@C};=|}Bh2}oI_EwDx^hWU@I_Wg!xg=_P zkon6S1_soV|9*_M7bKNNqi{kQb?A1zC{3u?FfUaOJjpGAPE$+ zC|eS-2qMB1gi$!mH4`B^3dKS^E=EJtppxzMW&|oxf+VB3KT09$IfAq;TPe~=5FDj= zJTeW(MD&pPoERP*OGrMi;wNbelY9Yt`XYiaAwW}{u32^^VhJG%fJhEu|M)%FqZ}cwN+Lt`-_bp6i)xyckC)HEju~gr6>tgA3 z$1V2umOFiE*Qpg(RnnB~yJAh1UU6hxTM_fvE5|N&t4%`5w)xTI#Qck?hSbFMrkfMD zOYa;?yLz>T85gHDT60?~@d(%sQ>@!`ZU5E%SatH1lW#nl3N5x>AI;RZylr^L{+9h# z!>x($Hr?*H!+vk~okMpfGCSZXvj0zsowb@CajY?>I_He(JzMqt5~g}%a@VUb&A3*| zs}~MjK9JgydOlNr_-=W3y1YA6egYhi(lR&#-B7(SetG=W7_7^=QnM*J@fX!I-OH6# zGe?&zs^?G5Jhf6;o7|P&xcwKE&2#Lscf*2v-u=e-;#0Rr()&*?y%0}tnt0Dkpdz_t z-g((_&&+IS$yyk9>7^61C+@m7rCpoeAlE)~^)rhjOPf1?;o5c2&D7T4FJo#OQ)d?I zuMcEu;EepMCCiW!Gx2)~0&itI;2-m#EHm$D-l-V~c~C8uYGR@7r9L9J7wO zj=SZJ>GH;(+a9LQg3OKwS)eI3p=6=PUOkk;6(X(9&kO?9DXBlF;MI40>S{Him#^>8 zgq12Yg%{SfDI&7_wJ2D=9}r|HmI{(`+D?^45#wzbM6Ygk#YQSE=n!6Eq#}LkMdm1q zO=IuqOBH{8>|Zaub`Y+*7^OtWXO|se2?Q6MGXzCN3Sx9ekv}FBH?liHCZoZCa)1$# zEsFS&O~YXUBnX6%Vp0GOmaaiFdL)$dmH!Oh<=XXfK`*{uL;L`MmziHXE9Zna>aY2( z_`ccy<}=ryS*mHh>)erc?x0LMd+e@jOWL(1wdKui*SFm?E#X@yoN!&5nw?5}wyb!| z7OeBug_8M_B99OZps4fO+=m4wGK(0O5L^TRXRV?TCC@`vf36~Xb77c( zc7^Jpb);8V{pZ-hUja~VnB>DK5K{pGmte8;dWZ%24%_%nHs#gLve@e9X zUG7UqXOGTJ%^Y2FZG!PU-WeN}f$PqwaF;3lmi%$_i-AJKFGvuC*1~MLU>p6aDm53o zgp{|rKfaa*CyP)+KEzEi&{io(It=5W=Fb%qsX%>j4;Bt43{lfS2XCU6n9RN8E6}47 zoIqM4j{<a8ij6yJlx}fi+Pgn9ux9AwS{zX z!HQx8w+M*RGytG8Z^d12W7^x85;F8!lDC42tjVwU{Uy8DakFBnu`|7SSH`>huJ>Tt zd+@d)<2`cMdm`;Uap!QxdkQWxsVy?zMtFlePj&NkNikE}@W4w?7Ro)kh2jP%Y=sQw zD}TUwB2rEMP*HY)!hjn)5YZotg{az)EsBXuQ0`NUSUGg0yWNgu`w$epJ%DF`1Odjl zq<;XQ#u?#OUQO-t#yYufn_Snr%rz`;+Ok~#@V(L<=BD?{Dzj#If>pAiCTqo%g~N@8FF{7a9wb&4W}Ok zcQh3v9eol-G3hN4BzMZ#1TuK|oC`}qdEUwyC_}Q}V{E{BBi{89zzH1G7~IeL4GH54^00))1t9;zHVF50t!nFpUhK!d+BM{3BD6Ifl&oU0#gyrDC zp}5f9Wp%G>M?Q_Up9KKCoX7kEmVObzmk^*lp}x-rk3V`g3LYB=SNPz*Mbm})CT`m# z9Mv}AXiXekbZ=TWK$EWp=71bdX;OAPEkq{8rwEA=xcT^PEYA;1$Dt$Qhm`OeD!V}i zgX|oh3`atEdr*v!TTqG3rDp-W%w%gBn`>t3i=SQbY)ILbP7kF$&n?-WTeiFBHfQYI zif7|;gf)&et+!Usm*UbVo(^S7Rh(=!v7&dr{?G(9s7h5sp`q5g%6Huh>&8w*y< zSK(E5(0=jIQv&<=yHKRFk7bvBZ1R1m{TX85(E{9&c9x_|x>AjI>)X@y?eEohWJ7yY_Ghu;CbeBfO<$@x5%y#N5j`Ehu_{WiSS))H0X8*;h&+A;zbJznUB3+j%1 zo@ze`Urfj0N*uxON!~(0dEp7n@d$zls8|CRf{?xVO0SI}fzy@TVCXaz9kz}Hm1BfYo}QUQ(2pJ s(yR+r2W<^n#A5r(GoOFvcOF{v7|V9ENy9b!6?>M!d@A@m`lLJYKX9@twEzGB delta 979 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 A)Bpeg literal 0 HcmV?d00001 diff --git a/recognition/adni_convnext_47280647/images/rerun_lr1e-4_hd0.3_dp0.2_s42_acc_curve.png b/recognition/adni_convnext_47280647/images/rerun_lr1e-4_hd0.3_dp0.2_s42_acc_curve.png new file mode 100644 index 0000000000000000000000000000000000000000..244149190051a45fc8f332ff72e95bc80da20166 GIT binary patch literal 27012 zcmaHT2Q*yK_wFD_)R5>cBqD-A^d3P-1R;7a(R(+#=tL0FTZ$4TSFjLdoN_A)x9#dW<0&_j$C!^ zooqjxI1Gu4yK(bYqXHaT@P?dfi9yFR)v_;I3zqFr$_CA%v8Zvm##IgJk|GE@z_|F(Rvl^fLEoH3R<5jj)FJ8RR zOlAhds^xA?)eKcyzk!P9G>@79k+1c6Kw{IbFDJfRYa$pKq z$Q5j3LqkS`dbfp1Tln%~G`HmXo?f-xLc>A--@J5(#c#2arpKET`T>_m4<0^z;sDOAkLJbcm{C9`3YmT| z?K*%p*yQKqe|_l*WX6TxXxZlt`10io8(ROW1U*-plOv1S#Exg~SC8jm2Pj^v!OsL2 zc%+Pd4X}d-Z$|fvEa6K!uoS0w*|u2Xn%36V9bH{o<5WycDqtgUadERsx@0IQC{)$e zm(E1R#1KC>{^s;QZ@QtfwfJo%_KdIu=TO_q zF~ADuJo@9npatAyj_OOLIKJF%Cp`q3)6*rKshpu~sg#U5AZiDjPIlYr@EXtA*}3X< zJW%3c%tZQ-Nb|Knxts>|cX{$*I`wYs1qB6x6W&6m9XAK|gl63CqzgHswh=oPTa#4{ z$CI|c#}k(C>4f^D_Wgc-SN(RnoNVN^*GW}2VK;6kn5Uc>g|zsK*lGGEAIk_{k6CU( z`^gSmD*dOvzTyiD3y13?(M$p8vbKZEM#6E6ioN40N7vtSRAjWYFE>Vu&(1gCwr3h( zoxAsT>nK~~qA7WeTOb2tf4gGwjhDLPUuB9W&LBK}+U`9)Kp|6k&G7#IjQQJ1Wt3w* zl-F>1JY|@Qo%cA*g7Wfze{ z`GF4)L`>;De3O1i+dJ%=u6VcC-EMCa-Q)j`mJD>VgtP`7dy55!4&b)%^$X*)^ZYVwJh&it z*p!!wD*E^7>F(T-NroDT8%Z6HoB5tCC;MI=jZJ%R6l-Lef#XTUBuO90mhJm7+0Z*O zl#t4$Uu1=T%=Qjh{Q1C^Af4)dw87oGh;D9nR`_>2MD6fFBib~Xtuhh6cDhZ?CAv?~ z@OSH08@Xr9uuY9~N1Rod0)82pvV&-&dCc(ZH@;BFOXS8xg=Hw*)zuZY6fd6wwhH07 zlUG^!>D{|`1YE_5YtwZVmSUWo@4LGd23#D0J;-a=Yg?zhUo-W&V=Hgi{PnuvBlkLX zce+ot)3wuhtk7?pv(q~+2zF;0^DYGGEFy+rKy^{j36`}n|5x99h2ybvf|0&A&o~8; z+d1Ayy&B#@jtJ{S1yjrFFg>%M0`#f zH_gqZHwTTi)}I8AlZpQJ1xM{lnUL7!Hu=&$&PRc9pUdGm$fRm8#|35lh5n~)J-YL4 z!KB2VSQOYsuLC`y_D%?56)&y;M~Q1D66@4Hjwdqq?%pV0rJ2HU{BgT#(LD=%T-*g0 z2EU@k={i>$0h_y225w}=)i)hx>Z`&E?~v^^kte5L!a6s9v70(NkX^V$Po(RqdvKIE zWox{C=4_2b|6X^YtL14)TUZP)o4VIx!q(z&AN(CO=~DGzn&)Y^_&hUBQaVM#xKT=G zTysa;<5gh0^5>bbZ+1p!Cpepe89^S;g8Swz!s;Q z73es5^Paxu)2F+XoZ2m;#d^0$N#W)0CE%RC%9hOhA`@;}G2yk_ia+v^jYYDR2W+sx z=71RW>@frR>hTg~qMyCL6kqdFZU38DcWM;B-q$u#YlFu-&p79_W*@t+Ixo+Wx{W*0 z;5i7HpWr#PZ$5OV!*lq?Nq@CvJHWeL?q?qBUPoL(@!J6p4pDKcU?6fjF2C(z<;5`v zCA9QFIO1`YeZV%B@L0+8eB!dCAnLaIR9YHqr|H5Ww@cz0eW35NRdu*A7CSz!4O|o> zS1cQg;T!Em7DQ>t<~#Cof|n9g6S%BzsgjarlH^gi zq(iy?gixVZP-b>^`m33@GJ=@p9Zn>{tp`sY1a{9qJ)k5v-Xi>%wi+k?QuG+~;1xH2TI{z@P$fj&K zUee4vtJ|~+ZVI=5o1pxH2ZNd6(i48_3inzKQ(hU$u1jJtDL0369S5;zfq5~kPF_c| zhsKzV{oyAI^P2rFuqL!2sSC)F-5nSryYeZwr-k6%1Q)my!m6CYk_Zg!iV^>b=&^|8QDcaNH$ z1=CaL?vU6cZ71)^8kDh^vg1B2PyMhuI#^Nc5ItXhxgmEDhC7JKpw zD&BA*$lop9^;6Z&fmSX*Pu0zuR>`IPwfpa=hyl#nVjVBtE%TLs*xUPVHY!tR6UimH zS^F6x#z205ZJIx-U+Ej>BguI|c-WkS#XSOG@Y7;2^qH6RE_HNRC{s7CZ~1A0-dNvArRbS1ys)H2Dk8Hra+7dlk%g5Tid*VW+% zQOA+-w1sReM<2micb_m%cAm1Y**h4J;&YmR3OU}H8vvjoMa1=CU0od!&65zeEHCW* zp#p9WOQmA7?5cK6}8@EJ9^n!?OlXwX9FJ@mOjP&C3G35`NJNh==0!ru@1k9 z@}eNgHf(F1jA|}SXB@kn{Xc=?@@9ni!H3oo@@8NHGedkUJ@Fti4 zi)Y7Hd-KNj6gOhHVc74?w}B^dfXd#|C=13M-tJ&#jCii^=kg@KGHOKJhh=f>E@Yz& zW3j36_$>N9YrzfC01Ye4sFDNbJAu{YWv5z4vA{Q`x2x-%NNOU!GFKbWajjJ zRD`xPP+Bp7G9x1+O=jxd^VdhG z)T_*lrOGLHpKB#7^Umd;{~`G+V~AIJs5&A-Js4=HWzEz?)yD4jI`EtR=WF={7e7BR z=q=c~--?Dma;`fwo)vs}T{~NGSFe^YFd8AW00yX3iYTppMiyT$2LdDC$pKqFFT0;M zf;+!(J}dbOf4dM`VKSh#os}eu|C^JY#j=P{C|2P*HAl zZB`$e9D-LByK;3fIV7&Bbr==ll>m;v_IC1-j~9As#ICy=_p`n44brT+pf|c*y85XR z3YR$$ZKdaD<=8IMhmMZ|YOGbDDt+4RDz#qOer@MSWW*^c2)q>(6d=1HPemlbLs=}ZOC(PYYIpdXmXJ_q_SzQs zuOg_5YzLej?8I2EIfcuz-oZq}n10jN`)X%#Cqo}yfj?=DEG$h=fmSrwXjx#j&!r7i z`nzf(4?fdmW z7FQ4Ryt*_HK)D;ZH~q6w>_6p%)-;x3&ClZr0)TL@z_kxkKi}(@_}WU+h{#ns-0hb) zJ0E=XYynJe3{0-uSqB*2L`N=TfMd7FSx$G)B|E~zilK@ilEV!BVIpm2LjUp@`*@jR zNRszsY{`zytCWYHlw<`Q7|~LSCKEeH9Jzr%zr~O)yT9P>S6L1eg8@SsjZ~Y;Q))F= z1Qxv`ujh|hytKWzO+jnrt^;_R_u%Oq-6V&Br8G~ZZp%3T z&CH^fyp~@Zp>BPJ^3xYPQUS>K(EfLv=kucn{0dTnU~JLL2UPJdFUy>{$C^&qLRlS@ z?HPL0#||Gn=r+N~wulP|Etixz!Oey96G~O}b=0AtbAz)I={uZ%vJnE=3XJ>fi5yOi zc{_6*`t8Od#!A*pcEQ>G$DDO-3am||7s#Uw9QxO94{&ICw9|aXT%7~ja1$~Ie`&Va zrTEC1Cc{+?o11v&`Rl-$^>SI+96ZeR3e7y{4RG0=E@Mn2{#ScF`ReP_J7fj$!#jo@ z1IW3C*Z}{1s$7R)!g@iJT&NUTytfKhAdA19fi8*JtdVS4FLFL%5q7m-BD^X1PYG%B zqz>fJ!vNeqZ$!jpJadKBP{CL8fJBRIU>ne{HKlKT%R~R#E_$Z>Gj#8`J1cEnPPQW! zNOPL{RgFr>zcj*qO$4CJRMC2&-(RFPlL6sU)y;-8UkGmkM~?tN>BF_5a1cOi!t7qW zxCO%R9P{3!8~FG`xr$U4$TW@Uk*Hw?hnm*RY;Mad)MbbmPKszf)eGJ){}~IztW{bM zQA_!YlS-j^6q7i=C?-RR7{ycGzkmNSHefyo-|KITkQWHxbsD_|Fq8}?C1Zk@0^zBO3qJJ&EyB*u$R(=nP?kyd;y+F25#8swVbF0aYD>IU}eQX z73ZDC6GjhT;0fyh(Lp2TQDiyCnymi}&}ks5HL2=P&CFC%SHG*Hqa*wLx%WzF#Ayn9 zv!lskO(B-o)jm{zy0M^>A~Hh3eOOj`uYj^;anY2L+pzDmbTDw)9%8|G#HOK6cq~(d4usuQD-aJmVoX91gih(0)C!Kc0cgkh2kozs$>%r6BG_CMgBhBZrj4?BYM~dnE*5sL$`s=7ol0EVL*o4vv{;}+ch~|R=2sIsDbo;_R35mwGtHVP# zz_vX0d!?^WZ{&AG0fWKprmADAY{$qM8I_|cxqgTbgxtE*-P5x_4M?T!kul~)!awB% zGy>+#6QQ|>3Jxub?E=3mA|g_JA4BkPaesf4C$$*J+L~DLK%`3e2m&fkV$m!(IN176 z&aK?sT=a_6^};(s==Xi?{Gk_sLJdt<@cU?>-S*v+3Pfyil+Gp%63^~PL`QT>OV&{C zp6>mn5pj7S;d7khxikAJOFR_-Trg?kmI$)@R$$D8hf3JSt?zI*r|S|*3=rk6E6R+p zC9-J;ASt?J0ik^p*D*lwH$kh1MEq!O~a zy0iue5A)F?oh(8b0R1@L?{IK(E@3EOK+lDQhx5JdzAbq=kMs50H!g2nmQiN}@%FhX zO@Q5`ymnl$r@Zx7OENN%i~%H$6)qCz(NSkks;$@GG31SL`v$Rks{HB?74;9m9nwes zjKDDZzgcluh%pkv*@Y{La-e|%CW3Rk9<-ymiTYixA-PREouue(g&DLU4g0eKLyc3f zzXNyr>e+fix^pkhze#)m?jeU{Dn-@#>x%sKZ`FS*rwW|Hu785(hnst(g${^yX#OmN_L=ZL3tLT`ER%~ zkRm8BV?^7qx(cP;kx*gyH`vlR<}J*8TALP=U$@5XcPB?AR>vRotbYVHkOUa_g8M*G z>UMf5Ak!|-Ig}m0VMJIu1@>qdYuOfUfl3~%v%XtHYGaSlaJ6vEa9D4y$9o*+k* zRzgh-Zn>>~<6vRwQfZcx^_{M-qz4v)GVo$x<7scLQ#1!~&GpJyuRFpi66d{0V%fJyY zq?ZSL;PuJG6Bh!-wNcKrG8|o9gO6a$H<&ROi{}I@jC=h-!1{`DG!asK4ALnAvdt#4 zzAZltbi_|CMInqd-sHcC+c`@VfM4VTel40JskZj-Q0}K%qWrJxTs0|ZX`@cXhI1hWzC0<>caX<$2CsR{O@7WTPlaV!lhCDKj2Q=CA z+1V4w^J68{`|6l~Z$e|%5EqaDO=}$E32$TzpD#@NlxSYuCX1g0Gd2wxpxQAW{lSmT zAo!4U@-5KxJqA3cWr%p3{(p!D@W0|FGK!1dU@j^<=SZ9mH3P&snp z=TPNjI9Ln77sTrx?uevoH*8^O&n!d(AqgVSmkhitAM!{|5nzzF`UWX-qX9z~!f_r^1ItIrtKh-iKTlv>)yC8~Bssu!#F>J#eFk;3+0`UmsOPadL& zuZ;Se8cZiyWt~>$tN%GKY>d9A3{Y*&cL~tk5ZP?tDdK1i(%q7gmGun#et`A_dtO5g>)M?K z$vNO&)d2TF#tmD}Ck8N3$<6AXiqRjPLO+mJ!B84|?{cU3VD9}7A&kwX_hVRs|1E8f z4R{mV_s`>)p$&rN;5pr{zdU*0bvZrEy?&du;-6QeW&Qu0r z9btR@B9W6e=0tY$ViX{c0e>HMm_7ml@d($}o%{dRxUn0OPeV~bUL-Ef@K5vHRL zvRd7d!0w8?11$AkHrKb>VWhSFV(kwVzCuNCE=5eh2FUNO576l}-H-kr_cW_1hfgrQ z-XB=`ZH$<0`wXcK=%MEjMnvg-z$#K|Jph=vS2C2sC=h_DNNt4;tw{B+r5&BM){4$` z#Qi%N-BR`Z)%7{``8|!*4`FM80&>z`juckRn5iB%nXnjufw!30U@Dnfo?%;R#w#qC7*q(y9-NY zgrd3z$}{DjIDMy5UL_0-UW8YFHe`5IeWt6hNfk0ju_-_!7g0dO<)MF!t6;L`n>j7;hi^Ow0@*Lz(| zhdXoG9PoyZfRX5zys!%+W%3`^l!@;F%(v6nN-q}mEP@7SQlPw+g-J^dqBt8Ka1u-k z4I#e9u^U9`*P?N5Mg`6&nslyEQaxtj^twYNR4IO^-hf!>ilBHP?9a2)bIsFr%18Y* z^)CxpB`F}$(a|~$Uc3O9?r&C1Z4uG(M}Rc1#(9eW<$0Oco~g?GR0E6;q_W?3$1!VU zAc9q=N*0Kdm!lzXi5c4DIq<&S_PNZ6{=F9jlT4X>*e(cvh&zK6;@? zuYzfWo05!!!XiDO2rEN;w1G4xXklo+55LtSJeMi8ILJpxS1W&*HoN}j z+nw3QT7Br^-@hE2v9Yn1AkUg8iR$5iV=iR4^srNhNn2a91KdAEh6rR+O6vd=*80d~ z?)HVh25mO?Y!DJ}z+g6%Ctt-++_$|Xm*;w}bYE&7%6lTmn%WA#xOp?O#0&karsbhAIV>b^-#=?ExI}cyofh}D0Nc|f&%Yf~eMbaxJ&~EhSm(zsL?m+I~b#{2TRA9?(>eINo z%Zs<&FW{YRayNLS+6fJXYA$B66OopaX#UzYA%$&6yv_mnR?m~C*3QLAkE^=!(}YdU z%$$YkbMm#&tnpb{${@zbjfH{aPaP`Fw)X0L18a4-;10m&(@yQg(*f6)<6GgD`uRhV zLu-dugmIz;z(5o;fviAt2w<7112pLh759llCw9leQQRN;>`{_FffJ7hS1hb1zj+S~ z?Wt9aIE|5~96TWMD?`~(#UlsCnc}c8h1x_H>OmMQj4Sxubm}St)ci zXYz-pO{7ZI{{!Y1AM%2ti11MH@OG@gFE60Ev{wSQTmiWjm3Z2X6mW#i#iyI-p)kbx0%;#KuT>>&3LKEb5rH=~5$X#R>Q8&R@O#~>HN+QO+dgXemWlvxXs}^H{^ai0d8am?VxA?})~Cz8yHj||O*EoLSki2}jtSFLlcWNOkIey)lGN?Q za=E2P9-LuYge_z@8h@%^JKGNkz{#)da$v02^2uptxC1E=k4uMQQeaI;=mg(`cxHD{ zhBQ7Y>G@cRL8^dl(n?$0@bEAJmp`faE<`Xr2A|O@FjLHvoQkRoBR9z87kS@Olee2n z?lLgq#OZSmuyl`*>bbvO26yDck4&}|8KmbJUVK%7J3xmgL9k)bnm19pCvPRNx*AIs zf9H+lo@z(V@emj|S6cjn7I znd{~v$E>JJEsJSRtNR7CNP3hCyTbd;U8Nb;a`;L^k)K9R`{DHKYGV@n& zHhqB+M&nxM7+vqji`p!tTb$bZPez7H1@hmMhXGLk7`jIpOJb~^HNZwv*KPwow+ifc?dGMUU z{I#2(pyfvqc_TMI;lHzKQbtxp8UC>_=U*x+PrDRbXjBhYihF)CEl7=}7T%|iy#Jl^ z_Rlo^qaGNvzt=?GOK26xDSjB<0}t!;|n& zR~Dxn&Dy!vDz5f>XYimx>VChUKJ9Ag4D8RRi7_;FuTmVKzWy75ni;=GW0Sq7s)o^D z2%KgQZr{}uTiJ4#K%XUv^sk)!;@NGT6xo^Enz~`6ZyVM;8q8c>H9;OQdS*L~Z20j<>l0;r6Y*=co4iP*@rGiQi}8;4ctw8$ zg+T24_k+|nUb@@f4}bNhAY z^k-IF^Dm{G)TqSp_C|ae(<|l+0=`1w0!k%T=dtqJIXg|!7`h%U?{E`(+mePgN^IQokw<18$lj1kk(PWw z`|cZu!_E|hu1N?1bJjfG#`0vkwW{_$}0ytm<{uxmQaA6ieole#NG zn41C)-zw_~pW|L;UyDCyk{^N5jNMi1=<>_jd5UHmb9iLNR|rT^HrH+(nEN();R8;< zV%DD^hUHx`^q1(tHF3qZKmaV}@?acJBCZ-{Qo{N1JY1x+uhVgOwr<8D#Y3Pd9g8f| zsh!>xTi?++G3ATJ^}`C*9CT8u_i5ROdk2rZ9!m7-4Q1UPT0HF%p;(drsp~2NKh(gN zF=`F!iS>M*#w99mJ3*xKh%lV1x6MT7wcQ{+cHy4L6QXV;kId{`yP@Ipkyyg$;93uh zF*EJ^uHxy4VYHA(vb~Q3ly6Y;wEnQWYl;ECT~DF%4EEhk?A&-9Mk~ zIX#j0((1LKfC;jOXX|kVYOm0?+P!%m;_DtgL9Pug`rSI(xO9c`S&gAYP#Cow()C^A zGjNAS&k7&k(~)O~jNZm=Ip3*}o2t8rUIrux(391%C!^~hixR5Z(X1L7gexl54h?_$ zS$9Vvt`}h<78}y`r!aE#{wJEI=K+>sC6jUKCQT?A858s3&{kD^9Fw!Hoi`4(GjVlQ zob$z!R^1+V$8~JsyVb#aMbXo`W?x}9wJ$A?5gI* zwk_?(jCN5p7LZ1>P!Yj`}MaUL(5LT^r}2( zC#x6a4`Ow+$SOq%!~C@2*8yj45I50Ge*4K!ASA&cpN}8c``dmK1Rf=rP)}d2sWll^ zM~8G9gVXw$*F->kem0rHqP?&Yxo4O^Ydtek6CmWnfD%U(!Wc&!WJ>JQ|QEZc5Nr`5b8(rLT(N6d4338d|pqq&{;x{Sn96U^79iIG;^dBj#Kl41r0l1S7-C4;qc6dmU|DF%Qb>jF=?oGorxCeT%>0S~yMvB3rBR`uO+>K0_bmP~7-|2S&nId&UB-G*~#>d^G{E zAL|^Nv175M?Wk;Ko?$!Yo3^VnD^vyVXRu_9__-r=E{$A&b*8CjcCJrF8tPD7FCdtdn^zCH{N3j5Tbq;ZnN%JV?3!6`4p4RRD)mVyqlCB9dO?1<&B%{5 z;oJy$50hIpzLg=S$>|lM9j=!JNMMq6OUPY`Rft@B5-BaU4Sp}w$MAGTQqTt)WgpER zKrim18ciLHPH7*w9nQK&(YN#4i1v7u$ znyl;4mw@!~#Ee^1|6!q6qeN9VMg!ZSZ^Q|}`kaZHRc9#d=Zhe$UI}VIDN>5PU z(Z8`{Wc=YW@iB}GGV3vA50c0(AiMM@Pl*nMOWdGTy6b4nNJU$lnw^~;Q+!cmfEd-w zzbn#*aIJY_t3fMGA_g}Y$^1Sj%0F88kbc~z-v{5Kh2UV}j8u>KN%DY8a!YJ%#<=UU zMaocPZPaI^2+zDf{L>?szISc&=oRGF89Y^y$9pPl2gy;IsX92)RuEG(TFdbsJ*uDV zbE4+u_#Kv8uij&e<#e<@(tzIY-JGn7{*}NsRQ7klfUB&m3=N7cW<_%qa23@^cbEDE zAmUE`KB7V~)z$Y3X~OCvysfcvG6)RdmGximS4{N9*;J&P<*BVO%^d*zRB78cLQPvO zIL0+n|HU+S)DkQgt%Iv@*wpgOO7`^+*qoUx5;~F+Ce(P+ zV9jCN3rBT6Y;-)FJs35QnQUK8MXAO5jHWU3_#}^LoYBK^TV5nQ zd^Y_=@AfT0>gx$LkWe2K7kr-&-Oh0iz{T&Y@^7WTrqm19l%m-U6e(`+m$QhFpbTij ziO$CstXt{s%5U!T zlex_t12+0}tf)#DXVVv^7yxw0r(38dUHVZq@${k3jNZewWXt)LE@bOL&6Q3g%~j2% z$%ixNxE0S`(T4p{fR;9>Xl8xAK)#^ifqrb;iv8_Qe{kX-I ze)qIRKaS`=-NDPXAGHRk+SY0QijCSfKiZ|d^qP#rs9r%qE0P-S;<98_jJf5D%*)M< zPw_f^wF0W+H)k8C^zuQTyA|apet}69Tz0)62!KqX606=}t*DgvfS|j8k5Vb9NL_o^ z(UpAZ{;z3=4;QW=WG(*S(GWqg=|L?@V<>=AL|k}7)wL(19otZJX$$Kw^-n82d1)`~j^W0&pAD3dgpVIn#5|$MbS<lg8moX=`3zpYXbwEn0;&M{AKTu19zABySka z+-R0?Y)bl3VhrsMx7L!rGIX7-A~Yqj6`JZPb1;M)G5nzMA}Bx^ z?JNE@t?hg2?v)gXFHQ&C&m^eF) z$`%%i@Meam76%}bQxEbpPo@{TsPTLcR1Q)G!z_|@B@77F*W-t_Lo|kvlm*$0!OR2n z7p|PycxM-VCgfZFC;l-~#ckcL zWRO}|*kqe;JTUo<7`(u9C7zbK+_d6a(zkI$4VIA4`Y)4VMG6ML7l~Z0pA#K?<O&W+tFRnzyi|$ZJ>AQbS*zSBI=LPBbUW#hhX_;9paYKtQQ%>r#G&QmbXIJ zgxY{xU*ImkrN4J;`U%tGWa=C22C*BOz6+U)wtq-TpSAj1xse|tkn1)!Ki5uD3LoXb z1BFzCxZNTZLQI%IF2X^!$Gx#f|3_J6i+=r{)tiH)n!D9v*Q+L~OE$7t(EaLWXQ?xJ^zA&Kek$02b@zgBSJ&u(JUx&5a^tF7A+ z87V!a)uM-U&%Jf8@wVe+ML$uMakcSzZ(<}d;17>3~H zG9dG9)QC^9+~RrCHU-)+DF3wBA?fyvtj71p39LrD?A)Vv-d;iccYL+a_sW`YJ31qf zB}SgNDn;a*iDcKKKV6|JpBpQ`(9ji3|M}&P)JfBqs}Dzsy32{y#xE5W&p>%Ejkp&N z!~>BDvQT4mDqQ{!z|Hn)m(g96y}BsB3fSH9>Yg?6pMg;m`IoO(y)C>idaEQ8*J#!X zI)62Sj4Q*#lQ8w58cX_0pK?K-a|OZ^o+TZ3+{j@zOQjI^xm2zC>phkXk7N-yRjb%3yDtfipFV|Ps;5FjLub82$y)SxJQr(FmE7$L4!9CZS!oFD|GVa|;*}+Di6AKVAJz!(=*e=RgN<-r zo#%jt8lb4Crd@vS0=yLc!CWTGLRj^A71uJMTd2f6{tYX zvM<54t@^H#d5cDKkR4lmEH8`iy z#{W0MoG~|D)u!{!T=DXek>GC@!S_0z8P8w0Fkd%MJ9I0BH%s!Yth!R9o0;|dezps& z)Y1|bFW)pkmA^$?JsB&M*b!OfT_4ZRucRLJ$BCW|EpBF*W||@LejnzbCF6A;p2)@G z-r_jWx*3T&{!TM{hVy1UC#{>^@ItB1yqD5ZBJs4&{$l?WeUg~*R?$)|`Q|&y2JF%wO!;Es*~ALO11HxH(&&o+l`pgw5WsY zep;rxB1fB&EYoKCSr!A%FNe^u#rGjgSDIS)kKT{zLN=aML3BUo+%B(94y`Y?xc!*W z3$~Vi({+XMY)>$Jg2ClnVsMXq10hR{~*M?}XHD&h+5hAzmWOf}`_Veoj1{ zkmq3`tB(!Y9($@jq|0Fd$|TZNY9LLZv`-#=1&Y2J+#eU%mV2ae>0byj+5PIB&{$qr z{wA*|V|pv%MG)RE)Yp~Yzfc}-YZo~XvX0FVQlU8 zJOg`Z?y+Z(iA5;~q5JmgTgFR?SlC6oNU@eDV{NTxaq>C^A~}7i{p}8q)~fJbN>b@l zmpg$S{kJns!IoM<{hrNevZbT3ks&YF-?rNGCGRJxUV%dy378h-E-x;KKj_t@Xc8m}2EDEa zAQ?Q^+zd53(Mz{TKj9iE+K=P%IY>G0(gbVKj1xqnX;0B{tn;h>%}L+-|XpeCT+^iK!r&fstU5|foMEZS2MCp z#i1TteGsAW{V;A$XmhzsU=5UJF$%+Fij&wtg)n@0?4f|=U*^Bpk5$)Q`1YnWAo{K@ zPl{6#htCtlf`_&;_IG^D&c)Z2xm>C}I$!!QW;#9>Sime;WASDjth8g$ z;+vxh+j=~Y&*U2ic)1j%bU|lSD(Vwch2;PWWXBgk`_BoG7y5`jq}Ua?^)fF2X8P0S zptriVG)hItTLUlp431Tu_15$**$J7{wqIytJFWF1|SV zb~ZJh60i75`*scSa3wc*NOKRwcVZr#Q#FoKcGvxxVwf^v0IQmyYz%+2Ac9uTVU{OB z4W@>t&N#-(7vvP=R!V;0xy43Et@n{lz2}bq_!a0JEd&1m`|mr0Dj#j`fZnFun857D zTZ&JEQ08|W-mE(6m~{!H!udS3_CP}7vh)%(3O~HpQYb^*nam)11@8+xq?b%-CJqee z---seE;k&fpKewRSb%8mt+_eu*xbUx1=O%OkLp;0uye6@diy2Le6N&!$FiU>LPq>j zw^MV^7^5g$hPG)wAP^DRsvJ^KR?Y+cJ`m9S{^|783sa?v5mV>3l0Sc;TwVQlJnEVdOdXu>BKX?#=A(R2Y!^RPqpX2^ib0r`} zf!$}0bv<35o$*x-6jA;bV4tj?1Os%m7X}}+wY3Q^e|lpzT13;?*$MGInel|6|1w?g zq6#{w65-cA3MY=RjX4^7x`ooI$3CCHjEA3q(}7GE!nDelnRXJ`3DSj`7>m6nH%{(# ze>HHjUI^a@jp=b9Hx7EfF~#C*z;fhvWr6Pu02~^3_!iTXw>Lq}hHy0%OIN-ysoF~V zRCv*~7=jZ*VmH?v^#e0Iazn(;C4kZLL1hdmGrmnr+tZWC0R}T#ov2`QMfHi~t)vOs z(*h{5vJlQ>@#`b2+uEPd)?oaE_;}T|y`g*+OTfZrO@XICU538Lgt?)Yxf<8h6-0|y zRvMj@I@Qw$2$e3 z&#+emM}YGK!c&7HW~Fcv1}|V8ZxIul_k3bcWMl$;?$4gRTRCngl>!p~fM}-+CV5_y z>~dZoztPz++ClNk;@Q%F2dfAN;euP8KA0-xHXU6rD1C+iC9nWpRwMpd|Eq$Xgwnc( z&uZ`AVL5 zD@E9u1@p}r!*5hDUjxx__(yS86sg*H&@bk;K3vd4_#6W2Ib_wKf2<#VS@ z6b?Y?rXFfMK$E$kpAhzpxKs=;LL4G5t zVxZe~WT#Bg%n^PCYb(um>do)Ed+}KP-fT8r!@OM~998b_>#6(EzQ?rkTA-KFK>*>?;R z6X$-IKt=4tIgO_Z2f$^0v7h;hF%!m9^Bv%i{%Udco^V^h{rQ+S6Z9N-$;+h=YD(*_ zzMe8c2Jv>7Ps#I9dxqI7(v|I4Qx@x`;zI&a>;OQuG_^+~&&{BrP27T#)SIcEX?W%)CS|FA{eESorBu2eE96_O9-%eO zqlKwN2i?~bPNc*c)IM*f_X3=*-^nY0y8gU}s6JtI-8O=u4AxB9%~o-|TEOIH^M^nz zcy zcs%Y-`)a9wo}h4IMkt`Xfzp+irhwKH0MjOw6blI7K7_w7OcKJ>!&0gC$0_(7&Z$%Y zo$K=K#ZE22tWX$dJNyc(T@0Z4=f9VLJC%;Jdg*!$;WEP7*9)EO1f!M)K@Y+HmcZ}( z4A6a{+sj*fYA3VUG^*37lU*u4K%Y;RKus}A!1lIed*DI`j~ecRq*Hv0I{I7*_?prv(;u`wv(N3LN(@Ah5A*JnRj&_e4Y?Q)74( zMVbj?N~h|L1MTHx7uHu*?}=-TOeRLR?g@;?)_DMbn{<1gVjL)CCO`Q0sD>Yjj+vhZ zA*C+R@v4KvOxB|uSU2IdY9!2C(yXl&9M_R4B%%5Bh1$Jt+`9T*Ui_s`pLX(lcvSE< z7;QaCaW5NUB<039jhfTCgO-?#oXnUE^g>4-saN9umpxl4J^;OPR18n2a@yG=+i@u3 z!rt79@>G~M=D?z!x<)OXKi4k`VD`nwoUN^U;0lB;e(&5izYlsMx7}J_4{7X(0X|zI zqPQlSj!__o*UOYQmQCZ}I#~9}E@1YDNlsI3I@yny8Ailb0#-B{=e(L5kei3*h>{45 zG^x~yRxdM7o$QeaeV3hOoGF)G^PC_;+~H*186LYp2P zZMr%^mbB0s!y&rmu>>`ksFi3ssk86!{*|RhpN|7H_01hSuiMz5wpE~e1tf)NcP$R3 zt^*51R|qDOrlwMchc~Hf?#!vnww{+tnHt7`z#zH+WQ2=3PBI0jg_ZfP8)djqaInTa zW?}p5wr);K%imvkN)zxg)5h$ZqUz*Ue)8E25ZZEAfnIRZ14e)eD&%wI+DYdTdRe!# z${2mi#!Ky>VTVtv`}FVdv!RzJn~!&^C`^6@9gu-$1*3I9@h6mkK0yeDovlYUf34H- z)mpy6sm;Iw-o70sglIR+?zikcmDIZ4fcy0Zm7vQDungQc)JRaqe;$koCyp)t`PZG0lhIN87J%P^F)Z5hkf) zHFqsC74strmYC{=>ryf>EXRt@;u_*er%d}O!;4=Pz3VRj7)Z?e8mDu6+v#&&R1~o` zE3QsXks%||en|M>cIB&M?dwe+=4Oe%dqYhZo45Q~BSRfk;-`CnI|(*0RR1pX#j(|7h8r~{F?TqR?YZYn%(mviEc04g zO3^R_qeH#A&MS2gS^Hcw}Wap^aAGxCq=Z;Tm#$EtlMW z=Ft)KeO+EEFz5d3lD9d`RQJ!FQ;s2)ccGRJ1cFW1%uBi0Sdkm)_zN4g)AwL;<=Yy} zyi;aof_nWM2XIhx9&bss^Y_`l9Vw~Fqn_ertfl3Xy2h0Wo9Koc?WQA~Xn~_q_fxHp zSfP)JgXVr+WXNz6$457q*dTj*fQh>yEfF>4k9czGv36z?D0;PUEf}>oJ$yaMeb`j<^{_VXTeZA0TMse7*z< z?eO;p;uqc$HdvUyLd7q%l@iqfoX_O#Jsg=A8XDRmMY1(q-p(s?Qi=NyWf)PQ%=?(i ztcCv}U}5SFAFXTb{+_L%pdhl!p9jUNWVOPsdI-@X745FR2|_Yj5KZkf`y9AiO)WNX z=b^d55S@Mp())kWCKrtC)udu?*$N$4I1S=danc-?y6eO!4@eIEwPv$zGKYw1ds_b4rck^5%%eSu$iETmEG921R}WnOEsd(ta<%2HL1d|&*i=}x_P);X8krwZ{I&*e&SYSl zEc_rnWK=51IdNfp49#q#kqgpZhYue{sZ)@&a$5wP9`H5K_LO)?dYqwA*V_y1ciHAs zvh6^g0p6(3PyJPYKP$W;&EUw%9Lu|~m(%Qe2{|akH7Dd%ZvgF+4pB4!s^Y)Y?S|@5 z1du5SiHH~>Nw|FU<2(~7TkiO)PO+yJs)Qs^HACWM2yy>aEY2LZ8T!SWdNS>{rO@I? zT_eZIQ!y_X+)F!lTuAaqalo%29VZ<9=KmQoeQ{#(0ZCq5{4s5Pnh0ucfv~x60>rm% z>AEu#B}%?}f7)<)&ozzYY~j3I%cT=P^ZOLyO=^5{9tUNlslgn{d{`&DxpGW2CHx>u?SABul(n$BgQZHoE$~qwbF^m!3DD0R+;Xy=C>>#V;=zBGpDiEnJ40 z(Tv^Ck~R6>%}W!Wr6~*3_hp_wW@+M5LJv8G%P9G3Xr@iI&W@5KhECUuBc8I?TTl4h z*}H%fJG=6cY~!o(z??h8^P^FBJ+uE0?cR(w+^PX>}*q zba5L?3?Km4vj$c7N_#D9KkMwtb_0?h$lh@B^bJOb9Gr%53o+$V_FaCHF=OMnHW;OH zp%g1J$(x$i^LWY06`v2A){i~n?9Dv4OT8y{Zslj&xo=_ODoM^?lftnT0>>M6XHidx z!e@T|mF2H~FVLs#8hL7|+KA?J=TyRZbHeT|sNadNF+-t=Rkl1blN&Z)RG+=3adkiK zN*-H76^aQQon&(p$Mg!Fbyh87Lw=kKhtNBD7_E`&rywmV=WFXWd+!X$5;?V3HnWW;eHr`1E@% zC^?SHkdc(WuQ?q_@vwbAlRD?Go;KPQ(5rDmng=;%ka5V6F;?0+x2SPU^iDxpw%gHr z9Kam*?P4^Di*|n~1{)YoBFKG2NV}4Yrd@a9=~N4QqVl=D0N&N~r}D@96)-MnBhsq3 zrg(HrrHrvFZEm7oM#^;cdCc#D4X#GN)~L%(8%N`UnfA>s|J$n7+KfI#?JRuAfz5`C zk4arO3P4Q7X2ug@Y3AQuu6GVg@z))x^iJK%#W6`7C8{6ENNZWzog zEU`}KVHNUa6W&{!UFbd*QRB9Ub9i$`yezaLi4C-HUOuNYTzEt38e%0+r9y%?+jEP8 zz~$FTXe%M~V)4_rWjaZ-6d)5rzNF zyWt6%9jqr!aEluFAJ<_D(%Klys=A`K^6fBec?|*jF%@XaleyN?yb%qn&DbG1wq;Kz z{%dK!V8bM;yA$sBPM+`WD7})lDITbHuT9b`Y&1&4*>7$WGX1^wMOcyiK3r?kc@}|t z#r~>A7;&ci9%xGS1Z#p&OR8mF)`uggQ;))n?$5pHQNCK8)wL4fO5_AB5SW95xykn$ z@^2iyJ&xSFvL>$KJGXg72On0;vh(9pyV}?9n$Jcp6S@Vz`CG6b{T(}w?O@O_$zPQu zaAzteD807erdMDDC6__T4}ZK$1_t4=LHu597608Nt{3aBpQ3Lo_%R>eDPJJXadue> z8unQW*Wf$ZQS|cDq2U1q>)MPMM3W?(S3J9jeil34RJQyMxT!zun8;C-#rh(b%B>)A z0j`HN7h8?LNqjq3W*`|k8AI^SC-u#x zUcrEP0{?u$^5~ZBs3Z0;Xb1)CovW~k`&E@bzJ$%3oO3uyZ@e)V`!_;JHNfu+Ww`3d z@DwXVn0%iW0bq=P!JYU4@3>k!XHrE+KMY(#_j$JXDo;v=Z>Es6NMT0kr}O1S@>ybH zlQhmJSt1fF&vi0C978Vz!CYyOb(&t!4Et=6Qa+?G+AZ}`G3|%e<%iomC5%^7f68Vg z2kXN#i%#Pj=zv`-NWHf^PO6imYU1e=q)uro|Aj(Op7QhSL6-s{ ziO2WuKvz$`LycGXCS?gy)^|Nta5edbo71~cX60l0Uaa*w?CNcW?mYBYR#@Kz!E?t} zBlh>j8=h<0>ba81dC%Dsi8aFU&!Kzj000Uwsqbmy8hAoY)+smep*qY^r4!W!Ye>x_$|ckTgL1`T<1$Y5c+4vIMpNzD zc1sO&v(u;18))$Vc0;XZ!S1_cYAQ0=Up{p4VY(xShZUGveVs=xU3m&^TxMa=kPOSG zIu&5|D^mXZI=xe@*IEpQ^TIthLZ<}GDlAtne#2{#N?TgsMn9q1c5E3k)(|QlArNE{ zudJ`)k)Gfor?HFH3dWC&2?T<)zT1xK5$d?g<_Wbm1-R*?>R`xMv;UFX8W!|8Ai#Vh z`J87%xs};(78=3<Qb$*sd{gx?t# zko%25q&>(_YOAl`f$fksXp`6#%>w0DCsNisB}IyC1GWELgNep060IYe{?-26yXcdG z58(89X72uH=xjvhxCG4Sl@Y8*%2x18U#tc)7Ta z1qrG~s=DP_#!)6MKp6z74;zu8v~QSsodIObW1zkrP8)hU;=fOcKod~T4Qi9;6Z35|`7p!1AVy*J+r#kcsQ zz8w0+4=d4H@M1VG4W(l^+1fW(%BILv+=7}S3`KBQm?o4)>Y~kOFD?%|``zA>UrQgA zYEh3gxff2I9ps7=pf|KjU2QCyc6d~Q0z1QMu|lu$H>N`69d@r)e&`#(m7<>7xQsk} zrTikwOSs?v;I(0A|9AT0K=nsP`hRxwwrftf63E$NS=rgrX>k9sgumw8bz=>R4&%0d z`T7+n3{utor_c`)5g{Q*p>kjlTVvrOr`>uUc5R^g0-fVsXOSH22tuPFp`jlaCn;cq zDZ!3k9)EKiT$>kMQ7b-CqB9*ly|(5C^4|c`I%wO;TiA8z^-?n(GYAMmEy+l7Nx1jOZWg>ZDgqe>78Vv$5RSHP%P4<&eZBcx_v`bokMCS0DUU=uriiz0ISZQK zSBvk#*B^d;{AmHm8bUhZ$|Eyi_(LUC3N$GHR8fgWNGwnk@|RdtZ*dK(#}5Qls+|Cw zQiwr)I%dfB_H5KA(DQKx$8agjoCT!PO=o(`VFnsrc^RtoBO#=%*e!o#5rid;uRJ%Y zsi}Dl>QP0jW65EjG^iyHgD7!7Ao?vq?dRd6NBdo7t|5T|k}l0DXNkEt+Oa-qhO`Q~ zfm@kJXp0~iDGS+?VR9T9w5T*d&I;s>6@V5TYX)8LS+U2x5(q%``$~URK<%txx!+$p zrK9xXh$XR{ao0IGT@1y_{gX1e5Lk@gT1r!ya_+)OXr#l5@Lj_Q% z1oe4chO%X-Osbj`cG)7|>jGx2vIh3ep4fEL9n5|E#l1vg5Ln;wz#TU)6UrIFn|Jxv zeEA}a4FDu^|K`eHwVN0j`QwOG(nut1p?SUbLlrT(dj9tB+mI_9fNExX@XsWM3?FVD zo^0UxGqUp!wnGTdchG7pNEBsf98hY8yn|I|!G39@oL*3K6qT268GD|<3gt8r$a_>+ zN=QgNQS(?Gl^ZgH(%MlNOe-keKyS!_PX&3R(y*{F28}akHe>!g``#ubBvg#N1N%65 z8zhN$`Tts1m00?ALWom9;52x3L{S0G_f@61R=PA(x235nP-YhrGBG8>f&g%n+;xecnxLL;$mGVssJJ`mxEHL)-3Lb~xtaRF8py5W(%Pz|NZk_j{^h20u-z}3%-r%Cj2oJ zC*bjR3@Nrl{Wh7bCtbMEYK%~YE0AHk*^!fENj!3d4vOmL78d&Wt8uS83|ibSzJS5Q zT!tKBr&tNN+R<;XnERU4}sRpPN`WccFZZbiiicolq_m6N#cV<`THJbQMo zPcAGbrVgkK^DpL14>?a!ax<$Pbm3m9fUX2EDOh z09}*MODlE2``?_7phNGM={1t|{N)1kSqrL)P@z|ZLjd;d+ZO}?0^=($F+g&N!5Xw1 zXhAT4W#SoQJa3@Fz-eBB-K?Afz2=*)t(sluBr@3}!*EidC1Z9f=1 z;%rdYdT3Bs>Erv(1ESHO0|&}zm+y8tp*&S&B(LbMU8Ko8$|xii0kx7<)w1UMy7)wt zf=&NGkh%p(W*oF`dp1-PAqNsB+J`0&0-nXapP~6nZk0nMSZh0g9bRy9ahcCgbx|VK zi6t$&wO*K3`tU(`aQxi4XWt8frZd$jw;#YlNrYNBz2gUfIpF9Egn);M+A_h$!&wToK2crtN_6l+ z5u~W$h=eCFmFyQ<41U78;1Wn0Pb)-^UIrM^V}F0M(S}!$sKzV^g#a)i0YCypdqFC5 z%v;o!g)by(FQ|AzCKEYw1V(`H%SW-><-yF%46758;!pvXB4w20=jDZ^6)$!exvq=$ zt6Te+^6K-5h+fbA>n~9_Sz#H3Vjz#5Zb?I|G*3@8#ZCe}J>%wHSZHp*bipQiO5-;b>ww?ycft7@>31AauQ!{2f^)YKz1A)8F2(?5vQQw6sZ{c$GRiut7GS@ zKG{8cd|uBZwKjU@@60`l$^z=S!I!4!xi)A}Q%lavb@Ck59|N;Q%^IA_0sP z4mxq9tuJi@$jl?2tITDH-SaKkV4tAd#D0{rhZ9neyd)Bc#!hN!nS#+Gm3uVo2p$Ce z4iJuVKoQxtoaW(Fhw7fF?l5S_?2?fQghEQD>rV@i2mH}pdY9QR$))ag+ zEWQEY=*UkZBt<>X7~uT h{qw($&4+0?ch@H$dve8?;6x-$OH=z;+L3Gb{|jx5-dO+u literal 0 HcmV?d00001 diff --git a/recognition/adni_convnext_47280647/images/rerun_lr1e-4_hd0.3_dp0.2_s42_loss_curve.png b/recognition/adni_convnext_47280647/images/rerun_lr1e-4_hd0.3_dp0.2_s42_loss_curve.png new file mode 100644 index 0000000000000000000000000000000000000000..b6ea2d37218063acb165eebdaa060a8b7231c554 GIT binary patch literal 28872 zcmaI81yodD)IK~kk|Ny*C@3x69R?sE-Jo=L4-HC*h)AOdNP|d8%+N}WigdT+(9O*M zjKBB$-v3(PS|4k%K<=G$&pl_Ky`TN;XZ!N0mO3dB0}%uQA$_Q!q6>jwheIG(3xxRK zFQUI@R=^KQA5|kCJ$E}Fe`~K75KU_z4;ObI7e^a5zZYKKj_z*4{1W^kylf6WJ|5mu z0s^l8?+yI!UiJd4cQ|~(AU8ZTjJ+Wca%;>t)@S(=M+n42^r4F4vw)nPS^t1%FD`ob zk%OF>oYA;SY+n`rs|)@cKiWqmxj?Wh+g_z^al8_)>};-NwfJ z(%nb}QN{cr5xXEhK3*eU2SPnSiXWq367kQz4-enHcdt~YIv^68 z?XC7@Z*TAYfAh0ESaEeS!Nok{|1-B=vf*6tMUSVtx;B3FSodUPn6iiL{OG~0k=RyA zO=kSRM@A$=&T1x$aKB3VZvL#XRz5ZXYax`0ZAIGDWf_G}8lRkO5H6|6pOJ8N74kq> z7|Y?$=hPq6FTuiy;lY@p~8K7RZN`93)KNihmPi;M;glS&i86nB{i z`}@}+jps(lZvKbeLd@_P^LTJPVj4l4ZbAlex5OSB>#+)B(XgG5stJU|#D&h|RV>wy zN7_rYxX| zR6zLrERKZBgkUTwqhDur%j!GRrnM~Rnn@Qf5huFAVW}b1f%{&slAaRX)jlP^7j_OHG}cn`^$xLDg)=|LE~!XCJrgi(_BbPrzng z6P`hu97pc%ww+qcg`S#sb~=d_NcwI*TUuJ`mw)r-O{-ZEGKpFGT|N|*X`dH}pU@N8 zW3xCUKHqIqySLLg|GQ9~eA`c&n3n(fz&k!OpI@q+ckkY!q*MlrdA8rjTHZRJst2b@ zh%vMbO@GYa91YfR>Z@Z{hGp2sGEx^zSxZfUKq0QHLv-A%OPO3&WxB!D`Rd!lw7aS4 z>H4Tz?{zH!1IJ|W?AWmD3!$*9fRo+Xfw{KefhaKPxq;;r`QdNATYrBS5jQtCld#I= zG<&aq@mhT}ERB>=OQcgYF`2ToSpE>lveeI0{rq@y6mySGp_=F3f}&+eFfbx7qN`mN zIAv;DTGy8s14hkWcN*uzq#;@*loJ^&KBe7A3WDMZ{FYE|Mcn8TPW%~N6Oy$ zdj8>RhUGi7S-G&^woz64g%gA&cv~nh^u+ibzj=S)dal3jd^Ruv!%7t0rbv`s=wMa1 z4;C}T*4Eaz&Q3FZ-6CKSZ*3^Mk1Ib!DS?Ks)oV8L76CKs*)R_>R0b+1^Y(AN-evvq z_GDfEI#?5#vxTU^JlX8F;8R@_lShh*9iHlt7M~5>Ftc(4J{w!x0sqUJ6cqbB!O%E_ zTlC)`55(P?*ppb?Fj|PWxA$agp!fMY*cIE+GX3tpzL5%J;k%ky-Mh0bftNdtwI-E5 zn}16Cw-`kn6rMj{(Lp?p;V?X$^G3*C?RAmMoUf#Pb(_ul{F$2{cIEXdApw05+S%1r z9^zJOrv- zA=&c7QMJ1_ksr;tC230f~Tp9UHzOp@d@cR={R8-hI zc6OUr^04TXswjNs+v1c5r(I2wKU?!B(?`EsuD(qxqQE%&8*aOo=r=*i2@$F!Via>(CWDsdv6dwXUq&IMm@e%w@Q8@oAw%O*Rrkj{@^tVL?%Ux3lr^J;`TP7(9cEEo3B_j=!!A+5kf znx&K+t+O>&L8ML{&kdJdJT}c>LzxoD=YQ8SPO74d1iB*8N*%ZB%5Rvha)itBd!RjO+SJ1vn#mW zq0^1$(zB!m!OYj9|DLwbV+wsx%r#5qfV=mpNgcu?0P%dxPaK}2yH(FTSGe zMpAlhuTOfH!;&C*H5`lztfdCB8rv>R?&NJc3V3j~u94-6wb%Aex*{fiv zpaz%8PE}fS6&o9NAt9lo&Cvu_=+&=JPO2)Oaz<`s51>M4*6o(}7ig9POC|kr{(hIb zIbU|+trWmuge9n9?%6}lB;gUBA9hiC)f`1Y{=j~iNiB&%)y<8+D~{ZLcc!_*tohC1 zuyhIqhuZ6R@9bEC2QD*YEj5o_i|6ktB z3pj${$`A?)NwU+;`FHjVmw$R2!><{W(&4^YnWm;}_beg0Lr2x7hbXJsj*d!?0Ioj| z4>VoF6tMQ5zg_9FO&V%G2@PiZXpp4EEb2tH+?TQ*Lu&zdYdo9_MtY9w8(PD;`?oF;5s?TsH@l6PKN>dHi*D7cPNIO2yOaiKC0BhXSrdZx%2V0pkdY_?Yb#$3CB=i%FJzPMW9_#07*)Lav4$P7n@rvff zicWvlDQsGZBsOaC;~TdKc0E6Io%*_1hmf=Fj;Ao|^ z)x3~@BO}q}t!QK|=P_0`v)2jT$o7rOHq-3Nr7ySZUZ$kjcg2#Lc@K%MBIK`+ zMlkk(y1o%MTa0OOTx8a^^!KI!bj}RMT@H z%=8odcl_<9FN7r?v$elaohW~z2NyL=!~Z$zsg2`KniRwK-yN6b8^iehUHGe8d3I{;I;Q+_}4;a0)zt7H2cX?&EBa%hIm2Yt^xu|n9 z<2jQv!`XzE-vwF$761FP)v^7(^< zPe3S-8nP2^;RJzm(GC4Z2EvZAX+cDfy-v4txv_3h!=}H(%ZMeDipQ2-FF`pL`T`d& zyt{8^9;Sg+gO33}Pg$^6SN@dhm0a21V`qOMmVpPi)kJ?AbtW}kE2SQ+&3S_Jm!n={ zB;biD_MI60OCfxEJU9agtRhZ-;_*otp6cGZ5|0g14hU#cL#e1FKg%JI4iimIsfToH z24kb^(;UH(h!(p!a?@)hY2*26=WDi4kN|8Kv23M85L1U6Ea5sNzFGPeN3!ptzdWt8 zJcSsPw_^Dqvc$vX-t^c+w~MmUUqhlKAWjs(g5r(uH$o-nu2**{;L6i*oz2;A*gAm_ znI9XLjqhHJz!m3in*0kcB>aX$NHk8oE4H=H0&h&0bSv;{_vf)zI1M0%Yb?jpZ8H-e z?r7Y@SlatVIE}CoV?F!>uqaNBvvf*N&p8vhSGUTEWf5-B^w*dB! zq3})@#5i^Q0~EJ2gxQedwd{v0Iru^jz5!;2`}O_TXf2gz%%An=CB~F)Dz9$Lr@2DR znei0nO=_&L00t`XTPdeySS80JN5P3A>GO&Ft;BiQ9VSL;z}nhi1tR-y%xy(j12WhkBCM}w5xxe z6VvcoeSdvBu5QFu4=AUqY_k1QtN#6LOiTVaS&U4)uP%{Knn1(PUF_MtOL4-tbI;1O;Kmo2pd4TKlcoz-RUo7fvpJ^_tPWH8n-m$RB_ z@(kJQAjpBD&h0Wl_T=@o%drL;^~>bbXn|gEHEw<6z6o#@`efmXBCEUp<>CL5k>(}V zzpm2_wN9h=!LmAzRnVv1)wBVzwe|XBI$UT+8e|H2fot>^M;ppPK~fMM9i8r;z{S|7 z7=XK&hjN`_{J5Dn6`Wp;OSTC4>js(qRAP?-Z;k?l36ZU#HSX6hU1!((uH0}jDG1o_ z%GQ{H?w6DsW0j2LGJ6n6@agUyKEBGUE0Dux0u*xh$$N!DwWNOe-PS`by-ivW&BxZ7 zdzXJSKuZUiA=BsNlV)+GJ(D|W9>&aY_bTd!JCA9VE8N@9?a~qJh2iF6#FHtaq z^?2ei(`K)i zHE1695VbO z*#BJJ+zQx+-#AXAhclP&z&6xEjGD2&Hu^zIlcgcM8lp7ExDx-6xVsk;wO9%q@zY%F zVI$LD5UCH9$g1grsM`3FuMj_h!&%=pm48z+Wx7MXV=40sUHt{zJGWYhnU_(dlj;8q zGfq2GjBN`_0N;G_L!ccn`5Tld90Ea2+@q5natAM z%)lMKrD3{#xfT5S-+eoM8$*Ea3{NMWge?&8Hh9Ci%?;vnEAnpLCgazNIaDdEC zC0wKLd#6C`kYs1qjwwF|uc0YLf$}vI3qKpe%_1uQ<)rt54V6$>HGLOV#-2VhDm{#S z9p7#~FJaw=cyR{rytar67tP#ZOkjObs`tgU!2h^tkJUriME!r==FCuyLlq6 zO9AA2bD@y!{ox$q@m9eodXjqd{XY}na>=ycCZ8_g=oa1Ish=fQjPxXG0{Q#ok$^$~ zXX3@g_I<5a`Kpe&akpK$`^@Exh6ZNEWGOOBXluuvk5Bf| zA{_M3lDMTKVwD(xmc@jZ?tgdr4HD=x4;@bRwPnTyOq zq}#aS`oK`t_opos(c{<|IApHkEm?Z&7FZPkhu2X_nGx{#Y&fbgqjtww-J?##w8>H`l>SC|Z((TfN8kOMy6H#T#Or@%JCb7ew2} zSSL+oirj-_x%|S(h6s@oG53d|WDSEcH5yG1$S%YBD*o@Y zFQr>4*W_H7$OCWMFzR0i^^f*0=UXc2RB1mx%*uu7hakF8I4_xpDodnxW7r~EZv~tZ z1+3p%ZaTgx8h&yMbMPdE_-O~*86O=K7|Z{aJNfIKb@G5l?d?j(MRlmf$3AMNSnX=@ z;uh@|s&6SmEgv^Won2+{HwhPZsrQC^p1#L>7@9abx)q!K$*L8(2&be}TA00e)g==;D-ihsE@E zS>!v6RsS*TxP5kF*}D&K`GB%KvfuJ5+HX3x|30^TJ=OYRa;s+dF~^<->RTV&_9hPs6iJB+b<(DPYi z>s+u3+Q0FXIa4g;%9XDh7n|x(SV+X)HL-hg`K@dIa|$);zo6=5cd2Bw-{2{;G?}#% zvbcNW?2@4OnG@T+;VA9ih{Epe`s$Eg#Nj5WJ}e=jXle1e0FL?4)7%UmYKj}TVKR=1+&v;9 z#Y)ci%gDa6x-k*_9`k@9qVqj-#Gz=At|LW=49zR=BoD7%`N68c-OV@)9elk=ME)nf2!8?n zvqKy#R>;W6$ipX3Dh|v50|?RqDk`dHms$Tev`^4gRl!Dl8MhkOjsHyAg%0*Q?U?NR zFoN-Fr?^*P4?b1L0*-!z$ErTl{MXC!kr@YLJrlyomcgoGrKzPadxkVs)#n-& z?cPrh(*TitA3f3RT{3y+W&Bxw#q8d)a_|1ay_@4JEAu=>759!V+^-ttXD=pej_xd< zEzzsHyVG*JSgR4U?B&O>N$U<2hkAeLp?|T6K%D5134w7L#auMh)YQ_~A3S&vpOkdx z{{6D4abR(vnou4B>PmO`Gir$%B>E=CD%X!oPQsFO3&IEri~92|1B%tOrCKTNX6=|x zTbQTvoZVzg`rEkn0J}SeRkJ+)YH$32w<)N&GoKaIrY5Q_<=2LDO;?*)!_FeYuAL@6 zDgaKPVV7q@kg>vA4FS{3C8{^NuZDH)Dq2%WCD%qlM9!F$hsdP>Yb>qz6KhDAC8WW;X2o4Dwj->w!!<8Z zTl+nlZrhCH#HeXolrTZC<)hv@3zmpCQNSg80P@wBJ@M~^(8W>2;7jDWImE2Ng&tG~ z3QZf`AWw26Qr%|1?Za`%AvONUxdTDMpC46;?GSlGAmdU139h#6F;#W@MVSd4U@RpQ|g0PF@e#iD6~R=anNH&`xQ^L*fBHOa*>Y%AIGf7P0* z2NEdYA)n});96b}N$mRN`ZKUK!XTZ_9Uok4w!{%IsHSAD+RdSXXnf|y*1n$Mb ziqDzhtJ6C0!p`|iFwsXCCrd(%w%-mlEsvQtX=I!iZ26-hxMhr}2-po|%A98}XlkO$d7TxV)iTnI*uH{{@=pJi6 zLt%1zgZ-Vs;6D;9C`*T*bEl$bO6PfSxrtkDTZp~6^rt2e*Ws8{oe%E;^x z|5<$I#GS>QqTaUfIfM85wWFpfoiRKJ5;P2QF^7LV9f)FB z62%1N4^G!EMyGd-T+}F6^k#Q=REo@8WiD()_i`S-uBY6*+tr*BgbqTn$&}fsj5ChF zVRFQA4az%e9&7TlXw;JazcIMcY=iC75-fY-4il57qCVed2=jvt;O$9sEyLfctw)2c zYCG=;&{sMrsh)?1vse&%IjHxP?R&H01-l~JVF9_AH~Q?^oTv|z3{DF7Sc46gaukR4 zJlozRZ}U!?3GF>45?s&%&{cYT-*67>O3NiEWM$<8Jao7noS^-0QM1yKz_pJFHf`&B zXS4a!JIbKsMjX_aAMO@p!&q-gcNnM1T%sc2jBOC{Yr-vL0Mkieej?JgeO+ zMDYFdAUtv<80)ytbZ2iefcr<(O2kjJyPe_lM7cb_TFWU?=iv;&SzIwtP^ca?8^&p=-VxigL!vH z;m|Owo^z1pYnA*uD3Ty}bD@Nm*1DF((RZP=h1uHQW191HLiz@9@do^sU^yUE>CFoY zwPE=8dKk~FpH2HDs;Rj6ot|o4`_mFhKs!`AYK^|3P1;!5Gx7M5fTHvnvZ^e?_l=Bg z_%RD;wZbi0teo&ADYZVrKimA*ey-okUv&b@rN3B>4zWZ9{|Cxl9 z?pvJvIfo~4e%~r=d|eA1Wa)tawz%t(iymL6VTv@SOk4)Ki_u970RgM#k_hK-w*r1V zu5oYMZ;`kRLyq1l#-xEa?t1TvHeA1Y9C?^e)|y^pO`+xyvkY(!pbF~x=w=xXYEGSr zpRe*nINyYL|F%Xg1LQ&=TE_6{-(vJk8P^<`!?813shW7iX3HbSp`qKV#Ln_@faE_d zF+aGaqHreWFyjTuZ|3Vx>L01qX=Qt#^nXJ&g}D=So+{|J7g(}N!_uhGlj+W z!$W^deXhHhWbcMgDWc=Vl1+wzKa|>K5p3+Hl%=E0t`T9}$o)LaeHxbw78gI;O>y4N zoD1wEzE&I2v)bYaXfWP`afZ{Y+{19hBT8?KFT?#JTW+<>(Yqg*VpwrP;^X^^0uYc-)Vq^POMD6PFV#Nrvx92fO|i?3A#$%-omQ_WV3O)0%~C?5hV))_otE z;yH6IihB@#vZUWweUl98gSHL(;r6Jytnp?-#(Jg{K`mXuAXW5xWHRHN-hV# zNVRwW%lK_uQkAg?Zx)gA2SSRFZ8H9U(zdfDvG4Mbm$^~UdbOJ{ZV)Z&Bv|a}S)4En|n|m5eyePKcr>K~~>VA=VU# zCGmLlVD*_XZ?DK+9(C_KkL=9E}1MyhiC zMTFkWDHaN4J=qWe-{Y#^{HJ#?bbbHvodV3#}8wiGIdBOi92)|IC!j1}OBY}p?fZ3jk8j>f;l ztn`(!ryOr^m&9(#4q}CBvUBl5($4U9n1UXhogL+J9QQPUZu744^xzQx*r%YKzrdI%G0c4U7wY2U&c<@Fp^rFUR<2OtO3dA;FoyU`bUI?hqnzpK1 zxOsVxbn4mJExx>90ZHoi+|M18x^jwCwk04ES{uy7z~*~;#WglPM7p}VbW%RtnVFfR zd1#Du0oiu?;6*>xSfy#7b9GzqV5V?0vi&l|tT{eDjxVeuVB0U(bEF;omL5+iVtkJle-BN=32n zoVjH^U{+)F0ikJU$ic4ITa9hy=_xdjAz;6^(6Ndzt~3A{SY zy*x#(AB|}Dt$OL0nDp~C&KzXu!BtgNL86)t&CAW*$#hpx>83m&X9%vxKGNT!rP!B& zF<{Bi17={owvS~QNGq&*UQvgD+8ag~rp^+$^1eD#%J;sfr{^l`!-o&$DvVUFlpbPj zpHb7G$aPDiMy&l*so%Ac6~+AI?s1snVPCQi6UBc2`gQn_9R~+TVgDgLJ-rC1nN5uY zeN-mcwq!4GqA%w`JrxB= zk-Fehx6Sct1fYb@VCYNvx^_H@{ks{&WUkeO!O|HDy(SCs6zKS=pI_}&C!Bv%vfHMF z$(u@$T+0WbAK8v4vDn^jFCuSGbpjg_Y!Ep_Cfk>(7Zo3ko3Lec_~|yMv~+e;lLROm z#wS&-$=_sOV%4<_R~=y!U;bM?>S#f60(k4FWYKx4p;eG_3t#Nk!yr{bh{6}+6Y7OY z_ZD^w0md3ERaCi37WaAu_?B1k&v?Ws!*Z~bZE9kbm#fr%dK0uyjS5heQzZ1z;e~k0 zRiGIf_`+%I1IiAvB){SS!2#20^JwEZP3mh5u#2#s+)_Jp{*BW+9J|MjXop6T{X7gh z+o68%%{p`CJ*0k9)_P-L!AohNj;*#3S;fPC``*GOV&j- ztnvw?^ga_m{cG8;f0G%R?c5+qYm#frD|H_B#lJe;WbCZD�y^;M4>NFMyZshBD9K zZ`m{Ug}Uv}`p&vq^ESL{5S)A6j^BW`F)JAlK$9_>w49QVd{Zxh);K}>_!2w)#r50&MvoB z3ppDzbr%Xg**-v^&k;~vAT_x-MIw@!rDc5SDQ-}g&&4e<0gP2SELMW=-VkGaVq$E& zmG}D3fH#tdT^EOiI6Ou6gic3C&3g=RaX=|OOsOrSsHBALKkJ)O!q36}0lpD_=fy~& zU8CB*n(6!gq{>UDX>p2u*_3NjU!d0la#MzIK!tivzR2(#(G(W~a%PO2OWRycMX940 zIbc%wN5}x3^#g3jees6ffL|bKivND&zzCA~^5r5t0tXU;zCH!21l;iN-!*{9?BC4s zcE2cu z1tQ$ZpBEs7DMDL+HQz58&1(!~%u*|*1MwPyrl?p7 zRMt^=ao7aI>a*!i-cWiE#jV>!P24-fcNl=bEM|79=)kcV;R-PKbX$Xp7|mB*#A6 z6BOav^4D*P`J2M~Sb`J5gbsn~JZSR)mUG`JqASi&?;!sjRak&*#ifb$(@$UO^OfE3 z|G2HpSL$dr@D0B5FOZ^!QmkmMw%{pI^&HMpOPZLN>B0v%9L!C z!jrf6TZVlcN+hhVBO;pl)v(GA?Vi7Ie^|Ulh2pTutR~=_=ggKG+C9nZqpLMjQO(Mo zk_*5tk1NEIeG z80K*A;S8%eu?mj5Jt3W^o-(HS_a`?^wtmFZ$hR%~-WjRAN(l-JZp4SvJm3IkL-ETi zFT7{xEfHI{W%~U^U&S*;O~I|g7~e;J)D^4D(ca%gIOOef95d6eS`PS6X0C7dYmcE; z#28h^4Kz1q+4F`qm*NS9J)zGiw1JP$+acoTnrWCIG>G_k*VWpuI@}OHLX-rBBIp)7 z2+U-^H38^F88K|a1rLe$qRuoxqJgvUbwq2qlUShJPP!LOO$^aF5&U+-f!W=*9-8wbRJwJflW)!lfl*| zTpsT_&+-c%`X6pmrG~F2ZYS0^W5Rj!cPP%;RgM<$av&BMEuRJGv04HvS53oM*% z?Ma-&QRIWYNI>&;&|p!%Qvk*Pa{!(Mw3X_@Jg%9%oiS4w$EpFESze3Q_rtl;hJmQl zd8j=+91BR8f|edwP~60-d5}T;+pufOxanPMbP;Dt=%3ZGkJ3hKPKZp`EzG08y4|$( zD`M|k^XWX4U0k4{LvbFlxcjMcAuxagKXZg$<6KI{cN zS~Iw&_-yx^JC8k@%|P!Fjex}Mf9Zm1z7!|krEXT&3#D(q0Z#&8G9OD)8ktiUutK>bypm#jO#X5U6+ydcjqLBm2ror&H+Rp~vLp)R5P6S0d`ERkLO%q_s~R^{E; z!FDOhZN2VYB2=d^Lo}u zc+0~U2pw-d$cZePu@VMLE|2DW4{xP1G?r%BM>b|wpR z!{G=k1ErrsY=&KnV$$7iC2=J-SQM zqvp}GkVSM23`*a4QZb}`n$xo?n}xkM**{AXdzSi?;6gZqm_}PXIXEd!@R)#_LMQ>->N8@fm&NeY7Nq*iF9&yfC%P0nlgkI9biWDviDIr6ehz_4r>I^PRe z{|xQhy-mw{Gx8H)$>?hBa1`C3*+k= zjtqSzjVJ3rcytLHjn9IPEl{#sB-{TX!ucV9lc$?JN?CxU&a>MP5vO zcQfMK>-a6gg7#}uDiGhXz!D&huFnT?FII+RJcLB(*!d)=oD^T)fk`0n#uvZ9%0GHu z5~O3HjvzrE#f-0lCHP$RPbA=>4)Z~=p%%I^V6S+=FpT^iUz*{Ezfgo{8fcB8@pdLv z}XTEMy!Zg#Epva&Obh=$wTkSaf-Q-5;_5n|EEv?U8=MFBT6pP2^Rv$Xy)! z8$NVcjlAKXEVxd?XcC0H2x~S&zZ-2eo5+5UAU9}z!Q_0TtNTFn$1HR64xck&YjJ$d=~aDW)*HWzpn<5xuDDD= zYwRQj(N4FglJta#qeDu7<|Yj*Tx6< zt3`jbSRO!x7*e7C?Y#=4+DOm}3fd_i+1c6EuhY`f3P1mT6SSQTgT}s2(DxYtbiWS2 zKfO%hA;&i0A<4qEPj`Ke-jT1pLfs}e`*xUgh2g#e*D^u>&soy}o7Wnw0O{Jh zPfJ=s_l*_f(&{4dJ}q?0KwAI($V&{0Rh-2 zOIX;G_kt1K-728Vx7#*FHnwYVPk*wb&nChfP5#ZoIngNeL2?Zr%9_u}NR2q@;YPtqoW>40)HPE!VN=u?JdV1p^PB=LK!v zV+lR_WouFL`7>zX+rH20gA$Ju_GIp_T<*9gMp*JzoJq()V)B;Ds0=EXxnZFdN{g!deWhsckZ0mEJ19DqXsOZ z-bEE~lFzl|yzgQRa5fW|x-)iHxFp`$(o{*!4`gE(89T&_*Qx)Nym{sUmojfI|A1!r zs>D+U?O8~gPN$}C!*jueFW<4 z373Y0bt#{}3P6Dkc67Ae?)RsM&tJSqhk}Ny5$}1Gnj3Xp`bb4xR&E&*q?uA~WNxds zvU^sKdYYeL{+5Ml=ZDr!=-6%hzG_Sz{*AlKhTp1mFXHETP@=Hftd}s`G2T3AbSPOY zK?^4%LaDNjk9UrNNU#a)n3{^pPtcW&k&U2oKnFW$iaP@2>C`w0onZJ-w)nD-)Ik1p zlP3q5Ea?9I_`kxH@0RhO@x}*%o?fjW;L?VniEM8!DO4k|zD|fEqt+dz=B86RnV!Yt zD%2X>EMzjZ=|XqT60M%4_t*$GM|M)e;)rBr=XA(ducT|{i(K@&jMWqMg!uUR{c;GQ zAV>=>0#~W$g0}cDd~{{w+#?GM7Qi;+Sb;5kk|k1lb!F<_mJYi5yg&~yhC;wdKY*3@ zM&FmBcY!`N|e_zveJalmP;+@B<}_0!xqdi2->qQ^=;`$NM;y^$$z zWE_Cl<+wd?5wY|2@dq74?qQ*zn?cjR4HIWI;eWX#_uP<{dO&Ob3be2T>p56S(>?&g zlb;~l2N1p|N7B2WHuh6dQ7uM@a+vn!4W=#Q3?!16Mr-Kh^n4R&GHgDb3myU!Gkk(W zca7IA7IDqEF#Fh%a(!vYv#pfcn!|Ss%MA*>bBAnwf1c<4wh2DcjHsvi=MqWNCeHla zp+7HhYxfwFE_XyFnudbMO-}Gbyi+_wE>t8QL!qFMFu3G(uH0zk{lea!l9pE8z~EhR zl(WseT7WIx3NmgD2T1WAY7W>BGCsb?@9Xuj@ve-oDv?1s?R4&0!tB7}PQnLn#y12E zdaD%mKonT!&o4Ap>(h?SA3S=9gdND*UMdLEgydX)_BWly{0|B4D^#Fs<1OSVjL+;e zv$mxxS#|Fh-Nl{0gQd^A(Z+ls?Ox*KVfF6{(9ncgo`s#qiF@M}FlelNcn3K{3P{#u z%VjR-%H<;k`rR7;hSqiF$7_T?g}hB0;qD=nvDWFGtIMAsl-#)$u-Q+=OA{dNOQ0PJ z`?Y<)%^q<-VBH2}e0JAXfBI1LDEOJVZ#z(i>?7|5!UhGE zoYu8#>y;KO(l~j!m`Q(cokzAT?Vl+j2=v#?3|beVW(|Rf7KXWYLeNWhB2g%ZADlR; ze_daP6c5wJ^tVQ7kAACOOk;$viuff;q8(%8foS+6guA48$g1h+1sr=|KAe?xTK&yr%93v6Zu)wV8VW*xU`v%;FcEtGJ@uuwB==wc+ zXy-JoQir0$Z=9;@KNG$jPjmLkfdp=&+$)9;y?+r~)ia}gfr~bz zg7gRnB?{3%AcLWV17F6QPg_sSW?Miz>(zCae#p0wjAqfiSA)KLV}zixm(R2g|AXfisZF=Drq zS);L#&O%!MrOvm#Z>7vJc;HFiF+ooe))<$dcc}5%v^(!-%n2>C_0#7nLAnAh4;fN0 z=ydc?vgKk_IY>*U@(40uSPdS?QOxK?V02#~7#R$Up zvTs@EeXrGDwdgu&UaTRDk|H&{NbTomim!c=xq@3XMzWMHe#;ikG&q?p{824-RqQP_ zZ=>H6=Cyp2zBU;xznJ>X&auF(6;+Yz5nx}`C-Qv`XPD!JscM`d>sWNE7SsV<4A);$ zmx8<}dtKR0iqkJv$WqDYsuWf|$yBtb#41rU*(9*H4Q0KY=})RM&2jvS+K)5W0{Ax` zaQ@hAMBx^T+%YL?U9polDQu%l?#gORXAj#Vq1yWq%$&yL9*&Q>%$@9QJqg-o5|?3( zqJtC3{DWkd;#lfe9A@Q;s%$QE2ts$Rpz5gzKDD!fNn%p1C?QMw)1P<=|8>OgIh;*D z`aHY$*ZTzJmKa83M=#xe=3T7}YKFy-zjwd{bae@lmRQp?bc$UxZqYB7ZzM!YJ-nIm zY@;@?$8#r^n&Z~$r*AYogy zPh*w&Z51w~q7aP72h+AnlIuiYzr|7BYNbsdIa23&rjT55e|j`jhtt{O#KNyLu6m@D zyE${cygir2UwFTe&Z#yRKNI9AfJWbzJ<=|J9#x)xt$rr1$jznJF3iEY`>tUc!*iEe zM@OOgu^@$UU*gt8Q(IyGY^OCKCR7O!%(WsE7}a-)-6G_Ty7Bj(u% z&Y{6!f_9CUeP23CZwH!M+ZJw@(o|k{6ViUlZE%wD;+AQi)zLB@+{5+GWWU@Qrgn7^ z2yT%byTzr9TF)DI?<*!WBo9_DlViBsADzcC1XX`<~2u_Ox@? z4{k`G=%Szs?Jo@#%h97^IBgkkbx(@9z=ucw&Z1;n_ju@VzMBTcYiZfqJn;fX_(;ho z{hZ#*b*RQ1XfnB>?;w=U592)HmvcY_q6Lph4=G?zPTfW2S1AZ zWl^iripmU6R@o?1Z~P1#Bx*;0pm57ORct8DO2-cJ0$HOJz2{rKP6-8Z4-8$~1w4aj zFzO@OEw6ChhgCy#d#$8impzWiY~CCH5U9v-9fz?s1~a4rBpXg%$_*PUv$r7K=P963 zCvMjH`j!oooHu{JqCW)=+JqvW5XYvd-L%KlcFY@koYzmr(Qm;I}oFu_({s zMkX$HLpw~!VV$-`p)3yz7Ysx^H7PMVgD*-t#ZdBlYYU%-5!fV)_}pDf(oj`YO2C{krnL>HQoR;TV&by^U$AiY`x^k1gQmxEb2 zzGM!%k2SBV6Cb$YPcK^fI*co2ZTkJw{3h_< zt^#u}Ozv`x^mY7CPkS@+rsrjb*{(MuB24|7Ipv`)Nf3(+Ijhg+y;Z-JB7-k^Oc`mr zm?{6TY~xwKG0YZc2$st788TG6{QfokkHesze&g<$7)%rsm=0u~REIG)kOe{fsr-#M{fI(mBF}e0kW73I@+;YKEkD~!{@j+g0m+bas1xndr0;v9s`mYca zFHwSgkKc|jgnA}Pj;T+|cQD$#+VCjH4#BJafzPe*vJ(@#t@XCQn!5?W301vE%yTEc92KF;kUMPGex_*%TjI zV{=^EEl2}TCcli){T8;lJQ7+xc7=y0w-Nv9)vGN1%8G$rgoFW#6Txbk>fJM!{}+1l zS|O%f+`az zi`-A@1uR7b7K>dAO-^Ry(qQ?k$}rb@`O7Ab56gj4+WQiA(A@v5Y3h3Mb!homd@0$t zmoEvQ@>2ouMD2ZBLqj64wq@cgjhiSC7YZOC8WZe<;!!Vsd~yw{k82g0sZH@ARN?WO zS5pZlCMIlza>1JH6id`NUnuIqR*Mj3x6H?TTqQL~@Jyx}!*duS2T)RO@FCHPx^g=$ z;nt?#DKgtlx2Aw2LMDEs!ov;pzN&#n&?wGXwju%I1vT)WPiGt*OXK_c`dI1E%x1fn zICd)Kg$#1PX=ey;Pp4(t2=bAG%EW%#vNY<*RmFD=(L6>o{rSuw&x8T+)8y0?k0nkJ zB^^qu0L6_4R1S>JKN3sxKQfM5|G=Z)^M6vZ`&eax!>=y8=ebiJVP{?NYvzbNz;+x1w-|kNb-vYd_1#YVFlHJAXh-SU= zsE_1CZTemD4c2jo3)6$9@y?-nh~)nB;5C)JS0MIQdstUdIpgh-&-1H|Gw&s3wxnM*V~_N54`=Ly52$P7219Y(Q!iFMUgcc)3pO{M4O`aVc7Y)W5V>wTbxzIyc{`~+N5mj(EtHn=XBSr7?iJqnJKkJGjl^>3CBXboj9x^L4 zO>y6%>}3ToZ&C(HY=8e|V{s5X9PHqmg~uJt$m=tWT*=8WQ}qs)cJW=^f`vw~l1IxP zxnD!{>uXm1x_OCFzSj2BT1(paa+1PPP&~*7$mMp!>yTF>HkyDv zA^3JNd(F_)dliWcC?(IzFei#-?!!w6VOPwtdX@t_vCxAOS*fLS-}p`YmW>=}2C#A( zdcOj<<0mCv9nR?O4pz@$Y}>*cge?jmi~a`EYb#Rm*hF~-IciQz7n7S{y4^r(d6EoX zdo@>+L&U>aqxGqF0tkam3~vW-%=RCydGx?*tdS%$PzMV~7#f-$jQ@@t4yMIh6-W=r zzE(w@=h3v6yqFWZC{o1uC6hGv#OuQ!C(dnPK{6E5T0Xf#Q>H+zXkmHFrCU zDTEzTRAjwdONY}3$cC$@p^wG+(shr%FS&j!@Yt~2&|d{~5aN0RrqfB}nxk^>RT1|w zQW{l0gSB_6zVimD4fIIn__coZ4~XVIVH?zoWK1aH5r>(oH7~U@?Ux-Dr8{U1#22MY0m$v$S=jtg=PdKrenha~+%fW~ zeO?n$Qjd+aSgAmN*CfD{zPT~dz{!5tPpfvw$Cuz3TXr*BfM=6irHJ;?qK}5V~vE^qcrI+!1h0fvTEDk4Eaz-J(R}=Z#zu)R`#p?QvuuhB@$V47MA*Gpb@a<4)?oagXK%)s@77+6B1yPWN&B4!? zsKcL)@VLZFpDCl3TxsyG2mEjNagplQa zo?1BpkS(9V3LL|0avhw0srO>40xq!0-o0x8;%|>J>f#bSUi?NehHHpi$LtwuX*V2B7qSvDq0rSlR+Z1%xVBckyjU zN9~1>heL#yMnPywbZMT3gvepOX9`3HO~9#GFDm{oh`nIZsb5kA!A(=+L$$)r+weM?~LgFMvhkpL>*Y^^YZ?M0cqdp z5J>pKvSn^jdGMZ7w}cum@=tY>j7Rxe(SpewRzZtWIu8UZmqnV7we456QEkQO#=<+F z!8-w91Qf!*0G8T>*dGW~)pPF`4c)nQvo`aei{3;<)kCYHE{eL|fI72^ z9{^2}^ewZ}Pe3U%gNW$*x;tRXd9U~jL+`q!Z>=E@923J5&y#xqvar(O$@!)zr+2NzH0M|x1&a^x z;@ICwBP`d{GcN*89?SzYGdutFcTw5%e1mia786bMwVS{fHB^8{Ua;(jL^paf%i8f}2bp+C39-mGO$plg_pQa>Y|a7eqE& zgyd)x^X0*5W(1UsFx8$b{BO4Ew#HIP^RcKl=%Y56B z6_PT+Pr|T{Mp_CttN`eu&}Xm!c&asR>1R%x9c36d)!v%2oiAnUdmVQ4mc%_m0g|WR z3q9h_8Ikf93z3C=8xMZ>5rr`G1kOhYZQJiy>P1(^z?zCmKS$Gg=BO4o=GJv^4O|5f z3OQgf5z&(xH`p{`qiP|>gJ8+444O-I*!}N1UarJ*k}}acAK;9tidsil_{(;Jy6)w! zq-|}3&8hc{5|*^c__ODunuCvOzdmauPz;vULGqzr3|%o{U$Sj!A=K77*j!THkCOD& zE>3l_vlPo`dX*c7e12U2e?GqjFeAHO<278F!fpi{@iRbVFt}Z{Y0~`sM1{F=67-37 zUl1flA@&LY)mNKR_e2D&&Q)576W+qMR2dJ&+0MhQdO4BbF9i5U%OCn-q?)H~i{Suw zrLASKCP$kD1@jGiErqA(jG4mxaUSIRh9rlstMjW|h`XxTB`&7YfDT1L{BER}tSDh9 zcJs`J04w@~)Svh5cFpPfxmXTkJhyq1c8mX_A-~j}cIoIK<5^4>M*8rid)Sthp9kYw z@OXZF3!R(I0TmlhPiqR4P%1RFAS-9qfb66mI1RzuL4J|nJ&ek)kZoo`Y(i=PsNSz5I$5oi2c~(}}bel(Bzsg@2F;pJo_jR(Yi4cwJER_hvVB1b3_6mm~lt>*1@# zqMB+03huaQ-amV7k-WP zd*kET!5NfB0=W`oVU>TJsOZ1i#_U(;BlnPk*!85U>dX-aSxg`QnxWL2ECeTZ`LsHl z8~(vJinYkXPW?~&X@P@j+jfIiOz-R4?$W%FU#@!tjzWAPDXMP;70s#ZO@ob0wFKl; z>kgeB;G;Z$QkSaJ?jhDi+}z1zwf;RjbUT|@l&i!YP4X?R_@%&$sE#4c@BE<5PII|C)pY1dY)|k<_y>Sa z>2yZDH}W8I{ipfD&Z*5==Naqkxm?pJ6^<)(%WGhBq{j1YaZne2NCKx{_Ool`-T;9tFfRe>%EQkTT0Nm3jQ66GD%5r67)3K*BX7{8I2|17?nuji zg(2;A6*+SpuU-GU812R1gVkAJdu33byIrnD_9HMiN>uuOPUNb)L{R*;7FkViKzlYb z+V&opOlYyQWSA7q@+Yy~f!Uk$QYj>xw47PzNrLO+iGu5!-=V=Dg1C>Cs*mxNU@Y9C z$w-;l&!xk$gfh=wQbYJ}XWfjwz9GLyvxNb{XxaO#(EhMji&4a zdw)Wqy$j&Te%4?;s2_3#5RJtRoY#!K^vH(_y(a>R@Bw}Lo4DsV!_8?5n`@OX z!TYwpAkmpm9Qu2|gY0Y49mAA~l0vk9uAg!ft@J&~i-imPg>_`Byhzn=*BR!XrKC~R zu^0S2M|ZFb)0t>3pfAdac{@;+X;cE}(&q)t2Llf1KBxx8Lyn0(jZWR0Lu_pU{@jAsMtnB4FsZMVGH@-H?@Hit6f8MSO zhbP%hF<9ftGI+LTaJfrrA}Tf+Z_7Ta*fxoUh&hJ)NKJUwmdQ;|UbxxVCK_&n>x~BY zK1PwOQX}6lUps!r`m*||muHNcBn)+G?FttS%rY406<`mQEW3(smXmx)=wL4OuJgBG zJSR9x-@aFC7|tSqS7ue^wGn@B;HM>)qlR*iS$m$G@3ITo@dWqfoTSe=Og=d#|Ey)9 zVZIi?rh=xkD5{z&C@_U*F&x4<3+8or88MSuS_~5_PTKxuaQX=<<*_;=K@!?HI%!TmR`cH99Prh2mrKrfB7DjVlu2&EA8Sl)y zGHtD*GnZI1cD+hx$1Z}lDTJneUPSLqT(`Gq^NBn%1LP5IJvEl1k61IVIU|wHy5M*&ExyC}Q zz(hisqR5;=GEuCO*0gOME~>FvxHjFWr#)w_{9rCIvF7e=8y~-g__HhP?GnjTaYqe$GJ*$?Jb3M!5kppYRIV8+$^XXA__;|?M5U7Gkb)-iQGwX@oyCy&L!Xn-HxEb+2MeP0LE z85L#3ro|2((x(@&k6wJ$2e-Vxe>w-n@;dV~Gs7g)_(zgEFuWJoi z;s^?rr?)L=%3HCdopS3LL#Zxc|7diNYK|v=`EjClirDXz{C74^rnE05wUTw-A8}Q0 z)@tv#)TUr{M(uAaepoGeYmkeTQ3}3AW0`qkV#!U4s!qyvn|a&=neJc|#&sLA2fbR+ zA*M^~lDd7jR&EWKH#*~;RQ2w6pJ?Z0On$gu#iMfO4l7|Z#RReV$;roP1_nTuk;+GL z?ls}X_qUA(ima3XWEL;%^b@FktI8?x;&H1ibz+EbFxE+Te)2pGVWpW7q36)zGk|-C zLr!PLUFyZlJ*mcf7hyxW8x+h~p-u?4o}@3)miPI})6aNKS|5YY614n%F0iS;1_7S- z4|l#dH)n0`0y--qGp8b!$`boLf=$B~G%D@j4<3#$+gMw0dXXKN*c9t9dln@{ZT$CR zMd3z1U=V7_$sb-=HQxC;RXCb+bbm!JO%wC&m00{i+Nwt0X|BO0C7Ky|YgyqAH2 zftnTWHvzE1>D3AdbLW*uaGC`s9#9=4@J9gI=Gu%83lrR)j(%A6rH6_YnA3 ztN4Mt*zU@mNDl2fK&w6dS%lPiJZkqkDunvx8;{?ch_uV=_wN;Zvvt8aptvIu3MZ_r zo`AoaKwBA)NOXdXQrz#Q(olYj0dDgic7VA(?kT#0yryV3k2EnSgA_JW|Lx4zGe^OP zj7ATGBL@EYkBk`zwI1d&k7xh!x?UXd2cTLiWJBE~lUbd_Rv%A5tmq0eGto0}wZCP9 zJIONi6ctH;{z>b?wQ;1T&&=|lc|XP@%)tx%HaxrrK>gk3?MkywlLza4i6`wzS)CW_IR z)_vtRJB55iC}akR(^Z$%{=d~|)u!elJqQu4^D|IXC%bT=7Mwf%1*UIr*7~MT?1j{Y z(762=>4y9aCW^WAPXJ_xsJ8>b3kj!iR%{#cKkd6e^{%Q)0ucl)cir#@GFe^|C(vW7 zlLPK3DR|15#5?pTd&v~xYy)Mob5Ju!|J&S#*eQ$MfN9zx`YP0i7%3;9byY+Jw*jS!O31ey5~3WHQ&8Z`Y<}%Hx!f>iTZ~M+-gu|s z*@$^l!uBS}E%7(YkwVrz*1-bPb1*nljhiBaF7pMzq|29oW7G#Z6=2;_+fRjL3o~75 zq5G-3&AU5z)Aqz8ivxudz+;>OG9V`EJP=;D z;p=UOL>`c)rg^SAbEmg*zZSX$p_eG`#GZOaz6SqO_r#^hQzF8-_@6C1GfAP?F<+{C zS@#&Nbsf6Zrs`q!O@+;_z^pr92S1JE1&DsAIfJ06y>?gqmMk`n>VMG;0oD7@tq`$< z%r-Zl_UHKkq%gjdkMG!m@yU}X0UVm7u5SGJF_Nd}?WZ@D2uF0_t3?wKr&~CsK8@{{ zc8olDWZIv{1b6=`UCV_OH}N@@`>}vjewC^a7b!jhPeOM?&L8Ut{D#w{8+=#|_>*EN z+8tpP0Ji+S7cS;u47EI@O3?oL^aGR*6NT=6((&;a0$p7bh=`AG0aDMpz*N2C;y^7p zdDoq$7+^&}z7DhJoeTaaq!uKQGW2 z?$0@$r=x3w^f@)gQMv%FIs?+h#0R_6;-*6-d_dsg%bjcIF{mOO_B!yW*d8Y~gDuEY zUmwK34)u|1ZK}@*AP7}(?7j*Po&tw*BE&e?7YBPM=k8RmddfhPjqPoXn1Ectk4%lw z?o172)v&3%WYP{%b5*WyZ03Pe*C6}JyG)?oW<&dHK(%!PDY^Jd8CiJ2#i#)q$vUvw zXTt=+N{3L-4wSj%_Z#~ZK(Bs3M!mrUtp!Zh0&gJC=dtndbQl*6>Q2A23o(c4{G>Rn zWOzctvJlWVbs@QmlaHTYX{>^z*MLrZ2uC57{J2ityZY-~*o zy+z>{klhcKEkEUCBWJREAZw;7Nx! zNMC@E#Wo;vp9ReJBP2qYm58l5Si}VrO(tS%8`K7FE*5ls8C-;9HTWN@N@_cZH~!#P zZzb4H2%(|G!CcR6v5<8dLDNHmE^-=Gz9gVu3ssJNVg{^6LNbGMri1LSJTj~jpZ|UhLs#q zJ3ut7cjiHxp#?UaZ;*qc36e57Lk0}k1v6CYVd%7}Dv8!YekI6^H36uGt?yf0=nvK7 zw#7OaN_r&e!bdcWz8zTYXM1z>ckqasPs^S3IE!Vda6236@l@XN0WO)Y)-OKWUYbWM~;311bgxv=G9@m-+`DHBFWO2_6YLe*dXRKn7)#* zI_=D5=D@r|C4!Wx`+A3r$jY~)$n!{+2tPW{=v<@vNc{;Y*<{7a|Nls) i_phq~EG5wgM4R?o)5T@SA3#$!v*7-v0-NT3UMm literal 0 HcmV?d00001 diff --git a/recognition/adni_convnext_47280647/images/rerun_subjectsplit_lr3e-4_hd0.2_s123_acc_curve.png b/recognition/adni_convnext_47280647/images/rerun_subjectsplit_lr3e-4_hd0.2_s123_acc_curve.png new file mode 100644 index 0000000000000000000000000000000000000000..d1cc1b36b2dccc87fcd93c7e6102b8bce8e78b44 GIT binary patch literal 26733 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

?2yyaOxTpsdT6szFORipHrM&H|)PP#W_ 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 z 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 shUk0ZC#D7d{NFt7=D&{F!`5k}E*bzW<(a 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

&0D{a)Hie331a=HFBHTNMn} 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 Ygc0H0wY~@bWCn$rvX)Z8IkSiV1K Date: Thu, 6 Nov 2025 14:49:04 +1000 Subject: [PATCH 30/31] final touches on report --- recognition/adni_convnext_47280647/README.md | 57 +++++++++++--------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/recognition/adni_convnext_47280647/README.md b/recognition/adni_convnext_47280647/README.md index 3484965f8..49002d37e 100644 --- a/recognition/adni_convnext_47280647/README.md +++ b/recognition/adni_convnext_47280647/README.md @@ -1,4 +1,4 @@ -# ADNI ConvNeXtLite Classifier – Problem 8 (COMP3710) +# ADNI ConvNeXtLite Classifier – Problem 8 Author: Shivam Garg Student Number: 47280647 @@ -27,29 +27,28 @@ Student Number: 47280647 1. [Environment Setup](#101-environment-setup) 2. [Training Commands](#102-training-commands) 3. [Evaluation Commands](#103-evaluation-commands) -11. [Reproducibility Checklist](#11-reproducibility-checklist) -12. [Dependencies](#12-dependencies) -13. [References](#13-references) +11. [Dependencies](#11-dependencies) +12. [References](#12-references) --- ## 1. Executive Summary -This project tackles **binary classification of Alzheimer's Disease (abbreviated as AD) vs Cognitively Normal (abbreviated as CN)** from **ADNI MRI 2D slices** data, targeting ≥ **80%** test accuracy on a strictly **patient-wise held-out** dataset. This implementation follows a leakage-safe pipeline including grayscale conversion, 224x224 resizing, normalisation (x-0.5)/0.25, and ligt MRI-appropriate augmentation, paired with strict **subject-wise** splits to prevent data leakage (patient overlap) across training, validation, and testing set. +This project tackles **binary classification of Alzheimer's Disease (abbreviated as AD) vs Cognitively Normal (abbreviated as CN)** from **ADNI MRI 2D slices** data, targeting ≥ **80%** test accuracy on a strictly **patient-wise held-out** dataset. This implementation follows a leakage-safe pipeline including grayscale conversion, 224×224 resizing, normalisation `(x-0.5)/0.25`, and light MRI-appropriate augmentation, paired with strict **subject-wise** splits to prevent data leakage (patient overlap) across training, validation, and testing sets. -Two models are implemented to bracket performance and guide design choices. A compact **TinyCNN** provides a clear, reproducible baseline. A **ConvNeXtLite** classifier then scales representational capacity using modern CNN components (eg., depthwise convolutions, LayerNorm, larger kernels) to better capture subtle brain textures. Training is implemented in PyTorch with **Adam**, checkpointing, seeded runs, and automatic curve exports. +Two models are implemented to bracket performance and guide design choices. A compact **TinyCNN** provides a clear, reproducible baseline. A **ConvNeXtLite** classifier then scales representational capacity using modern CNN components (e.g., depthwise convolutions, LayerNorm, larger kernels) to better capture subtle brain textures. Training is implemented in PyTorch with **Adam**, checkpointing, seeded runs, and automatic curve exports. ## 2. Problem Definition ### 2.1 Problem Statement -The task is **binary classification** of brain MRI slices into AD (Alzheimer's Disease) and CN (Cognitively Normal). Inputs are 2D axial slices derived from the ADNI scans; the output is a single class label per slice, with patient-lavel reporting obtained by aggregating slide predictions per subject. The primary objective is ≥ 0.80 accuracy on a strict patient held out test set (to prevent data leakage). +The task is **binary classification** of brain MRI slices into **AD (Alzheimer's Disease)** and **CN (Cognitively Normal)**. Inputs are 2D axial slices derived from the ADNI scans; the output is a single class label per slice, with patient-level reporting obtained by aggregating slice predictions per subject. The primary objective is ≥ 0.80 accuracy on a strict patient held-out test set (to prevent data leakage). ### 2.2 Dataset Overview -The data is categorised as follows - -- **Souces and Classes**: The dataset is a two-class subset of ADNI with labels AD and CN. Each subjec contributes a 3D MRI volume from which 2D axial slices are extracted for training and evaluation. -- **Data units**: Trainint operates at the slide level. Evaluation includes bnoth slice-level and patient-level (aggregated) metrics +The data is categorised as follows: +- **Sources and Classes**: The dataset is a two-class subset of ADNI with labels AD and CN. Each subject contributes a 3D MRI volume from which 2D axial slices are extracted for training and evaluation. +- **Data units**: Training operates at the slice level. Evaluation includes both slice-level and patient-level (aggregated) metrics. ## 3. Methodology Overview -The approach is an end-to-end pipeline that turn ADNI MRI 2D slices into patient-levl AD?/CN predictions while preventing data leakage and keeping runs easy to reproduce. It combined a transparent TinyCNN baseline with a stronger ConvNeXtLite classifier to bracket performance. +The approach is an end-to-end pipeline that turns ADNI MRI 2D slices into patient-level AD/CN predictions while preventing data leakage and keeping runs easy to reproduce. It combines a transparent TinyCNN baseline with a stronger ConvNeXtLite classifier to bracket performance. ## 4. Data Pipeline @@ -188,7 +187,7 @@ Slice metrics come directly from the validation loop / `predict.py` (averaged ov --num_workers 1 \ --save_dir runs/rerun_lr1e-4_hd0.3_dp0.2_s42 ``` -- Tweak `--batch_size`, `--num_workers`, and `--data_root` to match your environment; each run writes `config.json`, `best.pt`, and curves into `--save_dir`. +- Adjust `--batch_size`, `--num_workers`, and `--data_root` to match your environment; each run writes `config.json`, `best.pt`, and curves into `--save_dir`. ### 10.3 Evaluation Commands - Evaluate the best run on the held-out test split: @@ -206,15 +205,25 @@ Slice metrics come directly from the validation loop / `predict.py` (averaged ov ``` - Swap the checkpoint path/flags to evaluate other configs. Output prints slice accuracy and patient accuracy (if subject IDs were returned by the loader). -## 11. Reproducibility Checklist - -## 12. Dependencies - -## 13. References - -## Current Implementation Notes (for reference) -- `modules.py`: TinyCNN (baseline) and ConvNeXtLite model -- `dataset.py`: ADNI loaders, preprocessing, augmentation, subject-wise val split; returns `(x, y, subject_id)` -- `train.py`: training loop, validation, checkpointing (best.pt), plots -- `predict.py`: evaluation on test (slice-level and patient-level) -- Per-run outputs (config.json, curves, best.pt) are produced alongside training but left untracked; only the illustrative figures live in `images/`. +## 11. Dependencies +Install the following libraries (either individually or via `pip install -r requirements.txt`), then add the appropriate PyTorch CUDA wheels manually. + +``` +pillow==10.4.0 +numpy==1.26.4 +matplotlib==3.8.4 +tqdm==4.66.4 +scikit-learn==1.3.2 +scipy==1.11.4 +# GPU stack (choose wheel for your CUDA version) +# pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cu121 +``` + +## 12. References +- Chandra, S. (2025). *Pattern Analysis Report Specification v1.64*. COMP3710 Teaching Material. +- Liu, Z., Mao, H., Wu, C., Feichtenhofer, C., Darrell, T., & Xie, S. (2022). *ConvNeXt: A ConvNet for the 2020s*. CVPR. https://arxiv.org/abs/2201.03545 +- Alzheimer’s Disease Neuroimaging Initiative (ADNI). (n.d.). *ADNI MRI Collection*. https://adni.loni.usc.edu/ +- Wightman, R. (2021). *Stochastic depth and modern ConvNets in PyTorch*. TIMM GitHub. https://github.com/rwightman/pytorch-image-models + +## AI Declaration +ChatGPT 5 was used to refactor and improve the flow of this document. Github Copilot and ChatGPT (gpt-5-codex) was used to fix bugs relating to data leakage and optimise data loading pipeline. \ No newline at end of file From 4ec80153dd188f510db16b7de5ca28bf4942f300 Mon Sep 17 00:00:00 2001 From: shiv-0831 Date: Thu, 6 Nov 2025 14:55:34 +1000 Subject: [PATCH 31/31] fixed references --- recognition/adni_convnext_47280647/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/recognition/adni_convnext_47280647/README.md b/recognition/adni_convnext_47280647/README.md index 49002d37e..8a8e40a10 100644 --- a/recognition/adni_convnext_47280647/README.md +++ b/recognition/adni_convnext_47280647/README.md @@ -220,7 +220,6 @@ scipy==1.11.4 ``` ## 12. References -- Chandra, S. (2025). *Pattern Analysis Report Specification v1.64*. COMP3710 Teaching Material. - Liu, Z., Mao, H., Wu, C., Feichtenhofer, C., Darrell, T., & Xie, S. (2022). *ConvNeXt: A ConvNet for the 2020s*. CVPR. https://arxiv.org/abs/2201.03545 - Alzheimer’s Disease Neuroimaging Initiative (ADNI). (n.d.). *ADNI MRI Collection*. https://adni.loni.usc.edu/ - Wightman, R. (2021). *Stochastic depth and modern ConvNets in PyTorch*. TIMM GitHub. https://github.com/rwightman/pytorch-image-models