{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.14","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":71549,"databundleVersionId":8561470,"sourceType":"competition"}],"dockerImageVersionId":30786,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"from IPython.display import IFrame\nfrom IPython.display import HTML\n\n# YouTube video URL\nyoutube_url = \"https://www.youtube.com/embed/zzqTL7zNEXY\"\n\n# Embed the YouTube video in the notebook\niframe_code = f'''\n<iframe width=\"560\" height=\"315\" \nsrc=\"{youtube_url}\" \ntitle=\"YouTube video player\" \nframeborder=\"0\" \nallow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" \nallowfullscreen></iframe>\n'''\n\n# Display the iframe in the notebook\nHTML(iframe_code)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2024-10-06T14:22:04.908346Z","iopub.execute_input":"2024-10-06T14:22:04.908792Z","iopub.status.idle":"2024-10-06T14:22:04.945062Z","shell.execute_reply.started":"2024-10-06T14:22:04.908749Z","shell.execute_reply":"2024-10-06T14:22:04.943842Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"This code defines a custom Convolutional Neural Network (CNN) for a medical image classification task related to the RSNA-24 competition on the Kaggle platform. Let's break down the code step-by-step:\n\n1. Imports and Setup:\n\n    The code starts by importing various libraries like torch for deep learning, pandas for data manipulation, numpy for numerical computations, and libraries specific to medical image processing like pydicom.\n    It defines constants like IMG_SIZE, IN_CHANS, N_LABELS, N_CLASSES, BATCH_SIZE, SEED, etc., which control various aspects of the model and training process.\n    Paths to data directories and output directory are set.\n\n2. Data Loading and Preprocessing:\n\n    It reads a CSV file containing test series descriptions and extracts relevant information like study IDs.\n    It defines a custom dataset class RSNA24TestDataset that inherits from torch.utils.data.Dataset.\n        This class takes care of loading images from different series types (Sagittal T1, Sagittal T2/STIR, and Axial T2) for each study ID.\n        It uses the pydicom library to read DICOM medical images and preprocesses them by normalization and resizing.\n        An Albumentations transform is applied for further data augmentation during testing (optional).\n\n3. Custom CNN Model Definition:\n\n    The code defines a custom CNN model class named CustomCNN. This class inherits from nn.Module in PyTorch.\n    The model architecture consists of:\n        Three convolutional layers with ReLU activation and max pooling.\n        Two fully connected layers with ReLU activation for classification.\n    The model takes an input of size (IN_CHANS, IMG_SIZE[0], IMG_SIZE[1]) which represents multiple channels (10 images per series type) concatenated and resized to the specified image size.\n    The output of the model has N_CLASSES units, representing the probabilities for each of the three classes (normal, mild, moderate/severe) for each of the 25 conditions.\n\n4. Model Loading and Inference:\n\n    The code uses glob.glob to find all the saved checkpoint files (trained models) in the output directory.\n    It iterates through these checkpoints and loads each model using torch.load.\n    The model is set to evaluation mode (model.eval()) and transferred to the device (GPU if available).\n    A list of models (models) is created to store all the loaded checkpoints.\n\n5. Prediction and Saving Results:\n\n    The code uses a loop to iterate through the test data loader (test_dl).\n    It disables gradient calculation using torch.no_grad() for efficiency during testing.\n    For each data point:\n        It creates an empty array pred_per_study to store the predicted probabilities for each condition and level combination.\n        It loops through all conditions (CONDITIONS) and levels (LEVELS) and creates a corresponding entry in row_names for the submission file.\n        It uses automatic mixed precision (torch.cuda.amp.autocast) for faster training if a compatible GPU is available.\n        It loops through all the loaded models (models) and performs a forward pass to get the predictions.\n        The predicted probabilities for each class are averaged across all the loaded models for ensemble learning.\n        The predictions are appended to the y_preds list.\n\n6. Saving Submission File:\n\n    Finally, all the predictions are concatenated and saved to a pandas DataFrame named sub.\n    This DataFrame has two columns:\n        row_id: Contains a unique identifier for each prediction (study ID, condition, and level combination).\n        LABELS: This column contains the predicted probabilities for each of the 25 conditions (three classes each).\n    The DataFrame is saved as a CSV file named submission.csv which can be submitted to the Kaggle competition.\n\nOverall, this code demonstrates a custom CNN model for a multi-class classification task on medical images which can be impoved upon. \n\nIt leverages ensemble learning by averaging predictions from multiple trained models and performs data augmentation for potentially better generalization. If you find this code useful, kindly upvote, subscribe to Handsonlabs Software Academy on Facebook and YouTube.                                 ","metadata":{}},{"cell_type":"code","source":"#Custom CNN Model Explanation\nimport os\nimport gc\nimport sys\nfrom PIL import Image\nimport cv2\nimport math, random\nimport numpy as np\nimport pandas as pd\nfrom glob import glob\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import KFold\n\nimport torch\nimport torch.nn.functional as F\nfrom torch import nn\nfrom torch.utils.data import DataLoader, Dataset\nfrom torch.optim import AdamW\nimport albumentations as A\n\nimport re\nimport pydicom\nimport glob \n\nrd = '/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification'\nOUTPUT_DIR = f'/kaggle/input/resnet-50/tensorflow2/classification/1'\ndevice = 'cuda:0' if torch.cuda.is_available() else 'cpu'\nN_WORKERS = os.cpu_count()\nUSE_AMP = True\nSEED = 8620\n\nIMG_SIZE = [512, 512]\nIN_CHANS = 30  # 10 images per series type * 3 series types\nN_LABELS = 25  # 5 conditions * 5 levels\nN_CLASSES = 3 * N_LABELS  # 3 classes for each label\n\nBATCH_SIZE = 1\n\ndf = pd.read_csv(f'{rd}/test_series_descriptions.csv')\nstudy_ids = list(df['study_id'].unique())\nsample_sub = pd.read_csv(f'{rd}/sample_submission.csv')\nLABELS = list(sample_sub.columns[1:])\nCONDITIONS = [\n    'spinal_canal_stenosis',\n    'left_neural_foraminal_narrowing',\n    'right_neural_foraminal_narrowing',\n    'left_subarticular_stenosis',\n    'right_subarticular_stenosis'\n]\n\nLEVELS = [\n    'l1_l2',\n    'l2_l3',\n    'l3_l4',\n    'l4_l5',\n    'l5_s1',\n]\n\n\ndef atoi(text):\n    return int(text) if text.isdigit() else text\n\n\ndef natural_keys(text):\n    return [atoi(c) for c in re.split(r'(\\d+)', text)]\n\n\n# Dataset class remains unchanged\nclass RSNA24TestDataset(Dataset):\n    def __init__(self, df, study_ids, phase='test', transform=None):\n        self.df = df\n        self.study_ids = study_ids\n        self.transform = transform\n        self.phase = phase\n\n    def __len__(self):\n        return len(self.study_ids)\n\n    def get_img_paths(self, study_id, series_desc):\n        pdf = self.df[self.df['study_id'] == study_id]\n        pdf_ = pdf[pdf['series_description'] == series_desc]\n        allimgs = []\n        for i, row in pdf_.iterrows():\n            pimgs = glob.glob(f'{rd}/test_images/{study_id}/{row[\"series_id\"]}/*.dcm')\n            pimgs = sorted(pimgs, key=natural_keys)\n            allimgs.extend(pimgs)\n\n        return allimgs\n\n    def read_dcm_ret_arr(self, src_path):\n        dicom_data = pydicom.dcmread(src_path)\n        image = dicom_data.pixel_array\n        image = (image - image.min()) / (image.max() - image.min() + 1e-6) * 255\n        img = cv2.resize(image, (IMG_SIZE[0], IMG_SIZE[1]), interpolation=cv2.INTER_CUBIC)\n        assert img.shape == (IMG_SIZE[0], IMG_SIZE[1])\n        return img\n\n    def __getitem__(self, idx):\n        x = np.zeros((IMG_SIZE[0], IMG_SIZE[1], IN_CHANS), dtype=np.uint8)\n        st_id = self.study_ids[idx]\n\n        # Load images in a similar way for different series types\n        series_types = ['Sagittal T1', 'Sagittal T2/STIR', 'Axial T2']\n        for k, series in enumerate(series_types):\n            allimgs = self.get_img_paths(st_id, series)\n            if len(allimgs) == 0:\n                print(f'{st_id}: {series} has no images')\n            else:\n                step = len(allimgs) / 10.0\n                for j, i in enumerate(np.arange(len(allimgs) / 2.0 - 4.0 * step, len(allimgs) + 0.0001, step)):\n                    try:\n                        ind2 = max(0, int((i - 0.5001).round()))\n                        img = self.read_dcm_ret_arr(allimgs[ind2])\n                        x[..., j + 10 * k] = img.astype(np.uint8)\n                    except:\n                        print(f'failed to load {st_id}, {series}')\n        \n        if self.transform is not None:\n            x = self.transform(image=x)['image']\n        \n        x = x.transpose(2, 0, 1)  # Change to (channels, height, width)\n\n        return x, str(st_id)\n\n\n# Albumentations for preprocessing\ntransforms_test = A.Compose([\n    A.Resize(IMG_SIZE[0], IMG_SIZE[1]),\n    A.Normalize(mean=0.5, std=0.5)\n])\n\ntest_ds = RSNA24TestDataset(df, study_ids, transform=transforms_test)\ntest_dl = DataLoader(\n    test_ds,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=N_WORKERS,\n    pin_memory=True,\n    drop_last=False\n)\n\n\n# Define custom CNN architecture\nclass CustomCNN(nn.Module):\n    def __init__(self, in_c=30, n_classes=75):\n        super(CustomCNN, self).__init__()\n        self.conv1 = nn.Conv2d(in_c, 64, kernel_size=3, padding=1)\n        self.conv2 = nn.Conv2d(64, 128, kernel_size=3, padding=1)\n        self.conv3 = nn.Conv2d(128, 256, kernel_size=3, padding=1)\n        self.fc1 = nn.Linear(256 * (IMG_SIZE[0] // 8) * (IMG_SIZE[1] // 8), 512)\n        self.fc2 = nn.Linear(512, n_classes)\n\n        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)\n        self.relu = nn.ReLU()\n\n    def forward(self, x):\n        x = self.relu(self.conv1(x))\n        x = self.pool(x)\n        x = self.relu(self.conv2(x))\n        x = self.pool(x)\n        x = self.relu(self.conv3(x))\n        x = self.pool(x)\n        \n        x = x.view(x.size(0), -1)  # Flatten the tensor\n        x = self.relu(self.fc1(x))\n        x = self.fc2(x)\n        return x\n\n\n# Load the custom CNN model\nmodels = []\nCKPT_PATHS = glob.glob(OUTPUT_DIR + '/best_custom_cnn_model_fold-*.pt') # Use glob.glob to call the function from the glob module.\nCKPT_PATHS = sorted(CKPT_PATHS)\nfor i, cp in enumerate(CKPT_PATHS):\n    print(f'loading {cp}...')\n    model = CustomCNN(IN_CHANS, N_CLASSES)\n    model.load_state_dict(torch.load(cp))\n    model.eval()\n    model.to(device)\n    models.append(model)\n\n\n# Perform inference and save predictions to submission.csv\ny_preds = []\nrow_names = []\n\nwith tqdm(test_dl, leave=True) as pbar:\n    with torch.no_grad():\n        for idx, (x, si) in enumerate(pbar):\n            x = x.to(device)\n            pred_per_study = np.zeros((25, 3))\n\n            for cond in CONDITIONS:\n                for level in LEVELS:\n                    row_names.append(si[0] + '_' + cond + '_' + level)\n\n            with torch.cuda.amp.autocast(enabled=USE_AMP):\n                for m in models:\n                    y = m(x)[0]\n                    for col in range(N_LABELS):\n                        pred = y[col * 3:col * 3 + 3]\n                        y_pred = pred.float().softmax(0).cpu().numpy()\n                        pred_per_study[col] += y_pred / len(models)\n                y_preds.append(pred_per_study)\n\n\n# Concatenate predictions and save to CSV\ny_preds = np.concatenate(y_preds, axis=0)\nsub = pd.DataFrame()\nsub['row_id'] = row_names\nsub[LABELS] = y_preds\nsub.to_csv('submission.csv', index=False)\n\nprint(pd.read_csv('submission.csv').head())","metadata":{"execution":{"iopub.status.busy":"2024-10-06T14:25:20.185144Z","iopub.execute_input":"2024-10-06T14:25:20.186741Z","iopub.status.idle":"2024-10-06T14:25:29.193135Z","shell.execute_reply.started":"2024-10-06T14:25:20.186682Z","shell.execute_reply":"2024-10-06T14:25:29.191597Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}