{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.18","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"tpuV5e8","dataSources":[{"sourceId":3649,"databundleVersionId":46718,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CIFAR-10 from scratch (PyTorch) — local macOS (M-series) + create submission from your local test folder\n# No pretrained weights. Trains on torchvision CIFAR-10; infers on your local Kaggle test images.\n# Paths below are set to your OneDrive locations.\n\nimport os, glob, random, pathlib\nfrom contextlib import nullcontext\nfrom typing import List\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, Dataset\nfrom torchvision import datasets, transforms\n\n# ========= Local paths (from your message) =========\nBASE = pathlib.Path(\"/Users/la_anani/Library/CloudStorage/OneDrive-Personal/Documents/HBKU subjects/Applied Deep Learning/cifar-10\")\nLOCAL_TRAIN_DIR = BASE / \"train\"                   # not used (we train on torchvision's CIFAR-10)\nLOCAL_TEST_DIR  = BASE / \"test\"                    # folder with test PNGs\nLOCAL_SAMPLE_SUB = BASE / \"sampleSubmission.csv\"   # sampleSubmission.csv (note the camel case)\nOUTPUT_SUBMISSION = BASE / \"submission.csv\"        # where we'll write the CSV\n\n# ========= Reproducibility =========\ndef set_seed(seed=42):\n    random.seed(seed); np.random.seed(seed)\n    torch.manual_seed(seed); torch.cuda.manual_seed_all(seed)\nset_seed(42)\n\n# ========= Device selection (MPS > CUDA > CPU) =========\nif torch.backends.mps.is_available():\n    device = torch.device(\"mps\")\nelif torch.cuda.is_available():\n    device = torch.device(\"cuda\")\nelse:\n    device = torch.device(\"cpu\")\nprint(\"Device:\", device)\n\n# Use AMP only on CUDA (MPS autocast can be finicky for training)\nuse_amp = (device.type == \"cuda\")\ndef autocast_ctx():\n    if device.type == \"cuda\":\n        return torch.autocast(device_type=\"cuda\", dtype=torch.float16)\n    return nullcontext()\n\nscaler = torch.cuda.amp.GradScaler(enabled=use_amp)\n\n# ========= CIFAR-10 (official) via torchvision =========\nCIFAR_MEAN = (0.4914, 0.4822, 0.4465)\nCIFAR_STD  = (0.2470, 0.2435, 0.2616)\n\ntry:\n    randaug = transforms.RandAugment(num_ops=2, magnitude=9)\nexcept AttributeError:\n    class _Identity:\n        def __call__(self, x): return x\n    randaug = _Identity()\n\ntrain_tfms = transforms.Compose([\n    transforms.RandomCrop(32, padding=4),\n    transforms.RandomHorizontalFlip(),\n    randaug,\n    transforms.ToTensor(),\n    transforms.Normalize(CIFAR_MEAN, CIFAR_STD),\n])\n\ntest_tfms = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(CIFAR_MEAN, CIFAR_STD),\n])\n\ndata_root = './data'  # torchvision cache\ntrain_set = datasets.CIFAR10(root=data_root, train=True,  download=True, transform=train_tfms)\ntest_set  = datasets.CIFAR10(root=data_root, train=False, download=True, transform=test_tfms)\nclasses = train_set.classes  # ['airplane','automobile','bird','cat','deer','dog','frog','horse','ship','truck']\nprint(\"Classes:\", classes)\n\nBATCH_SIZE = 128\nNUM_WORKERS = 2 if device.type != \"mps\" else 0  # mps sometimes happier with 0 workers\ntrain_loader = DataLoader(train_set, batch_size=BATCH_SIZE, shuffle=True,  num_workers=NUM_WORKERS, pin_memory=False)\ntest_loader  = DataLoader(test_set,  batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=False)\n\n# ========= Model (ResNet-9, from scratch) =========\ndef conv_bn(in_ch, out_ch, ks=3, stride=1, padding=1):\n    return nn.Sequential(\n        nn.Conv2d(in_ch, out_ch, kernel_size=ks, stride=stride, padding=padding, bias=False),\n        nn.BatchNorm2d(out_ch),\n        nn.ReLU(inplace=True),\n    )\n\nclass ResBlock(nn.Module):\n    def __init__(self, in_ch, out_ch, stride=1):\n        super().__init__()\n        self.conv1 = nn.Conv2d(in_ch, out_ch, 3, stride=stride, padding=1, bias=False)\n        self.bn1   = nn.BatchNorm2d(out_ch)\n        self.conv2 = nn.Conv2d(out_ch, out_ch, 3, stride=1, padding=1, bias=False)\n        self.bn2   = nn.BatchNorm2d(out_ch)\n        self.down  = (stride != 1 or in_ch != out_ch)\n        if self.down:\n            self.proj = nn.Sequential(\n                nn.Conv2d(in_ch, out_ch, 1, stride=stride, bias=False),\n                nn.BatchNorm2d(out_ch)\n            )\n    def forward(self, x):\n        y = F.relu(self.bn1(self.conv1(x)), inplace=True)\n        y = self.bn2(self.conv2(y))\n        if self.down: x = self.proj(x)\n        return F.relu(x + y, inplace=True)\n\nclass ResNet9(nn.Module):\n    def __init__(self, num_classes=10):\n        super().__init__()\n        self.stem = nn.Sequential(\n            conv_bn(3, 64),\n            conv_bn(64, 128),\n            nn.MaxPool2d(2),\n        )\n        self.block1 = nn.Sequential(ResBlock(128, 128), conv_bn(128, 256, stride=2, padding=1))\n        self.block2 = nn.Sequential(ResBlock(256, 256), conv_bn(256, 512, stride=2, padding=1))\n        self.block3 = ResBlock(512, 512)\n        self.head = nn.Sequential(\n            nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.2), nn.Linear(512, num_classes)\n        )\n        self.apply(self._init)\n    @staticmethod\n    def _init(m):\n        if isinstance(m, nn.Conv2d):  nn.init.kaiming_normal_(m.weight, nonlinearity='relu')\n        elif isinstance(m, nn.Linear):\n            nn.init.kaiming_normal_(m.weight, nonlinearity='relu'); \n            if m.bias is not None: nn.init.zeros_(m.bias)\n        elif isinstance(m, nn.BatchNorm2d):\n            nn.init.ones_(m.weight); nn.init.zeros_(m.bias)\n    def forward(self, x):\n        x = self.stem(x); x = self.block1(x); x = self.block2(x); x = self.block3(x); x = self.head(x)\n        return x\n\nmodel = ResNet9().to(device)\nprint(f\"Params: {sum(p.numel() for p in model.parameters())/1e6:.2f} M\")\n\n# ========= Optim / Loss / Scheduler =========\nEPOCHS = 200\nLR = 0.2\ncriterion = nn.CrossEntropyLoss(label_smoothing=0.1)\noptimizer = torch.optim.SGD(model.parameters(), lr=LR, momentum=0.9, weight_decay=5e-4, nesterov=True)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\n\n# ========= Train / Eval =========\ndef train_one_epoch():\n    model.train()\n    total, correct, loss_sum = 0, 0, 0.0\n    for imgs, labels in train_loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n        optimizer.zero_grad(set_to_none=True)\n        with autocast_ctx():\n            logits = model(imgs)\n            loss = criterion(logits, labels)\n        if use_amp:\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n        else:\n            loss.backward(); optimizer.step()\n        bs = labels.size(0)\n        loss_sum += loss.item() * bs\n        total += bs\n        correct += (logits.argmax(1) == labels).sum().item()\n    scheduler.step()\n    return loss_sum/total, correct/total\n\n@torch.no_grad()\ndef evaluate(loader):\n    model.eval()\n    total, correct, loss_sum = 0, 0, 0.0\n    for imgs, labels in loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n        with autocast_ctx():\n            logits = model(imgs)\n            loss = criterion(logits, labels)\n        bs = labels.size(0)\n        loss_sum += loss.item() * bs\n        total += bs\n        correct += (logits.argmax(1) == labels).sum().item()\n    return loss_sum/total, correct/total\n\nbest_acc, best_path = 0.0, \"best_cifar10_from_scratch.pth\"\nfor epoch in range(1, EPOCHS+1):\n    tr_loss, tr_acc = train_one_epoch()\n    te_loss, te_acc = evaluate(test_loader)\n    if te_acc > best_acc:\n        best_acc = te_acc\n        torch.save({\"model\": model.state_dict(), \"acc\": best_acc, \"epoch\": epoch}, best_path)\n    if epoch % 10 == 0 or epoch == 1:\n        print(f\"Epoch {epoch:03d}/{EPOCHS} | train_acc={tr_acc:.4f} | test_acc={te_acc:.4f} | best={best_acc:.4f}\")\nprint(\"Best official CIFAR-10 test acc (offline):\", best_acc)\n\n# ========= Build submission from YOUR local test/ + sampleSubmission.csv =========\nassert LOCAL_SAMPLE_SUB.exists(), f\"sampleSubmission.csv not found at: {LOCAL_SAMPLE_SUB}\"\nassert LOCAL_TEST_DIR.exists(),    f\"test/ folder not found at: {LOCAL_TEST_DIR}\"\n\n# Load best weights\nckpt = torch.load(best_path, map_location=device)\nmodel.load_state_dict(ckpt[\"model\"])\nmodel.eval()\n\n# Read sample submission and infer column names (usually 'id','label')\nsample_sub = pd.read_csv(LOCAL_SAMPLE_SUB)\nid_col = \"id\" if \"id\" in sample_sub.columns else sample_sub.columns[0]\nlabel_col = \"label\" if \"label\" in sample_sub.columns else (sample_sub.columns[1] if len(sample_sub.columns) > 1 else \"label\")\n\n# Collect test files (png/jpg just in case)\ntest_files: List[str] = sorted(glob.glob(str(LOCAL_TEST_DIR / \"*.png\"))) + sorted(glob.glob(str(LOCAL_TEST_DIR / \"*.jpg\")))\nprint(f\"Found {len(test_files)} test images under {LOCAL_TEST_DIR}\")\n\nclass LocalTestDS(Dataset):\n    def __init__(self, files, tfms):\n        self.files = files; self.tfms = tfms\n    def __len__(self): return len(self.files)\n    def __getitem__(self, i):\n        fp = self.files[i]\n        img = Image.open(fp).convert(\"RGB\")\n        return self.tfms(img), os.path.basename(fp)\n\ntest_ds = LocalTestDS(test_files, test_tfms)\ntest_loader_local = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n\npreds, names = [], []\nwith torch.no_grad():\n    for imgs, fnames in test_loader_local:\n        imgs = imgs.to(device)\n        with autocast_ctx():\n            logits = model(imgs)\n        y = logits.argmax(1).cpu().numpy().tolist()\n        preds.extend([classes[i] for i in y])\n        names.extend(fnames)\n\ndef file_to_id(name: str):\n    stem = os.path.splitext(name)[0]\n    return int(stem) if str(stem).isdigit() else stem\n\nids = [file_to_id(n) if id_col == \"id\" else n for n in names]\npred_df = pd.DataFrame({id_col: ids, label_col: preds})\n\n# Ensure correct row order by merging into the sample frame\nmerged = sample_sub[[id_col]].merge(pred_df, on=id_col, how=\"left\")\n# Fill any missing with a safe default (rare, e.g., file missing)\nif merged[label_col].isna().any():\n    merged[label_col] = merged[label_col].fillna(classes[0])\n\nOUTPUT_SUBMISSION.parent.mkdir(parents=True, exist_ok=True)\nmerged.to_csv(OUTPUT_SUBMISSION, index=False)\nprint(f\"\\nWrote submission to: {OUTPUT_SUBMISSION}\")\nprint(merged.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}