{"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":"gpu","dataSources":[{"sourceType":"competition","sourceId":97984,"databundleVersionId":14096757}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Image Type: <span style='color:#F1A424'>Multi-class Classification</span><span style='color:#ABABAB'> [Train]</span></b> \n\n***\n\n\n### <b><span style='color:#F1A424'>Table of Contents</span></b> <a class='anchor' id='top'></a>\n<div style=\" background-color:#3b3745; padding: 13px 13px; border-radius: 8px; color: white\">\n<li> <a href=\"#introduction\">Introduction</a></li>\n<li> <a href=\"#install_libraries\">Install libraries</a></li>\n<li><a href=\"#import_libraries\">Import Libraries</a></li>\n<li><a href=\"#configuration\">Configuration</a></li>\n<li><a href=\"#utils\">Utils</a></li>\n<li><a href=\"#load_data\">Load Data</a></li>\n<li><a href=\"#validation\">Validation</a></li>\n<li><a href=\"#data_augmentation\">Data Augmentation</a></li>\n<li><a href=\"#dataset\">Dataset</a></li>\n<li><a href=\"#model\">Model</a></li>\n<li><a href=\"#loss\">Loss Function</a></li>\n<li><a href=\"#functions\">Train and Validation Functions</a></li>\n<li><a href=\"#train_loop\">Train Loop</a></li>\n<li><a href=\"#train\">Train</a></li>\n<li><a href=\"#analysis\">Post Analysis</a></li>\n</div>\n\n\n# <b><span style='color:#F1A424'>|</span> Introduction</b><a class='anchor' id='introduction'></a> [↑](#top) \n\n***\n\n### <b><span style='color:#F1A424'>Useful References</span></b>","metadata":{"papermill":{"duration":0.00725,"end_time":"2022-09-27T21:55:03.193062","exception":false,"start_time":"2022-09-27T21:55:03.185812","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Import Libraries</b><a class='anchor' id='import_libraries'></a> [↑](#top) \n\n***\n\nImport all the required libraries for this notebook.","metadata":{"papermill":{"duration":0.005692,"end_time":"2022-09-27T21:55:03.216765","exception":false,"start_time":"2022-09-27T21:55:03.211073","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import cv2\nimport datetime\nimport gc\nimport math\nimport matplotlib.pyplot as plt\nimport multiprocessing\nimport numpy as np\nimport os\nimport pandas as pd\nimport random\nimport seaborn as sns\nimport timm\nimport torch\nimport torch.nn as nn\nimport torchvision\nimport time\n\n\nfrom glob import glob\nfrom PIL import Image\nfrom tqdm import tqdm\nfrom torch.utils.data import DataLoader, Dataset\nfrom torch.optim.lr_scheduler import OneCycleLR\nfrom transformers import get_linear_schedule_with_warmup, get_cosine_schedule_with_warmup\nfrom sklearn.metrics import confusion_matrix\n\n\n!mkdir logs\n!mkdir saved_models","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","papermill":{"duration":5.492351,"end_time":"2022-09-27T21:55:08.714899","exception":false,"start_time":"2022-09-27T21:55:03.222548","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2025-10-30T13:33:03.860783Z","iopub.execute_input":"2025-10-30T13:33:03.861054Z","iopub.status.idle":"2025-10-30T13:33:18.382808Z","shell.execute_reply.started":"2025-10-30T13:33:03.861030Z","shell.execute_reply":"2025-10-30T13:33:18.381640Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Configuration</b><a class='anchor' id='configuration'></a> [↑](#top) \n\n***\n\nCentral repository for this notebook's hyperparameters.","metadata":{"papermill":{"duration":0.005908,"end_time":"2022-09-27T21:55:08.727552","exception":false,"start_time":"2022-09-27T21:55:08.721644","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class config:\n    AMP = False # Averaged Precision Enabled\n    BATCH_SCHEDULER = True\n    BATCH_SIZE_TRAIN = 32\n    BATCH_SIZE_VALID = 32\n    DEBUG = True\n    EPOCHS = 2\n    FOLDS = 5\n    FREEZE = False\n    GRADIENT_ACCUMULATION_STEPS = 1\n    GRADIENT_CHECKPOINTING = True # whether to apply gradient checkpointing, less VRAM but more compute time\n    LEARNING_RATE = 3e-4\n    LR_DIV = 1.0\n    LR_FINAL_DIV = 10000.0\n    LR_FACTOR = 0.4  # BY HOW MUCH THE LR IS DECREASING\n    MAX_GRAD_NORM = 1000\n    MODEL = \"efficientnet_b2\"\n    NUM_CLASSES = 12\n    NUM_WARMUP_STEPS = 0\n    NUM_CYCLES = 0.5\n    NUM_WORKERS = multiprocessing.cpu_count()\n    NUM_FROZEN_LAYERS = 101\n    OUTPUT_SIZE = 1\n    PRINT_FREQ = 500\n    RESOLUTION = 256\n    SCHEDULER = \"cosine\"\n    SEED = 20\n    TRAIN_FOLDS = [0]\n    WEIGHT_DECAY = 0.01\n    \n\nclass paths:\n    TRAIN_FOLDER = \"/kaggle/input/physionet-ecg-image-digitization/train\"\n    OUTPUT_DIR = \"/kaggle/working/\"\n    \ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint('Device available now: ', device)","metadata":{"papermill":{"duration":0.077749,"end_time":"2022-09-27T21:55:08.811169","exception":false,"start_time":"2022-09-27T21:55:08.73342","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2025-10-30T14:02:06.245074Z","iopub.execute_input":"2025-10-30T14:02:06.245970Z","iopub.status.idle":"2025-10-30T14:02:06.252305Z","shell.execute_reply.started":"2025-10-30T14:02:06.245941Z","shell.execute_reply":"2025-10-30T14:02:06.251521Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Utils</b><a class='anchor' id='utils'></a> [↑](#top) \n\n***\n\nUtility functions used throughout the notebook.","metadata":{"papermill":{"duration":0.005927,"end_time":"2022-09-27T21:55:08.823029","exception":false,"start_time":"2022-09-27T21:55:08.817102","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class AverageMeter(object):\n    \"\"\"Computes and stores the average and current value\"\"\"\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.val = 0\n        self.avg = 0\n        self.sum = 0\n        self.count = 0\n\n    def update(self, val, n=1):\n        self.val = val\n        self.sum += val * n\n        self.count += n\n        self.avg = self.sum / self.count\n\n\ndef asMinutes(s):\n    m = math.floor(s / 60)\n    s -= m * 60\n    return '%dm %ds' % (m, s)\n\n\ndef timeSince(since, percent):\n    now = time.time()\n    s = now - since\n    es = s / (percent)\n    rs = es - s\n    return '%s (remain %s)' % (asMinutes(s), asMinutes(rs))\n    \n\ndef dict_average(dictionary):\n    values = list(dictionary.values())\n    if len(values) == 0:\n        return 0\n    return sum(values) / len(values)\n\n\ndef get_config_dict(config):\n    \"\"\"\n    Return the config, which is originally a class, as a Python dictionary.\n    \"\"\"\n    config_dict = dict((key, value) for key, value in config.__dict__.items() \n    if not callable(value) and not key.startswith('__'))\n    return config_dict\n\n\ndef get_image_size(image_path):\n    try:\n        with Image.open(image_path) as img:\n            width, height = img.size\n            return width, height\n    except:\n        # If there's an error reading the image, return None for width and height\n        return None, None\n\n\ndef get_logger(filename=paths.OUTPUT_DIR):\n    from logging import getLogger, INFO, StreamHandler, FileHandler, Formatter\n    logger = getLogger(__name__)\n    logger.setLevel(INFO)\n    handler1 = StreamHandler()\n    handler1.setFormatter(Formatter(\"%(message)s\"))\n    handler2 = FileHandler(filename=f\"{filename}.log\")\n    handler2.setFormatter(Formatter(\"%(message)s\"))\n    logger.addHandler(handler1)\n    logger.addHandler(handler2)\n    return logger\n\n\ndef get_optimal_threshold(thresholds, precision, recall):\n    \"\"\"\n    Returns threshold t that maximizes ROC-AUC score.\n    \"\"\"\n    fscore = (2 * precision * recall) / (precision + recall)\n    ix = np.argmax(fscore)\n    optimal_threshold = thresholds[ix]\n    return optimal_threshold, precision[ix], recall[ix]\n \n\ndef get_scheduler(cfg, optimizer, num_train_steps):\n    if cfg.SCHEDULER == 'linear':\n        scheduler = get_linear_schedule_with_warmup(\n            optimizer, num_warmup_steps=cfg.NUM_WARMUP_STEPS,\n            num_training_steps=num_train_steps\n        )\n    elif cfg.SCHEDULER == 'cosine':\n        scheduler = get_cosine_schedule_with_warmup(\n            optimizer, num_warmup_steps=cfg.NUM_WARMUP_STEPS,\n            num_training_steps=num_train_steps, num_cycles=cfg.NUM_CYCLES\n        )\n    return scheduler\n\n    \ndef binarize(x, threshold):\n    if x>=threshold:\n        x=1\n    else:\n        x=0\n    return x\n\n\ndef binarize_array(x, threshold):\n    return np.array([binarize(xi, threshold) for xi in x])\n\n\ndef get_id(x):\n    return x.split(\"/\")[-1].replace(\".png\",\"\")\n\n\ndef sep():\n    print(\"-\"*100)\n    \n    \ndef seed_everything(seed):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed) \n    \n    \nLOGGER = get_logger()\ntqdm.pandas()\nseed_everything(config.SEED)","metadata":{"papermill":{"duration":0.015465,"end_time":"2022-09-27T21:55:08.844536","exception":false,"start_time":"2022-09-27T21:55:08.829071","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2025-10-30T13:44:28.985837Z","iopub.execute_input":"2025-10-30T13:44:28.986430Z","iopub.status.idle":"2025-10-30T13:44:29.001574Z","shell.execute_reply.started":"2025-10-30T13:44:28.986405Z","shell.execute_reply":"2025-10-30T13:44:29.000765Z"},"trusted":true,"_kg_hide-input":true,"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Load Data</b><a class='anchor' id='load_data'></a> [↑](#top) \n\n***\n\nLoad data.","metadata":{"papermill":{"duration":0.00567,"end_time":"2022-09-27T21:55:08.855861","exception":false,"start_time":"2022-09-27T21:55:08.850191","status":"completed"},"tags":[]}},{"cell_type":"code","source":"image_paths = glob(paths.TRAIN_FOLDER + \"/*/*.png\")\ndf = pd.DataFrame(image_paths)\ndf.columns = [\"image_path\"]\ndf[\"base_id\"] = df[\"image_path\"].apply(lambda x: x.split(\"/\")[-1].split(\"-\")[0])\ndf[\"image_id\"] = df[\"image_path\"].apply(lambda x: x.split(\"/\")[-1].replace(\".png\", \"\"))\ndf[\"label\"] = df[\"image_id\"].apply(lambda x: int(x.split(\"-\")[-1]) -1)\n\nprint(f\"Dataframe has shape: {df.shape}\")\ndf.head()","metadata":{"papermill":{"duration":0.192894,"end_time":"2022-09-27T21:55:09.054663","exception":false,"start_time":"2022-09-27T21:55:08.861769","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2025-10-30T13:39:06.648687Z","iopub.execute_input":"2025-10-30T13:39:06.648989Z","iopub.status.idle":"2025-10-30T13:39:07.834035Z","shell.execute_reply.started":"2025-10-30T13:39:06.648966Z","shell.execute_reply":"2025-10-30T13:39:07.833349Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Validation</b><a class='anchor' id='validation'></a> [↑](#top) \n\n***\n\nSince this is a Mult-Task Learning problem where we need to solve two tasks we need to stratify our data by `gender` and `race` simultaneously. We created a `gender_race` column and we will use this as our stratification column.","metadata":{"papermill":{"duration":0.00627,"end_time":"2022-09-27T21:55:09.127968","exception":false,"start_time":"2022-09-27T21:55:09.121698","status":"completed"},"tags":[]}},{"cell_type":"code","source":"from sklearn.model_selection import StratifiedKFold\n\nX = df.loc[:, df.columns != \"label\"]\ny = df.loc[:, df.columns == \"label\"]\nskf = StratifiedKFold(n_splits=config.FOLDS, shuffle=True, random_state=config.SEED)\n\nfor fold, (train_index, valid_index) in enumerate(skf.split(X, y)):\n    df.loc[valid_index, \"fold\"] = int(fold)\n    \ndisplay(df.groupby('fold').size()), sep()\ndisplay(df.head())","metadata":{"papermill":{"duration":0.023891,"end_time":"2022-09-27T21:55:09.158114","exception":false,"start_time":"2022-09-27T21:55:09.134223","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2025-10-30T13:39:26.651776Z","iopub.execute_input":"2025-10-30T13:39:26.652054Z","iopub.status.idle":"2025-10-30T13:39:26.690026Z","shell.execute_reply.started":"2025-10-30T13:39:26.652038Z","shell.execute_reply":"2025-10-30T13:39:26.689402Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Data Augmentation</b><a class='anchor' id='data_augmentation'></a> [↑](#top) \n\n***","metadata":{}},{"cell_type":"code","source":"import albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\ntrain_transforms = A.Compose([ \n    A.Resize (height=config.RESOLUTION, width=config.RESOLUTION, p=1),\n    A.HorizontalFlip(p=0.5),\n    # A.ToGray(p=0.2),\n    # A.RGBShift(r_shift_limit=30, g_shift_limit=30, b_shift_limit=30, p=0.2),\n    # A.GaussianBlur(blur_limit=(9,21),sigma_limit=5, p=0.2),\n    # A.RandomBrightnessContrast(brightness_limit=(-0.2,0.2),contrast_limit=(-0.2,0.2),p=0.2),\n    A.Normalize(p=1),\n    ToTensorV2(p=1)\n])\nvalid_transforms = A.Compose([\n    A.Resize (height=config.RESOLUTION, width=config.RESOLUTION, p=1),\n    A.Normalize(p=1),\n    ToTensorV2(p=1)\n])\ntransforms = {\n    \"train_transforms\": train_transforms,\n    \"valid_transforms\": valid_transforms,\n}","metadata":{"execution":{"iopub.status.busy":"2025-10-30T13:40:13.851639Z","iopub.execute_input":"2025-10-30T13:40:13.852438Z","iopub.status.idle":"2025-10-30T13:40:14.347277Z","shell.execute_reply.started":"2025-10-30T13:40:13.852413Z","shell.execute_reply":"2025-10-30T13:40:14.346707Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Dataset</b><a class='anchor' id='dataset'></a> [↑](#top) \n\n***","metadata":{"papermill":{"duration":0.006205,"end_time":"2022-09-27T21:55:09.170792","exception":false,"start_time":"2022-09-27T21:55:09.164587","status":"completed"},"tags":[]}},{"cell_type":"code","source":"class CustomDataset(Dataset):\n    def __init__(self, dataframe, is_train, transforms):\n        self.dataframe = dataframe\n        self.is_train = is_train\n        \n        if self.is_train:\n            self.transform = transforms[\"train_transforms\"]\n        else:\n            self.transform = transforms[\"valid_transforms\"]\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def __getitem__(self, index):\n        output = {}\n        image_id = self.dataframe['image_id'][index] # Select image id for OOF\n        image_path = self.dataframe['image_path'][index] # Get image path\n        label = self.dataframe['label'][index] # Get image path\n        image = cv2.imread(image_path) # Read image\n        image = self.transform(image=image) # Apply transforms\n        image = image['image'] # Extract image from dictionary\n        output = {\n            \"image_id\": image_id,\n            \"image\": image,\n            \"label\": label\n            \n        }\n        return output","metadata":{"papermill":{"duration":0.842291,"end_time":"2022-09-27T21:55:10.019252","exception":false,"start_time":"2022-09-27T21:55:09.176961","status":"completed"},"tags":[],"execution":{"iopub.status.busy":"2025-10-30T13:41:12.428277Z","iopub.execute_input":"2025-10-30T13:41:12.429091Z","iopub.status.idle":"2025-10-30T13:41:12.434579Z","shell.execute_reply.started":"2025-10-30T13:41:12.429064Z","shell.execute_reply":"2025-10-30T13:41:12.433800Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Model</b><a class='anchor' id='model'></a> [↑](#top) \n\n***","metadata":{"papermill":{"duration":0.006116,"end_time":"2022-09-27T21:55:10.032219","exception":false,"start_time":"2022-09-27T21:55:10.026103","status":"completed"},"tags":[]}},{"cell_type":"code","source":"def get_model(config):\n    model = timm.create_model(\n        config.MODEL,\n        drop_rate = 0.1,\n        drop_path_rate = 0.2,\n        pretrained=True\n    )\n    model.reset_classifier(config.NUM_CLASSES)\n    \n    if config.FREEZE:\n        for i,(name, param) in enumerate(list(model.named_parameters())\\\n                                         [0:config.NUM_FROZEN_LAYERS]):\n            param.requires_grad = False\n            \n    return model\n\n\ndef load_best_model(config, best_model_path):\n    model =  timm.create_model(config.MODEL, pretrained=False)\n    model.reset_classifier(config.NUM_CLASSES)\n    checkpoint = torch.load(best_model_path)\n    model.load_state_dict(checkpoint)\n    return model\n\nmodel = get_model(config)\nmodel","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"papermill":{"duration":236.95753,"end_time":"2022-09-27T21:59:06.995917","exception":false,"start_time":"2022-09-27T21:55:10.038387","status":"completed"},"scrolled":true,"tags":[],"execution":{"iopub.status.busy":"2025-10-30T13:43:56.667543Z","iopub.execute_input":"2025-10-30T13:43:56.667825Z","iopub.status.idle":"2025-10-30T13:43:57.094358Z","shell.execute_reply.started":"2025-10-30T13:43:56.667807Z","shell.execute_reply":"2025-10-30T13:43:57.093541Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Metric</b><a class='anchor' id='model'></a> [↑](#top) \n\n***","metadata":{}},{"cell_type":"code","source":"from sklearn.metrics import roc_auc_score\n\ndef roc_auc_score_multiclass(y_true, y_pred, average = \"macro\"):\n    unique_classes = set(y_true)\n    roc_auc_dict = {}\n    for class_ in unique_classes:\n        \n        #creating a list of all the classes except the current class \n        other_class = [x for x in unique_classes if x != class_]\n\n        #marking the current class as 1 and all other classes as 0\n        new_true_class = [0 if x in other_class else 1 for x in y_true]\n        new_pred_class = [0 if x in other_class else 1 for x in y_pred]\n\n        #using the sklearn metrics method to calculate the roc_auc_score\n        roc_auc = roc_auc_score(new_true_class, new_pred_class, average = average)\n        roc_auc_dict[class_] = roc_auc\n\n    return roc_auc_dict, dict_average(roc_auc_dict)","metadata":{"execution":{"iopub.status.busy":"2025-10-30T13:42:11.776528Z","iopub.execute_input":"2025-10-30T13:42:11.776848Z","iopub.status.idle":"2025-10-30T13:42:11.782431Z","shell.execute_reply.started":"2025-10-30T13:42:11.776824Z","shell.execute_reply":"2025-10-30T13:42:11.781584Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Train and Validation Functions</b><a class='anchor' id='functions'></a> [↑](#top) \n\n***","metadata":{}},{"cell_type":"code","source":"def train_epoch(fold, train_loader, model, criterion, optimizer, epoch, scheduler, device):\n    \"\"\"One epoch training pass.\"\"\"\n    model.train() # set model in train mode\n    scaler = torch.amp.GradScaler(enabled=config.AMP) # Automatic Mixed Precision tries to match each op to its appropriate datatype.\n    losses = AverageMeter() # initiate AverageMeter to track the loss.\n    start = end = time.time() # track the execution time.\n    global_step = 0\n    \n    # ========== ITERATE OVER TRAIN BATCHES ============\n    with tqdm(train_loader, unit=\"train_batch\", desc='Train') as tqdm_train_loader:\n        for step, batch in enumerate(tqdm_train_loader):\n            inputs = batch.pop(\"image\")\n            labels = batch.pop(\"label\")\n            inputs = inputs.to(device) # send images to `device`\n            labels = labels.to(device) # send labels to `device`\n            batch_size = labels.size(0)\n            with torch.cuda.amp.autocast(enabled=config.AMP):\n                y_preds = model(inputs) # forward propagation pass\n                loss = criterion(y_preds, labels) # get loss\n            if config.GRADIENT_ACCUMULATION_STEPS > 1:\n                loss = loss / config.GRADIENT_ACCUMULATION_STEPS\n            losses.update(loss.item(), batch_size) # update loss function tracking\n            scaler.scale(loss).backward() # backward propagation pass\n            grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), config.MAX_GRAD_NORM)\n\n            if (step + 1) % config.GRADIENT_ACCUMULATION_STEPS == 0:\n                scaler.step(optimizer) # update optimizer parameters\n                scaler.update()\n                optimizer.zero_grad() # zero out the gradients\n                global_step += 1\n                if config.BATCH_SCHEDULER:\n                    scheduler.step() # update learning rate\n            end = time.time() # get finish time\n\n            # ========== LOG INFO ==========\n            if step % config.PRINT_FREQ == 0 or step == (len(train_loader)-1):\n                print('Epoch: [{0}][{1}/{2}] '\n                      'Elapsed {remain:s} '\n                      'Loss: {loss.avg:.4f} '\n                      'Grad: {grad_norm:.4f}  '\n                      'LR: {lr:.8f}  '\n                      .format(epoch+1, step, len(train_loader), \n                              remain=timeSince(start, float(step+1)/len(train_loader)),\n                              loss=losses,\n                              grad_norm=grad_norm,\n                              lr=scheduler.get_lr()[0]))\n\n    return losses.avg\n\n\ndef valid_epoch(valid_loader, model, criterion, device):\n    model.eval() # set model in evaluation mode\n    softmax = nn.Softmax(dim=1)\n    losses = AverageMeter() # initiate AverageMeter for tracking the loss.\n    prediction_dict = {}\n    preds = []\n    start = end = time.time() # track the execution time.\n    with tqdm(valid_loader, unit=\"valid_batch\", desc='Validation') as tqdm_valid_loader:\n        for step, batch in enumerate(tqdm_valid_loader):\n            inputs = batch.pop(\"image\")\n            labels = batch.pop(\"label\")\n            image_ids = batch.pop(\"image_id\")\n            inputs = inputs.to(device) # send images to `device`\n            labels = labels.to(device) # send labels to `device`\n            batch_size = labels.size(0)\n            with torch.no_grad():\n                y_preds = model(inputs) # forward propagation pass\n                loss = criterion(y_preds, labels) # get loss\n            if config.GRADIENT_ACCUMULATION_STEPS > 1:\n                loss = loss / config.GRADIENT_ACCUMULATION_STEPS\n            losses.update(loss.item(), batch_size) # update loss function tracking\n            y_preds = torch.argmax(softmax(y_preds), dim=1)\n            preds.append(y_preds.to('cpu').numpy()) # save predictions\n            end = time.time() # get finish time\n\n            # ========== LOG INFO ==========\n            if step % config.PRINT_FREQ == 0 or step == (len(valid_loader)-1):\n                print('EVAL: [{0}/{1}] '\n                      'Elapsed {remain:s} '\n                      'Loss: {loss.avg:.4f} '\n                      .format(step, len(valid_loader),\n                              loss=losses,\n                              remain=timeSince(start, float(step+1)/len(valid_loader))))\n                \n    prediction_dict[\"predictions\"] = np.concatenate(preds) # np.array() of shape (fold_size, target_cols)\n    prediction_dict[\"image_ids\"] = image_ids\n    return losses.avg, prediction_dict","metadata":{"execution":{"iopub.status.busy":"2025-10-30T14:03:50.291828Z","iopub.execute_input":"2025-10-30T14:03:50.292144Z","iopub.status.idle":"2025-10-30T14:03:50.304988Z","shell.execute_reply.started":"2025-10-30T14:03:50.292121Z","shell.execute_reply":"2025-10-30T14:03:50.304068Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Train Loop</b><a class='anchor' id='train_loop'></a> [↑](#top) \n\n***","metadata":{}},{"cell_type":"code","source":"def train_loop(df, fold):\n    \n    LOGGER.info(f\"========== Fold: {fold} training ==========\")\n\n    # ======== SPLIT ==========\n    train_folds = df[df['fold'] != fold].reset_index(drop=True)\n    valid_folds = df[df['fold'] == fold].reset_index(drop=True)\n    valid_labels = valid_folds[\"label\"].values\n    \n    # ======== DATASETS ==========\n    train_dataset = CustomDataset(train_folds, is_train=True, transforms=transforms)\n    valid_dataset = CustomDataset(valid_folds, is_train=False, transforms=transforms)\n    \n    # ======== DATALOADERS ==========\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=config.BATCH_SIZE_TRAIN,\n        shuffle=True,\n        num_workers=config.NUM_WORKERS, \n        pin_memory=True, drop_last=True\n    )\n    valid_loader = DataLoader(\n        valid_dataset,\n        batch_size=config.BATCH_SIZE_VALID,\n        shuffle=False,\n        num_workers=config.NUM_WORKERS, \n        pin_memory=True, drop_last=False\n    )\n    \n    # ======== MODEL ==========\n    model = get_model(config)\n    model.to(device)\n\n    optimizer = torch.optim.AdamW(\n        model.parameters(),\n        lr=config.LEARNING_RATE,\n        weight_decay=config.WEIGHT_DECAY\n    )\n    \n    num_train_steps = int(len(train_folds) / config.BATCH_SIZE_TRAIN * config.EPOCHS)\n    scheduler = get_scheduler(config, optimizer, num_train_steps)\n\n    # ======= LOSS ==========\n    criterion = nn.CrossEntropyLoss()\n    \n    best_score = -np.inf\n    # ====== ITERATE EPOCHS ========\n    for epoch in range(config.EPOCHS):\n        start_time = time.time()\n\n        # ======= TRAIN ==========\n        avg_loss = train_epoch(fold, train_loader, model, criterion, optimizer, epoch, scheduler, device)\n\n        # ======= EVALUATION ==========\n        avg_val_loss, prediction_dict = valid_epoch(valid_loader, model, criterion, device)\n        predictions = prediction_dict[\"predictions\"]\n        \n        # ======= SCORING ==========\n        score_dict, score = roc_auc_score_multiclass(valid_labels, predictions)\n        print(score_dict, score)\n        elapsed = time.time() - start_time\n\n        LOGGER.info(f'Epoch {epoch+1} - avg_train_loss: {avg_loss:.4f}  avg_val_loss: {avg_val_loss:.4f}  time: {elapsed:.0f}s')\n        LOGGER.info(f'Epoch {epoch+1} - Score: {score:.4f}')\n        \n        if score > best_score:\n            best_score = score\n            LOGGER.info(f'Epoch {epoch+1} - Save Best Score: {best_score:.4f} Model')\n            torch.save(\n                {\n                    'model': model.state_dict(),\n                    'predictions': predictions\n                },\n                paths.OUTPUT_DIR + f\"/{config.MODEL.replace('/', '_')}_fold_{fold}_best.pth\"\n            )\n\n    predictions = torch.load(\n        paths.OUTPUT_DIR + f\"/{config.MODEL.replace('/', '_')}_fold_{fold}_best.pth\", \n        weights_only=False,\n        map_location=torch.device('cpu')\n    )['predictions']\n    valid_folds[\"preds\"] = predictions\n\n    torch.cuda.empty_cache()\n    gc.collect()\n    \n    return valid_folds","metadata":{"execution":{"iopub.status.busy":"2025-10-30T14:03:25.047997Z","iopub.execute_input":"2025-10-30T14:03:25.048545Z","iopub.status.idle":"2025-10-30T14:03:25.059142Z","shell.execute_reply.started":"2025-10-30T14:03:25.048521Z","shell.execute_reply":"2025-10-30T14:03:25.058241Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Train</b><a class='anchor' id='train'></a> [↑](#top) \n\n***","metadata":{}},{"cell_type":"code","source":"if __name__ == '__main__':\n    \n    def get_result(oof_df):\n        labels = oof_df[\"label\"].values\n        preds = oof_df[\"preds\"].values\n        _, score = roc_auc_score_multiclass(labels, preds)\n        LOGGER.info(f'Score: {score:<.4f}')\n    \n    oof_df = pd.DataFrame()\n    for fold in range(config.FOLDS):\n        if fold in [0]:#config.TRAIN_FOLDS:\n            _oof_df = train_loop(df, fold)\n            oof_df = pd.concat([oof_df, _oof_df])\n            LOGGER.info(f\"========== Fold: {fold} result ==========\")\n            get_result(_oof_df)\n    oof_df = oof_df.reset_index(drop=True)\n    LOGGER.info(f\"========== CV ==========\")\n    get_result(oof_df)\n    oof_df.to_csv(paths.OUTPUT_DIR + '/oof_df.csv', index=False)","metadata":{"scrolled":true,"execution":{"iopub.status.busy":"2025-10-30T14:03:53.016558Z","iopub.execute_input":"2025-10-30T14:03:53.016905Z","iopub.status.idle":"2025-10-30T14:32:52.576769Z","shell.execute_reply.started":"2025-10-30T14:03:53.016869Z","shell.execute_reply":"2025-10-30T14:32:52.575257Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# <b><span style='color:#F1A424'>|</span> Post Analysis</b><a class='anchor' id='analysis'></a> [↑](#top) \n\n***","metadata":{}},{"cell_type":"code","source":"true_labels = _oof_df[\"label\"].values\npredicted_labels = _oof_df[\"preds\"].values\n\n# Get the unique classes from both true and predicted labels\nclasses = np.unique(np.concatenate((true_labels, predicted_labels)))\nclasses_ = classes\n# Compute the confusion matrix\ncm = confusion_matrix(true_labels, predicted_labels, labels=classes)\n\n# Calculate accuracy for each class\nclass_accuracies = cm.diagonal() / cm.sum(axis=1)\n\n# Plot the confusion matrix\nplt.figure(figsize=(10, 8))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\", xticklabels=classes, yticklabels=classes)\n\n# Annotate cells with accuracy values\nfor i, j in enumerate(classes_):\n    plt.text(j + 0.5, i + 0.75, f'{class_accuracies[i]*100:.2f}%', ha='center', va='center', color='red')\n\nplt.xlabel(\"Predicted Labels\")\nplt.ylabel(\"True Labels\")\nplt.title(\"Confusion Matrix with Class Accuracies\")\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-10-30T14:38:06.882394Z","iopub.execute_input":"2025-10-30T14:38:06.882922Z","iopub.status.idle":"2025-10-30T14:38:07.470475Z","shell.execute_reply.started":"2025-10-30T14:38:06.882895Z","shell.execute_reply":"2025-10-30T14:38:07.469709Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_full(df):\n\n    # ======== SPLIT ==========\n    train_folds = df.copy()\n    \n    # ======== DATASETS ==========\n    train_dataset = CustomDataset(train_folds, is_train=True, transforms=transforms)\n    \n    # ======== DATALOADERS ==========\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=config.BATCH_SIZE_TRAIN,\n        shuffle=True,\n        num_workers=config.NUM_WORKERS, \n        pin_memory=True, drop_last=True\n    )\n    \n    # ======== MODEL ==========\n    model = get_model(config)\n    model.to(device)\n\n    optimizer = torch.optim.AdamW(\n        model.parameters(),\n        lr=config.LEARNING_RATE,\n        weight_decay=config.WEIGHT_DECAY\n    )\n    \n    num_train_steps = int(len(train_folds) / config.BATCH_SIZE_TRAIN * config.EPOCHS)\n    scheduler = get_scheduler(config, optimizer, num_train_steps)\n\n    # ======= LOSS ==========\n    criterion = nn.CrossEntropyLoss()\n    \n    # ====== ITERATE EPOCHS ========\n    for epoch in range(config.EPOCHS):\n        start_time = time.time()\n\n        # ======= TRAIN ==========\n        avg_loss = train_epoch(fold, train_loader, model, criterion, optimizer, epoch, scheduler, device)\n\n        elapsed = time.time() - start_time\n\n        LOGGER.info(f'Epoch {epoch+1} - avg_train_loss: {avg_loss:.4f}  time: {elapsed:.0f}s')\n        torch.save(\n            model.state_dict(),\n            paths.OUTPUT_DIR + f\"/{config.MODEL.replace('/', '_')}_full_train.pth\"\n        )\n\n    torch.cuda.empty_cache()\n    gc.collect()\n    \n    return _\n\ntrain_full(df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-30T14:42:55.813064Z","iopub.execute_input":"2025-10-30T14:42:55.813356Z","iopub.status.idle":"2025-10-30T14:43:20.024133Z","shell.execute_reply.started":"2025-10-30T14:42:55.813334Z","shell.execute_reply":"2025-10-30T14:43:20.022685Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}