{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.13","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":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":560.671778,"end_time":"2025-11-05T22:36:45.111205","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-11-05T22:27:24.439427","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"981a3e2c-f7bf-4a95-8c50-ea6012c2f0ec","cell_type":"markdown","source":"# Project 2 \n\n**Course:** `DATA-621`  \n**Semester:** `Fall`  &nbsp; **Year:** `2025`\n\n**Team:**\n\n**Sujan Neupane**  \n_Email:_ `sneupan2@umbc.edu`  \n\n**Nick Devroye**  \n_Email:_ `ndevroy1@umbc.edu`  \n\n**Anjali Jha**  \n_Email:_ `qd98375@umbc.edu` \n\n**Jason M. Miller**  \n_Email:_ `jmille16@umbc.edu` \n\n---","metadata":{}},{"id":"4bc9383e-34e6-464b-9e17-a0d0bbda15ab","cell_type":"markdown","source":"## Data Pipeline","metadata":{}},{"id":"ff042e71","cell_type":"markdown","source":"Import all necessary libraries for the project: \n- pandas for data manipulation,\n- `os` for file paths,\n- `sklearn` for splitting data,\n- `torch` and `torchvision` for building and training the deep learning model,\n- `PIL` for image loading, \n- `matplotlib`/`seaborn` for plotting","metadata":{}},{"id":"6f9b5ca6","cell_type":"code","source":"### Importing Necessary Libraries\nimport pandas as pd\nimport os\nimport zipfile\nfrom sklearn.model_selection import train_test_split\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm import tqdm\nimport torch\nimport torch.nn.functional as F","metadata":{"papermill":{"duration":11.181869,"end_time":"2025-11-05T22:27:39.056853","exception":false,"start_time":"2025-11-05T22:27:27.874984","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T17:21:24.712637Z","iopub.execute_input":"2025-11-12T17:21:24.712874Z","iopub.status.idle":"2025-11-12T17:21:34.648331Z","shell.execute_reply.started":"2025-11-12T17:21:24.712851Z","shell.execute_reply":"2025-11-12T17:21:34.647739Z"}},"outputs":[],"execution_count":null},{"id":"922136ba","cell_type":"markdown","source":"Import additional components from \n- `torchvision` (models) and\n- `torch` (`nn`, `optim`),\n- `copy` library, for use in early stopping","metadata":{}},{"id":"c209df6e","cell_type":"code","source":"### Importing Necessary Libraries\nfrom torchvision import transforms, models\nfrom PIL import Image\nimport torch.nn as nn\nimport torch.optim as optim\nimport copy  # For deep copy in early stopping","metadata":{"papermill":{"duration":0.015311,"end_time":"2025-11-05T22:27:39.082960","exception":false,"start_time":"2025-11-05T22:27:39.067649","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T17:21:34.649627Z","iopub.execute_input":"2025-11-12T17:21:34.649983Z","iopub.status.idle":"2025-11-12T17:21:34.653964Z","shell.execute_reply.started":"2025-11-12T17:21:34.649963Z","shell.execute_reply":"2025-11-12T17:21:34.653226Z"}},"outputs":[],"execution_count":null},{"id":"60a091b0-b222-4b92-9b26-6fe742d20d4c","cell_type":"markdown","source":"### Data Mapping, Labeling, Loading ","metadata":{}},{"id":"f8d8c735","cell_type":"markdown","source":"Define a dictionary to map the numeric labels (0 and 1) to human-readable string labels ('No Cancer' and 'Cancer').","metadata":{}},{"id":"e386ca9b","cell_type":"code","source":"index_to_labels_mapping = {\n    0: \"No Cancer\",\n    1: \"Cancer\"\n}","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:21:34.654610Z","iopub.execute_input":"2025-11-12T17:21:34.654800Z","iopub.status.idle":"2025-11-12T17:21:34.672221Z","shell.execute_reply.started":"2025-11-12T17:21:34.654786Z","shell.execute_reply":"2025-11-12T17:21:34.671577Z"},"papermill":{"duration":0.015739,"end_time":"2025-11-05T22:27:39.108464","exception":false,"start_time":"2025-11-05T22:27:39.092725","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b5b5cf5d","cell_type":"markdown","source":"Set variable to store the file path to the directory containing the training images.","metadata":{}},{"id":"793b17e4","cell_type":"code","source":"train_images_root_dir = '/kaggle/input/histopathologic-cancer-detection/train'","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:21:34.672980Z","iopub.execute_input":"2025-11-12T17:21:34.673285Z","iopub.status.idle":"2025-11-12T17:21:34.684752Z","shell.execute_reply.started":"2025-11-12T17:21:34.673262Z","shell.execute_reply":"2025-11-12T17:21:34.684069Z"},"papermill":{"duration":0.015291,"end_time":"2025-11-05T22:27:39.133987","exception":false,"start_time":"2025-11-05T22:27:39.118696","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"f328f097","cell_type":"markdown","source":"Read the training labels from the CSV file into a pandas DataFrame.","metadata":{}},{"id":"de5b8b26","cell_type":"code","source":"df = pd.read_csv('/kaggle/input/histopathologic-cancer-detection/train_labels.csv')","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:21:34.685799Z","iopub.execute_input":"2025-11-12T17:21:34.686063Z","iopub.status.idle":"2025-11-12T17:21:35.020118Z","shell.execute_reply.started":"2025-11-12T17:21:34.686043Z","shell.execute_reply":"2025-11-12T17:21:35.019473Z"},"papermill":{"duration":0.460253,"end_time":"2025-11-05T22:27:39.604643","exception":false,"start_time":"2025-11-05T22:27:39.144390","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"83b9067e","cell_type":"code","source":"df","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:21:35.020831Z","iopub.execute_input":"2025-11-12T17:21:35.021102Z","iopub.status.idle":"2025-11-12T17:21:35.045943Z","shell.execute_reply.started":"2025-11-12T17:21:35.021078Z","shell.execute_reply":"2025-11-12T17:21:35.045353Z"},"papermill":{"duration":0.034461,"end_time":"2025-11-05T22:27:39.651313","exception":false,"start_time":"2025-11-05T22:27:39.616852","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"058976eb","cell_type":"markdown","source":"Opens first image from the dataset and check its properties.\n- Print image mode (e.g., 'RGB')\n- Use `transforms.ToTensor()` to check its tensor shape (Channels, Height, Width).","metadata":{}},{"id":"b68aeb03","cell_type":"code","source":"img_path = os.path.join(train_images_root_dir, df[\"id\"].values[0]+\".tif\")\nimg = Image.open(img_path)\nprint(\"Mode:\", img.mode)  # 'RGB' for color, 'L' for grayscale\n\nto_tensor = transforms.ToTensor()\nimg_tensor = to_tensor(img)  # shape: [C, H, W]\nimg_tensor.shape","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:03.594410Z","iopub.execute_input":"2025-11-12T17:22:03.595205Z","iopub.status.idle":"2025-11-12T17:22:03.792581Z","shell.execute_reply.started":"2025-11-12T17:22:03.595168Z","shell.execute_reply":"2025-11-12T17:22:03.791812Z"},"papermill":{"duration":0.199857,"end_time":"2025-11-05T22:27:39.861299","exception":false,"start_time":"2025-11-05T22:27:39.661442","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5b560c4b","cell_type":"markdown","source":"**Perfect. Our images are RGB**","metadata":{"papermill":{"duration":0.010039,"end_time":"2025-11-05T22:27:39.881965","exception":false,"start_time":"2025-11-05T22:27:39.871926","status":"completed"},"tags":[]}},{"id":"ed93c63b","cell_type":"markdown","source":"Create a balanced subset of the data. \n- Samples 5000 images with\n- label 0 (No Cancer) and\n- 5000 images with label 1 (Cancer)\n- Concatenates into a new DataFrame `df_sampled`.\n- Needed for, potentially imbalanced dataset.","metadata":{}},{"id":"d11dc497","cell_type":"code","source":"df_class_0 = df[df['label'] == 0].sample(n=5000, random_state=42)\ndf_class_1 = df[df['label'] == 1].sample(n=5000, random_state=42)\ndf_sampled = pd.concat([df_class_0, df_class_1]).reset_index(drop=True)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:05.055868Z","iopub.execute_input":"2025-11-12T17:22:05.056166Z","iopub.status.idle":"2025-11-12T17:22:05.082673Z","shell.execute_reply.started":"2025-11-12T17:22:05.056136Z","shell.execute_reply":"2025-11-12T17:22:05.081878Z"},"papermill":{"duration":0.036269,"end_time":"2025-11-05T22:27:39.928360","exception":false,"start_time":"2025-11-05T22:27:39.892091","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"4458b485","cell_type":"markdown","source":"Create a list of filenames by appending '.tif' to each image ID in the sampled DataFrame.","metadata":{}},{"id":"d5763526","cell_type":"code","source":"filenames = [f\"{id}.tif\" for id in df_sampled['id']]","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:05.516017Z","iopub.execute_input":"2025-11-12T17:22:05.516637Z","iopub.status.idle":"2025-11-12T17:22:05.524766Z","shell.execute_reply.started":"2025-11-12T17:22:05.516611Z","shell.execute_reply":"2025-11-12T17:22:05.524054Z"},"papermill":{"duration":0.017738,"end_time":"2025-11-05T22:27:39.957276","exception":false,"start_time":"2025-11-05T22:27:39.939538","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"823bae4c","cell_type":"markdown","source":"Check the length of the `filenames` list and displays the first and last filenames to verify its content.","metadata":{}},{"id":"b1af46b7","cell_type":"code","source":"len(filenames), filenames[0], filenames[-1]","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:06.972942Z","iopub.execute_input":"2025-11-12T17:22:06.973222Z","iopub.status.idle":"2025-11-12T17:22:06.978637Z","shell.execute_reply.started":"2025-11-12T17:22:06.973201Z","shell.execute_reply":"2025-11-12T17:22:06.977811Z"},"papermill":{"duration":0.016209,"end_time":"2025-11-05T22:27:39.983424","exception":false,"start_time":"2025-11-05T22:27:39.967215","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c4d5c43a","cell_type":"markdown","source":"Split the 10,000-sample DataFrame into: \n- training, 60% training (`train_df`) and 40% temporary (`temp_df`)\n- validation, equally (50/50) into a 20% validation set (`val_df`)\n- test 20% test set (`test_df`)\n- The splits are stratified to maintain the class balance.","metadata":{}},{"id":"0b7046b2","cell_type":"code","source":"# 80% data train; 40% explicitly test + validation\ntrain_df, temp_df = train_test_split(df_sampled, test_size=0.4, stratify=df_sampled['label'], random_state=42)\n\n# 20% for test and 20% for validation\nval_df, test_df = train_test_split(temp_df, test_size=0.5, stratify=temp_df['label'], random_state=42)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:09.729256Z","iopub.execute_input":"2025-11-12T17:22:09.729928Z","iopub.status.idle":"2025-11-12T17:22:09.743483Z","shell.execute_reply.started":"2025-11-12T17:22:09.729901Z","shell.execute_reply":"2025-11-12T17:22:09.742955Z"},"papermill":{"duration":0.030977,"end_time":"2025-11-05T22:27:40.024609","exception":false,"start_time":"2025-11-05T22:27:39.993632","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"142ec9fb","cell_type":"markdown","source":"Resets the indices for the new train, validation, and test DataFrames. Note: will only display the output of the last line (`test_df.reset_index`). The first two operations are not saved back to the DataFrames as `inplace=True` was not set.","metadata":{}},{"id":"ba39a194","cell_type":"code","source":"train_df.reset_index(drop=True)\nval_df.reset_index(drop=True)\ntest_df.reset_index(drop=True)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:10.265108Z","iopub.execute_input":"2025-11-12T17:22:10.265780Z","iopub.status.idle":"2025-11-12T17:22:10.275257Z","shell.execute_reply.started":"2025-11-12T17:22:10.265757Z","shell.execute_reply":"2025-11-12T17:22:10.274612Z"},"papermill":{"duration":0.022656,"end_time":"2025-11-05T22:27:40.057543","exception":false,"start_time":"2025-11-05T22:27:40.034887","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"917b6a1e","cell_type":"markdown","source":"Prints final size (number of samples) of the training, validation, and test sets to confirm the 60/20/20 split.","metadata":{}},{"id":"df81c446","cell_type":"code","source":"print(f\"Train: {len(train_df)}, Val: {len(val_df)}, Test: {len(test_df)}\")","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:10.347118Z","iopub.execute_input":"2025-11-12T17:22:10.347343Z","iopub.status.idle":"2025-11-12T17:22:10.351251Z","shell.execute_reply.started":"2025-11-12T17:22:10.347327Z","shell.execute_reply":"2025-11-12T17:22:10.350674Z"},"papermill":{"duration":0.015583,"end_time":"2025-11-05T22:27:40.083836","exception":false,"start_time":"2025-11-05T22:27:40.068253","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5b159bcc","cell_type":"markdown","source":"---\n## Helper Functions","metadata":{}},{"id":"70a0c275-dbd4-4a43-a543-55a35e4c0019","cell_type":"markdown","source":"#### plot_a_random_image_from_trainDf","metadata":{}},{"id":"e56aeb9c","cell_type":"markdown","source":"Define a helper function `plot_a_random_image_from_trainDf` \n- takes a label (0 or 1),\n- samples a random image from the training set with that label,\n- displays it using matplotlib.","metadata":{}},{"id":"4a1959fc","cell_type":"code","source":"def plot_a_random_image_from_trainDf(label=0):\n    df_random = train_df[train_df.label == label].sample(1)\n    filename = df_random[\"id\"].values[0]\n    file_path = os.path.join(train_images_root_dir, filename + \".tif\")\n\n    img = Image.open(file_path)\n\n    plt.figure(figsize=(6, 6)) \n    plt.imshow(img)\n    plt.axis('off')  \n    plt.title(f\"Class: {index_to_labels_mapping[label]}\", fontsize=16, fontweight='bold', pad=15)\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:10.485669Z","iopub.execute_input":"2025-11-12T17:22:10.485930Z","iopub.status.idle":"2025-11-12T17:22:10.490273Z","shell.execute_reply.started":"2025-11-12T17:22:10.485911Z","shell.execute_reply":"2025-11-12T17:22:10.489671Z"},"papermill":{"duration":0.016079,"end_time":"2025-11-05T22:27:40.110234","exception":false,"start_time":"2025-11-05T22:27:40.094155","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"d61b458d","cell_type":"markdown","source":"Call the new helper function plot_a_random_image_from_trainDf to display a random 'No Cancer' (label 0) image.","metadata":{}},{"id":"cd430f52","cell_type":"code","source":"plot_a_random_image_from_trainDf(0)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:10.549438Z","iopub.execute_input":"2025-11-12T17:22:10.549646Z","iopub.status.idle":"2025-11-12T17:22:10.666059Z","shell.execute_reply.started":"2025-11-12T17:22:10.549630Z","shell.execute_reply":"2025-11-12T17:22:10.665439Z"},"papermill":{"duration":0.140267,"end_time":"2025-11-05T22:27:40.261048","exception":false,"start_time":"2025-11-05T22:27:40.120781","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"fe2310a4-d78d-4321-99e1-f02ee8a76d6d","cell_type":"markdown","source":"Call the new helper function plot_a_random_image_from_trainDf to display a random 'Cancer' (label 1) image.","metadata":{}},{"id":"9eec7d82","cell_type":"code","source":"plot_a_random_image_from_trainDf(1)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:10.667002Z","iopub.execute_input":"2025-11-12T17:22:10.667238Z","iopub.status.idle":"2025-11-12T17:22:10.749948Z","shell.execute_reply.started":"2025-11-12T17:22:10.667213Z","shell.execute_reply":"2025-11-12T17:22:10.749313Z"},"papermill":{"duration":0.09735,"end_time":"2025-11-05T22:27:40.370565","exception":false,"start_time":"2025-11-05T22:27:40.273215","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c3327c16-6d64-4e3e-97cb-510f93ff22ac","cell_type":"markdown","source":"#### plot_batch_of_images","metadata":{}},{"id":"22a519b7","cell_type":"markdown","source":"helper function `plot_batch_of_images` \n- display a 4x4 grid of random images for a specified label.","metadata":{}},{"id":"c498bc8d","cell_type":"code","source":"def plot_batch_of_images(number_of_images = 16, label = 0):\n    df_random = train_df[train_df.label == label].sample(number_of_images, random_state = 42)[\"id\"].values\n    sixteen_random_samples = [os.path.join(train_images_root_dir, i + \".tif\") for i in df_random]\n    \n    fig, axes = plt.subplots(4, 4, figsize=(12, 12))\n    fig.suptitle(f\"16 Random '{index_to_labels_mapping[label]}' Samples\", fontsize=18, fontweight='bold', y=1)\n    \n    for i, img_path in tqdm(enumerate(sixteen_random_samples)):\n        ax = axes[i // 4, i % 4]  \n        img = Image.open(img_path)\n        ax.imshow(img)\n        ax.axis('off')\n    \n    plt.tight_layout()\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:10.750759Z","iopub.execute_input":"2025-11-12T17:22:10.751103Z","iopub.status.idle":"2025-11-12T17:22:10.756238Z","shell.execute_reply.started":"2025-11-12T17:22:10.751086Z","shell.execute_reply":"2025-11-12T17:22:10.755526Z"},"papermill":{"duration":0.019265,"end_time":"2025-11-05T22:27:40.402142","exception":false,"start_time":"2025-11-05T22:27:40.382877","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"effeb644","cell_type":"markdown","source":"Call the function `plot_batch_of_images` to display a grid of 'No Cancer' (label 0) images.","metadata":{}},{"id":"a8eb605a","cell_type":"code","source":"# Plot a 4x4 grid of images with label 0\nplot_batch_of_images(label=0)","metadata":{"papermill":{"duration":1.280274,"end_time":"2025-11-05T22:27:41.694070","exception":false,"start_time":"2025-11-05T22:27:40.413796","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T17:22:10.780022Z","iopub.execute_input":"2025-11-12T17:22:10.780544Z","iopub.status.idle":"2025-11-12T17:22:11.892539Z","shell.execute_reply.started":"2025-11-12T17:22:10.780526Z","shell.execute_reply":"2025-11-12T17:22:11.891755Z"}},"outputs":[],"execution_count":null},{"id":"5f9dde15","cell_type":"markdown","source":"Call the function `plot_batch_of_images` to display a grid of 'Cancer' (label 1) images.","metadata":{}},{"id":"c770959c","cell_type":"code","source":"# Plot a 4x4 grid of images with label 1\nplot_batch_of_images(label=1)","metadata":{"papermill":{"duration":1.358231,"end_time":"2025-11-05T22:27:43.102619","exception":false,"start_time":"2025-11-05T22:27:41.744388","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T17:22:11.894225Z","iopub.execute_input":"2025-11-12T17:22:11.894645Z","iopub.status.idle":"2025-11-12T17:22:13.111942Z","shell.execute_reply.started":"2025-11-12T17:22:11.894616Z","shell.execute_reply":"2025-11-12T17:22:13.111180Z"}},"outputs":[],"execution_count":null},{"id":"4956e37d-5ec7-42be-8713-6b731791459c","cell_type":"markdown","source":"---\n## CNN Build","metadata":{}},{"id":"53c4dc40","cell_type":"markdown","source":"Now that we have our dataset ready, we will perform 2 sets of experiments:\n\n1. Use ImageNet pretrained ResNet50 and replace its classification head (that classifies for 1000 classes) with our custom classification head that does binary classification, and use a very small learning rate for a limited number of epochs to train the entire model (basic finetuning/transfer learning). For this, we will use ImageNet stats for each RGB channel for normalzing our input. We can use the Adam optimizer and step decay method for learning rate scheduling.\n\n2. Create a simple ResNet with PyTorch's nn module with batchnorm (to reduce internal covariate shift), LeakyReLU (for introducing non-linearity), Dropout (to reduce overfitting) and MaxPool2d ( to reduce the size of the feature map after each layer of convolution) and skip connections, and Residual blocks. We can use SGD as an optimizer and an Exponential decay for the learning rate scheduling.\n\nAlong with this, early stopping to ensure model training ends before the model starts overfitting, data augmentation (same for both models) to create variations in training data.","metadata":{"papermill":{"duration":0.095357,"end_time":"2025-11-05T22:27:43.293584","exception":false,"start_time":"2025-11-05T22:27:43.198227","status":"completed"},"tags":[]}},{"id":"864b36f3","cell_type":"markdown","source":"---\n## Finetuining ResNet-50 \n\nwith Adam optimizer and Step Decay LR scheduling","metadata":{"papermill":{"duration":0.093275,"end_time":"2025-11-05T22:27:43.480995","exception":false,"start_time":"2025-11-05T22:27:43.387720","status":"completed"},"tags":[]}},{"id":"5add2992","cell_type":"code","source":"# Define a custom Dataset class inheriting from torch.utils.data.Dataset\nclass CancerDataset(Dataset):\n    # Constructor\n    def __init__(self, df, img_dir, transform=None):\n        self.df = df # DataFrame with labels\n        self.img_dir = img_dir # Directory with images\n        self.transform = transform # Transformations to apply\n    \n    # Returns the total number of samples\n    def __len__(self):\n        return len(self.df)\n    \n    # Returns a single sample (image and label) at a given index\n    def __getitem__(self, idx):\n        # Get the image ID and label from the DataFrame at the specified index\n        img_id = self.df.iloc[idx]['id']\n        label = self.df.iloc[idx]['label']\n        \n        # Construct the full image path\n        img_path = os.path.join(self.img_dir, img_id + '.tif')\n        # Open the image using PIL\n        image = Image.open(img_path)\n        \n        # Apply transformations if they exist\n        if self.transform:\n            image = self.transform(image)\n        \n        # Return the transformed image and its label\n        return image, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T17:22:55.734381Z","iopub.execute_input":"2025-11-12T17:22:55.734681Z","iopub.status.idle":"2025-11-12T17:22:55.740357Z","shell.execute_reply.started":"2025-11-12T17:22:55.734658Z","shell.execute_reply":"2025-11-12T17:22:55.739487Z"}},"outputs":[],"execution_count":null},{"id":"4841de52","cell_type":"markdown","source":"Define the image transformations for the training and validation sets. \n- `train_transform`:\n- Apply random resized cropping,\n- random horizontal and vertical flips (data augmentation),\n- and convert to tensor.\n- `val_transform`:\n- Apply a simple resize,\n- center crop,\n- and convert to tensor.\n- No augmentation is used for validation","metadata":{}},{"id":"f8bac4e2","cell_type":"code","source":"# we only apply augmentation to training data\ntrain_transform = transforms.Compose([\n    transforms.RandomHorizontalFlip(), # randomly flipping image horizontally\n    transforms.RandomVerticalFlip(), # randomly flipping image vertically\n    transforms.RandomRotation(degrees=90),  # radomly rotating image up to 90 degrees\n    transforms.ToTensor(),\n    # transforms.Pad(padding=64, fill=0),           # 96 + 128 = 224; as resnet requires input at 3 channel * 224 * 224 W H\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # these are imagenet dataset RGB mean and std for normalizing our input\n])\n\nval_transform = transforms.Compose([\n    transforms.ToTensor(),\n    # transforms.Pad(padding=64, fill=0),           # 96 + 128 = 224; as resnet requires input at 3 channel * 224 * 224 W H\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:57.231432Z","iopub.execute_input":"2025-11-12T17:22:57.232115Z","iopub.status.idle":"2025-11-12T17:22:57.236609Z","shell.execute_reply.started":"2025-11-12T17:22:57.232092Z","shell.execute_reply":"2025-11-12T17:22:57.235885Z"},"papermill":{"duration":0.104129,"end_time":"2025-11-05T22:27:43.878823","exception":false,"start_time":"2025-11-05T22:27:43.774694","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"a253ca58","cell_type":"markdown","source":"Instantiate the `CancerDataset` for the training, validation, and test sets, passing the respective DataFrames and transformations.\n- (%%time) to measure the execution time of the cell","metadata":{}},{"id":"2125d278","cell_type":"code","source":"%%time\n\ntrain_dataset = CancerDataset(train_df, train_images_root_dir, transform=train_transform)\nval_dataset = CancerDataset(val_df, train_images_root_dir, transform=val_transform)\ntest_dataset = CancerDataset(test_df, train_images_root_dir, transform=val_transform)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:58.727675Z","iopub.execute_input":"2025-11-12T17:22:58.727990Z","iopub.status.idle":"2025-11-12T17:22:58.732700Z","shell.execute_reply.started":"2025-11-12T17:22:58.727966Z","shell.execute_reply":"2025-11-12T17:22:58.731901Z"},"papermill":{"duration":0.098793,"end_time":"2025-11-05T22:27:44.069646","exception":false,"start_time":"2025-11-05T22:27:43.970853","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"eb5a2ba9","cell_type":"markdown","source":"Retrieve and prints the shape and label of the first item in the `train_dataset` to confirm working as expected.","metadata":{}},{"id":"55d6ea93","cell_type":"code","source":"# first image in train dataset\nimage, label = train_dataset[0]\nprint(\"Image shape:\", image.shape)\nprint(\"Label:\", label)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:22:59.071050Z","iopub.execute_input":"2025-11-12T17:22:59.071684Z","iopub.status.idle":"2025-11-12T17:22:59.092472Z","shell.execute_reply.started":"2025-11-12T17:22:59.071659Z","shell.execute_reply":"2025-11-12T17:22:59.091869Z"},"papermill":{"duration":0.112016,"end_time":"2025-11-05T22:27:44.274915","exception":false,"start_time":"2025-11-05T22:27:44.162899","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"aca40ce0","cell_type":"markdown","source":"Create `DataLoader` instances for the training, validation, and test datasets. \n- `DataLoader` manages batching (32 images per batch), shuffling (only for training), and parallel data loading (`num_workers=2`).","metadata":{}},{"id":"9d5be7ad","cell_type":"code","source":"# Create a DataLoader for the training set\ntrain_dl = DataLoader(train_dataset, \n                    batch_size=32, \n                    num_workers=2, # Use 2 worker processes to load data\n                    shuffle=True) # Shuffle the training data each epoch\n\n# Create a DataLoader for the validation set\nval_dl = DataLoader(val_dataset, \n                  batch_size=32, \n                  num_workers=2, \n                  shuffle=False) # No need to shuffle validation data\n\n# Create a DataLoader for the test set\ntest_dl = DataLoader(test_dataset, \n                   batch_size=32, \n                   num_workers=2, \n                   shuffle=False) # No need to shuffle test data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-12T17:23:00.347496Z","iopub.execute_input":"2025-11-12T17:23:00.348097Z","iopub.status.idle":"2025-11-12T17:23:00.352562Z","shell.execute_reply.started":"2025-11-12T17:23:00.348075Z","shell.execute_reply":"2025-11-12T17:23:00.351725Z"}},"outputs":[],"execution_count":null},{"id":"34aa9330","cell_type":"markdown","source":"#### DeviceDataLoader","metadata":{"papermill":{"duration":0.097064,"end_time":"2025-11-05T22:27:44.648238","exception":false,"start_time":"2025-11-05T22:27:44.551174","status":"completed"},"tags":[]}},{"id":"6f42f5d6","cell_type":"markdown","source":"Define a helper class, `DeviceDataLoader`, \n- automatically move data batches (inputs and labels) to the selected device (GPU/CPU) during iteration.\n\nAt every batch iteration of epoch, we move input array tensor to GPU, which introduces overhead. We can instead, push the entire dataloader to GPU.Then, we wont have to push image tensor and labels to gpu at every batch iteration of every epoch.","metadata":{}},{"id":"4e690d23","cell_type":"code","source":"# THIS IS COPIED FROM JOVIAN.AI\ndef get_default_device():\n    \"\"\"Pick GPU if available, else CPU\"\"\"\n    if torch.cuda.is_available():\n        return torch.device('cuda')\n    else:\n        return torch.device('cpu')\n    \ndef to_device(data, device):\n    \"\"\"Move tensor(s) to chosen device\"\"\"\n    if isinstance(data, (list,tuple)):\n        return [to_device(x, device) for x in data]\n    return data.to(device, non_blocking=True)\n\nclass DeviceDataLoader():\n    \"\"\"Wrap a dataloader to move data to a device\"\"\"\n    def __init__(self, dl, device):\n        self.dl = dl\n        self.device = device\n        \n    def __iter__(self):\n        \"\"\"Yield a batch of data after moving it to device\"\"\"\n        for b in self.dl: \n            yield to_device(b, self.device)\n\n    def __len__(self):\n        \"\"\"Number of batches\"\"\"\n        return len(self.dl)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:08.874333Z","iopub.execute_input":"2025-11-12T17:23:08.874861Z","iopub.status.idle":"2025-11-12T17:23:08.880551Z","shell.execute_reply.started":"2025-11-12T17:23:08.874815Z","shell.execute_reply":"2025-11-12T17:23:08.879823Z"},"papermill":{"duration":0.10068,"end_time":"2025-11-05T22:27:44.864533","exception":false,"start_time":"2025-11-05T22:27:44.763853","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"6aa193ff","cell_type":"code","source":"device = get_default_device()\ndevice","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:09.815884Z","iopub.execute_input":"2025-11-12T17:23:09.816127Z","iopub.status.idle":"2025-11-12T17:23:09.897633Z","shell.execute_reply.started":"2025-11-12T17:23:09.816111Z","shell.execute_reply":"2025-11-12T17:23:09.897008Z"},"papermill":{"duration":0.181519,"end_time":"2025-11-05T22:27:45.139626","exception":false,"start_time":"2025-11-05T22:27:44.958107","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"456f5a81","cell_type":"markdown","source":"Wraps the existing `DataLoader` instances with the `DeviceDataLoader` to automate data transfer to the device.","metadata":{}},{"id":"6f7e3c2b","cell_type":"code","source":"train_dl = DeviceDataLoader(train_dl, device)\nval_dl = DeviceDataLoader(val_dl, device)\ntest_dl = DeviceDataLoader(test_dl, device)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:10.204211Z","iopub.execute_input":"2025-11-12T17:23:10.204902Z","iopub.status.idle":"2025-11-12T17:23:10.208355Z","shell.execute_reply.started":"2025-11-12T17:23:10.204872Z","shell.execute_reply":"2025-11-12T17:23:10.207584Z"},"papermill":{"duration":0.098717,"end_time":"2025-11-05T22:27:45.332641","exception":false,"start_time":"2025-11-05T22:27:45.233924","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"6894852a-0973-4ceb-89a0-0c5c62778d80","cell_type":"markdown","source":"### Training","metadata":{}},{"id":"a42adbf6","cell_type":"markdown","source":"Set hyperparameters for the training process: \n- number of epochs (50) and\n- patience for early stopping (10).","metadata":{}},{"id":"0f934004","cell_type":"markdown","source":"### Annotation: Initialize ResNet‑50\n\n- Loads pretrained ResNet‑50 backbone.\n- Replaces the final `fc` layer to match number of classes.\n","metadata":{}},{"id":"3d0e449d","cell_type":"code","source":"# pretrained imagenet ResNet50\nmodel = models.resnet50(pretrained=True)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:12.971887Z","iopub.execute_input":"2025-11-12T17:23:12.972482Z","iopub.status.idle":"2025-11-12T17:23:14.021697Z","shell.execute_reply.started":"2025-11-12T17:23:12.972458Z","shell.execute_reply":"2025-11-12T17:23:14.020955Z"},"papermill":{"duration":1.216562,"end_time":"2025-11-05T22:27:46.641490","exception":false,"start_time":"2025-11-05T22:27:45.424928","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"dfec2062","cell_type":"code","source":"model","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:14.022896Z","iopub.execute_input":"2025-11-12T17:23:14.023095Z","iopub.status.idle":"2025-11-12T17:23:14.029316Z","shell.execute_reply.started":"2025-11-12T17:23:14.023080Z","shell.execute_reply":"2025-11-12T17:23:14.028602Z"},"papermill":{"duration":0.098285,"end_time":"2025-11-05T22:27:46.837448","exception":false,"start_time":"2025-11-05T22:27:46.739163","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"3b86c4b9","cell_type":"markdown","source":"The name of last layer is fc. lets replace the linear layer with our new layer that gives us 2 logits as output.","metadata":{"papermill":{"duration":0.083413,"end_time":"2025-11-05T22:27:47.006361","exception":false,"start_time":"2025-11-05T22:27:46.922948","status":"completed"},"tags":[]}},{"id":"27466b81","cell_type":"code","source":"model.fc ","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:14.602474Z","iopub.execute_input":"2025-11-12T17:23:14.602749Z","iopub.status.idle":"2025-11-12T17:23:14.607583Z","shell.execute_reply.started":"2025-11-12T17:23:14.602729Z","shell.execute_reply":"2025-11-12T17:23:14.606969Z"},"papermill":{"duration":0.093312,"end_time":"2025-11-05T22:27:47.183079","exception":false,"start_time":"2025-11-05T22:27:47.089767","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"afb809cb","cell_type":"code","source":"# only 2 predictions (one for each class; will go through softmax activation to get predicted probaility)\n# cross entropy loss will use softmax activation by default; can replace with just one neuron to be passed through sigmoid but will require using\n# binary cross entropy loss instead of CE loss\nmodel.fc = nn.Linear(model.fc.in_features , 2) ","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:14.779530Z","iopub.execute_input":"2025-11-12T17:23:14.780366Z","iopub.status.idle":"2025-11-12T17:23:14.784418Z","shell.execute_reply.started":"2025-11-12T17:23:14.780343Z","shell.execute_reply":"2025-11-12T17:23:14.783735Z"},"papermill":{"duration":0.092321,"end_time":"2025-11-05T22:27:47.361083","exception":false,"start_time":"2025-11-05T22:27:47.268762","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1f58375b","cell_type":"code","source":"model","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:14.938719Z","iopub.execute_input":"2025-11-12T17:23:14.939575Z","iopub.status.idle":"2025-11-12T17:23:14.945353Z","shell.execute_reply.started":"2025-11-12T17:23:14.939544Z","shell.execute_reply":"2025-11-12T17:23:14.944690Z"},"papermill":{"duration":0.093866,"end_time":"2025-11-05T22:27:47.541020","exception":false,"start_time":"2025-11-05T22:27:47.447154","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8eb6763b","cell_type":"markdown","source":"**Perfect. Our model is ready.**","metadata":{"papermill":{"duration":0.08412,"end_time":"2025-11-05T22:27:47.709924","exception":false,"start_time":"2025-11-05T22:27:47.625804","status":"completed"},"tags":[]}},{"id":"8b1e0433","cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nif torch.cuda.device_count() > 1:\n    print(f\"Using {torch.cuda.device_count()} GPUs\")\n    model = nn.DataParallel(model)\nmodel = model.to(device)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:17.962207Z","iopub.execute_input":"2025-11-12T17:23:17.962792Z","iopub.status.idle":"2025-11-12T17:23:18.190712Z","shell.execute_reply.started":"2025-11-12T17:23:17.962768Z","shell.execute_reply":"2025-11-12T17:23:18.190067Z"},"papermill":{"duration":0.306132,"end_time":"2025-11-05T22:27:48.099538","exception":false,"start_time":"2025-11-05T22:27:47.793406","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"49f490a3","cell_type":"markdown","source":"We have 2 gpus. so, will train in a cluster with 2x T4 GPUs for faster training.","metadata":{"papermill":{"duration":0.084355,"end_time":"2025-11-05T22:27:48.270004","exception":false,"start_time":"2025-11-05T22:27:48.185649","status":"completed"},"tags":[]}},{"id":"0756775c","cell_type":"code","source":"# these will be same for both model\npatience = 8  # number of epochs to wait for improvement for early stopping\nnum_epochs = 50","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:18.617317Z","iopub.execute_input":"2025-11-12T17:23:18.617560Z","iopub.status.idle":"2025-11-12T17:23:18.621111Z","shell.execute_reply.started":"2025-11-12T17:23:18.617542Z","shell.execute_reply":"2025-11-12T17:23:18.620410Z"},"papermill":{"duration":0.091795,"end_time":"2025-11-05T22:27:48.445764","exception":false,"start_time":"2025-11-05T22:27:48.353969","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"840fac51","cell_type":"markdown","source":"### Optimizer: Adam\n\n- Creates Adam with chosen `lr`, `betas`, and `weight_decay`.\n- Defines parameter groups if needed for head/backbone.\n- Configures `StepLR` with step size and decay factor `gamma`.\n- Scheduler stepped at epoch boundaries.\n- Uses cross‑entropy for multi‑class classification.\n- Optionally applies label smoothing / class weights.\n- Iterates epochs with forward/backward passes.\n- Tracks running loss/accuracy; saves best model.\n- Switches to eval mode, disables grads, computes metrics.\n- Optionally builds confusion matrix / classification report.\n","metadata":{}},{"id":"dce9e64a","cell_type":"code","source":"criterion = nn.CrossEntropyLoss()\n\n# very small learning rate; we don't want rapid updates, otherwise no point in using ImageNet weights\noptimizer = optim.Adam(model.parameters(), lr=0.0001) \n\n# reduce LR by factor of 0.9 every 5 epochs\nscheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.9)\n\nbest_val_loss = float('inf')\nbest_model_wts = copy.deepcopy(model.state_dict())\nearly_stop_counter = 0\n\ntrain_losses = []\ntrain_accs = []\nval_losses = []\nval_accs = []\n\nfor epoch in range(num_epochs):\n    model.train()\n    train_loss = 0.0\n    train_correct = 0\n    train_total = 0\n    for inputs, labels in train_dl:\n        optimizer.zero_grad()\n        outputs = model(inputs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        train_loss += loss.item() * inputs.size(0)\n        _, predicted = torch.max(outputs.data, 1)\n        train_total += labels.size(0)\n        train_correct += (predicted == labels).sum().item()\n    \n    train_loss /= len(train_dataset)\n    train_acc = 100 * train_correct / train_total\n\n    train_losses.append(train_loss)\n    train_accs.append(train_acc)\n    \n    model.eval()\n    val_loss = 0.0\n    val_correct = 0\n    val_total = 0\n    with torch.no_grad():\n        for inputs, labels in val_dl:\n            outputs = model(inputs)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item() * inputs.size(0)\n            _, predicted = torch.max(outputs.data, 1)\n            val_total += labels.size(0)\n            val_correct += (predicted == labels).sum().item()\n    \n    val_loss /= len(val_dataset)\n    val_acc = 100 * val_correct / val_total\n\n    \n    val_losses.append(val_loss)\n    val_accs.append(val_acc)\n    \n    \n    print(f\"Epoch {epoch+1}/{num_epochs} - Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}% - Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%\")\n    \n    scheduler.step()\n    \n    # early stopping check to prevent overfitting\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        best_model_wts = copy.deepcopy(model.state_dict())\n        early_stop_counter = 0\n    else:\n        early_stop_counter += 1\n        if early_stop_counter >= patience:\n            print(f\"Early stopping triggered after {epoch+1} epochs\")\n            break\n\n# will load best weights after training is done\nmodel.load_state_dict(best_model_wts)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:23:21.570854Z","iopub.execute_input":"2025-11-12T17:23:21.571511Z","iopub.status.idle":"2025-11-12T17:36:59.878976Z","shell.execute_reply.started":"2025-11-12T17:23:21.571484Z","shell.execute_reply":"2025-11-12T17:36:59.878076Z"},"papermill":{"duration":196.091335,"end_time":"2025-11-05T22:31:04.620911","exception":false,"start_time":"2025-11-05T22:27:48.529576","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"80867632","cell_type":"markdown","source":"### Validation / evaluation\n\n- Switches to eval mode, disables grads, computes metrics.\n- Builds confusion matrix / classification report.\n","metadata":{}},{"id":"95d8ead1","cell_type":"code","source":"model.eval();","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:36:59.880594Z","iopub.execute_input":"2025-11-12T17:36:59.880912Z","iopub.status.idle":"2025-11-12T17:36:59.885260Z","shell.execute_reply.started":"2025-11-12T17:36:59.880890Z","shell.execute_reply":"2025-11-12T17:36:59.884579Z"},"papermill":{"duration":0.096314,"end_time":"2025-11-05T22:31:04.806872","exception":false,"start_time":"2025-11-05T22:31:04.710558","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"d01489d7","cell_type":"code","source":"training_stats = pd.DataFrame({\"Training_Accuracy\": train_accs,\n                               \"Training_Loss\": train_losses,\n                               \"Val_Loss\": val_losses,\n                               \"Val_Accuracy\": val_accs})","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:36:59.885945Z","iopub.execute_input":"2025-11-12T17:36:59.886148Z","iopub.status.idle":"2025-11-12T17:36:59.897814Z","shell.execute_reply.started":"2025-11-12T17:36:59.886133Z","shell.execute_reply":"2025-11-12T17:36:59.897235Z"},"papermill":{"duration":0.096464,"end_time":"2025-11-05T22:31:04.991873","exception":false,"start_time":"2025-11-05T22:31:04.895409","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"a3c0fb99","cell_type":"code","source":"training_stats","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:36:59.899544Z","iopub.execute_input":"2025-11-12T17:36:59.899793Z","iopub.status.idle":"2025-11-12T17:36:59.916481Z","shell.execute_reply.started":"2025-11-12T17:36:59.899778Z","shell.execute_reply":"2025-11-12T17:36:59.915710Z"},"papermill":{"duration":0.102241,"end_time":"2025-11-05T22:31:05.180380","exception":false,"start_time":"2025-11-05T22:31:05.078139","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2bdeaa57","cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(20, 8))\n\nn_epochs = len(training_stats)\nax1 = plt.subplot(1,2, 1)\n\nplot_1 = ax1.plot(range(n_epochs), training_stats['Training_Loss'], color = 'blue', label = 'Train Loss',\\\n             marker = 's', linewidth=2.0, markersize = 10)\n\nplot_2 = ax1.plot(range(n_epochs), training_stats['Val_Loss'], color = 'blue', label = 'Val Loss',\\\n             marker = 'o', linewidth=2.0, markersize = 10)\n\nax1.tick_params(axis ='y', labelcolor = 'blue',labelsize=20, width=3) \nax1.tick_params(axis ='x', labelcolor = 'black',labelsize=20, width=3)\nax1.legend(fontsize = 30)\nplt.xticks(range(0,n_epochs,1))\nax1.set_ylabel(\"Cross Entropy Loss\", fontsize = 30, labelpad = 10, color = 'blue')\n\n\nax1a = plt.subplot(1,2, 2)\nplot_11 = ax1a.plot(range(n_epochs), training_stats['Training_Accuracy'], color = 'red', label = 'Train Acc',\\\n             marker = 's', linewidth=2.0, markersize = 10)\n\nplot_22 = ax1a.plot(range(n_epochs), training_stats['Val_Accuracy'], color = 'red', label = 'Val Acc',\\\n             marker = 'o', linewidth=2.0, markersize = 10)\nax1a.legend(fontsize = 30)\nplt.xticks(range(0,n_epochs,1))\nax1a.tick_params(axis ='y', labelcolor = 'red',labelsize=20, width=3) \nax1a.tick_params(axis ='x', labelcolor = 'black',labelsize=20, width=3)\nax1a.set_ylabel(\"Accuracy\", fontsize = 30, labelpad = 10, color = 'red')\n\n\nax1.tick_params(which='both', width=2.5)\nax1.tick_params(which='major', length=15)\nax1.tick_params(which='minor', length=5)\nax1.tick_params(which = 'both', direction = 'in')\n\nax1a.tick_params(which='both', width=2.5)\nax1a.tick_params(which='major', length=15)\nax1a.tick_params(which='minor', length=5)\nax1a.tick_params(which = 'both', direction = 'in') \n\nax1a.spines['bottom'].set_color('black')\nax1a.spines['top'].set_color('black') \nax1a.spines['right'].set_color('black')\nax1a.spines['right'].set_linewidth(2)\nax1a.spines['top'].set_linewidth(2)\nax1a.spines['bottom'].set_linewidth(2)\nax1a.spines['left'].set_color('black')\nax1a.spines['left'].set_lw(2)\n\nax1.spines['bottom'].set_color('black')\nax1.spines['top'].set_color('black') \nax1.spines['right'].set_color('black')\nax1.spines['right'].set_linewidth(2)\nax1.spines['top'].set_linewidth(2)\nax1.spines['bottom'].set_linewidth(2)\nax1.spines['left'].set_color('black')\nax1.spines['left'].set_lw(2)\n\n\nax1.grid(True, which = 'major', alpha = 1, linestyle='--', linewidth = 1)\nax1a.grid(True, which = 'major', alpha = 1, linestyle='--', linewidth = 1)\n\n\nplt.subplots_adjust(wspace=0.25,hspace=0.)\nfig.text(0.5, 0.01, 'Epochs', ha='center', va='center', fontsize = 30)\n\nfig.text(0.5, 0.95, 'ResNet-Finetuned Performance on Training and Validation Datasets', ha='center', va='center', fontsize = 30)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:36:59.917257Z","iopub.execute_input":"2025-11-12T17:36:59.917475Z","iopub.status.idle":"2025-11-12T17:37:00.567525Z","shell.execute_reply.started":"2025-11-12T17:36:59.917459Z","shell.execute_reply":"2025-11-12T17:37:00.566597Z"},"papermill":{"duration":0.895068,"end_time":"2025-11-05T22:31:06.164140","exception":false,"start_time":"2025-11-05T22:31:05.269072","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"f321d339","cell_type":"code","source":"def get_test_predictions_with_probs(best_model, dataloader, device=\"cuda\"):\n    all_labels = []\n    all_preds = []\n    all_probs = []\n\n    with torch.no_grad():\n        for inputs, labels in tqdm(dataloader):\n            outputs = best_model(inputs)  \n            probs = F.softmax(outputs, dim=1)  # convert logits to probabilities with softmax\n            _, predicted = torch.max(outputs, 1)  \n\n            all_labels.extend(labels.cpu().numpy())\n            all_preds.extend(predicted.cpu().numpy())\n            all_probs.extend(probs.cpu().numpy())\n\n    return all_labels, all_preds, all_probs","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:00.568322Z","iopub.execute_input":"2025-11-12T17:37:00.568537Z","iopub.status.idle":"2025-11-12T17:37:00.573498Z","shell.execute_reply.started":"2025-11-12T17:37:00.568520Z","shell.execute_reply":"2025-11-12T17:37:00.572886Z"},"papermill":{"duration":0.118603,"end_time":"2025-11-05T22:31:06.382392","exception":false,"start_time":"2025-11-05T22:31:06.263789","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"7fcee3d8","cell_type":"code","source":"%%time\n\ntrain_actuals, train_predictions, train_predicted_probabilities = get_test_predictions_with_probs(model, train_dl)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:00.574272Z","iopub.execute_input":"2025-11-12T17:37:00.574534Z","iopub.status.idle":"2025-11-12T17:37:14.451124Z","shell.execute_reply.started":"2025-11-12T17:37:00.574513Z","shell.execute_reply":"2025-11-12T17:37:14.450309Z"},"papermill":{"duration":5.306699,"end_time":"2025-11-05T22:31:11.794796","exception":false,"start_time":"2025-11-05T22:31:06.488097","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"d1dc4a86","cell_type":"code","source":"val_actuals, val_predictions, val_predicted_probabilities = get_test_predictions_with_probs(model, val_dl)\ntest_actuals, test_predictions, test_predicted_probabilities = get_test_predictions_with_probs(model, test_dl)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:14.452080Z","iopub.execute_input":"2025-11-12T17:37:14.452695Z","iopub.status.idle":"2025-11-12T17:37:29.629757Z","shell.execute_reply.started":"2025-11-12T17:37:14.452670Z","shell.execute_reply":"2025-11-12T17:37:29.628994Z"},"papermill":{"duration":6.751727,"end_time":"2025-11-05T22:31:18.641298","exception":false,"start_time":"2025-11-05T22:31:11.889571","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b22a35fa","cell_type":"code","source":"from sklearn.metrics import accuracy_score, classification_report, roc_auc_score, confusion_matrix, roc_curve, precision_score, recall_score, f1_score","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:29.630722Z","iopub.execute_input":"2025-11-12T17:37:29.631078Z","iopub.status.idle":"2025-11-12T17:37:29.635350Z","shell.execute_reply.started":"2025-11-12T17:37:29.631051Z","shell.execute_reply":"2025-11-12T17:37:29.634577Z"},"papermill":{"duration":0.101621,"end_time":"2025-11-05T22:31:18.839791","exception":false,"start_time":"2025-11-05T22:31:18.738170","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"0905249e","cell_type":"code","source":"print(\"The testing accuracy is: {}\".format(accuracy_score(train_actuals, train_predictions)*100))","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:29.638103Z","iopub.execute_input":"2025-11-12T17:37:29.638490Z","iopub.status.idle":"2025-11-12T17:37:29.652424Z","shell.execute_reply.started":"2025-11-12T17:37:29.638472Z","shell.execute_reply":"2025-11-12T17:37:29.651665Z"},"papermill":{"duration":0.109322,"end_time":"2025-11-05T22:31:19.043886","exception":false,"start_time":"2025-11-05T22:31:18.934564","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"f4edea4b-da42-44db-bbe1-b06d98970b60","cell_type":"markdown","source":"### Performance and Classification Reports","metadata":{}},{"id":"a97a3cd2","cell_type":"code","source":"print(\"Classification report for training set\")\nprint(classification_report(train_actuals, train_predictions))","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:29.653228Z","iopub.execute_input":"2025-11-12T17:37:29.653477Z","iopub.status.idle":"2025-11-12T17:37:29.671711Z","shell.execute_reply.started":"2025-11-12T17:37:29.653453Z","shell.execute_reply":"2025-11-12T17:37:29.671188Z"},"papermill":{"duration":0.112807,"end_time":"2025-11-05T22:31:19.254991","exception":false,"start_time":"2025-11-05T22:31:19.142184","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"37599f8c","cell_type":"code","source":"print(\"Classification report for val set\")\nprint(classification_report(val_actuals, val_predictions))","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:29.672411Z","iopub.execute_input":"2025-11-12T17:37:29.672591Z","iopub.status.idle":"2025-11-12T17:37:29.683560Z","shell.execute_reply.started":"2025-11-12T17:37:29.672576Z","shell.execute_reply":"2025-11-12T17:37:29.682878Z"},"papermill":{"duration":0.108134,"end_time":"2025-11-05T22:31:19.468128","exception":false,"start_time":"2025-11-05T22:31:19.359994","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"32985048","cell_type":"code","source":"print(\"Classification report for test set\")\nprint(classification_report(test_actuals, test_predictions))","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:29.684235Z","iopub.execute_input":"2025-11-12T17:37:29.684405Z","iopub.status.idle":"2025-11-12T17:37:29.724241Z","shell.execute_reply.started":"2025-11-12T17:37:29.684391Z","shell.execute_reply":"2025-11-12T17:37:29.723658Z"},"papermill":{"duration":0.110466,"end_time":"2025-11-05T22:31:19.688636","exception":false,"start_time":"2025-11-05T22:31:19.578170","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"a2144d9c","cell_type":"code","source":"cf_matrix = confusion_matrix(test_actuals, test_predictions)\n\nclasses = list(index_to_labels_mapping.values())\ndataframe = pd.DataFrame(cf_matrix, index = classes, columns = classes)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:29.724852Z","iopub.execute_input":"2025-11-12T17:37:29.725058Z","iopub.status.idle":"2025-11-12T17:37:29.732615Z","shell.execute_reply.started":"2025-11-12T17:37:29.725044Z","shell.execute_reply":"2025-11-12T17:37:29.731936Z"},"papermill":{"duration":0.103838,"end_time":"2025-11-05T22:31:19.888065","exception":false,"start_time":"2025-11-05T22:31:19.784227","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8af7440f","cell_type":"code","source":"fig, axes = plt.subplots(1, 1, figsize=(8, 6))\n\nax1 = plt.subplot(1, 1, 1)\n\nsns.heatmap(dataframe, cmap=\"Blues\", annot = True, fmt=\"d\", cbar =False)\nfig.text(0.5, 0.00, 'Predicted', ha='center', va='center', fontsize = 20)\nfig.text(0.0, 0.5, 'Actual', ha='center', va='center', rotation='vertical', fontsize = 20)\nax1.text(0.5, 1.08, 'ResNet50-Finetuned',\n    horizontalalignment='center',\n    fontsize=20,\n    transform = ax1.transAxes);","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:29.733340Z","iopub.execute_input":"2025-11-12T17:37:29.733556Z","iopub.status.idle":"2025-11-12T17:37:29.841087Z","shell.execute_reply.started":"2025-11-12T17:37:29.733542Z","shell.execute_reply":"2025-11-12T17:37:29.840351Z"},"papermill":{"duration":0.207655,"end_time":"2025-11-05T22:31:20.193078","exception":false,"start_time":"2025-11-05T22:31:19.985423","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"eea59e8e","cell_type":"code","source":"import numpy as np\n\ntest_predicted_probabilities = np.array(test_predicted_probabilities)\ntest_actuals = np.array(test_actuals)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:29.841887Z","iopub.execute_input":"2025-11-12T17:37:29.842151Z","iopub.status.idle":"2025-11-12T17:37:29.847915Z","shell.execute_reply.started":"2025-11-12T17:37:29.842129Z","shell.execute_reply":"2025-11-12T17:37:29.847318Z"},"papermill":{"duration":0.103457,"end_time":"2025-11-05T22:31:20.397700","exception":false,"start_time":"2025-11-05T22:31:20.294243","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"4aff01a5","cell_type":"code","source":"y_true = test_actuals\ny_score = test_predicted_probabilities[:, 1]  # column 1 = cancer probabilities\n\nfpr, tpr, thresholds = roc_curve(y_true, y_score)\nroc_auc_score_value = roc_auc_score(y_true, y_score)\nprint(\"AUC (Test Set):\", round(roc_auc_score_value, 3))","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:29.848627Z","iopub.execute_input":"2025-11-12T17:37:29.848828Z","iopub.status.idle":"2025-11-12T17:37:29.868178Z","shell.execute_reply.started":"2025-11-12T17:37:29.848813Z","shell.execute_reply":"2025-11-12T17:37:29.867552Z"},"papermill":{"duration":0.110677,"end_time":"2025-11-05T22:31:20.601326","exception":false,"start_time":"2025-11-05T22:31:20.490649","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"ffb99bb0","cell_type":"code","source":"import matplotlib.pyplot as plt\n\nplt.figure(figsize=(10, 8))\nplt.plot(fpr, tpr, color='red', lw=3, linestyle='--',\n         label=f'Cancer vs No Cancer, AUC = {roc_auc_score_value:.3f}')\nplt.plot([0, 1], [0, 1], color='black', lw=2, linestyle='--')\n\nplt.xlabel('False Positive Rate', fontsize=18)\nplt.ylabel('True Positive Rate', fontsize=18)\nplt.title('ROC Curve - ResNet Finetuned', fontsize=22, fontweight='bold')\nplt.legend(loc='lower right', fontsize=14)\nplt.grid(True, linestyle='--', alpha=0.7)\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:29.868862Z","iopub.execute_input":"2025-11-12T17:37:29.869102Z","iopub.status.idle":"2025-11-12T17:37:30.051810Z","shell.execute_reply.started":"2025-11-12T17:37:29.869077Z","shell.execute_reply":"2025-11-12T17:37:30.051153Z"},"papermill":{"duration":0.291816,"end_time":"2025-11-05T22:31:20.984901","exception":false,"start_time":"2025-11-05T22:31:20.693085","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"50dcaff6","cell_type":"markdown","source":"---\n## Custom ResNet\n\nwith SGD optimizer and exponential decay for LR scheduling","metadata":{"papermill":{"duration":0.093241,"end_time":"2025-11-05T22:31:21.181796","exception":false,"start_time":"2025-11-05T22:31:21.088555","status":"completed"},"tags":[]}},{"id":"cf91e0dc","cell_type":"markdown","source":"<b> Since we are building our own model, we will need to compute mean and standard deviation of our dataset from train set and use that to normalize channels on all 3 sets. We cannot be using imagenet stats here.","metadata":{"papermill":{"duration":0.092883,"end_time":"2025-11-05T22:31:21.367687","exception":false,"start_time":"2025-11-05T22:31:21.274804","status":"completed"},"tags":[]}},{"id":"fd392e4a","cell_type":"code","source":"image_size = (96, 96)\nbatch_size = 256\nCHANNELS_IMG = 3","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:30.052545Z","iopub.execute_input":"2025-11-12T17:37:30.052755Z","iopub.status.idle":"2025-11-12T17:37:30.056396Z","shell.execute_reply.started":"2025-11-12T17:37:30.052740Z","shell.execute_reply":"2025-11-12T17:37:30.055746Z"},"papermill":{"duration":0.100199,"end_time":"2025-11-05T22:31:21.560418","exception":false,"start_time":"2025-11-05T22:31:21.460219","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b7f11135","cell_type":"code","source":"class RawDataset(Dataset):\n    def __init__(self, df, root_dir):\n        self.df = df.reset_index(drop=True)\n        self.root_dir = root_dir\n        self.transform = transforms.ToTensor()  # [0,1]\n\n    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        img_path = os.path.join(self.root_dir, f\"{self.df.iloc[idx]['id']}.tif\")\n        img = Image.open(img_path).convert('RGB')\n        return self.transform(img)\n\n# Compute stats\nraw_dataset = RawDataset(train_df, '/kaggle/input/histopathologic-cancer-detection/train')\nloader = DataLoader(raw_dataset, batch_size=64, shuffle=False, num_workers=4)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:30.057160Z","iopub.execute_input":"2025-11-12T17:37:30.057400Z","iopub.status.idle":"2025-11-12T17:37:30.074029Z","shell.execute_reply.started":"2025-11-12T17:37:30.057375Z","shell.execute_reply":"2025-11-12T17:37:30.073364Z"},"papermill":{"duration":0.109944,"end_time":"2025-11-05T22:31:21.764758","exception":false,"start_time":"2025-11-05T22:31:21.654814","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c6b9135d","cell_type":"code","source":"mean = 0.0\nstd = 0.0\nn_pixels = 0\n\nfor images in tqdm(loader, desc=\"Computing stats\"):\n    batch_size, c, h, w = images.shape\n    n_pixels += batch_size * h * w\n    mean += images.sum([0, 2, 3])\n    std += (images ** 2).sum([0, 2, 3])\n\nmean /= n_pixels\nstd = torch.sqrt(std / n_pixels - mean ** 2)\n\nprint(f\"Mean: {mean.tolist()}\")\nprint(f\"Std : {std.tolist()}\")","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:30.074664Z","iopub.execute_input":"2025-11-12T17:37:30.074885Z","iopub.status.idle":"2025-11-12T17:37:33.155641Z","shell.execute_reply.started":"2025-11-12T17:37:30.074863Z","shell.execute_reply":"2025-11-12T17:37:33.154693Z"},"papermill":{"duration":3.383218,"end_time":"2025-11-05T22:31:25.241558","exception":false,"start_time":"2025-11-05T22:31:21.858340","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"cc08c1f4","cell_type":"code","source":"mean = mean.tolist()\nstd = std.tolist()","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:33.156891Z","iopub.execute_input":"2025-11-12T17:37:33.157315Z","iopub.status.idle":"2025-11-12T17:37:33.161278Z","shell.execute_reply.started":"2025-11-12T17:37:33.157289Z","shell.execute_reply":"2025-11-12T17:37:33.160576Z"},"papermill":{"duration":0.103489,"end_time":"2025-11-05T22:31:25.444113","exception":false,"start_time":"2025-11-05T22:31:25.340624","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"83d2511f","cell_type":"code","source":"# we only apply augmentation to training data\ntrain_transform = transforms.Compose([\n    transforms.RandomHorizontalFlip(), # randomly flipping image horizontally\n    transforms.RandomVerticalFlip(), # randomly flipping image vertically\n    transforms.RandomRotation(degrees=90),  # radomly rotating image up to 90 degrees\n    transforms.ToTensor(),\n    transforms.Normalize(mean=mean, std=std) \n])\n\nval_transform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(mean=mean, std=std) \n])","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:33.162098Z","iopub.execute_input":"2025-11-12T17:37:33.162425Z","iopub.status.idle":"2025-11-12T17:37:33.173981Z","shell.execute_reply.started":"2025-11-12T17:37:33.162401Z","shell.execute_reply":"2025-11-12T17:37:33.173258Z"},"papermill":{"duration":0.101232,"end_time":"2025-11-05T22:31:25.638862","exception":false,"start_time":"2025-11-05T22:31:25.537630","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1978f737","cell_type":"code","source":"%%time\n\ntrain_dataset = CancerDataset(train_df, train_images_root_dir, transform=train_transform)\nval_dataset = CancerDataset(val_df, train_images_root_dir, transform=val_transform)\ntest_dataset = CancerDataset(test_df, train_images_root_dir, transform=val_transform)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:33.174706Z","iopub.execute_input":"2025-11-12T17:37:33.175145Z","iopub.status.idle":"2025-11-12T17:37:33.186211Z","shell.execute_reply.started":"2025-11-12T17:37:33.175118Z","shell.execute_reply":"2025-11-12T17:37:33.185597Z"},"papermill":{"duration":0.109859,"end_time":"2025-11-05T22:31:25.842100","exception":false,"start_time":"2025-11-05T22:31:25.732241","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"db8e2230","cell_type":"code","source":"# first image in train dataset\nimage, label = train_dataset[0]\nprint(\"Image shape:\", image.shape)\nprint(\"Label:\", label)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:33.186957Z","iopub.execute_input":"2025-11-12T17:37:33.187153Z","iopub.status.idle":"2025-11-12T17:37:33.201093Z","shell.execute_reply.started":"2025-11-12T17:37:33.187138Z","shell.execute_reply":"2025-11-12T17:37:33.200330Z"},"papermill":{"duration":0.106378,"end_time":"2025-11-05T22:31:26.042544","exception":false,"start_time":"2025-11-05T22:31:25.936166","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1f9e7e6d","cell_type":"code","source":"batch_size = 256  # we have 2x T4 gpu so this should be good; each t4 will get 128 batch size in parallel in each epoch\ntrain_dl = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4, pin_memory=True)\nval_dl = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4, pin_memory=True)\ntest_dl = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=4, pin_memory=True)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:33.201872Z","iopub.execute_input":"2025-11-12T17:37:33.202258Z","iopub.status.idle":"2025-11-12T17:37:33.209898Z","shell.execute_reply.started":"2025-11-12T17:37:33.202232Z","shell.execute_reply":"2025-11-12T17:37:33.209205Z"},"papermill":{"duration":0.103038,"end_time":"2025-11-05T22:31:26.240121","exception":false,"start_time":"2025-11-05T22:31:26.137083","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2fec605e","cell_type":"code","source":"train_dl = DeviceDataLoader(train_dl, device)\nval_dl = DeviceDataLoader(val_dl, device)\ntest_dl = DeviceDataLoader(test_dl, device)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:33.210762Z","iopub.execute_input":"2025-11-12T17:37:33.211031Z","iopub.status.idle":"2025-11-12T17:37:33.222404Z","shell.execute_reply.started":"2025-11-12T17:37:33.211011Z","shell.execute_reply":"2025-11-12T17:37:33.221786Z"},"papermill":{"duration":0.100796,"end_time":"2025-11-05T22:31:26.434977","exception":false,"start_time":"2025-11-05T22:31:26.334181","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"01349cc1","cell_type":"code","source":"class ResidualBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, stride=1):\n        super().__init__()\n        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1, bias=False)\n        self.bn1 = nn.BatchNorm2d(out_channels)\n        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1, bias=False)\n        self.bn2 = nn.BatchNorm2d(out_channels)\n\n        # if input and output shapes differ, we will adjust skip connection to make original input and output same to add them\n        self.skip = None\n        if in_channels != out_channels or stride != 1:\n            self.skip = nn.Sequential(nn.Conv2d(in_channels, out_channels, 1, stride, bias=False), nn.BatchNorm2d(out_channels))\n\n    def forward(self, x):\n        identity = x\n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        if self.skip: # if not the same shape, \n            identity = self.skip(identity)\n            \n        out += identity # adding original and new output \n        return F.relu(out)\n\nclass SimpleResNet(nn.Module):\n    def __init__(self, num_classes=2):\n        super().__init__()\n        self.layer1 = nn.Sequential(\n            nn.Conv2d(3, 32, 3, stride=1, padding=1, bias=False), \n            nn.BatchNorm2d(32),\n            nn.ReLU(inplace=True)\n        )\n\n        self.res1 = ResidualBlock(32, 64, stride=2) # first residual block\n        self.res2 = ResidualBlock(64, 128, stride=2) # second one\n        self.res3 = ResidualBlock(128, 256, stride=2) # third one\n\n        # average pooling followed by linear layer\n        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))\n        self.fc = nn.Linear(256, num_classes)\n\n    def forward(self, x):\n        x = self.layer1(x)\n        x = self.res1(x)\n        x = self.res2(x)\n        x = self.res3(x)\n        x = self.avgpool(x)\n        x = torch.flatten(x, 1)\n        x = self.fc(x)\n        return x","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:33.226746Z","iopub.execute_input":"2025-11-12T17:37:33.226997Z","iopub.status.idle":"2025-11-12T17:37:33.238916Z","shell.execute_reply.started":"2025-11-12T17:37:33.226982Z","shell.execute_reply":"2025-11-12T17:37:33.238162Z"},"papermill":{"duration":0.105528,"end_time":"2025-11-05T22:31:26.634029","exception":false,"start_time":"2025-11-05T22:31:26.528501","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5717ee6f","cell_type":"code","source":"model = SimpleResNet(num_classes=2)\nx = torch.randn(1, 3, 96, 96) # batch, channel, widdth, hight\nout = model(x)\nprint(out.shape)  # torch.Size([1, 2])","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:33.239583Z","iopub.execute_input":"2025-11-12T17:37:33.239769Z","iopub.status.idle":"2025-11-12T17:37:33.346047Z","shell.execute_reply.started":"2025-11-12T17:37:33.239754Z","shell.execute_reply":"2025-11-12T17:37:33.345286Z"},"papermill":{"duration":0.199707,"end_time":"2025-11-05T22:31:26.928487","exception":false,"start_time":"2025-11-05T22:31:26.728780","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c73ed4c7","cell_type":"markdown","source":"**Perfect. We have our custom ResNet model with 3 residual blocks. lets train it.**","metadata":{"papermill":{"duration":0.099775,"end_time":"2025-11-05T22:31:27.124893","exception":false,"start_time":"2025-11-05T22:31:27.025118","status":"completed"},"tags":[]}},{"id":"71999658","cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nif torch.cuda.device_count() > 1:\n    print(f\"Using {torch.cuda.device_count()} GPUs\")\n    model = nn.DataParallel(model)\nmodel = model.to(device)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:33.346923Z","iopub.execute_input":"2025-11-12T17:37:33.347244Z","iopub.status.idle":"2025-11-12T17:37:33.356020Z","shell.execute_reply.started":"2025-11-12T17:37:33.347215Z","shell.execute_reply":"2025-11-12T17:37:33.355357Z"},"papermill":{"duration":0.109661,"end_time":"2025-11-05T22:31:27.334079","exception":false,"start_time":"2025-11-05T22:31:27.224418","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"d43660a8","cell_type":"code","source":"criterion = nn.CrossEntropyLoss()\n\noptimizer = optim.SGD(\n    model.parameters(),\n    lr=0.01,          \n    momentum=0.9,\n    weight_decay=1e-4 # L2 regularization\n)\n# reduce LR by 5% every epoch\nscheduler = optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.95)\nbest_val_loss = float('inf')\nbest_model_wts = copy.deepcopy(model.state_dict())\nearly_stop_counter = 0\n\ntrain_losses = []\ntrain_accs = []\nval_losses = []\nval_accs = []\n\nfor epoch in range(num_epochs):\n    model.train()\n    train_loss = 0.0\n    train_correct = 0\n    train_total = 0\n    for inputs, labels in train_dl:\n        optimizer.zero_grad()\n        outputs = model(inputs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        train_loss += loss.item() * inputs.size(0)\n        _, predicted = torch.max(outputs.data, 1)\n        train_total += labels.size(0)\n        train_correct += (predicted == labels).sum().item()\n    \n    train_loss /= len(train_dataset)\n    train_acc = 100 * train_correct / train_total\n\n    train_losses.append(train_loss)\n    train_accs.append(train_acc)\n    \n    model.eval()\n    val_loss = 0.0\n    val_correct = 0\n    val_total = 0\n    with torch.no_grad():\n        for inputs, labels in val_dl:\n            outputs = model(inputs)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item() * inputs.size(0)\n            _, predicted = torch.max(outputs.data, 1)\n            val_total += labels.size(0)\n            val_correct += (predicted == labels).sum().item()\n    \n    val_loss /= len(val_dataset)\n    val_acc = 100 * val_correct / val_total\n\n    \n    val_losses.append(val_loss)\n    val_accs.append(val_acc)\n    \n    \n    print(f\"Epoch {epoch+1}/{num_epochs} - Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.2f}% - Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%\")\n    \n    scheduler.step()\n    \n    # early stopping check to prevent overfitting\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        best_model_wts = copy.deepcopy(model.state_dict())\n        early_stop_counter = 0\n    else:\n        early_stop_counter += 1\n        if early_stop_counter >= patience:\n            print(f\"Early stopping triggered after {epoch+1} epochs\")\n            break\n\n# will load best weights after training is done\nmodel.load_state_dict(best_model_wts)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:37:33.356661Z","iopub.execute_input":"2025-11-12T17:37:33.356870Z","iopub.status.idle":"2025-11-12T17:43:02.533867Z","shell.execute_reply.started":"2025-11-12T17:37:33.356830Z","shell.execute_reply":"2025-11-12T17:43:02.532692Z"},"papermill":{"duration":302.841891,"end_time":"2025-11-05T22:36:30.269800","exception":false,"start_time":"2025-11-05T22:31:27.427909","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"ebab466f","cell_type":"code","source":"model.eval();","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:02.534883Z","iopub.execute_input":"2025-11-12T17:43:02.535212Z","iopub.status.idle":"2025-11-12T17:43:02.540598Z","shell.execute_reply.started":"2025-11-12T17:43:02.535170Z","shell.execute_reply":"2025-11-12T17:43:02.539434Z"},"papermill":{"duration":0.101253,"end_time":"2025-11-05T22:36:30.468667","exception":false,"start_time":"2025-11-05T22:36:30.367414","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"ba5893fa","cell_type":"code","source":"training_stats = pd.DataFrame({\"Training_Accuracy\": train_accs,\n                               \"Training_Loss\": train_losses,\n                               \"Val_Loss\": val_losses,\n                               \"Val_Accuracy\": val_accs})","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:02.541487Z","iopub.execute_input":"2025-11-12T17:43:02.541746Z","iopub.status.idle":"2025-11-12T17:43:02.560735Z","shell.execute_reply.started":"2025-11-12T17:43:02.541723Z","shell.execute_reply":"2025-11-12T17:43:02.560103Z"},"papermill":{"duration":0.102501,"end_time":"2025-11-05T22:36:30.667587","exception":false,"start_time":"2025-11-05T22:36:30.565086","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"cc52241d","cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(20, 8))\n\nn_epochs = len(training_stats)\nax1 = plt.subplot(1,2, 1)\n\nplot_1 = ax1.plot(range(n_epochs), training_stats['Training_Loss'], color = 'blue', label = 'Train Loss',\\\n             marker = 's', linewidth=2.0, markersize = 10)\n\nplot_2 = ax1.plot(range(n_epochs), training_stats['Val_Loss'], color = 'blue', label = 'Val Loss',\\\n             marker = 'o', linewidth=2.0, markersize = 10)\n\nax1.tick_params(axis ='y', labelcolor = 'blue',labelsize=20, width=3) \nax1.tick_params(axis ='x', labelcolor = 'black',labelsize=20, width=3)\nax1.legend(fontsize = 30)\n# plt.xticks(range(0,n_epochs,1))\nax1.set_ylabel(\"Cross Entropy Loss\", fontsize = 30, labelpad = 10, color = 'blue')\n\n\nax1a = plt.subplot(1,2, 2)\nplot_11 = ax1a.plot(range(n_epochs), training_stats['Training_Accuracy'], color = 'red', label = 'Train Acc',\\\n             marker = 's', linewidth=2.0, markersize = 10)\n\nplot_22 = ax1a.plot(range(n_epochs), training_stats['Val_Accuracy'], color = 'red', label = 'Val Acc',\\\n             marker = 'o', linewidth=2.0, markersize = 10)\nax1a.legend(fontsize = 30)\n# plt.xticks(range(0,n_epochs,1), )\nax1a.tick_params(axis ='y', labelcolor = 'red',labelsize=20, width=3) \nax1a.tick_params(axis ='x', labelcolor = 'black',labelsize=20, width=3)\nax1a.set_ylabel(\"Accuracy\", fontsize = 30, labelpad = 10, color = 'red')\n\n\nax1.tick_params(which='both', width=2.5)\nax1.tick_params(which='major', length=15)\nax1.tick_params(which='minor', length=5)\nax1.tick_params(which = 'both', direction = 'in')\n\nax1a.tick_params(which='both', width=2.5)\nax1a.tick_params(which='major', length=15)\nax1a.tick_params(which='minor', length=5)\nax1a.tick_params(which = 'both', direction = 'in') \n\nax1a.spines['bottom'].set_color('black')\nax1a.spines['top'].set_color('black') \nax1a.spines['right'].set_color('black')\nax1a.spines['right'].set_linewidth(2)\nax1a.spines['top'].set_linewidth(2)\nax1a.spines['bottom'].set_linewidth(2)\nax1a.spines['left'].set_color('black')\nax1a.spines['left'].set_lw(2)\n\nax1.spines['bottom'].set_color('black')\nax1.spines['top'].set_color('black') \nax1.spines['right'].set_color('black')\nax1.spines['right'].set_linewidth(2)\nax1.spines['top'].set_linewidth(2)\nax1.spines['bottom'].set_linewidth(2)\nax1.spines['left'].set_color('black')\nax1.spines['left'].set_lw(2)\n\n\nax1.grid(True, which = 'major', alpha = 1, linestyle='--', linewidth = 1)\nax1a.grid(True, which = 'major', alpha = 1, linestyle='--', linewidth = 1)\n\n\nplt.subplots_adjust(wspace=0.25,hspace=0.)\nfig.text(0.5, 0.01, 'Epochs', ha='center', va='center', fontsize = 30)\n\nfig.text(0.5, 0.95, 'ResNet-Finetuned Performance on Training and Validation Datasets', ha='center', va='center', fontsize = 30)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:02.561441Z","iopub.execute_input":"2025-11-12T17:43:02.561717Z","iopub.status.idle":"2025-11-12T17:43:02.998035Z","shell.execute_reply.started":"2025-11-12T17:43:02.561692Z","shell.execute_reply":"2025-11-12T17:43:02.997268Z"},"papermill":{"duration":0.555551,"end_time":"2025-11-05T22:36:31.317969","exception":false,"start_time":"2025-11-05T22:36:30.762418","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8acd47af","cell_type":"code","source":"%%time\n\ntrain_actuals, train_predictions, train_predicted_probabilities = get_test_predictions_with_probs(model, train_dl)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:02.998967Z","iopub.execute_input":"2025-11-12T17:43:02.999255Z","iopub.status.idle":"2025-11-12T17:43:07.505454Z","shell.execute_reply.started":"2025-11-12T17:43:02.999230Z","shell.execute_reply":"2025-11-12T17:43:07.504677Z"},"papermill":{"duration":4.74135,"end_time":"2025-11-05T22:36:36.168358","exception":false,"start_time":"2025-11-05T22:36:31.427008","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1b92d482","cell_type":"code","source":"val_actuals, val_predictions, val_predicted_probabilities = get_test_predictions_with_probs(model, val_dl)\ntest_actuals, test_predictions, test_predicted_probabilities = get_test_predictions_with_probs(model, test_dl)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:07.506944Z","iopub.execute_input":"2025-11-12T17:43:07.507186Z","iopub.status.idle":"2025-11-12T17:43:10.631254Z","shell.execute_reply.started":"2025-11-12T17:43:07.507163Z","shell.execute_reply":"2025-11-12T17:43:10.630315Z"},"papermill":{"duration":3.384914,"end_time":"2025-11-05T22:36:39.654967","exception":false,"start_time":"2025-11-05T22:36:36.270053","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"d783f660","cell_type":"code","source":"print(\"Classification report for training set\")\nprint(classification_report(train_actuals, train_predictions))","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:10.632466Z","iopub.execute_input":"2025-11-12T17:43:10.632782Z","iopub.status.idle":"2025-11-12T17:43:10.649629Z","shell.execute_reply.started":"2025-11-12T17:43:10.632746Z","shell.execute_reply":"2025-11-12T17:43:10.648889Z"},"papermill":{"duration":0.119086,"end_time":"2025-11-05T22:36:39.878092","exception":false,"start_time":"2025-11-05T22:36:39.759006","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"cdd5b901","cell_type":"code","source":"print(\"Classification report for val set\")\nprint(classification_report(val_actuals, val_predictions))","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:10.650512Z","iopub.execute_input":"2025-11-12T17:43:10.650765Z","iopub.status.idle":"2025-11-12T17:43:10.663404Z","shell.execute_reply.started":"2025-11-12T17:43:10.650741Z","shell.execute_reply":"2025-11-12T17:43:10.662794Z"},"papermill":{"duration":0.11365,"end_time":"2025-11-05T22:36:40.094654","exception":false,"start_time":"2025-11-05T22:36:39.981004","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8b67e108","cell_type":"code","source":"print(\"Classification report for test set\")\nprint(classification_report(test_actuals, test_predictions))","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:10.663999Z","iopub.execute_input":"2025-11-12T17:43:10.664156Z","iopub.status.idle":"2025-11-12T17:43:10.674734Z","shell.execute_reply.started":"2025-11-12T17:43:10.664143Z","shell.execute_reply":"2025-11-12T17:43:10.674229Z"},"papermill":{"duration":0.116743,"end_time":"2025-11-05T22:36:40.313523","exception":false,"start_time":"2025-11-05T22:36:40.196780","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"d778a27e","cell_type":"code","source":"cf_matrix = confusion_matrix(test_actuals, test_predictions)\n\nclasses = list(index_to_labels_mapping.values())\ndataframe = pd.DataFrame(cf_matrix, index = classes, columns = classes)\n\nfig, axes = plt.subplots(1, 1, figsize=(8, 6))\n\nax1 = plt.subplot(1, 1, 1)\n\nsns.heatmap(dataframe, cmap=\"Blues\", annot = True, fmt=\"d\", cbar =False)\nfig.text(0.5, 0.00, 'Predicted', ha='center', va='center', fontsize = 20)\nfig.text(0.0, 0.5, 'Actual', ha='center', va='center', rotation='vertical', fontsize = 20)\nax1.text(0.5, 1.08, 'Custom-ResNet',\n    horizontalalignment='center',\n    fontsize=20,\n    transform = ax1.transAxes);","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:10.675533Z","iopub.execute_input":"2025-11-12T17:43:10.676187Z","iopub.status.idle":"2025-11-12T17:43:10.781488Z","shell.execute_reply.started":"2025-11-12T17:43:10.676160Z","shell.execute_reply":"2025-11-12T17:43:10.780755Z"},"papermill":{"duration":0.220624,"end_time":"2025-11-05T22:36:40.633746","exception":false,"start_time":"2025-11-05T22:36:40.413122","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"0b869887","cell_type":"code","source":"test_predicted_probabilities = np.array(test_predicted_probabilities)\ntest_actuals = np.array(test_actuals)","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:10.782365Z","iopub.execute_input":"2025-11-12T17:43:10.782957Z","iopub.status.idle":"2025-11-12T17:43:10.788351Z","shell.execute_reply.started":"2025-11-12T17:43:10.782939Z","shell.execute_reply":"2025-11-12T17:43:10.787767Z"},"papermill":{"duration":0.112383,"end_time":"2025-11-05T22:36:40.923781","exception":false,"start_time":"2025-11-05T22:36:40.811398","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b411c6de","cell_type":"code","source":"y_true = test_actuals\ny_score = test_predicted_probabilities[:, 1]  # column 1 = cancer probabilities\n\nfpr, tpr, thresholds = roc_curve(y_true, y_score)\nroc_auc_score_value = roc_auc_score(y_true, y_score)\nprint(\"AUC (Test Set):\", round(roc_auc_score_value, 3))","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:10.789127Z","iopub.execute_input":"2025-11-12T17:43:10.789351Z","iopub.status.idle":"2025-11-12T17:43:10.801207Z","shell.execute_reply.started":"2025-11-12T17:43:10.789328Z","shell.execute_reply":"2025-11-12T17:43:10.800479Z"},"papermill":{"duration":0.113347,"end_time":"2025-11-05T22:36:41.136331","exception":false,"start_time":"2025-11-05T22:36:41.022984","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1f0611de","cell_type":"code","source":"import matplotlib.pyplot as plt\n\nplt.figure(figsize=(10, 8))\nplt.plot(fpr, tpr, color='red', lw=3, linestyle='--',\n         label=f'Cancer vs No Cancer, AUC = {roc_auc_score_value:.3f}')\nplt.plot([0, 1], [0, 1], color='black', lw=2, linestyle='--')\n\nplt.xlabel('False Positive Rate', fontsize=18)\nplt.ylabel('True Positive Rate', fontsize=18)\nplt.title('ROC Curve - Custom ResNet', fontsize=22, fontweight='bold')\nplt.legend(loc='lower right', fontsize=14)\nplt.grid(True, linestyle='--', alpha=0.7)\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-11-12T17:43:10.802008Z","iopub.execute_input":"2025-11-12T17:43:10.802193Z","iopub.status.idle":"2025-11-12T17:43:10.982084Z","shell.execute_reply.started":"2025-11-12T17:43:10.802179Z","shell.execute_reply":"2025-11-12T17:43:10.981312Z"},"papermill":{"duration":0.333249,"end_time":"2025-11-05T22:36:41.573050","exception":false,"start_time":"2025-11-05T22:36:41.239801","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"edf8c26c-ebd7-46ff-8b29-872737524a5b","cell_type":"markdown","source":"---\n## Conclusion","metadata":{"papermill":{"duration":0.100945,"end_time":"2025-11-05T22:36:41.786109","exception":false,"start_time":"2025-11-05T22:36:41.685164","status":"completed"},"tags":[]}},{"id":"5e5553b1","cell_type":"markdown","source":"We train two models: \n\n    1. ResNet=50 finetuning with Adam Optimizer and Step Decay learning LR scheduling\n    2. Custom ResNet (with PyTorch) with SGD optimizer and Exponential Decay LR scheduling\n\nEach training was done with data augmentation enabled (random rotation and random horizontal/vertical flip).\n\n\nModel with imagenet weights that was finetuned is slightly more accurate than the ResNet trianed from scratch but it quickly starts overfitting and early stopping kicks in to end model training.","metadata":{"papermill":{"duration":0.100945,"end_time":"2025-11-05T22:36:41.786109","exception":false,"start_time":"2025-11-05T22:36:41.685164","status":"completed"},"tags":[]}},{"id":"e75e7f63-c50b-474f-bf32-cc2a29f9f479","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"6912b061-e64c-4262-a47c-0dd3268c6d12","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}