{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":45867,"databundleVersionId":6924515,"sourceType":"competition"},{"sourceId":251265663,"sourceType":"kernelVersion"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Install Dependencies","metadata":{}},{"cell_type":"code","source":"# Installs the C library without showing output\n!apt-get update &> /dev/null && apt-get install -y libvips &> /dev/null\n# Installs the Python wrapper without showing output\n!pip install -q --no-cache-dir pyvips &> /dev/null","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:04:11.878522Z","iopub.execute_input":"2025-07-19T17:04:11.878749Z","iopub.status.idle":"2025-07-19T17:05:11.004842Z","shell.execute_reply.started":"2025-07-19T17:04:11.878732Z","shell.execute_reply":"2025-07-19T17:05:11.00408Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## VGG19_1STL","metadata":{}},{"cell_type":"code","source":"from torch import nn\nimport torchvision\nimport torch\n\n\nclass VGG19_1STL(nn.Module):\n    def __init__(self, num_classes=5, pretrained=True):\n        super(VGG19_1STL, self).__init__()\n        self.vgg19_1stl = torchvision.models.vgg19_bn(pretrained=pretrained)\n        self.vgg19_1stl.classifier[6] = nn.Linear(4096, num_classes)\n\n    def forward(self, x):\n        logits = self.vgg19_1stl(x)\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:11.006567Z","iopub.execute_input":"2025-07-19T17:05:11.006817Z","iopub.status.idle":"2025-07-19T17:05:22.457551Z","shell.execute_reply.started":"2025-07-19T17:05:11.006782Z","shell.execute_reply":"2025-07-19T17:05:22.456847Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Fast Patch Selection Algorithm","metadata":{}},{"cell_type":"code","source":"import cv2\nimport numpy as np\n# from openslide import OpenSlide\nimport openslide\nfrom pathlib import Path\nimport glob\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nfrom multiprocessing import Pool\nfrom tqdm import tqdm\nimport os\nimport pandas as pd\nimport random\nfrom sklearn.neighbors import KernelDensity\nfrom PIL import Image\n\ndef get_sampled_points_density_proportional_KDE(points, desired_sample_size):\n    num_points = len(points)\n    if num_points <= desired_sample_size:\n        return points\n\n    points_arr = np.array(points)\n    \n    # Fit KDE model to the points\n    kde = KernelDensity(bandwidth=0.1)  # You can adjust the bandwidth\n    kde.fit(points_arr)\n\n    # Generate samples from the KDE\n    samples = kde.sample(desired_sample_size)\n    final_sample = samples.tolist()\n\n    return final_sample\n\n\ndef RGB2HSD(X):\n    eps = np.finfo(float).eps\n    X[np.where(X==0.0)] = eps\n    \n    OD = -np.log(X / 1.0)\n    D  = np.mean(OD,3)\n    D[np.where(D==0.0)] = eps\n    \n    cx = OD[:,:,:,0] / (D) - 1.0\n    cy = (OD[:,:,:,1]-OD[:,:,:,2]) / (np.sqrt(3.0)*D)\n    \n    D = np.expand_dims(D,3)\n    cx = np.expand_dims(cx,3)\n    cy = np.expand_dims(cy,3)\n            \n    X_HSD = np.concatenate((D,cx,cy),3)\n    return X_HSD\n\n\ndef clean_thumbnail(thumbnail):\n    thumbnail_arr = np.asarray(thumbnail)\n    \n    wthumbnail = np.zeros_like(thumbnail_arr)\n    wthumbnail[:, :, :] = thumbnail_arr[:, :, :]\n\n    thumbnail_std = np.std(wthumbnail, axis=2)\n    wthumbnail[thumbnail_std<5] = (np.ones((1,3), dtype=\"uint8\")*255)\n    thumbnail_HSD = RGB2HSD( np.array([wthumbnail.astype('float32')/255.]) )[0]\n    kernel = np.ones((30,30),np.float32)/900\n    thumbnail_HSD_mean = cv2.filter2D(thumbnail_HSD[:,:,2],-1,kernel)\n    wthumbnail[thumbnail_HSD_mean<0.05] = (np.ones((1,3),dtype=\"uint8\")*255)\n    return wthumbnail\n\n                \ndef is_far_enough(new_point, existing_points, min_distance):\n    for point in existing_points:\n        if np.sqrt((new_point[0] - point[0])**2 + (new_point[1] - point[1])**2) < min_distance:\n            return False\n    return True\n\n\ndef get_patch_locations(tissue_mask, cthumbnail,  mask_hratio, mask_wratio, tissue_threshold, stride):\n    contours, mm = cv2.findContours(tissue_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    image_with_contours = cthumbnail.copy()\n    cv2.drawContours(image_with_contours, contours, -1, (0, 255, 0), 2)  # Draw contours on the image\n    \n    image_with_rectangles = cthumbnail.copy()\n    \n    # Calculate the step size for the grid based on the stride\n    step_w = int(mask_wratio * stride)\n    step_h = int(mask_hratio * stride)\n    \n    patch_locations = []\n    for contour in contours:\n        x, y, w, h = cv2.boundingRect(contour)\n        # plot the rectangles on the image_with_rectangles\n        cv2.rectangle(image_with_rectangles, (x, y), (x + w, y + h), (0, 255, 0), 2)\n        \n        if w >= mask_wratio and h >= mask_hratio:\n            for i in range(x, x + w - mask_wratio, step_w):\n                for j in range(y, y + h - mask_hratio, step_h):\n                    tissue_patch = tissue_mask[j:j + mask_hratio, i:i + mask_wratio]\n                    # if np.sum(tissue_patch) / (mask_hratio ** 2) > tissue_threshold:\n                    tissue_magnitude = np.count_nonzero(tissue_patch)/tissue_patch.size\n                    if tissue_magnitude  >= tissue_threshold:\n                        patch_locations.append(((i, j),tissue_magnitude))\n\n    return patch_locations, image_with_contours, image_with_rectangles\n\ndef process_wsi(wsi_obj, wsi_path, thumbnail_path, is_tma, output_patch_size=1000, tissue_percent=0.9, returnSamples=30, stride=1):\n    wsi_name = Path(wsi_path).stem + \".svs\"\n\n    if is_tma:\n        thumbnail = Image.open(wsi_path)\n        objective_power = 40\n    else:\n        thumbnail = Image.open(thumbnail_path)\n        objective_power = 20\n    \n    cthumbnail = clean_thumbnail(thumbnail)\n    tissue_mask = ((cthumbnail.mean(axis=2) != 255) * 255).astype(np.uint8)\n    # print(f\"the shape of tissue_mask is {tissue_mask.shape}\")\n    \n    # try:\n    #     objective_power = int(wsi_obj.properties['openslide.objective-power'])\n    # except:\n    #     objective_power = 20\n         \n    w, h = wsi_obj.dimensions\n    mask_hratio = int((tissue_mask.shape[0] / h) * output_patch_size)\n    mask_wratio = int((tissue_mask.shape[1] / w) * output_patch_size)\n    # Ensure the step size is at least 1 pixel\n    if mask_hratio == 0:\n        mask_hratio = 1\n    if mask_wratio == 0:\n        mask_wratio = 1\n    # print(f\"mask_hratio is {mask_hratio} and mask_wratio is {mask_wratio}\")\n    # estimate the mask patch size given the size of the WSI, the size of the mask, and the output patch size\n    mask_patch_size = int(output_patch_size / mask_wratio)\n    \n    Mask_to_WSI_ratioW = int(w / tissue_mask.shape[1])\n    Mask_to_WSI_ratioH = int(h / tissue_mask.shape[0])\n    \n    patch_locations, image_with_contours, image_with_rectangles = get_patch_locations(tissue_mask, cthumbnail, mask_hratio, mask_wratio, tissue_percent, stride)\n    # print(f\"initially generated {len(patch_locations)} patch locations\")\n    min_distance = mask_hratio * 2  # Minimum distance between points\n\n    filtered_patch_locations = []\n    for (x, y), _ in patch_locations:\n        if is_far_enough((x, y), filtered_patch_locations, min_distance):\n            filtered_patch_locations.append((x, y))\n\n    # print(f\"after is_far_enough there are {len(filtered_patch_locations)} patch locations\")\n    filtered_patch_locations = get_sampled_points_density_proportional_KDE(filtered_patch_locations, returnSamples)\n\n    scaled_patch_coordinates = []\n    for (x, y) in filtered_patch_locations:\n        scaled_patch_coordinates.append((int(x * Mask_to_WSI_ratioW), int(y * Mask_to_WSI_ratioH)))\n\n    return scaled_patch_coordinates\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:22.458281Z","iopub.execute_input":"2025-07-19T17:05:22.458553Z","iopub.status.idle":"2025-07-19T17:05:24.439439Z","shell.execute_reply.started":"2025-07-19T17:05:22.458536Z","shell.execute_reply":"2025-07-19T17:05:24.438934Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Import, Patch Selection, and Tensor Computation","metadata":{}},{"cell_type":"code","source":"import os\nimport torch\nimport torchvision.transforms as T\nimport openslide\nimport pyvips\n\nclass SlidePatchExtractor:\n    def __init__(self, image_id, patch_size=224, mode='train', tissue_threshold=0.9, num_patches=100):\n        \n        self.image_id = image_id\n        self.patch_size = patch_size\n        self.mode = mode\n        self.transform = T.Compose([\n            T.ToTensor(),\n            T.Resize((self.patch_size, self.patch_size), antialias=True),\n            # T.Normalize(mean=[0.2585, 0.2556, 0.2506], std=[0.229, 0.224, 0.225])\n            T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n        self.tissue_threshold = tissue_threshold\n        self.num_patches = num_patches\n        \n        # self.train_transform = T.Compose([\n        #     T.RandomHorizontalFlip(p=0.5),\n        #     T.RandomVerticalFlip(p=0.5),\n        #     T.RandomRotation(degrees=45),\n        #     T.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.1),\n        #     T.ToTensor(),\n        #     T.Resize((224, 224), antialias=True),\n        #     T.Normalize(mean=[0.2585, 0.2556, 0.2506], std=[0.229, 0.224, 0.225])\n        # ])\n\n        # Define paths for the source WSI and its thumbnail\n        self.source_path = os.path.join('/kaggle/input/UBC-OCEAN', f'{self.mode}_images', self.image_id + '.png')\n        self.thumbnail_path = os.path.join('/kaggle/input/UBC-OCEAN', f'{self.mode}_thumbnails', self.image_id + '_thumbnail.png')\n        \n        try:\n            self.slide = openslide.open_slide(self.source_path)\n        except openslide.OpenSlideError as e:\n            print(f\"Could not open slide {self.source_path}: {e}\")\n            self.patch_locations = []\n            return\n\n        self.width, self.height = self.slide.dimensions\n        self.is_tma = self.width < 5000 and self.height < 5000\n        standard_magnification = 20\n        self.objective_power = 40 if self.is_tma else 20\n        magnification_factor = self.objective_power / standard_magnification\n        self.extraction_patch_size = int(self.patch_size * magnification_factor)\n        \n        self.stride = 1 if self.is_tma else 4\n        \n        self.patch_locations = process_wsi(\n            wsi_obj=self.slide,\n            wsi_path=self.source_path,\n            thumbnail_path=self.thumbnail_path,\n            is_tma=self.is_tma,\n            output_patch_size=patch_size,\n            tissue_percent=tissue_threshold,\n            returnSamples=num_patches,\n            stride=self.stride\n        )\n    \n    def __len__(self):\n        \"\"\"Returns the number of patches found for this slide.\"\"\"\n        return len(self.patch_locations)\n    \n    def get_all_patch_tensors(self):\n        \"\"\"\n        Extracts all patches from the slide and returns them as a stacked tensor.\n        \"\"\"\n        patch_tensors = []\n        flat_feature_size = 3 * self.patch_size * self.patch_size\n        self.slide = pyvips.Image.new_from_file(self.source_path)\n        if not self.patch_locations:\n            # If no patches were found, return an empty tensor with the correct shape\n            return torch.empty((0, flat_feature_size))\n\n        for (x, y) in self.patch_locations:\n            try:\n                # patch_image = self.slide.read_region(\n                #     (x, y), 0, (self.extraction_patch_size, self.extraction_patch_size)\n                # ).convert('RGB')\n                patch_image = self.slide.crop(x, y, self.patch_size, self.patch_size).numpy()[..., :3]\n                patch_tensor = self.transform(patch_image)\n                patch_tensors.append(patch_tensor)\n            except Exception as e:\n                print(f\"Error reading patch at ({x},{y}) for slide {self.image_id}: {e}\")\n                continue\n        \n        if not patch_tensors:\n            return torch.empty((0, flat_feature_size))\n            \n        # Stack all patch tensors into a single 4D tensor (num_patches, 3, H, W)\n        stacked_patches = torch.stack(patch_tensors)\n        # Flatten the patch dimensions (3, H, W) into a single vector for each patch\n        # flattened_patches = torch.flatten(stacked_patches, start_dim=1)\n        \n        # return flattened_patches\n        return stacked_patches\n        \n    def get_patch(self, idx):\n        x, y = self.patch_locations[idx]\n        self.slide = pyvips.Image.new_from_file(self.source_path)\n        patch_image = self.slide.crop(x, y, self.patch_size, self.patch_size).numpy()[..., :3]\n        # patch_image = self.slide.read_region((x, y), 0, (self.extraction_patch_size, self.extraction_patch_size)).convert('RGB')\n        patch_tensor = self.transform(patch_image)\n        return patch_tensor, patch_image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:24.440291Z","iopub.execute_input":"2025-07-19T17:05:24.441137Z","iopub.status.idle":"2025-07-19T17:05:24.829763Z","shell.execute_reply.started":"2025-07-19T17:05:24.441102Z","shell.execute_reply":"2025-07-19T17:05:24.829289Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport openslide\nfrom PIL import Image\nimport numpy as np\nfrom tqdm import tqdm\n\nclass UBCDataset(Dataset):\n    \"\"\"\n    The main Dataset class for loading slides and their labels.\n    \"\"\"\n    def __init__(self, dataframe, label_map, mode='train', patch_size=224, tissue_threshold=0.9, num_patches=100):\n        self.df = dataframe\n        self.label_map = label_map\n        self.mode = mode\n        self.patch_size = patch_size\n        self.tissue_threshold = tissue_threshold\n        self.num_patches = num_patches\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_id = str(row['image_id'])\n        \n        string_label = row['label']\n        # Use the label map to convert the string label to an integer\n        int_label = self.label_map[string_label]\n        # Create the tensor from the integer\n        label = torch.tensor(int_label, dtype=torch.long)\n\n        extractor = SlidePatchExtractor(\n            image_id=image_id,\n            mode=self.mode,\n            patch_size=self.patch_size,\n            tissue_threshold=self.tissue_threshold,\n            num_patches=self.num_patches\n        )\n        \n        patch_tensors = extractor.get_all_patch_tensors()\n\n        return {\"patches\": patch_tensors, \"label\": label, \"image_id\": image_id}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:24.830525Z","iopub.execute_input":"2025-07-19T17:05:24.830736Z","iopub.status.idle":"2025-07-19T17:05:24.837527Z","shell.execute_reply.started":"2025-07-19T17:05:24.830713Z","shell.execute_reply":"2025-07-19T17:05:24.837031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Collate Function for the DataLoader ---\ndef collate_fn(batch):\n    patches_list = [item['patches'] for item in batch]\n    labels = torch.stack([item['label'] for item in batch])\n    image_ids = [item['image_id'] for item in batch]\n\n    return {\"patches\": patches_list, \"labels\": labels, \"image_ids\": image_ids}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:24.838147Z","iopub.execute_input":"2025-07-19T17:05:24.838311Z","iopub.status.idle":"2025-07-19T17:05:24.863841Z","shell.execute_reply.started":"2025-07-19T17:05:24.838297Z","shell.execute_reply":"2025-07-19T17:05:24.86319Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Main Training Block","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport os\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom torch.utils.data import Dataset, DataLoader\nimport torch\nimport gzip\nfrom sklearn.model_selection import train_test_split\nfrom collections import Counter\n\n# Disable the Decompression Bomb check\nImage.MAX_IMAGE_PIXELS = None\n\n# Hyperparameters\nbatch_size = 2\npatch_size = 512\nnum_patches = 256\ntissue_threshold = 0.9\n\nbase_path = '/kaggle/input/UBC-OCEAN'\ntrain_df = pd.read_csv(os.path.join(base_path, 'train.csv'))\n\n# Create a mapping from string labels to integers\nunique_labels = sorted(train_df['label'].unique())\nlabel_to_int = {label: i for i, label in enumerate(unique_labels)}\nint_to_label = {i: label for label, i in label_to_int.items()}\n\n# testing the code, only take the top 20 reocrds in training file\ntrain_df = train_df.head(100)\n\n# Split train into train(60%)/val(20%)/test(20%) \ndf_train, df_val_test = train_test_split(train_df, test_size=0.4, random_state=42) # stratify=df_val_test['label']\ndf_val, df_test = train_test_split(df_val_test, test_size=0.5, random_state=42) # stratify=df_val_test['label']\n\nprint(f\"Train/Val/Test sizes: {len(df_train)}/{len(df_val)}/{len(df_test)}\")\n\n# create the dataset using PyTorch Dataset\nubc_dataset_train = UBCDataset(dataframe=df_train, label_map=label_to_int, patch_size=patch_size, \n                               tissue_threshold=tissue_threshold, num_patches=num_patches)\n\nubc_dataset_val = UBCDataset(dataframe=df_val, label_map=label_to_int, patch_size=patch_size, \n                               tissue_threshold=tissue_threshold, num_patches=num_patches)\n\nubc_dataset_test = UBCDataset(dataframe=df_test, label_map=label_to_int, patch_size=patch_size, \n                               tissue_threshold=tissue_threshold, num_patches=num_patches)\n\n# create the training data loader, potentially change shuffle and num_workers\ntrain_loader = DataLoader(ubc_dataset_train, batch_size=batch_size, shuffle=False, num_workers=4, collate_fn=collate_fn)\nval_loader = DataLoader(ubc_dataset_val, batch_size=batch_size, shuffle=False, num_workers=4, collate_fn=collate_fn)\ntest_loader = DataLoader(ubc_dataset_test, batch_size=batch_size, shuffle=False, num_workers=4, collate_fn=collate_fn)\n\nprint(f\"Train batches: {len(train_loader)}, Val batches: {len(val_loader)}, Test batches: {len(test_loader)}\")\n\n# all_patch_tensors = {}\n\n# for i, batch in enumerate(tqdm(train_loader, desc=\"Processing Batches\")):\n#     print(f\"\\n--- Batch {i+1} ---\")\n    \n#     patches_list = batch['patches']\n#     labels = batch['labels']\n#     image_ids = batch['image_ids']\n    \n#     print(f\"Number of slides in this batch: {len(patches_list)}\")\n#     print(f\"Labels for this batch (as integers): {labels.numpy()}\")\n#     print(f\"Image IDs for this batch: {image_ids}\")\n\n#     for slide_idx in range(len(image_ids)):\n#         slide_id = image_ids[slide_idx]\n#         slide_patches = patches_list[slide_idx]\n#         slide_label_int = labels[slide_idx].item()\n\n#         file_name = slide_id + '_patches.pt.gz'\n#         # torch.save(slide_patches, file_name)\n#         with gzip.open(file_name, 'wb') as f:\n#             torch.save(slide_patches, f)\n        \n#         print(f\"  - Slide ID: {slide_id}, Label: {int_to_label[slide_label_int]} ({slide_label_int}), Patches: {slide_patches.shape[0]}\")\n        \n#         # all_patch_tensors[slide_id] = slide_patches\n\n# # print(f\"\\n--- Finished processing. Total slides with stored tensors: {len(all_patch_tensors)} ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:24.865941Z","iopub.execute_input":"2025-07-19T17:05:24.866434Z","iopub.status.idle":"2025-07-19T17:05:24.92923Z","shell.execute_reply.started":"2025-07-19T17:05:24.866417Z","shell.execute_reply":"2025-07-19T17:05:24.928686Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training ","metadata":{}},{"cell_type":"code","source":"from tqdm import tqdm\nimport torch\n\ndef train_epoch(model, loader, optimizer, criterion, device):\n    \"\"\"\n    Performs one epoch of training, feeding one patch at a time to the model.\n    Displays a dynamic progress bar for patches.\n    Returns the average training loss per slide.\n    \"\"\"\n    model.train()\n    running_loss = 0.0\n    slide_counter = 0\n    total_slides = len(loader.dataset)\n\n    for batch in loader:\n        patches_list = batch['patches']   # list of (P_i, 3, H, W) tensors\n        labels       = batch['labels']    # tensor of slide labels\n\n        for slide_idx in range(len(patches_list)):\n            slide_counter += 1\n            slide_patches   = patches_list[slide_idx].to(device)\n            slide_label_int = labels[slide_idx].item()\n            num_patches     = slide_patches.size(0)\n\n            print(f\"\\n=== Slide {slide_counter}/{total_slides} ===\")\n\n            slide_loss = 0.0\n            optimizer.zero_grad()\n\n            # Progress bar over patches\n            pbar = tqdm(range(num_patches),\n                        desc=\"Processing patches\",\n                        unit=\"patch\",\n                        leave=False)\n            for p_idx in pbar:\n                patch = slide_patches[p_idx].unsqueeze(0)  # (1, 3, H, W)\n                label = torch.tensor([slide_label_int], device=device)\n\n                outputs = model(patch)\n                loss    = criterion(outputs, label)\n                loss.backward()\n                optimizer.step()\n                optimizer.zero_grad()\n\n                slide_loss += loss.item()\n                # update bar with current average patch loss\n                avg_patch_loss = slide_loss / (p_idx + 1)\n                pbar.set_postfix({'avg_patch_loss': f'{avg_patch_loss:.4f}'})\n\n            avg_slide_loss = slide_loss / num_patches\n            running_loss   += avg_slide_loss\n            print(f\"  → Slide loss: {avg_slide_loss:.4f}\")\n\n    overall_avg_loss = running_loss / total_slides if total_slides else 0\n    return overall_avg_loss\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:24.930036Z","iopub.execute_input":"2025-07-19T17:05:24.930264Z","iopub.status.idle":"2025-07-19T17:05:24.936964Z","shell.execute_reply.started":"2025-07-19T17:05:24.930247Z","shell.execute_reply":"2025-07-19T17:05:24.936282Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Evaluation","metadata":{}},{"cell_type":"code","source":"from tqdm import tqdm\nimport torch\nfrom collections import Counter\n\ndef evaluate_epoch(model, loader, criterion, device):\n    \"\"\"\n    Validates the model over slides in loader.\n    Displays a dynamic progress bar for patches.\n    Returns tuple (avg_loss_per_slide, accuracy).\n    \"\"\"\n    model.eval()\n    running_loss = 0.0\n    correct = total = 0\n    slide_counter = 0\n    total_slides = len(loader.dataset)\n\n    with torch.no_grad():\n        for batch in loader:\n            patches_list = batch['patches']   # list of (P_i, 3, H, W)\n            labels       = batch['labels']    # tensor of slide labels\n\n            for slide_idx in range(len(patches_list)):\n                slide_counter += 1\n                print(f\"\\n=== Eval Slide {slide_counter}/{total_slides} ===\")\n\n                slide_patches   = patches_list[slide_idx].to(device)\n                true_label_int  = labels[slide_idx].item()\n                num_patches     = slide_patches.size(0)\n\n                # per‑slide accumulators\n                slide_loss = 0.0\n                preds      = []\n\n                # dynamic progress bar over patches\n                pbar = tqdm(\n                    range(num_patches),\n                    desc=\"Evaluating patches\",\n                    unit=\"patch\",\n                    leave=False\n                )\n                for p_idx in pbar:\n                    patch = slide_patches[p_idx].unsqueeze(0)  # (1,3,H,W)\n                    outputs = model(patch)\n                    label_chunk = torch.tensor([true_label_int], device=device)\n\n                    # accumulate loss\n                    loss = criterion(outputs, label_chunk)\n                    slide_loss += loss.item()\n\n                    # accumulate prediction\n                    preds.extend(torch.argmax(outputs, dim=1).cpu().tolist())\n\n                    # update bar with running avg loss\n                    avg_patch_loss = slide_loss / (p_idx + 1)\n                    pbar.set_postfix({'avg_patch_loss': f'{avg_patch_loss:.4f}'})\n\n                # majority vote for this slide\n                voted = Counter(preds).most_common(1)[0][0]\n                correct += int(voted == true_label_int)\n                total   += 1\n\n                # accumulate slide‑level loss\n                running_loss += slide_loss / num_patches\n                print(f\"  → Slide loss: {slide_loss/num_patches:.4f}, Vote: {voted}, True: {true_label_int}\")\n\n    avg_loss  = running_loss / total if total > 0 else 0.0\n    accuracy  = correct / total if total > 0 else 0.0\n    return avg_loss, accuracy","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:24.937675Z","iopub.execute_input":"2025-07-19T17:05:24.937894Z","iopub.status.idle":"2025-07-19T17:05:24.963585Z","shell.execute_reply.started":"2025-07-19T17:05:24.937879Z","shell.execute_reply":"2025-07-19T17:05:24.962947Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Testing","metadata":{}},{"cell_type":"code","source":"from tqdm import tqdm\nimport torch\nfrom collections import Counter\n\ndef test_model(model, loader, device):\n    \"\"\"\n    Tests the model over slides in loader.\n    Prints the per‑patch vote distribution for each slide.\n    Returns the test accuracy.\n    \"\"\"\n    model.eval()\n    correct = total = 0\n    slide_counter = 0\n    total_slides = len(loader.dataset)\n\n    with torch.no_grad():\n        for batch in loader:\n            patches_list = batch['patches']\n            labels       = batch['labels']\n\n            for slide_idx in range(len(patches_list)):\n                slide_counter += 1\n                print(f\"\\n=== Test Slide {slide_counter}/{total_slides} ===\")\n\n                slide_patches  = patches_list[slide_idx].to(device)\n                true_label_int = labels[slide_idx].item()\n                num_patches    = slide_patches.size(0)\n\n                preds = []\n                for p_idx in range(num_patches):\n                    patch = slide_patches[p_idx].unsqueeze(0)\n                    outputs = model(patch)\n                    pred = torch.argmax(outputs, dim=1).item()\n                    preds.append(pred)\n\n                # Compute vote counts\n                vote_counts = Counter(preds)\n                print(\"  Vote distribution:\")\n                for cls_idx, cnt in vote_counts.items():\n                    print(f\"    {int_to_label[cls_idx]} ({cls_idx}): {cnt} patch votes\")\n\n                # Majority vote\n                voted = vote_counts.most_common(1)[0][0]\n                print(f\"  → Final vote: {int_to_label[voted]} ({voted}), True: {int_to_label[true_label_int]} ({true_label_int})\")\n\n                correct += int(voted == true_label_int)\n                total   += 1\n\n    accuracy = correct / total if total > 0 else 0.0\n    print(f\"\\nOverall Test Accuracy: {accuracy:.4f}\")\n    return accuracy\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:15:44.592694Z","iopub.execute_input":"2025-07-19T17:15:44.593499Z","iopub.status.idle":"2025-07-19T17:15:44.600566Z","shell.execute_reply.started":"2025-07-19T17:15:44.593474Z","shell.execute_reply":"2025-07-19T17:15:44.599945Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train and Plot","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef train_and_plot(model, optimizer, criterion,\n                   train_loader, val_loader, test_loader,\n                   device, num_epochs):\n    train_history = []\n    val_loss_hist = []\n    val_acc_hist  = []\n\n    for epoch in range(1, num_epochs+1):\n        print(f\"\\n=== Epoch {epoch}/{num_epochs} ===\")\n        t_loss = train_epoch(model, train_loader, optimizer, criterion, device)\n        v_loss, v_acc = evaluate_epoch(model, val_loader, criterion, device)\n\n        train_history.append(t_loss)\n        val_loss_hist.append(v_loss)\n        val_acc_hist.append(v_acc)\n\n        print(f\"Train Loss: {t_loss:.4f} | Val Loss: {v_loss:.4f} | Val Acc: {v_acc:.4f}\")\n\n    # Plot losses\n    plt.figure()\n    plt.plot(train_history, label='Train Loss')\n    plt.plot(val_loss_hist,  label='Val Loss')\n    plt.legend(); plt.show()\n\n    # Plot val accuracy\n    plt.figure()\n    plt.plot(val_acc_hist, label='Val Acc')\n    plt.legend(); plt.show()\n\n    # Final test\n    test_acc = test_model(model, test_loader, device)\n    print(f\"\\nTest Accuracy: {test_acc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:24.987077Z","iopub.execute_input":"2025-07-19T17:05:24.987257Z","iopub.status.idle":"2025-07-19T17:05:25.009055Z","shell.execute_reply.started":"2025-07-19T17:05:24.987244Z","shell.execute_reply":"2025-07-19T17:05:25.008615Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%bash\n# remove previous patch dumps and old checkpoints\nrm -f /kaggle/working/*_patches.pt.gz\nrm -f /kaggle/working/vgg19_1stl_best.pth","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:25.00982Z","iopub.execute_input":"2025-07-19T17:05:25.009998Z","iopub.status.idle":"2025-07-19T17:05:25.044201Z","shell.execute_reply.started":"2025-07-19T17:05:25.009985Z","shell.execute_reply":"2025-07-19T17:05:25.043697Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Set Hyperparameters","metadata":{}},{"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel     = VGG19_1STL(pretrained=True).to(device)\ncriterion = nn.CrossEntropyLoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=2e-4, amsgrad=True)\nnum_epochs = 2\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:25.044744Z","iopub.execute_input":"2025-07-19T17:05:25.044954Z","iopub.status.idle":"2025-07-19T17:05:30.334089Z","shell.execute_reply.started":"2025-07-19T17:05:25.04494Z","shell.execute_reply":"2025-07-19T17:05:30.333532Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Running","metadata":{}},{"cell_type":"code","source":"# How much free vs total space on the root filesystem:\n!df -h /\n\n# Disk usage of your working directory:\n!du -sh /kaggle/working","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:30.334772Z","iopub.execute_input":"2025-07-19T17:05:30.335006Z","iopub.status.idle":"2025-07-19T17:05:30.612765Z","shell.execute_reply.started":"2025-07-19T17:05:30.334991Z","shell.execute_reply":"2025-07-19T17:05:30.612113Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\nstart_time = time.time()\ntrain_and_plot(model, optimizer, criterion, train_loader, val_loader, test_loader, device, num_epochs)\nend_time = time.time()\nprint(\"Time taken:\", (end_time - start_time)/60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T17:05:30.613584Z","iopub.execute_input":"2025-07-19T17:05:30.613783Z","iopub.status.idle":"2025-07-19T17:13:18.491714Z","shell.execute_reply.started":"2025-07-19T17:05:30.613763Z","shell.execute_reply":"2025-07-19T17:13:18.490695Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# After training/testing finishes, verify disk state:\nimport os, glob\n\nprint(\"=== Workspace files ===\")\nfor filepath in glob.glob('/kaggle/working/*'):\n    size_mb = os.path.getsize(filepath) / (1024*1024)\n    print(f\"{filepath:<50} {size_mb:6.1f} MB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-19T06:57:19.452822Z","iopub.execute_input":"2025-07-19T06:57:19.453201Z","iopub.status.idle":"2025-07-19T06:57:19.459181Z","shell.execute_reply.started":"2025-07-19T06:57:19.453176Z","shell.execute_reply":"2025-07-19T06:57:19.45851Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import gzip\n\n# # torch.save(slide_patches, 'slide_patches.pt')\n# with gzip.open('slide_patches.pt.gz', 'wb') as f:\n#     torch.save(slide_patches, f)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# !rm /kaggle/working/*.gz","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Store Patch Tensors","metadata":{}},{"cell_type":"code","source":"# from joblib import dump\n\n# dump(all_patch_tensors, 'all_patch_tensors.joblib')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# from joblib import load\n# import os\n\n# input_path = '/kaggle/input/ovarian-caner-subtype-classification'\n# file_path = os.path.join(input_path, 'all_patch_tensors.joblib')\n# new_dict = load(file_path)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Test the data class","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport os\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\n# Hyperparameters\nnum_patches = 512\ntissue_threshold = 0.9\n\n# Disable the Decompression Bomb check\nImage.MAX_IMAGE_PIXELS = None\n\nbase_path = '/kaggle/input/UBC-OCEAN'\ntrain_labels_df = pd.read_csv(os.path.join(base_path, 'train.csv'))\n# image_id = str(train_labels_df.loc[2, 'image_id'])\nimage_id = str(4)\nwsi = SlidePatchExtractor(image_id=image_id, tissue_threshold=tissue_threshold, num_patches=num_patches)\nprint(f'The image is {wsi.width} width and {wsi.height} height')\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualize patches for sanity check","metadata":{}},{"cell_type":"code","source":"print(len(wsi.patch_locations))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"patch_tensors = wsi.get_all_patch_tensors()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(patch_tensors.shape)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\n\nstart_time = time.perf_counter()\n\npatch_tensor, patch_image = wsi.get_patch(7)\n\nend_time = time.perf_counter()\n\nduration = end_time - start_time\nprint(f\"The code block took {duration:.4f} seconds to execute.\")\nprint(patch_tensor)\n\n# The code block took 154.5610 seconds to execute. openslide 66\n\n# The code block took 105.2772 seconds to execute. pyvips 2666\n# The code block took 140.6164 seconds to execute. openslide 2666","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nplt.imshow(patch_image)\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(wsi.patch_locations)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nfrom PIL import Image\n\n# --- 1. Define Thumbnail and Get Image Dimensions ---\n# Get the full resolution image dimensions (width, height)\noriginal_width = wsi.width\noriginal_height = wsi.height\n\npatch_coordinates = wsi.patch_locations\npatch_size = 224\n\n# --- 2. Generate the Thumbnail ---\n# The get_thumbnail function maintains the aspect ratio,\n# creating an image that fits within the given size.\nif wsi.is_tma:\n    thumbnail = wsi.slide.get_thumbnail((1024, 1024))\nelse:\n    thumbnail = Image.open(wsi.thumbnail_path)\n# Get the actual size of the generated thumbnail\nthumb_width, thumb_height = thumbnail.size\n\n# --- 3. Calculate Scaling Factors ---\n# These factors will scale coordinates from the original image to the thumbnail\nwidth_scale = thumb_width / original_width\nheight_scale = thumb_height / original_height\n\n# --- 4. Visualize the Thumbnail and Patches ---\n# Create a figure and axes for plotting\nfig, ax = plt.subplots(figsize=(10, 10))\n\n# Display the thumbnail image\nax.imshow(thumbnail)\n\n# Loop through each patch coordinate to draw it on the thumbnail\nfor x, y in patch_coordinates:\n    # Scale the patch's top-left corner coordinates\n    scaled_x = x * width_scale\n    scaled_y = y * height_scale\n\n    # Scale the patch's dimensions\n    scaled_patch_width = patch_size * width_scale\n    scaled_patch_height = patch_size * height_scale\n\n    # Create a rectangle patch with a red edge and no fill\n    rect = patches.Rectangle(\n        (scaled_x, scaled_y),\n        scaled_patch_width,\n        scaled_patch_height,\n        linewidth=1,\n        edgecolor='r',  # Red color for the patch border\n        facecolor='none'  # No fill\n    )\n\n    # Add the rectangle to the plot\n    ax.add_patch(rect)\n\n# --- 5. Finalize and Show the Plot ---\nax.set_title(\"WSI Thumbnail with Selected Patches\")\nplt.axis('off')  # Hide the axes ticks and labels\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}