{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"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":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import 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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:07.302261Z","iopub.execute_input":"2025-11-05T22:13:07.302517Z","iopub.status.idle":"2025-11-05T22:13:07.320542Z","shell.execute_reply.started":"2025-11-05T22:13:07.302492Z","shell.execute_reply":"2025-11-05T22:13:07.319995Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from 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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:07.321240Z","iopub.execute_input":"2025-11-05T22:13:07.321523Z","iopub.status.idle":"2025-11-05T22:13:07.338030Z","shell.execute_reply.started":"2025-11-05T22:13:07.321496Z","shell.execute_reply":"2025-11-05T22:13:07.337476Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"index_to_labels_mapping = {\n    0: \"No Cancer\",\n    1: \"Cancer\"\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:07.340916Z","iopub.execute_input":"2025-11-05T22:13:07.341179Z","iopub.status.idle":"2025-11-05T22:13:07.363187Z","shell.execute_reply.started":"2025-11-05T22:13:07.341153Z","shell.execute_reply":"2025-11-05T22:13:07.362578Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_images_root_dir = '/kaggle/input/histopathologic-cancer-detection/train'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:07.363813Z","iopub.execute_input":"2025-11-05T22:13:07.364063Z","iopub.status.idle":"2025-11-05T22:13:07.378970Z","shell.execute_reply.started":"2025-11-05T22:13:07.364046Z","shell.execute_reply":"2025-11-05T22:13:07.378368Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = pd.read_csv('/kaggle/input/histopathologic-cancer-detection/train_labels.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:07.380693Z","iopub.execute_input":"2025-11-05T22:13:07.380905Z","iopub.status.idle":"2025-11-05T22:13:07.615703Z","shell.execute_reply.started":"2025-11-05T22:13:07.380890Z","shell.execute_reply":"2025-11-05T22:13:07.614859Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:07.616509Z","iopub.execute_input":"2025-11-05T22:13:07.616706Z","iopub.status.idle":"2025-11-05T22:13:07.628484Z","shell.execute_reply.started":"2025-11-05T22:13:07.616690Z","shell.execute_reply":"2025-11-05T22:13:07.627839Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:07.629415Z","iopub.execute_input":"2025-11-05T22:13:07.629759Z","iopub.status.idle":"2025-11-05T22:13:07.676388Z","shell.execute_reply.started":"2025-11-05T22:13:07.629729Z","shell.execute_reply":"2025-11-05T22:13:07.675617Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Perfect. Our images are RGB**","metadata":{}},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:14.131684Z","iopub.execute_input":"2025-11-05T22:13:14.132403Z","iopub.status.idle":"2025-11-05T22:13:14.160531Z","shell.execute_reply.started":"2025-11-05T22:13:14.132378Z","shell.execute_reply":"2025-11-05T22:13:14.159660Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"filenames = [f\"{id}.tif\" for id in df_sampled['id']]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:15.354809Z","iopub.execute_input":"2025-11-05T22:13:15.355590Z","iopub.status.idle":"2025-11-05T22:13:15.363737Z","shell.execute_reply.started":"2025-11-05T22:13:15.355562Z","shell.execute_reply":"2025-11-05T22:13:15.363000Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"len(filenames), filenames[0], filenames[-1]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:16.029699Z","iopub.execute_input":"2025-11-05T22:13:16.030049Z","iopub.status.idle":"2025-11-05T22:13:16.035113Z","shell.execute_reply.started":"2025-11-05T22:13:16.030006Z","shell.execute_reply":"2025-11-05T22:13:16.034493Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:47.502783Z","iopub.execute_input":"2025-11-05T22:13:47.503492Z","iopub.status.idle":"2025-11-05T22:13:47.516607Z","shell.execute_reply.started":"2025-11-05T22:13:47.503463Z","shell.execute_reply":"2025-11-05T22:13:47.515992Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df.reset_index(drop=True)\nval_df.reset_index(drop=True)\ntest_df.reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:47.779551Z","iopub.execute_input":"2025-11-05T22:13:47.780136Z","iopub.status.idle":"2025-11-05T22:13:47.789835Z","shell.execute_reply.started":"2025-11-05T22:13:47.780107Z","shell.execute_reply":"2025-11-05T22:13:47.789246Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Train: {len(train_df)}, Val: {len(val_df)}, Test: {len(test_df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:48.021918Z","iopub.execute_input":"2025-11-05T22:13:48.022257Z","iopub.status.idle":"2025-11-05T22:13:48.026117Z","shell.execute_reply.started":"2025-11-05T22:13:48.022222Z","shell.execute_reply":"2025-11-05T22:13:48.025283Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:53.180972Z","iopub.execute_input":"2025-11-05T22:13:53.181722Z","iopub.status.idle":"2025-11-05T22:13:53.186912Z","shell.execute_reply.started":"2025-11-05T22:13:53.181697Z","shell.execute_reply":"2025-11-05T22:13:53.186092Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_a_random_image_from_trainDf(0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:53.319388Z","iopub.execute_input":"2025-11-05T22:13:53.319584Z","iopub.status.idle":"2025-11-05T22:13:53.411074Z","shell.execute_reply.started":"2025-11-05T22:13:53.319570Z","shell.execute_reply":"2025-11-05T22:13:53.410317Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_a_random_image_from_trainDf(1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:54.990523Z","iopub.execute_input":"2025-11-05T22:13:54.991006Z","iopub.status.idle":"2025-11-05T22:13:55.106772Z","shell.execute_reply.started":"2025-11-05T22:13:54.990970Z","shell.execute_reply":"2025-11-05T22:13:55.105987Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:55.235605Z","iopub.execute_input":"2025-11-05T22:13:55.235910Z","iopub.status.idle":"2025-11-05T22:13:55.241690Z","shell.execute_reply.started":"2025-11-05T22:13:55.235887Z","shell.execute_reply":"2025-11-05T22:13:55.241064Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_batch_of_images(label=0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:55.437777Z","iopub.execute_input":"2025-11-05T22:13:55.438405Z","iopub.status.idle":"2025-11-05T22:13:56.634022Z","shell.execute_reply.started":"2025-11-05T22:13:55.438385Z","shell.execute_reply":"2025-11-05T22:13:56.632832Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_batch_of_images(label=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:13:56.636926Z","iopub.execute_input":"2025-11-05T22:13:56.637225Z","iopub.status.idle":"2025-11-05T22:13:57.967452Z","shell.execute_reply.started":"2025-11-05T22:13:56.637204Z","shell.execute_reply":"2025-11-05T22:13:57.966686Z"}},"outputs":[],"execution_count":null},{"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":{}},{"cell_type":"markdown","source":"# Finetuining ResNet-50 \n\nwith Adam optimizer and Step Decay LR scheduling","metadata":{}},{"cell_type":"code","source":"class CancerDataset(Dataset):\n    def __init__(self, df, root_dir, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.root_dir = root_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_name = os.path.join(self.root_dir, f\"{self.df.iloc[idx]['id']}.tif\")\n        image = Image.open(img_name)\n        label = self.df.iloc[idx]['label']\n        \n        if self.transform:\n            image = self.transform(image)\n        \n        return image, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:02.314212Z","iopub.execute_input":"2025-11-05T22:14:02.314709Z","iopub.status.idle":"2025-11-05T22:14:02.319952Z","shell.execute_reply.started":"2025-11-05T22:14:02.314682Z","shell.execute_reply":"2025-11-05T22:14:02.319261Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:05.337540Z","iopub.execute_input":"2025-11-05T22:14:05.337821Z","iopub.status.idle":"2025-11-05T22:14:05.343178Z","shell.execute_reply.started":"2025-11-05T22:14:05.337800Z","shell.execute_reply":"2025-11-05T22:14:05.342351Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:09.471055Z","iopub.execute_input":"2025-11-05T22:14:09.471806Z","iopub.status.idle":"2025-11-05T22:14:09.477420Z","shell.execute_reply.started":"2025-11-05T22:14:09.471780Z","shell.execute_reply":"2025-11-05T22:14:09.476693Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# first image in train dataset\nimage, label = train_dataset[0]\nprint(\"Image shape:\", image.shape)\nprint(\"Label:\", label)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:09.703104Z","iopub.execute_input":"2025-11-05T22:14:09.703353Z","iopub.status.idle":"2025-11-05T22:14:09.725701Z","shell.execute_reply.started":"2025-11-05T22:14:09.703336Z","shell.execute_reply":"2025-11-05T22:14:09.725145Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:09.954932Z","iopub.execute_input":"2025-11-05T22:14:09.955205Z","iopub.status.idle":"2025-11-05T22:14:09.959733Z","shell.execute_reply.started":"2025-11-05T22:14:09.955189Z","shell.execute_reply":"2025-11-05T22:14:09.959017Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n**At 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.**\n","metadata":{}},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:10.419428Z","iopub.execute_input":"2025-11-05T22:14:10.419640Z","iopub.status.idle":"2025-11-05T22:14:10.425186Z","shell.execute_reply.started":"2025-11-05T22:14:10.419624Z","shell.execute_reply":"2025-11-05T22:14:10.424428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = get_default_device()\ndevice","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:11.967441Z","iopub.execute_input":"2025-11-05T22:14:11.967725Z","iopub.status.idle":"2025-11-05T22:14:12.028560Z","shell.execute_reply.started":"2025-11-05T22:14:11.967704Z","shell.execute_reply":"2025-11-05T22:14:12.027921Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dl = DeviceDataLoader(train_dl, device)\nval_dl = DeviceDataLoader(val_dl, device)\ntest_dl = DeviceDataLoader(test_dl, device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:13.347497Z","iopub.execute_input":"2025-11-05T22:14:13.348109Z","iopub.status.idle":"2025-11-05T22:14:13.351611Z","shell.execute_reply.started":"2025-11-05T22:14:13.348082Z","shell.execute_reply":"2025-11-05T22:14:13.350996Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# pretrained imagenet ResNet50\nmodel = models.resnet50(pretrained=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:13.537341Z","iopub.execute_input":"2025-11-05T22:14:13.537549Z","iopub.status.idle":"2025-11-05T22:14:14.031882Z","shell.execute_reply.started":"2025-11-05T22:14:13.537533Z","shell.execute_reply":"2025-11-05T22:14:14.030929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:14.033328Z","iopub.execute_input":"2025-11-05T22:14:14.033597Z","iopub.status.idle":"2025-11-05T22:14:14.039819Z","shell.execute_reply.started":"2025-11-05T22:14:14.033577Z","shell.execute_reply":"2025-11-05T22:14:14.039094Z"}},"outputs":[],"execution_count":null},{"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":{}},{"cell_type":"code","source":"model.fc ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:16.502905Z","iopub.execute_input":"2025-11-05T22:14:16.503247Z","iopub.status.idle":"2025-11-05T22:14:16.508599Z","shell.execute_reply.started":"2025-11-05T22:14:16.503221Z","shell.execute_reply":"2025-11-05T22:14:16.507974Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:16.714069Z","iopub.execute_input":"2025-11-05T22:14:16.714799Z","iopub.status.idle":"2025-11-05T22:14:16.718876Z","shell.execute_reply.started":"2025-11-05T22:14:16.714774Z","shell.execute_reply":"2025-11-05T22:14:16.718069Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:16.873127Z","iopub.execute_input":"2025-11-05T22:14:16.873333Z","iopub.status.idle":"2025-11-05T22:14:16.879925Z","shell.execute_reply.started":"2025-11-05T22:14:16.873318Z","shell.execute_reply":"2025-11-05T22:14:16.879146Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Perfect. Our model is ready.**","metadata":{}},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:17.194867Z","iopub.execute_input":"2025-11-05T22:14:17.195625Z","iopub.status.idle":"2025-11-05T22:14:17.378701Z","shell.execute_reply.started":"2025-11-05T22:14:17.195600Z","shell.execute_reply":"2025-11-05T22:14:17.378061Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We have 2 gpus. so, will train in a cluster with 2x T4 GPUs for faster training.","metadata":{}},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:43.164605Z","iopub.execute_input":"2025-11-05T22:14:43.164924Z","iopub.status.idle":"2025-11-05T22:14:43.168883Z","shell.execute_reply.started":"2025-11-05T22:14:43.164902Z","shell.execute_reply":"2025-11-05T22:14:43.168322Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:14:45.879099Z","iopub.execute_input":"2025-11-05T22:14:45.879404Z","iopub.status.idle":"2025-11-05T22:17:53.218082Z","shell.execute_reply.started":"2025-11-05T22:14:45.879383Z","shell.execute_reply":"2025-11-05T22:17:53.217008Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval();","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:17:53.219633Z","iopub.execute_input":"2025-11-05T22:17:53.219904Z","iopub.status.idle":"2025-11-05T22:17:53.224385Z","shell.execute_reply.started":"2025-11-05T22:17:53.219884Z","shell.execute_reply":"2025-11-05T22:17:53.223626Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:17:53.225077Z","iopub.execute_input":"2025-11-05T22:17:53.225263Z","iopub.status.idle":"2025-11-05T22:17:53.242453Z","shell.execute_reply.started":"2025-11-05T22:17:53.225249Z","shell.execute_reply":"2025-11-05T22:17:53.241685Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"training_stats","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:17:53.244061Z","iopub.execute_input":"2025-11-05T22:17:53.244247Z","iopub.status.idle":"2025-11-05T22:17:53.265980Z","shell.execute_reply.started":"2025-11-05T22:17:53.244234Z","shell.execute_reply":"2025-11-05T22:17:53.265384Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:17:53.266531Z","iopub.execute_input":"2025-11-05T22:17:53.266715Z","iopub.status.idle":"2025-11-05T22:17:53.823720Z","shell.execute_reply.started":"2025-11-05T22:17:53.266701Z","shell.execute_reply":"2025-11-05T22:17:53.822992Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:17:53.824541Z","iopub.execute_input":"2025-11-05T22:17:53.824783Z","iopub.status.idle":"2025-11-05T22:17:53.830735Z","shell.execute_reply.started":"2025-11-05T22:17:53.824764Z","shell.execute_reply":"2025-11-05T22:17:53.830002Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%time\n\ntrain_actuals, train_predictions, train_predicted_probabilities = get_test_predictions_with_probs(model, train_dl)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:17:53.831471Z","iopub.execute_input":"2025-11-05T22:17:53.831721Z","iopub.status.idle":"2025-11-05T22:17:59.579446Z","shell.execute_reply.started":"2025-11-05T22:17:53.831699Z","shell.execute_reply":"2025-11-05T22:17:59.578494Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:17:59.580530Z","iopub.execute_input":"2025-11-05T22:17:59.580790Z","iopub.status.idle":"2025-11-05T22:18:07.511235Z","shell.execute_reply.started":"2025-11-05T22:17:59.580768Z","shell.execute_reply":"2025-11-05T22:18:07.510409Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:07.512451Z","iopub.execute_input":"2025-11-05T22:18:07.512788Z","iopub.status.idle":"2025-11-05T22:18:07.517280Z","shell.execute_reply.started":"2025-11-05T22:18:07.512753Z","shell.execute_reply":"2025-11-05T22:18:07.516587Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"The testing accuracy is: {}\".format(accuracy_score(train_actuals, train_predictions)*100))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:07.520264Z","iopub.execute_input":"2025-11-05T22:18:07.520702Z","iopub.status.idle":"2025-11-05T22:18:07.540779Z","shell.execute_reply.started":"2025-11-05T22:18:07.520683Z","shell.execute_reply":"2025-11-05T22:18:07.539968Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Classification report for training set\")\nprint(classification_report(train_actuals, train_predictions))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:07.541624Z","iopub.execute_input":"2025-11-05T22:18:07.541842Z","iopub.status.idle":"2025-11-05T22:18:07.567443Z","shell.execute_reply.started":"2025-11-05T22:18:07.541826Z","shell.execute_reply":"2025-11-05T22:18:07.566574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Classification report for val set\")\nprint(classification_report(val_actuals, val_predictions))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:07.568334Z","iopub.execute_input":"2025-11-05T22:18:07.568604Z","iopub.status.idle":"2025-11-05T22:18:07.587680Z","shell.execute_reply.started":"2025-11-05T22:18:07.568574Z","shell.execute_reply":"2025-11-05T22:18:07.586986Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Classification report for test set\")\nprint(classification_report(test_actuals, test_predictions))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:07.588446Z","iopub.execute_input":"2025-11-05T22:18:07.588702Z","iopub.status.idle":"2025-11-05T22:18:07.601299Z","shell.execute_reply.started":"2025-11-05T22:18:07.588674Z","shell.execute_reply":"2025-11-05T22:18:07.600564Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:07.601974Z","iopub.execute_input":"2025-11-05T22:18:07.602304Z","iopub.status.idle":"2025-11-05T22:18:07.621221Z","shell.execute_reply.started":"2025-11-05T22:18:07.602284Z","shell.execute_reply":"2025-11-05T22:18:07.620438Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:07.621886Z","iopub.execute_input":"2025-11-05T22:18:07.622253Z","iopub.status.idle":"2025-11-05T22:18:07.752408Z","shell.execute_reply.started":"2025-11-05T22:18:07.622210Z","shell.execute_reply":"2025-11-05T22:18:07.751679Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\ntest_predicted_probabilities = np.array(test_predicted_probabilities)\ntest_actuals = np.array(test_actuals)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:07.753124Z","iopub.execute_input":"2025-11-05T22:18:07.753379Z","iopub.status.idle":"2025-11-05T22:18:07.758910Z","shell.execute_reply.started":"2025-11-05T22:18:07.753356Z","shell.execute_reply":"2025-11-05T22:18:07.758241Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:07.759666Z","iopub.execute_input":"2025-11-05T22:18:07.759936Z","iopub.status.idle":"2025-11-05T22:18:07.779850Z","shell.execute_reply.started":"2025-11-05T22:18:07.759915Z","shell.execute_reply":"2025-11-05T22:18:07.779124Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:07.780659Z","iopub.execute_input":"2025-11-05T22:18:07.780868Z","iopub.status.idle":"2025-11-05T22:18:08.007654Z","shell.execute_reply.started":"2025-11-05T22:18:07.780845Z","shell.execute_reply":"2025-11-05T22:18:08.006766Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Custom ResNet\n\nwith SGD optimizer and exponential decay for LR scheduling","metadata":{}},{"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":{}},{"cell_type":"code","source":"image_size = (96, 96)\nbatch_size = 256\nCHANNELS_IMG = 3","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:08.008616Z","iopub.execute_input":"2025-11-05T22:18:08.008932Z","iopub.status.idle":"2025-11-05T22:18:08.012931Z","shell.execute_reply.started":"2025-11-05T22:18:08.008906Z","shell.execute_reply":"2025-11-05T22:18:08.012214Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:08.013675Z","iopub.execute_input":"2025-11-05T22:18:08.013888Z","iopub.status.idle":"2025-11-05T22:18:08.038377Z","shell.execute_reply.started":"2025-11-05T22:18:08.013872Z","shell.execute_reply":"2025-11-05T22:18:08.037731Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:08.039393Z","iopub.execute_input":"2025-11-05T22:18:08.039615Z","iopub.status.idle":"2025-11-05T22:18:12.187061Z","shell.execute_reply.started":"2025-11-05T22:18:08.039599Z","shell.execute_reply":"2025-11-05T22:18:12.186224Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mean = mean.tolist()\nstd = std.tolist()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:12.188344Z","iopub.execute_input":"2025-11-05T22:18:12.188679Z","iopub.status.idle":"2025-11-05T22:18:12.193216Z","shell.execute_reply.started":"2025-11-05T22:18:12.188652Z","shell.execute_reply":"2025-11-05T22:18:12.192291Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:12.194203Z","iopub.execute_input":"2025-11-05T22:18:12.194712Z","iopub.status.idle":"2025-11-05T22:18:12.214612Z","shell.execute_reply.started":"2025-11-05T22:18:12.194685Z","shell.execute_reply":"2025-11-05T22:18:12.213991Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:12.215525Z","iopub.execute_input":"2025-11-05T22:18:12.216232Z","iopub.status.idle":"2025-11-05T22:18:12.243567Z","shell.execute_reply.started":"2025-11-05T22:18:12.216213Z","shell.execute_reply":"2025-11-05T22:18:12.242882Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# first image in train dataset\nimage, label = train_dataset[0]\nprint(\"Image shape:\", image.shape)\nprint(\"Label:\", label)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:12.244356Z","iopub.execute_input":"2025-11-05T22:18:12.244617Z","iopub.status.idle":"2025-11-05T22:18:12.263891Z","shell.execute_reply.started":"2025-11-05T22:18:12.244601Z","shell.execute_reply":"2025-11-05T22:18:12.263243Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:12.264689Z","iopub.execute_input":"2025-11-05T22:18:12.264967Z","iopub.status.idle":"2025-11-05T22:18:12.273991Z","shell.execute_reply.started":"2025-11-05T22:18:12.264925Z","shell.execute_reply":"2025-11-05T22:18:12.273431Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dl = DeviceDataLoader(train_dl, device)\nval_dl = DeviceDataLoader(val_dl, device)\ntest_dl = DeviceDataLoader(test_dl, device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:12.274800Z","iopub.execute_input":"2025-11-05T22:18:12.275229Z","iopub.status.idle":"2025-11-05T22:18:12.288488Z","shell.execute_reply.started":"2025-11-05T22:18:12.275212Z","shell.execute_reply":"2025-11-05T22:18:12.287899Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:12.292359Z","iopub.execute_input":"2025-11-05T22:18:12.292639Z","iopub.status.idle":"2025-11-05T22:18:12.304615Z","shell.execute_reply.started":"2025-11-05T22:18:12.292617Z","shell.execute_reply":"2025-11-05T22:18:12.304080Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:12.305403Z","iopub.execute_input":"2025-11-05T22:18:12.305633Z","iopub.status.idle":"2025-11-05T22:18:12.366345Z","shell.execute_reply.started":"2025-11-05T22:18:12.305608Z","shell.execute_reply":"2025-11-05T22:18:12.365597Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Perfect. We have our custom ResNet model with 3 residual blocks. lets train it.**","metadata":{}},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:12.367106Z","iopub.execute_input":"2025-11-05T22:18:12.367387Z","iopub.status.idle":"2025-11-05T22:18:12.376589Z","shell.execute_reply.started":"2025-11-05T22:18:12.367364Z","shell.execute_reply":"2025-11-05T22:18:12.376002Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:18:12.377287Z","iopub.execute_input":"2025-11-05T22:18:12.377544Z","iopub.status.idle":"2025-11-05T22:24:31.602301Z","shell.execute_reply.started":"2025-11-05T22:18:12.377524Z","shell.execute_reply":"2025-11-05T22:24:31.601359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval();","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:24:31.603562Z","iopub.execute_input":"2025-11-05T22:24:31.603921Z","iopub.status.idle":"2025-11-05T22:24:31.608243Z","shell.execute_reply.started":"2025-11-05T22:24:31.603888Z","shell.execute_reply":"2025-11-05T22:24:31.607638Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:24:31.608903Z","iopub.execute_input":"2025-11-05T22:24:31.609178Z","iopub.status.idle":"2025-11-05T22:24:31.627823Z","shell.execute_reply.started":"2025-11-05T22:24:31.609156Z","shell.execute_reply":"2025-11-05T22:24:31.627204Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:25:23.966136Z","iopub.execute_input":"2025-11-05T22:25:23.966422Z","iopub.status.idle":"2025-11-05T22:25:24.436649Z","shell.execute_reply.started":"2025-11-05T22:25:23.966398Z","shell.execute_reply":"2025-11-05T22:25:24.435877Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%time\n\ntrain_actuals, train_predictions, train_predicted_probabilities = get_test_predictions_with_probs(model, train_dl)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:25:27.781081Z","iopub.execute_input":"2025-11-05T22:25:27.781341Z","iopub.status.idle":"2025-11-05T22:25:32.944589Z","shell.execute_reply.started":"2025-11-05T22:25:27.781324Z","shell.execute_reply":"2025-11-05T22:25:32.943741Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:25:32.946292Z","iopub.execute_input":"2025-11-05T22:25:32.946576Z","iopub.status.idle":"2025-11-05T22:25:36.487530Z","shell.execute_reply.started":"2025-11-05T22:25:32.946556Z","shell.execute_reply":"2025-11-05T22:25:36.486594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Classification report for training set\")\nprint(classification_report(train_actuals, train_predictions))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:24:42.017109Z","iopub.execute_input":"2025-11-05T22:24:42.017457Z","iopub.status.idle":"2025-11-05T22:24:42.032987Z","shell.execute_reply.started":"2025-11-05T22:24:42.017420Z","shell.execute_reply":"2025-11-05T22:24:42.032201Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Classification report for val set\")\nprint(classification_report(val_actuals, val_predictions))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:24:42.033662Z","iopub.execute_input":"2025-11-05T22:24:42.033967Z","iopub.status.idle":"2025-11-05T22:24:42.048815Z","shell.execute_reply.started":"2025-11-05T22:24:42.033916Z","shell.execute_reply":"2025-11-05T22:24:42.048157Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Classification report for test set\")\nprint(classification_report(test_actuals, test_predictions))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:24:42.049547Z","iopub.execute_input":"2025-11-05T22:24:42.049798Z","iopub.status.idle":"2025-11-05T22:24:42.065286Z","shell.execute_reply.started":"2025-11-05T22:24:42.049776Z","shell.execute_reply":"2025-11-05T22:24:42.064520Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:24:42.066057Z","iopub.execute_input":"2025-11-05T22:24:42.066351Z","iopub.status.idle":"2025-11-05T22:24:42.183129Z","shell.execute_reply.started":"2025-11-05T22:24:42.066329Z","shell.execute_reply":"2025-11-05T22:24:42.182378Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_predicted_probabilities = np.array(test_predicted_probabilities)\ntest_actuals = np.array(test_actuals)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:24:42.183875Z","iopub.execute_input":"2025-11-05T22:24:42.184147Z","iopub.status.idle":"2025-11-05T22:24:42.189392Z","shell.execute_reply.started":"2025-11-05T22:24:42.184126Z","shell.execute_reply":"2025-11-05T22:24:42.188684Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:24:42.190104Z","iopub.execute_input":"2025-11-05T22:24:42.190814Z","iopub.status.idle":"2025-11-05T22:24:42.205733Z","shell.execute_reply.started":"2025-11-05T22:24:42.190785Z","shell.execute_reply":"2025-11-05T22:24:42.205184Z"}},"outputs":[],"execution_count":null},{"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":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-05T22:24:42.206374Z","iopub.execute_input":"2025-11-05T22:24:42.206579Z","iopub.status.idle":"2025-11-05T22:24:42.404012Z","shell.execute_reply.started":"2025-11-05T22:24:42.206564Z","shell.execute_reply":"2025-11-05T22:24:42.403297Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Conclusion\n\nWe 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":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}