{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nfor root, dirs, files in os.walk('/kaggle/input'):\n    print(root, len(files))\n    if len(files) > 0 and root.count('/') > 5:\n        break","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:06:30.544768Z","iopub.execute_input":"2026-09-29T06:06:30.545107Z","iopub.status.idle":"2026-09-29T06:06:30.555069Z","shell.execute_reply.started":"2026-09-29T06:06:30.545078Z","shell.execute_reply":"2026-09-29T06:06:30.554232Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nBASE = '/kaggle/input/competitions/dogs-vs-cats-redux-kernels-edition'\nprint(os.listdir(BASE))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:08:50.579214Z","iopub.execute_input":"2026-09-29T06:08:50.579525Z","iopub.status.idle":"2026-09-29T06:08:50.585635Z","shell.execute_reply.started":"2026-09-29T06:08:50.579495Z","shell.execute_reply":"2026-09-29T06:08:50.584835Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import zipfile, os\n\nBASE = '/kaggle/input/competitions/dogs-vs-cats-redux-kernels-edition'\n\nwith zipfile.ZipFile(f'{BASE}/train.zip') as z:\n    z.extractall('/kaggle/working/')\n\nprint(os.listdir('/kaggle/working'))\nprint(len(os.listdir('/kaggle/working/train')), 'images')\nprint(os.listdir('/kaggle/working/train')[:5])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:17:09.744623Z","iopub.execute_input":"2026-09-29T06:17:09.744964Z","iopub.status.idle":"2026-09-29T06:17:28.046715Z","shell.execute_reply.started":"2026-09-29T06:17:09.744933Z","shell.execute_reply":"2026-09-29T06:17:28.04586Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, random, numpy as np, cv2\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\n\nDATA_DIR = '/kaggle/working/train'\nIMG_SIZE = 64\nN_PER_CLASS = 4000\nrandom.seed(42)\n\nfiles = os.listdir(DATA_DIR)\ncat_files = [f for f in files if f.startswith('cat')]\ndog_files = [f for f in files if f.startswith('dog')]\nprint(len(cat_files), 'cats,', len(dog_files), 'dogs')\nrandom.shuffle(cat_files); random.shuffle(dog_files)\n\ndef load_images(file_list, label):\n    X, y = [], []\n    for f in file_list[:N_PER_CLASS]:\n        img = cv2.imread(os.path.join(DATA_DIR, f))\n        if img is None: continue\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))\n        X.append(img); y.append(label)\n    return X, y\n\nXc, yc = load_images(cat_files, 0)\nXd, yd = load_images(dog_files, 1)\nX = np.array(Xc + Xd, dtype=np.float32) / 255.0\ny = np.array(yc + yd, dtype=np.float32)\nprint(X.shape, y.shape)\n\n# 70% train, 15% validation, 15% test\nX_train, X_tmp, y_train, y_tmp = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42)\nX_val, X_test, y_val, y_test = train_test_split(X_tmp, y_tmp, test_size=0.5, stratify=y_tmp, random_state=42)\nprint(X_train.shape, X_val.shape, X_test.shape)\n\n# Sample images\nfig, axs = plt.subplots(2, 5, figsize=(12, 5))\nfor i, ax in enumerate(axs.flat):\n    ax.imshow(X_train[i]); ax.set_title('Dog' if y_train[i] else 'Cat'); ax.axis('off')\nplt.savefig('samples.png', dpi=150, bbox_inches='tight'); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:18:32.856099Z","iopub.execute_input":"2026-09-29T06:18:32.85645Z","iopub.status.idle":"2026-09-29T06:18:48.919666Z","shell.execute_reply.started":"2026-09-29T06:18:32.856417Z","shell.execute_reply":"2026-09-29T06:18:48.918375Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch, torch.nn as nn\nfrom torch.utils.data import TensorDataset, DataLoader\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint('Using:', device)\n\ndef to_tensor(X, y):\n    Xt = torch.tensor(X).permute(0, 3, 1, 2)          # N,3,64,64\n    return TensorDataset(Xt, torch.tensor(y))\n\ntrain_dl = DataLoader(to_tensor(X_train, y_train), batch_size=64, shuffle=True)\nval_dl   = DataLoader(to_tensor(X_val, y_val), batch_size=64)\ntest_dl  = DataLoader(to_tensor(X_test, y_test), batch_size=64)\n\nclass LogisticRegression(nn.Module):\n    def __init__(self, in_features):\n        super().__init__()\n        self.linear = nn.Linear(in_features, 1)\n    def forward(self, x):\n        return self.linear(x.flatten(1)).squeeze(1)    # logits; sigmoid is inside the loss\n\nmodel = LogisticRegression(3 * IMG_SIZE * IMG_SIZE).to(device)\ncriterion = nn.BCEWithLogitsLoss()                     # sigmoid + binary cross entropy\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-4, weight_decay=1e-4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:19:53.175657Z","iopub.execute_input":"2026-09-29T06:19:53.176283Z","iopub.status.idle":"2026-09-29T06:20:01.774498Z","shell.execute_reply.started":"2026-09-29T06:19:53.176245Z","shell.execute_reply":"2026-09-29T06:20:01.773599Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 30\nhistory = {'train_loss': [], 'val_loss': [], 'train_acc': [], 'val_acc': []}\n\ndef run_epoch(dl, train=True):\n    model.train(train)\n    total_loss, correct, n = 0, 0, 0\n    with torch.set_grad_enabled(train):\n        for xb, yb in dl:\n            xb, yb = xb.to(device), yb.to(device)\n            logits = model(xb)\n            loss = criterion(logits, yb)\n            if train:\n                optimizer.zero_grad(); loss.backward(); optimizer.step()\n            total_loss += loss.item() * len(yb)\n            correct += ((logits > 0).float() == yb).sum().item()\n            n += len(yb)\n    return total_loss / n, correct / n\n\nfor epoch in range(EPOCHS):\n    tl, ta = run_epoch(train_dl, True)\n    vl, va = run_epoch(val_dl, False)\n    for k, v in zip(history, [tl, vl, ta, va]): history[k].append(v)\n    print(f'Epoch {epoch+1:02d} | train loss {tl:.4f} acc {ta:.4f} | val loss {vl:.4f} acc {va:.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:21:32.74932Z","iopub.execute_input":"2026-09-29T06:21:32.749991Z","iopub.status.idle":"2026-09-29T06:21:41.465701Z","shell.execute_reply.started":"2026-09-29T06:21:32.749949Z","shell.execute_reply":"2026-09-29T06:21:41.464739Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axs = plt.subplots(1, 2, figsize=(12, 4))\naxs[0].plot(history['train_loss'], label='Train'); axs[0].plot(history['val_loss'], label='Validation')\naxs[0].set_title('Loss'); axs[0].set_xlabel('Epoch'); axs[0].set_ylabel('BCE Loss'); axs[0].legend()\naxs[1].plot(history['train_acc'], label='Train'); axs[1].plot(history['val_acc'], label='Validation')\naxs[1].set_title('Accuracy'); axs[1].set_xlabel('Epoch'); axs[1].set_ylabel('Accuracy'); axs[1].legend()\nplt.savefig('training_curves.png', dpi=150, bbox_inches='tight'); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:22:31.270921Z","iopub.execute_input":"2026-09-29T06:22:31.271291Z","iopub.status.idle":"2026-09-29T06:22:31.872689Z","shell.execute_reply.started":"2026-09-29T06:22:31.271255Z","shell.execute_reply":"2026-09-29T06:22:31.871904Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def saliency(img_tensor):\n    x = img_tensor.unsqueeze(0).to(device).requires_grad_(True)\n    logit = model(x)\n    logit.backward()\n    g = x.grad.abs().squeeze(0).max(dim=0)[0].cpu().numpy()   # max over RGB channels\n    return (g - g.min()) / (g.max() - g.min() + 1e-8)\n\ntest_ds = to_tensor(X_test, y_test)\nfig, axs = plt.subplots(3, 6, figsize=(15, 8))\nfor i in range(6):\n    img, lab = test_ds[i * 20]\n    sal = saliency(img)\n    with torch.no_grad():\n        prob = torch.sigmoid(model(img.unsqueeze(0).to(device))).item()\n    axs[0, i].imshow(img.permute(1, 2, 0).numpy()); axs[0, i].axis('off')\n    axs[0, i].set_title(f\"True: {'Dog' if lab else 'Cat'}\\nP(dog)={prob:.2f}\")\n    axs[1, i].imshow(sal, cmap='jet'); axs[1, i].axis('off')\n    axs[2, i].imshow(img.permute(1, 2, 0).numpy())\n    axs[2, i].imshow(sal, cmap='jet', alpha=0.5); axs[2, i].axis('off')\naxs[1, 0].set_ylabel('Saliency'); axs[2, 0].set_ylabel('Overlay')\nplt.suptitle('Saliency heatmaps (Grad-CAM style) for Logistic Regression')\nplt.savefig('gradcam_saliency.png', dpi=150, bbox_inches='tight'); plt.show()\n\n# Learned weights: red = pushes toward Dog, blue = toward Cat\nw = model.linear.weight.detach().cpu().numpy().reshape(3, IMG_SIZE, IMG_SIZE).mean(0)\nplt.figure(figsize=(5, 5))\nplt.imshow(w, cmap='seismic', vmin=-abs(w).max(), vmax=abs(w).max()); plt.colorbar()\nplt.title('Learned weights (mean over RGB)')\nplt.savefig('weight_map.png', dpi=150, bbox_inches='tight'); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:23:55.410931Z","iopub.execute_input":"2026-09-29T06:23:55.411268Z","iopub.status.idle":"2026-09-29T06:23:57.128648Z","shell.execute_reply.started":"2026-09-29T06:23:55.411235Z","shell.execute_reply":"2026-09-29T06:23:57.127647Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, classification_report, roc_curve, auc\n\nmodel.eval()\npreds, labels, probs = [], [], []\nwith torch.no_grad():\n    for xb, yb in test_dl:\n        logits = model(xb.to(device))\n        probs.extend(torch.sigmoid(logits).cpu().numpy())\n        preds.extend((logits > 0).float().cpu().numpy())\n        labels.extend(yb.numpy())\n\n# Confusion matrix\ncm = confusion_matrix(labels, preds)\nConfusionMatrixDisplay(cm, display_labels=['Cat', 'Dog']).plot(cmap='Blues')\nplt.title('Confusion Matrix (Test Set)')\nplt.savefig('confusion_matrix.png', dpi=150, bbox_inches='tight'); plt.show()\n\n# Classification report\nprint(classification_report(labels, preds, target_names=['Cat', 'Dog']))\n\n# ROC curve\nfpr, tpr, _ = roc_curve(labels, probs)\nplt.figure(figsize=(5, 5))\nplt.plot(fpr, tpr, label=f'AUC = {auc(fpr, tpr):.3f}')\nplt.plot([0, 1], [0, 1], '--', label='Random guess')\nplt.xlabel('False Positive Rate'); plt.ylabel('True Positive Rate')\nplt.title('ROC Curve'); plt.legend()\nplt.savefig('roc_curve.png', dpi=150, bbox_inches='tight'); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:27:40.599526Z","iopub.execute_input":"2026-09-29T06:27:40.600035Z","iopub.status.idle":"2026-09-29T06:27:41.298476Z","shell.execute_reply.started":"2026-09-29T06:27:40.6Z","shell.execute_reply":"2026-09-29T06:27:41.297533Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SmallCNN(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.features = nn.Sequential(\n            nn.Conv2d(3, 16, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),   # 32x32\n            nn.Conv2d(16, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),  # 16x16\n            nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(),                   # 16x16 (Grad-CAM layer)\n        )\n        self.fc = nn.Linear(64, 1)\n    def forward(self, x):\n        f = self.features(x)\n        return self.fc(f.mean(dim=(2, 3))).squeeze(1)\n\ncnn = SmallCNN().to(device)\nopt = torch.optim.Adam(cnn.parameters(), lr=1e-3)\n\nfor epoch in range(15):\n    cnn.train()\n    for xb, yb in train_dl:\n        xb, yb = xb.to(device), yb.to(device)\n        loss = criterion(cnn(xb), yb)\n        opt.zero_grad(); loss.backward(); opt.step()\n    cnn.eval()\n    correct = n = 0\n    with torch.no_grad():\n        for xb, yb in val_dl:\n            correct += ((cnn(xb.to(device)) > 0).float().cpu() == yb).sum().item(); n += len(yb)\n    print(f'Epoch {epoch+1:02d} | val acc {correct/n:.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:35:04.954891Z","iopub.execute_input":"2026-09-29T06:35:04.956183Z","iopub.status.idle":"2026-09-29T06:37:20.490277Z","shell.execute_reply.started":"2026-09-29T06:35:04.956137Z","shell.execute_reply":"2026-09-29T06:37:20.489236Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn.functional as F\n\ndef gradcam(img_np):\n    cnn.eval()\n    x = torch.tensor(img_np).permute(2, 0, 1).unsqueeze(0).to(device)\n    feats = cnn.features(x)\n    feats.retain_grad()\n    logit = cnn.fc(feats.mean(dim=(2, 3))).squeeze()\n    pred_dog = logit.item() > 0\n    (logit if pred_dog else -logit).backward()        # evidence for the predicted class\n    w = feats.grad.mean(dim=(2, 3), keepdim=True)\n    cam = F.relu((w * feats).sum(dim=1)).squeeze().detach().cpu().numpy()\n    cam = cv2.resize(cam, (IMG_SIZE, IMG_SIZE))\n    cam = (cam - cam.min()) / (cam.max() - cam.min() + 1e-8)\n    return cam, torch.sigmoid(logit).item()\n\nfig, axs = plt.subplots(2, 6, figsize=(15, 5.5))\nfor j, i in enumerate([0, 20, 40, 60, 80, 100]):\n    img, true = X_test[i], 'Dog' if y_test[i] == 1 else 'Cat'\n    cam, p = gradcam(img)\n    pred = 'Dog' if p > 0.5 else 'Cat'\n    axs[0, j].imshow(img, interpolation='bilinear'); axs[0, j].axis('off')\n    axs[0, j].set_title(f'True: {true}\\nPred: {pred} ({max(p, 1-p):.0%})',\n                        color='green' if pred == true else 'red')\n    axs[1, j].imshow(img, interpolation='bilinear')\n    axs[1, j].imshow(cam, cmap='jet', alpha=0.5, interpolation='bilinear'); axs[1, j].axis('off')\nplt.suptitle('Grad-CAM (small CNN): red = regions that drove the prediction')\nplt.savefig('gradcam_cnn.png', dpi=150, bbox_inches='tight'); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:41:18.876667Z","iopub.execute_input":"2026-09-29T06:41:18.877068Z","iopub.status.idle":"2026-09-29T06:41:20.473499Z","shell.execute_reply.started":"2026-09-29T06:41:18.87703Z","shell.execute_reply":"2026-09-29T06:41:20.471324Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cnn.eval()\ncorrect = n = 0\nwith torch.no_grad():\n    for xb, yb in test_dl:\n        correct += ((cnn(xb.to(device)) > 0).float().cpu() == yb).sum().item(); n += len(yb)\nprint(f'CNN test accuracy: {correct/n:.4f}')\nprint(f'Logistic Regression test accuracy: {(cm[0,0]+cm[1,1])/cm.sum():.4f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-29T06:43:41.981301Z","iopub.execute_input":"2026-09-29T06:43:41.982279Z","iopub.status.idle":"2026-09-29T06:43:43.11095Z","shell.execute_reply.started":"2026-09-29T06:43:41.982241Z","shell.execute_reply":"2026-09-29T06:43:43.110007Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}