{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":11848,"databundleVersionId":862157,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### Problem Description: Histopathologic Cancer Detection\nThe Histopathologic Cancer Detection challenge on Kaggle involves building a binary classifier to detect metastatic cancer in histopathologic tissue images. The dataset consists of histology image patches, each labeled as either 1 (tumor) or 0 (normal). The training set contains approximately 220025 labeled images, while the test set includes 57458 unlabeled samples. Images are stored in a flat folder structure and must be loaded using custom datasets. ","metadata":{}},{"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\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport os\nfrom skimage.io import imread\nimport torch\nimport torch.nn as nn\nimport torchvision.transforms as transforms\nfrom torch.utils.data import Dataset,DataLoader\nfrom torchvision.io import read_image\nimport os\nfrom sklearn.model_selection import train_test_split\nimport matplotlib.pyplot as plt\n# import seaborn as sns\nfrom sklearn.metrics import accuracy_score,roc_auc_score\nimport time\nimport copy\nfrom tqdm import tqdm_notebook as tqdm\nfrom torchmetrics.classification import BinaryAUROC\nimport contextlib\nfrom IPython.display import clear_output \n\n\nimport warnings\nwarnings.filterwarnings(\"ignore\", category=DeprecationWarning)\nwarnings.filterwarnings(\"ignore\", category=UserWarning)\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:09:59.502232Z","iopub.execute_input":"2025-06-16T04:09:59.502597Z","iopub.status.idle":"2025-06-16T04:09:59.508581Z","shell.execute_reply.started":"2025-06-16T04:09:59.502572Z","shell.execute_reply":"2025-06-16T04:09:59.507771Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"path='/kaggle/input/histopathologic-cancer-detection/train/'\nannotation_file='/kaggle/input/histopathologic-cancer-detection/train_labels.csv'\ntest_path='/kaggle/input/histopathologic-cancer-detection/test/'\nfirstTrainImage = os.listdir(path)[0].split('.')[0]\nprint(firstTrainImage)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:09:59.514054Z","iopub.execute_input":"2025-06-16T04:09:59.514291Z","iopub.status.idle":"2025-06-16T04:10:02.062930Z","shell.execute_reply.started":"2025-06-16T04:09:59.514275Z","shell.execute_reply":"2025-06-16T04:10:02.062208Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_data =pd.read_csv('/kaggle/input/histopathologic-cancer-detection/train_labels.csv')\nsub = pd.read_csv('/kaggle/input/histopathologic-cancer-detection/sample_submission.csv')\ntrain_data[train_data['id'] == firstTrainImage]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:10:02.064278Z","iopub.execute_input":"2025-06-16T04:10:02.064575Z","iopub.status.idle":"2025-06-16T04:10:02.322156Z","shell.execute_reply.started":"2025-06-16T04:10:02.064556Z","shell.execute_reply":"2025-06-16T04:10:02.321490Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check how balanced are the label\nplt.bar(['No Cancer', 'Cancer'], train_data.label.value_counts().values, color=['blue', 'white'], edgecolor='black')\nplt.show()\nlen(train_data)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:10:02.322856Z","iopub.execute_input":"2025-06-16T04:10:02.323077Z","iopub.status.idle":"2025-06-16T04:10:02.422478Z","shell.execute_reply.started":"2025-06-16T04:10:02.323052Z","shell.execute_reply":"2025-06-16T04:10:02.421697Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cancer = np.random.choice(train_data[train_data.label==1].id, size=25, replace=False)\nno_cancer = np.random.choice(train_data[train_data.label==0].id, size=25, replace=False)\n\nfig, ax = plt.subplots(5, 10, figsize=(20, 10))\nfig.suptitle(\"Cancer (Red Border) vs No Cancer (Green Border)\", fontsize=20)\n\nfor i in range(5):\n    for j in range(10):\n        idx = i * 5 + (j % 5)\n        img_id = cancer[idx] if j < 5 else no_cancer[idx]\n        image = plt.imread(path + img_id + \".tif\")\n\n        ax[i, j].imshow(image)\n        ax[i, j].tick_params(labelbottom=False, labelleft=False)\n        for spine in ax[i, j].spines.values():\n            spine.set_edgecolor('red' if j < 5 else 'green')\n            spine.set_linewidth(3)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:10:02.424394Z","iopub.execute_input":"2025-06-16T04:10:02.424637Z","iopub.status.idle":"2025-06-16T04:10:08.044749Z","shell.execute_reply.started":"2025-06-16T04:10:02.424620Z","shell.execute_reply":"2025-06-16T04:10:08.040976Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df, val_df = train_test_split(train_data, test_size=0.1, stratify=train_data['label'], random_state=42)\n\n# 2. Plot class distributions\nfig, ax = plt.subplots(1, 2, figsize=(10, 4))\n\n# Training set\ntrain_counts = train_df['label'].value_counts().sort_index()\nax[0].bar(['No Cancer', 'Cancer'], train_counts, color=['blue', 'white'], edgecolor='black')\nax[0].set_title('Training Set')\n\n# Validation set\nval_counts = val_df['label'].value_counts().sort_index()\nax[1].bar(['No Cancer', 'Cancer'], val_counts, color=['blue', 'white'], edgecolor='black')\nax[1].set_title('Validation Set')\n\nplt.tight_layout()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:10:08.045547Z","iopub.execute_input":"2025-06-16T04:10:08.045779Z","iopub.status.idle":"2025-06-16T04:10:08.404243Z","shell.execute_reply.started":"2025-06-16T04:10:08.045763Z","shell.execute_reply":"2025-06-16T04:10:08.403640Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CancerDataset(Dataset):\n    \"\"\"\n    Custom PyTorch Dataset for loading cancer image data. To be used with DataLoader()\n    \n    Each sample consists of an image loaded from disk and its associated label.\n    \"\"\"\n    \n    def __init__(self, dataframe, image_dir='./', transform=None):\n        \"\"\"\n        Args:\n            dataframe (pd.DataFrame): A DataFrame with columns [image_id, label].\n            image_dir (str): Directory where image files are stored.\n            transform (callable, optional): Optional transform to apply to each image.\n        \"\"\"\n        self.image_labels = dataframe.values         # Convert to NumPy array for indexing\n        self.image_dir = image_dir\n        self.transform = transform\n\n    def __len__(self):\n        \"\"\"Return total number of samples.\"\"\"\n        return len(self.image_labels)\n\n    def __getitem__(self, index):\n        \"\"\"Load and return a single (image, label) pair.\"\"\"\n        image_id, label = self.image_labels[index]\n        image_path = os.path.join(self.image_dir, f\"{image_id}.tif\")\n        \n        image = imread(image_path)  # Load image from disk\n        \n        if self.transform:\n            image = self.transform(image)\n\n        return image, label\n\n# funciton to get sample of the data formodel selection and hyperparam tuning\ndef get_sample_loader(df, image_dir, fraction=0.1, batch_size=32, transform=None):\n    df_small, _ = train_test_split(df, test_size=1-fraction, stratify=df['label'], random_state=42)\n    ds = CancerDataset(df_small, image_dir, transform=transform)\n    return DataLoader(ds, batch_size=batch_size, shuffle=True, num_workers=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:10:08.405371Z","iopub.execute_input":"2025-06-16T04:10:08.405676Z","iopub.status.idle":"2025-06-16T04:10:08.412170Z","shell.execute_reply.started":"2025-06-16T04:10:08.405652Z","shell.execute_reply":"2025-06-16T04:10:08.411427Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Model Architecture and Justification\nThe BasicCNN model is a lightweight convolutional neural network designed for binary image classification of histopathologic images. The architecture follows a modular design consisting of:\n\n-   Three convolutional blocks, each composed of:\n\n    -   A Conv2d layer with increasing channels (e.g., 16 to 32 to 64)\n\n    -   A ReLU activation for non-linearity\n\n    -   A downsampling operation via MaxPool2d\n\n-   An adaptive average pooling layer to reduce the final feature map to a 1×1 spatial size regardless of input dimensions\n\n-   A classifier head, consisting of:\n\n    -   A Flatten layer\n\n    -   A Dropout layer to mitigate overfitting\n\n    -   A fully connected Linear layer with a single output (one logit) for binary classification\n\nThe architecture is controlled by 3 tunable hyperparameters:\n\n-   base_channels: controls the number of filters in the first conv layer (e.g., 16), which then scale up in deeper layers\n\n-   dropout: controls regularization strength in the classifier (e.g., 0.4)\n\n-   learning rate: controls the learning rate of the scaler (e.g., 0.01)\n\n\nThis model structure is well-suited to the Histopathologic Cancer Detection task for several reasons:\n\n-   Small input images do not require deep or heavy architectures; three convolutional layers are sufficient to capture both texture and structural patterns.\n\n-   MaxPooling and channel expansion allow the network to progressively learn more abstract features while reducing spatial resolution, balancing efficiency and expressiveness.\n\n-   Adaptive pooling removes the need to hard-code output sizes and ensures flexibility if image resolution changes.\n\n-   Dropout and batch normalization help regularize the network and stabilize training, which is particularly helpful when using small training subsets for experimentation.\n\n-   Most importantly, the model is small and fast: it can train in minutes on Kaggle’s free GPU while still \"hopefully\" reaching competitive AUC scores.\n\nThis architecture is intentionally compact to optimize learning speed and memory usage, ideal for a learning project with computational constraints, where frequent iteration is more important than state-of-the-art results.","metadata":{}},{"cell_type":"code","source":"class BasicCNN(nn.Module):\n    def __init__(self, base_channels=8, dropout=0.3):\n        super().__init__()\n        self.conv1 = nn.Sequential(\n            nn.Conv2d(3, base_channels, kernel_size = 3, stride = 1, padding = 1),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2)\n        )\n        self.conv2 = nn.Sequential(\n            nn.Conv2d(base_channels, base_channels*2, kernel_size = 3, stride = 1, padding = 1),\n            nn.ReLU(inplace=True),\n            nn.MaxPool2d(2)\n        )\n        self.conv3 = nn.Sequential(\n            nn.Conv2d(base_channels*2, base_channels*4, kernel_size = 3, stride = 1, padding = 1),\n            nn.ReLU(inplace=True),\n            nn.AdaptiveAvgPool2d(1)\n        )\n        self.classifier = nn.Sequential(\n            nn.Flatten(),\n            nn.Dropout(dropout),\n            nn.Linear(base_channels*4, 1)\n        )\n\n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.conv2(x)\n        x = self.conv3(x)\n        return self.classifier(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:10:08.412982Z","iopub.execute_input":"2025-06-16T04:10:08.413260Z","iopub.status.idle":"2025-06-16T04:10:08.431569Z","shell.execute_reply.started":"2025-06-16T04:10:08.413237Z","shell.execute_reply":"2025-06-16T04:10:08.430845Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchmetrics.classification import BinaryAUROC\nfrom tqdm import tqdm\nimport torch\n\ndef train_one_epoch(model, dataloader, device, criterion, optimizer):\n    model.train()\n    for images, labels in dataloader:\n        images = images.to(device)\n        labels = labels.float().unsqueeze(1).to(device)\n\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\ndef evaluate(model, dataloader, device):\n    model.eval()\n    metric = BinaryAUROC().to(device)\n    with torch.no_grad():\n        for images, labels in dataloader:\n            images = images.to(device)\n            labels = labels.float().unsqueeze(1).to(device)\n            outputs = model(images)\n            probs = torch.sigmoid(outputs)\n            metric.update(probs, labels)\n    return metric.compute().item()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:10:08.432477Z","iopub.execute_input":"2025-06-16T04:10:08.433125Z","iopub.status.idle":"2025-06-16T04:10:08.451701Z","shell.execute_reply.started":"2025-06-16T04:10:08.433107Z","shell.execute_reply":"2025-06-16T04:10:08.450969Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\ntransform = transforms.Compose([\n    transforms.ToPILImage(),  # Convert NumPy array to PIL image (required torchvision transforms)    \n    transforms.ToTensor(),  # Convert PIL image to PyTorch tensor and scale pixel values to [0, 1]\n    transforms.Normalize(mean=[0.5, 0.5, 0.5],  \n                         std=[0.5, 0.5, 0.5])    # Normalize using midpoints. It takes too long to get the real mean and std \n])\n\n# Load metadata\ntrain_data = pd.read_csv(\"/kaggle/input/histopathologic-cancer-detection/train_labels.csv\")\nimage_dir = \"/kaggle/input/histopathologic-cancer-detection/train\"\n\n# Create data subset\ntrain_loader = get_sample_loader(train_data, image_dir, fraction=0.1, transform=transform)\nval_loader   = get_sample_loader(train_data, image_dir, fraction=0.05, transform=transform)\n\n# List of configs to try\nconfigs = [\n    {\"channels\": 4,  \"dropout\": 0.3, \"lr\": 1e-2},\n    {\"channels\": 8, \"dropout\": 0.25, \"lr\": 1e-3},\n    {\"channels\": 16,  \"dropout\": 0.2, \"lr\": 1e-4},\n]\n\nresults = []\n\nfor cfg in configs:\n    print(f\"Testing config: {cfg}\")\n    model = BasicCNN(base_channels=cfg[\"channels\"], dropout=cfg[\"dropout\"]).to(device)\n    optimizer = torch.optim.Adam(model.parameters(), lr=cfg[\"lr\"])\n    criterion = nn.BCEWithLogitsLoss()\n\n    train_one_epoch(model, train_loader, device, criterion, optimizer)\n    val_auc = evaluate(model, val_loader, device)\n\n    results.append((cfg, val_auc))\n    print(f\"AUC: {val_auc:.4f}\\n\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:10:08.452299Z","iopub.execute_input":"2025-06-16T04:10:08.452524Z","iopub.status.idle":"2025-06-16T04:11:40.398774Z","shell.execute_reply.started":"2025-06-16T04:10:08.452509Z","shell.execute_reply":"2025-06-16T04:11:40.397732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"records = []\nfor cfg, auc in results:\n    row = cfg.copy()\n    row[\"val_auc\"] = auc\n    records.append(row)\n\ndf = pd.DataFrame.from_records(records)\ndf.index.name = \"run_id\"\n\n\n# Bar chart \nplt.figure(figsize=(6, 4))\nplt.bar(df.index.astype(str), df[\"val_auc\"])\nplt.xlabel(\"Run ID\")\nplt.ylabel(\"Validation AUC\")\nplt.title(\"Model Comparison (Rescaled Y-axis)\")\nplt.ylim(0.80, 0.87)  # Tight Y-axis to amplify visual differences\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:12:06.635135Z","iopub.execute_input":"2025-06-16T04:12:06.635723Z","iopub.status.idle":"2025-06-16T04:12:06.786117Z","shell.execute_reply.started":"2025-06-16T04:12:06.635699Z","shell.execute_reply":"2025-06-16T04:12:06.785323Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_cfg = max(results, key=lambda x: x[1])\nprint(f\"Best config: {best_cfg[0]}, AUC: {best_cfg[1]:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:12:09.608054Z","iopub.execute_input":"2025-06-16T04:12:09.608334Z","iopub.status.idle":"2025-06-16T04:12:09.613065Z","shell.execute_reply.started":"2025-06-16T04:12:09.608313Z","shell.execute_reply":"2025-06-16T04:12:09.612368Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dataset = CancerDataset(dataframe=train_df, image_dir=path, transform=transform)\nval_dataset = CancerDataset(dataframe=val_df, image_dir=path, transform=transform)\ntest_dataset = CancerDataset(dataframe=sub, image_dir=test_path, transform=transform)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:12:11.657692Z","iopub.execute_input":"2025-06-16T04:12:11.657955Z","iopub.status.idle":"2025-06-16T04:12:11.682556Z","shell.execute_reply.started":"2025-06-16T04:12:11.657935Z","shell.execute_reply":"2025-06-16T04:12:11.681988Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Hyperparameter and system setings\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nbatch_size= 256\nshuffle=False\nnum_workers= os.cpu_count()\npin_memory=True\npersistent_workers=True\nlearning_rate = best_cfg[0]['lr']\ndropout = best_cfg[0]['dropout']\nchannels = best_cfg[0]['channels']\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=batch_size,\n    shuffle=shuffle,\n    num_workers=num_workers,            \n    pin_memory=pin_memory,\n    persistent_workers=persistent_workers\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=batch_size,\n    shuffle=shuffle,\n    num_workers=num_workers,            \n    pin_memory=pin_memory,\n    persistent_workers=persistent_workers\n)\ntest_dataloader = DataLoader(\n    test_dataset,\n    batch_size=batch_size,\n    shuffle=shuffle,\n    num_workers=num_workers,            \n    pin_memory=pin_memory,\n    persistent_workers=persistent_workers\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:12:13.461625Z","iopub.execute_input":"2025-06-16T04:12:13.462152Z","iopub.status.idle":"2025-06-16T04:12:13.475376Z","shell.execute_reply.started":"2025-06-16T04:12:13.462122Z","shell.execute_reply":"2025-06-16T04:12:13.474608Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = BasicCNN(base_channels=channels, dropout=dropout).to(device)\noptimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)  \nscaler = torch.cuda.amp.GradScaler(enabled=device.type == \"cuda\")\n\nprint(model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:12:17.734853Z","iopub.execute_input":"2025-06-16T04:12:17.735348Z","iopub.status.idle":"2025-06-16T04:12:17.742591Z","shell.execute_reply.started":"2025-06-16T04:12:17.735323Z","shell.execute_reply":"2025-06-16T04:12:17.741834Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"NUM_EPOCHS = 20\nis_cuda = device.type == \"cuda\"                 \nbest_val_auc = 0.0\nBEST_MODEL_PATH = \"best_model.pt\"\n\n#  Metric history containers\ntrain_loss_hist = []\nval_loss_hist   = []\ntrain_auc_hist  = []\nval_auc_hist    = []\n\n# Define AUC metrics and move them to the correct device\ntrain_auc_metric = BinaryAUROC().to(device)\nval_auc_metric   = BinaryAUROC().to(device)\n\n# Training loop\nfor epoch in range(NUM_EPOCHS):\n    model.train()\n    train_auc_metric.reset()\n    running_train_loss = 0.0\n\n    for batch_idx, (images, labels) in enumerate(tqdm(train_loader, total=len(train_loader), desc=f\"Epoch {epoch+1}/{NUM_EPOCHS}\")):\n\n        # Move data to GPU / CPU\n        images = images.to(device, non_blocking=True)\n        labels = labels.float().unsqueeze(1).to(device, non_blocking=True)\n\n        optimizer.zero_grad()\n\n        #  forward / backward (mixed precision if CUDA) \n        with torch.cuda.amp.autocast(enabled=is_cuda):\n            outputs = model(images)\n            loss    = criterion(outputs, labels)\n\n        if scaler.is_enabled():       \n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n        else:                          # CPU fallback\n            loss.backward()\n            optimizer.step()\n\n\n        running_train_loss += loss.item()\n        train_auc_metric.update(outputs, labels)\n\n    # Epoch-level metrics\n    avg_train_loss = running_train_loss / len(train_loader)\n    avg_train_auc  = train_auc_metric.compute().item()\n\n\n    # Validation\n    model.eval()\n    val_auc_metric.reset()\n    running_val_loss = 0.0\n\n    with torch.no_grad():\n        \n        for batch_idx, (images, labels) in enumerate(tqdm(val_loader, total=len(val_loader))):\n\n            images = images.to(device, non_blocking=True)\n            labels = labels.float().unsqueeze(1).to(device, non_blocking=True)\n\n            with torch.cuda.amp.autocast(enabled=is_cuda):\n                outputs = model(images)\n                loss    = criterion(outputs, labels)\n\n            running_val_loss += loss.item()\n            val_auc_metric.update(outputs, labels)\n\n    avg_val_loss = running_val_loss / len(val_loader)\n    avg_val_auc  = val_auc_metric.compute().item()\n    \n    # Save best model (based on validation AUC)\n    if avg_val_auc > best_val_auc:\n        best_val_auc = avg_val_auc\n        torch.save(model.state_dict(), BEST_MODEL_PATH)\n        print(f\"Saved new best model at epoch {epoch+1} (Val AUC: {avg_val_auc:.4f})\")\n    \n    # Append to history lists\n    train_loss_hist.append(avg_train_loss)\n    val_loss_hist.append(avg_val_loss)\n    train_auc_hist.append(avg_train_auc)\n    val_auc_hist.append(avg_val_auc)    \n    \n    # Epoch summary\n    print(f\"Epoch {epoch+1}/{NUM_EPOCHS}\")\n    print(f\"Train Loss: {avg_train_loss:.4f} | Val Loss: {avg_val_loss:.4f}\")\n    print(f\"Train AUC : {avg_train_auc :.4f} | Val AUC : {avg_val_auc :.4f}\")\n    print(\"-\" * 60)\n\n    # Reset resume pointer for the next epoch\n    start_batch = -1\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T04:12:21.320754Z","iopub.execute_input":"2025-06-16T04:12:21.321064Z","iopub.status.idle":"2025-06-16T05:00:00.103303Z","shell.execute_reply.started":"2025-06-16T04:12:21.321041Z","shell.execute_reply":"2025-06-16T05:00:00.102453Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(20, 5))\nplt.plot(range(NUM_EPOCHS), train_loss_hist, label=\"Train Loss\")\nplt.plot(range(NUM_EPOCHS), val_loss_hist, label=\"Val Loss\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.title(\"Loss Change Over Epochs\")\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T05:11:10.779923Z","iopub.execute_input":"2025-06-16T05:11:10.780198Z","iopub.status.idle":"2025-06-16T05:11:10.995093Z","shell.execute_reply.started":"2025-06-16T05:11:10.780177Z","shell.execute_reply":"2025-06-16T05:11:10.994299Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(20,5))\nplt.plot(range(NUM_EPOCHS),train_auc_hist, label=\"train\")\nplt.plot(range(NUM_EPOCHS),val_auc_hist, label=\"val\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy\")\nplt.title(\"Accuracy over epoch\")\nplt.legend()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T05:12:53.807045Z","iopub.execute_input":"2025-06-16T05:12:53.807322Z","iopub.status.idle":"2025-06-16T05:12:54.055826Z","shell.execute_reply.started":"2025-06-16T05:12:53.807300Z","shell.execute_reply":"2025-06-16T05:12:54.055105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load model to device and set to eval mode\nmodel.load_state_dict(torch.load('best_model.pt', map_location=device))\nmodel.to(device)\nmodel.eval()\n\npredictions = []\n\nwith torch.no_grad():\n    for i, (images, labels) in enumerate(tqdm(test_dataloader, total=len(test_dataloader))):\n        images = images.to(device, non_blocking=True)\n\n        outputs = model(images)                   # shape: (B, 1)\n        probs = torch.sigmoid(outputs)            # convert logits to probabilities\n        probs = probs.squeeze(1).cpu().numpy()    # shape: (B,) on CPU\n\n        predictions.extend(probs)                 # add to final list\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T05:13:04.398980Z","iopub.execute_input":"2025-06-16T05:13:04.399530Z","iopub.status.idle":"2025-06-16T05:14:21.997152Z","shell.execute_reply.started":"2025-06-16T05:13:04.399505Z","shell.execute_reply":"2025-06-16T05:14:21.996062Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub['label'] = predictions\nsub.to_csv('submission.csv', index=False)\nsub.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-16T05:14:27.658342Z","iopub.execute_input":"2025-06-16T05:14:27.659295Z","iopub.status.idle":"2025-06-16T05:14:27.864741Z","shell.execute_reply.started":"2025-06-16T05:14:27.659249Z","shell.execute_reply":"2025-06-16T05:14:27.864008Z"}},"outputs":[],"execution_count":null}]}