{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":3362,"databundleVersionId":31148}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"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,"execution":{"iopub.status.busy":"2026-04-11T17:38:45.537918Z","iopub.execute_input":"2026-04-11T17:38:45.538407Z","iopub.status.idle":"2026-04-11T17:38:45.544169Z","shell.execute_reply.started":"2026-04-11T17:38:45.538383Z","shell.execute_reply":"2026-04-11T17:38:45.543609Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 1. Import","metadata":{}},{"cell_type":"code","source":"\nimport os\nimport random\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom pathlib import Path\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom sklearn.model_selection import train_test_split\nfrom tqdm.auto import tqdm\n\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Reproducibility\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Using device: {DEVICE}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:38:45.544914Z","iopub.execute_input":"2026-04-11T17:38:45.545200Z","iopub.status.idle":"2026-04-11T17:38:45.561656Z","shell.execute_reply.started":"2026-04-11T17:38:45.545180Z","shell.execute_reply":"2026-04-11T17:38:45.560953Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 2. Unzip Dataset & Load File Paths","metadata":{}},{"cell_type":"code","source":"import zipfile\n\nBASE_INPUT = Path('/kaggle/input/competitions/dogs-vs-cats')\nEXTRACT_DIR = Path('/kaggle/working/data')\nEXTRACT_DIR.mkdir(exist_ok=True)\n\n# Unzip train.zip\n\ntrain_zip = BASE_INPUT / 'train.zip'\nif not (EXTRACT_DIR / 'train').exists():\n    print('Extracting train.zip ...')\n    with zipfile.ZipFile(train_zip, 'r') as z:\n        z.extractall(EXTRACT_DIR)\n    print('Done.')\nelse:\n    print('train/ already extracted.')\n\n# Unzip test1.zip\n\ntest_zip = BASE_INPUT / 'test1.zip'\nif not (EXTRACT_DIR / 'test1').exists():\n    print('Extracting test1.zip ...')\n    with zipfile.ZipFile(test_zip, 'r') as z:\n        z.extractall(EXTRACT_DIR)\n    print('Done.')\nelse:\n    print('test1/ already extracted.')\n\nTRAIN_DIR = EXTRACT_DIR / 'train'\nTEST_DIR  = EXTRACT_DIR / 'test1'\n\nall_paths = list(TRAIN_DIR.glob('*.jpg'))\nprint(f'\\nTotal train images: {len(all_paths)}')\n\n# Label from filename: 'cat.123.jpg' → 0, 'dog.123.jpg' → 1\nall_labels = [1 if p.name.startswith('dog') else 0 for p in all_paths]\nprint(f'Dogs: {sum(all_labels)}  |  Cats: {len(all_labels) - sum(all_labels)}')\n\n# Train/Val split (80/20)\n\ntrain_paths, val_paths, train_labels, val_labels = train_test_split(\n    all_paths, all_labels, test_size=0.2, stratify=all_labels, random_state=SEED\n)\nprint(f'Train: {len(train_paths)}  |  Val: {len(val_paths)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:38:45.563260Z","iopub.execute_input":"2026-04-11T17:38:45.563500Z","iopub.status.idle":"2026-04-11T17:38:45.858124Z","shell.execute_reply.started":"2026-04-11T17:38:45.563470Z","shell.execute_reply":"2026-04-11T17:38:45.857507Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 3. Custom Dataset & Transforms","metadata":{}},{"cell_type":"code","source":"IMG_SIZE = 128   # increase to 224 for better accuracy (slower)\nBATCH_SIZE = 64\n\ntrain_transforms = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomRotation(15),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],   # ImageNet stats\n                         std=[0.229, 0.224, 0.225])\n])\n\nval_transforms = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                         std=[0.229, 0.224, 0.225])\n])\n\nclass DogCatDataset(Dataset):\n    def __init__(self, paths, labels, transform=None):\n        self.paths = paths\n        self.labels = labels\n        self.transform = transform\n    \n    def __len__(self):\n        return len(self.paths)\n    \n    def __getitem__(self, idx):\n        img = Image.open(self.paths[idx]).convert('RGB')\n        label = self.labels[idx]\n        if self.transform:\n            img = self.transform(img)\n        return img, torch.tensor(label, dtype=torch.float32)\n\ntrain_ds = DogCatDataset(train_paths, train_labels, train_transforms)\nval_ds   = DogCatDataset(val_paths,   val_labels,   val_transforms)\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True,  num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_ds,   batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\nprint(f'Train batches: {len(train_loader)}  |  Val batches: {len(val_loader)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:38:45.859098Z","iopub.execute_input":"2026-04-11T17:38:45.859399Z","iopub.status.idle":"2026-04-11T17:38:45.869211Z","shell.execute_reply.started":"2026-04-11T17:38:45.859376Z","shell.execute_reply":"2026-04-11T17:38:45.868485Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 4. Visualise a Batch","metadata":{}},{"cell_type":"code","source":"def denormalize(tensor):\n    mean = torch.tensor([0.485, 0.456, 0.406]).view(3,1,1)\n    std  = torch.tensor([0.229, 0.224, 0.225]).view(3,1,1)\n    return (tensor * std + mean).clamp(0, 1)\n\nimgs, labels = next(iter(train_loader))\nfig, axes = plt.subplots(2, 8, figsize=(18, 5))\nfor i, ax in enumerate(axes.flatten()):\n    img = denormalize(imgs[i]).permute(1, 2, 0).numpy()\n    ax.imshow(img)\n    ax.set_title('Dog' if labels[i].item() == 1 else 'Cat', fontsize=9)\n    ax.axis('off')\nplt.suptitle('Sample Training Images', fontsize=13, fontweight='bold')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:38:45.870312Z","iopub.execute_input":"2026-04-11T17:38:45.870592Z","iopub.status.idle":"2026-04-11T17:38:47.788287Z","shell.execute_reply.started":"2026-04-11T17:38:45.870572Z","shell.execute_reply":"2026-04-11T17:38:47.787354Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 5. CNN Architecture","metadata":{}},{"cell_type":"code","source":"class DogCatCNN(nn.Module):\n    \"\"\"\n    Custom CNN: 4 conv blocks + classifier head.\n    Each block: Conv2d → BN → ReLU → MaxPool → Dropout\n    \"\"\"\n    def __init__(self):\n        super().__init__()\n        \n        def conv_block(in_ch, out_ch, pool=True):\n            layers = [\n                nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1),\n                nn.BatchNorm2d(out_ch),\n                nn.ReLU(inplace=True),\n            ]\n            if pool:\n                layers.append(nn.MaxPool2d(2, 2))   # halves spatial dims\n            layers.append(nn.Dropout2d(0.1))\n            return nn.Sequential(*layers)\n        \n        self.features = nn.Sequential(\n            conv_block(3,   32),    # 128 → 64\n            conv_block(32,  64),    # 64  → 32\n            conv_block(64, 128),    # 32  → 16\n            conv_block(128, 256),   # 16  →  8\n        )\n        \n        self.classifier = nn.Sequential(\n            nn.AdaptiveAvgPool2d(1),   # global average pool → (B, 256, 1, 1)\n            nn.Flatten(),\n            nn.Linear(256, 128),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.4),\n            nn.Linear(128, 1)          # single logit for BCEWithLogitsLoss\n        )\n    \n    def forward(self, x):\n        x = self.features(x)\n        x = self.classifier(x)\n        return x.squeeze(1)   # shape: (B,)\n\nmodel = DogCatCNN().to(DEVICE)\n\n# Quick param count\ntotal_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f'Trainable parameters: {total_params:,}')\nprint(model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:38:47.789577Z","iopub.execute_input":"2026-04-11T17:38:47.790021Z","iopub.status.idle":"2026-04-11T17:38:47.819195Z","shell.execute_reply.started":"2026-04-11T17:38:47.789983Z","shell.execute_reply":"2026-04-11T17:38:47.818639Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 6. Loss, Optimizer & Scheduler","metadata":{}},{"cell_type":"code","source":"criterion = nn.BCEWithLogitsLoss()   # sigmoid + BCE combined, numerically stable\n\noptimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4)\n\n# CosineAnnealingLR — smoothly decays LR each epoch\nEPOCHS = 15\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS, eta_min=1e-5)\n\nprint(f'Training for {EPOCHS} epochs')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:38:47.820161Z","iopub.execute_input":"2026-04-11T17:38:47.820613Z","iopub.status.idle":"2026-04-11T17:38:47.825409Z","shell.execute_reply.started":"2026-04-11T17:38:47.820591Z","shell.execute_reply":"2026-04-11T17:38:47.824841Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 7. Training Loop","metadata":{}},{"cell_type":"code","source":"def train_one_epoch(model, loader, optimizer, criterion, device):\n    model.train()\n    total_loss, correct, total = 0, 0, 0\n    for imgs, labels in tqdm(loader, leave=False, desc='Train'):\n        imgs, labels = imgs.to(device), labels.to(device)\n        optimizer.zero_grad()\n        logits = model(imgs)\n        loss = criterion(logits, labels)\n        loss.backward()\n        optimizer.step()\n        \n        total_loss += loss.item() * len(labels)\n        preds = (torch.sigmoid(logits) > 0.5).float()\n        correct += (preds == labels).sum().item()\n        total   += len(labels)\n    return total_loss / total, correct / total\n\n\n@torch.no_grad()\ndef evaluate(model, loader, criterion, device):\n    model.eval()\n    total_loss, correct, total = 0, 0, 0\n    for imgs, labels in tqdm(loader, leave=False, desc='Val'):\n        imgs, labels = imgs.to(device), labels.to(device)\n        logits = model(imgs)\n        loss = criterion(logits, labels)\n        \n        total_loss += loss.item() * len(labels)\n        preds = (torch.sigmoid(logits) > 0.5).float()\n        correct += (preds == labels).sum().item()\n        total   += len(labels)\n    return total_loss / total, correct / total\n\n\nhistory = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': []}\nbest_val_acc = 0.0\n\nfor epoch in range(1, EPOCHS + 1):\n    tr_loss, tr_acc = train_one_epoch(model, train_loader, optimizer, criterion, DEVICE)\n    vl_loss, vl_acc = evaluate(model, val_loader, criterion, DEVICE)\n    scheduler.step()\n    \n    history['train_loss'].append(tr_loss)\n    history['train_acc'].append(tr_acc)\n    history['val_loss'].append(vl_loss)\n    history['val_acc'].append(vl_acc)\n    \n    # Save best model\n    if vl_acc > best_val_acc:\n        best_val_acc = vl_acc\n        torch.save(model.state_dict(), 'best_model.pth')\n    \n    lr_now = scheduler.get_last_lr()[0]\n    print(f'Epoch {epoch:02d}/{EPOCHS} | '\n          f'Train Loss: {tr_loss:.4f}  Acc: {tr_acc:.4f} | '\n          f'Val Loss: {vl_loss:.4f}  Acc: {vl_acc:.4f} | '\n          f'LR: {lr_now:.2e}')\n\nprint(f'\\n Best Val Accuracy: {best_val_acc:.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:38:47.826779Z","iopub.execute_input":"2026-04-11T17:38:47.827055Z","iopub.status.idle":"2026-04-11T17:50:11.438264Z","shell.execute_reply.started":"2026-04-11T17:38:47.827034Z","shell.execute_reply":"2026-04-11T17:50:11.437411Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 8. Training Curves","metadata":{}},{"cell_type":"code","source":"epochs_range = range(1, EPOCHS + 1)\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\naxes[0].plot(epochs_range, history['train_loss'], label='Train Loss', marker='o')\naxes[0].plot(epochs_range, history['val_loss'],   label='Val Loss',   marker='o')\naxes[0].set_title('Loss Curve', fontsize=13)\naxes[0].set_xlabel('Epoch'); axes[0].set_ylabel('Loss')\naxes[0].legend(); axes[0].grid(True, alpha=0.3)\n\naxes[1].plot(epochs_range, history['train_acc'], label='Train Acc', marker='o')\naxes[1].plot(epochs_range, history['val_acc'],   label='Val Acc',   marker='o')\naxes[1].set_title('Accuracy Curve', fontsize=13)\naxes[1].set_xlabel('Epoch'); axes[1].set_ylabel('Accuracy')\naxes[1].legend(); axes[1].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:50:11.441685Z","iopub.execute_input":"2026-04-11T17:50:11.441939Z","iopub.status.idle":"2026-04-11T17:50:31.786472Z","shell.execute_reply.started":"2026-04-11T17:50:11.441907Z","shell.execute_reply":"2026-04-11T17:50:31.785721Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 9. Inference on Validation Set","metadata":{}},{"cell_type":"code","source":"# Load best weights\nmodel.load_state_dict(torch.load('best_model.pth', map_location=DEVICE))\nmodel.eval()\n\n# Grab one batch from val\nimgs, labels = next(iter(val_loader))\nimgs = imgs.to(DEVICE)\n\nwith torch.no_grad():\n    logits = model(imgs)\n    probs  = torch.sigmoid(logits).cpu().numpy()\n    preds  = (probs > 0.5).astype(int)\n\nlabels = labels.numpy().astype(int)\n\nfig, axes = plt.subplots(2, 8, figsize=(18, 6))\nfor i, ax in enumerate(axes.flatten()):\n    img = denormalize(imgs[i].cpu()).permute(1, 2, 0).numpy()\n    ax.imshow(img)\n    pred_label = 'Dog' if preds[i] == 1 else 'Cat'\n    true_label = 'Dog' if labels[i] == 1 else 'Cat'\n    color = 'green' if preds[i] == labels[i] else 'red'\n    ax.set_title(f'P:{pred_label}\\nT:{true_label}\\n{probs[i]:.2f}',\n                 fontsize=7, color=color)\n    ax.axis('off')\n\nplt.suptitle('Predictions — Green=Correct | Red=Wrong', fontsize=12, fontweight='bold')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:50:31.787344Z","iopub.execute_input":"2026-04-11T17:50:31.787634Z","iopub.status.idle":"2026-04-11T17:50:33.324242Z","shell.execute_reply.started":"2026-04-11T17:50:31.787599Z","shell.execute_reply":"2026-04-11T17:50:33.323284Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 10. Generate Test Predictions","metadata":{}},{"cell_type":"code","source":"\ntest_paths = sorted(TEST_DIR.glob('*.jpg'), key=lambda x: int(x.stem))  # sort by id\nprint(f'Test images: {len(test_paths)}')\n\nclass TestDataset(Dataset):\n    def __init__(self, paths, transform):\n        self.paths = paths\n        self.transform = transform\n    def __len__(self): return len(self.paths)\n    def __getitem__(self, idx):\n        img = Image.open(self.paths[idx]).convert('RGB')\n        return self.transform(img), int(self.paths[idx].stem)\n\ntest_ds     = TestDataset(test_paths, val_transforms)\ntest_loader = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=2)\n\nmodel.eval()\nall_ids, all_preds = [], []\n\nwith torch.no_grad():\n    for imgs, ids in tqdm(test_loader, desc='Predicting'):\n        imgs = imgs.to(DEVICE)\n        logits = model(imgs)\n        probs  = torch.sigmoid(logits).cpu().numpy()\n        all_ids.extend(ids.numpy().tolist())\n        all_preds.extend(probs.tolist())\n\nsubmission = pd.DataFrame({'id': all_ids, 'label': all_preds})\nsubmission = submission.sort_values('id').reset_index(drop=True)\nsubmission.to_csv('submission.csv', index=False)\nprint(' submission.csv saved')\nsubmission.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-11T17:50:33.325824Z","iopub.execute_input":"2026-04-11T17:50:33.326096Z","iopub.status.idle":"2026-04-11T17:51:21.382836Z","shell.execute_reply.started":"2026-04-11T17:50:33.326069Z","shell.execute_reply":"2026-04-11T17:51:21.381954Z"}},"outputs":[],"execution_count":null}]}