{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"accelerator":"GPU"},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, random, copy, warnings\nfrom tqdm.auto import tqdm\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom sklearn.model_selection import GroupShuffleSplit\nfrom sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\n\nwarnings.filterwarnings('ignore')\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nUSE_AMP = torch.cuda.is_available()\nprint('Device:', device, '| AMP:', USE_AMP)\nif USE_AMP:\n    print('GPU:', torch.cuda.get_device_name(0))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:19.746215Z","iopub.execute_input":"2026-06-16T10:21:19.746589Z","iopub.status.idle":"2026-06-16T10:21:28.819818Z","shell.execute_reply.started":"2026-06-16T10:21:19.746546Z","shell.execute_reply":"2026-06-16T10:21:28.819072Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------- MANDATORY (don't change this) --------------------------------------->\ndef seed_everything(seed=None):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nseed_everything(np.random.randint(1, 10000))\n# <---------------------------- MANDATORY (don't change this) --------------------------------------","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:28.821280Z","iopub.execute_input":"2026-06-16T10:21:28.821718Z","iopub.status.idle":"2026-06-16T10:21:28.830930Z","shell.execute_reply.started":"2026-06-16T10:21:28.821688Z","shell.execute_reply":"2026-06-16T10:21:28.830030Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------- MANDATORY (don't change this) --------------------------------------->\nROOT      = '/kaggle/input/competitions/state-farm-distracted-driver-detection'\nTRAIN_DIR = ROOT + '/imgs/train'\ncsv_path  = ROOT + '/driver_imgs_list.csv'\ndf = pd.read_csv(csv_path)\n\nCLASS_NAMES = {\n    'c0': 'safe driving',\n    'c1': 'texting - right',\n    'c2': 'talking on the phone - right',\n    'c3': 'texting - left',\n    'c4': 'talking on the phone - left',\n    'c5': 'operating the radio',\n    'c6': 'drinking',\n    'c7': 'reaching behind',\n    'c8': 'hair and makeup',\n    'c9': 'talking to passenger',\n}\nCLASS_ORDER = [f'c{i}' for i in range(10)]\nNUM_CLASSES = len(CLASS_ORDER)\n\ndf['class_name'] = df['classname'].map(CLASS_NAMES)\ndf['path'] = df.apply(lambda r: f\"{TRAIN_DIR}/{r['classname']}/{r['img']}\", axis=1)\n# <---------------------------- MANDATORY (don't change this) --------------------------------------\nprint('Dataset:', df.shape, '| Drivers:', df['subject'].nunique())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:28.831798Z","iopub.execute_input":"2026-06-16T10:21:28.832024Z","iopub.status.idle":"2026-06-16T10:21:29.015587Z","shell.execute_reply.started":"2026-06-16T10:21:28.832003Z","shell.execute_reply":"2026-06-16T10:21:29.014822Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------- MANDATORY (don't change this) --------------------------------------->\nTEST_SUBJECTS = ['p014', 'p021', 'p061', 'p082', 'p050', 'p051', 'p072']\ntest_df        = df[df['subject'].isin(TEST_SUBJECTS)].reset_index(drop=True)\ntrain_valid_df = df[~df['subject'].isin(TEST_SUBJECTS)].reset_index(drop=True)\n# <---------------------------- MANDATORY (don't change this) --------------------------------------\nprint(f'Test: {len(test_df):,} | Train+Val: {len(train_valid_df):,}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:29.017262Z","iopub.execute_input":"2026-06-16T10:21:29.017544Z","iopub.status.idle":"2026-06-16T10:21:29.036348Z","shell.execute_reply.started":"2026-06-16T10:21:29.017520Z","shell.execute_reply":"2026-06-16T10:21:29.035312Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"gss = GroupShuffleSplit(n_splits=1, test_size=0.20, random_state=42)\ntr_idx, va_idx = next(gss.split(train_valid_df, groups=train_valid_df['subject']))\ntrain_df = train_valid_df.iloc[tr_idx].reset_index(drop=True)\nvalid_df = train_valid_df.iloc[va_idx].reset_index(drop=True)\nprint(f'Train: {len(train_df):,} ({train_df[\"subject\"].nunique()} drivers)')\nprint(f'Valid: {len(valid_df):,} ({valid_df[\"subject\"].nunique()} drivers)')\nprint(f'Overlap: {set(train_df[\"subject\"]) & set(valid_df[\"subject\"])}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:29.037359Z","iopub.execute_input":"2026-06-16T10:21:29.037712Z","iopub.status.idle":"2026-06-16T10:21:29.059388Z","shell.execute_reply.started":"2026-06-16T10:21:29.037684Z","shell.execute_reply":"2026-06-16T10:21:29.058361Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE     = 256\nIMGNET_MEAN  = [0.485, 0.456, 0.406]\nIMGNET_STD   = [0.229, 0.224, 0.225]\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((288, 288)),\n    transforms.RandomCrop(IMG_SIZE),\n    transforms.RandomApply([\n        transforms.RandomAffine(degrees=10, translate=(0.08, 0.08), scale=(0.90, 1.10))\n    ], p=0.5),\n    transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2, hue=0.05),\n    transforms.RandomGrayscale(p=0.04),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=IMGNET_MEAN, std=IMGNET_STD),\n    transforms.RandomErasing(p=0.2, scale=(0.02, 0.10)),\n])\n\neval_transform = transforms.Compose([\n    transforms.Resize((288, 288)),\n    transforms.CenterCrop(IMG_SIZE),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=IMGNET_MEAN, std=IMGNET_STD),\n])\n\ntta_transforms = [\n    transforms.Compose([transforms.Resize((288,288)), transforms.CenterCrop(IMG_SIZE), transforms.ToTensor(), transforms.Normalize(IMGNET_MEAN, IMGNET_STD)]),\n    transforms.Compose([transforms.Resize((300,300)), transforms.CenterCrop(IMG_SIZE), transforms.ToTensor(), transforms.Normalize(IMGNET_MEAN, IMGNET_STD)]),\n    transforms.Compose([transforms.Resize((320,320)), transforms.CenterCrop(IMG_SIZE), transforms.ToTensor(), transforms.Normalize(IMGNET_MEAN, IMGNET_STD)]),\n    transforms.Compose([transforms.Resize((300,300)), transforms.RandomCrop(IMG_SIZE), transforms.ToTensor(), transforms.Normalize(IMGNET_MEAN, IMGNET_STD)]),\n    transforms.Compose([transforms.Resize((288,288)), transforms.CenterCrop(IMG_SIZE), transforms.ColorJitter(brightness=0.15), transforms.ToTensor(), transforms.Normalize(IMGNET_MEAN, IMGNET_STD)]),\n    transforms.Compose([transforms.Resize((288,288)), transforms.CenterCrop(IMG_SIZE), transforms.ColorJitter(brightness=(0.7,0.9)), transforms.ToTensor(), transforms.Normalize(IMGNET_MEAN, IMGNET_STD)]),\n    transforms.Compose([transforms.Resize((288,288)), transforms.CenterCrop(IMG_SIZE), transforms.ColorJitter(contrast=0.15), transforms.ToTensor(), transforms.Normalize(IMGNET_MEAN, IMGNET_STD)]),\n    transforms.Compose([transforms.Resize((300,300)), transforms.RandomAffine(degrees=5, translate=(0.03,0.03)), transforms.CenterCrop(IMG_SIZE), transforms.ToTensor(), transforms.Normalize(IMGNET_MEAN, IMGNET_STD)]),\n]\nprint('Transforms ready | TTA passes:', len(tta_transforms))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:29.060662Z","iopub.execute_input":"2026-06-16T10:21:29.060983Z","iopub.status.idle":"2026-06-16T10:21:29.073150Z","shell.execute_reply.started":"2026-06-16T10:21:29.060954Z","shell.execute_reply":"2026-06-16T10:21:29.072493Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_to_idx = {c: i for i, c in enumerate(CLASS_ORDER)}\nidx_to_class = {i: c for c, i in class_to_idx.items()}\n\nclass DriverImageDataset(Dataset):\n    def __init__(self, dataframe, transform=None):\n        self.dataframe = dataframe.reset_index(drop=True)\n        self.transform = transform\n    def __len__(self):\n        return len(self.dataframe)\n    def __getitem__(self, idx):\n        row   = self.dataframe.iloc[idx]\n        image = Image.open(row['path']).convert('RGB')\n        if self.transform:\n            image = self.transform(image)\n        return image, class_to_idx[row['classname']]\n\ntrain_ds = DriverImageDataset(train_df.sample(frac=1, random_state=42).reset_index(drop=True), train_transform)\nvalid_ds = DriverImageDataset(valid_df.sample(frac=1, random_state=42).reset_index(drop=True), eval_transform)\ntest_ds  = DriverImageDataset(test_df, eval_transform)\nprint(f'Train: {len(train_ds)} | Valid: {len(valid_ds)} | Test: {len(test_ds)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:29.074029Z","iopub.execute_input":"2026-06-16T10:21:29.074318Z","iopub.status.idle":"2026-06-16T10:21:29.092580Z","shell.execute_reply.started":"2026-06-16T10:21:29.074298Z","shell.execute_reply":"2026-06-16T10:21:29.091782Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 64\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True,  num_workers=4, pin_memory=True, persistent_workers=True)\nvalid_loader = DataLoader(valid_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=4, pin_memory=True, persistent_workers=True)\ntest_loader  = DataLoader(test_ds,  batch_size=BATCH_SIZE, shuffle=False, num_workers=4, pin_memory=True)\nprint('Loaders ready')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:29.093600Z","iopub.execute_input":"2026-06-16T10:21:29.094725Z","iopub.status.idle":"2026-06-16T10:21:29.100115Z","shell.execute_reply.started":"2026-06-16T10:21:29.094695Z","shell.execute_reply":"2026-06-16T10:21:29.099305Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_resnet18_transfer_model(num_classes=10, freeze_features=False):\n    # ---------------------------- MANDATORY (don't change this) ------------------------------>\n    weights = models.ResNet18_Weights.DEFAULT\n    model   = models.resnet18(weights=weights)\n    print('Loaded ImageNet-pretrained ResNet18 weights.')\n    # <---------------------------- MANDATORY (don't change this) -----------------------------\n    if freeze_features:\n        for p in model.parameters(): p.requires_grad = False\n    in_f = model.fc.in_features\n    model.fc = nn.Sequential(\n        nn.BatchNorm1d(in_f),\n        nn.Dropout(p=0.5),\n        nn.Linear(in_f, 256),\n        nn.ReLU(),\n        nn.Dropout(p=0.4),\n        nn.Linear(256, num_classes),\n    )\n    return model\n\ndef count_trainable_parameters(m): return sum(p.numel() for p in m.parameters() if p.requires_grad)\ndef count_total_parameters(m):     return sum(p.numel() for p in m.parameters())\n\nmodel = create_resnet18_transfer_model(num_classes=NUM_CLASSES, freeze_features=False).to(device)\nprint(f'Total: {count_total_parameters(model):,} | Trainable: {count_trainable_parameters(model):,}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:29.101028Z","iopub.execute_input":"2026-06-16T10:21:29.101338Z","iopub.status.idle":"2026-06-16T10:21:30.083456Z","shell.execute_reply.started":"2026-06-16T10:21:29.101280Z","shell.execute_reply":"2026-06-16T10:21:30.082257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===== Helper functions =====\n\ndef mixup_data(x, y, alpha=0.4):\n    lam   = np.random.beta(alpha, alpha)\n    idx   = torch.randperm(x.size(0), device=x.device)\n    return lam * x + (1 - lam) * x[idx], y, y[idx], lam\n\n\nclass ModelEMA:\n    def __init__(self, model, decay=0.9995):\n        self.ema   = copy.deepcopy(model).eval()\n        self.decay = decay\n        for p in self.ema.parameters(): p.requires_grad_(False)\n    @torch.no_grad()\n    def update(self, model):\n        msd = model.state_dict()\n        for k, v in self.ema.state_dict().items():\n            if v.dtype.is_floating_point:\n                v.mul_(self.decay).add_(msd[k].detach(), alpha=1 - self.decay)\n            else:\n                v.copy_(msd[k])\n\n\ndef train_one_epoch(model, dataloader, loss_fn, optimizer, device,\n                    scheduler=None, scaler=None, ema=None,\n                    mixup_alpha=0.4, mixup_prob=0.85, clip_grad=1.0):\n    model.train()\n    total_loss = total_correct = total_n = 0\n    pbar = tqdm(dataloader, desc='Train', leave=False)\n    for images, labels in pbar:\n        images = images.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True)\n        use_mix = np.random.rand() < mixup_prob\n        if use_mix:\n            mx, ya, yb, lam = mixup_data(images, labels, mixup_alpha)\n        optimizer.zero_grad(set_to_none=True)\n        if USE_AMP:\n            with torch.autocast(device_type='cuda', dtype=torch.float16):\n                out  = model(mx if use_mix else images)\n                loss = lam*loss_fn(out,ya)+(1-lam)*loss_fn(out,yb) if use_mix else loss_fn(out,labels)\n            scaler.scale(loss).backward()\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), clip_grad)\n            scaler.step(optimizer); scaler.update()\n        else:\n            out  = model(mx if use_mix else images)\n            loss = lam*loss_fn(out,ya)+(1-lam)*loss_fn(out,yb) if use_mix else loss_fn(out,labels)\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(model.parameters(), clip_grad)\n            optimizer.step()\n        if scheduler: scheduler.step()\n        if ema:       ema.update(model)\n        bs = labels.size(0)\n        total_loss += loss.item() * bs\n        if use_mix:\n            correct = (lam*(out.argmax(1)==ya).float()+(1-lam)*(out.argmax(1)==yb).float()).sum().item()\n        else:\n            correct = (out.argmax(1)==labels).sum().item()\n        total_correct += correct; total_n += bs\n        pbar.set_postfix(loss=f'{loss.item():.3f}')\n    return total_loss/total_n, total_correct/total_n\n\n\n# ---------------------------- MANDATORY (don't change this) --------------------------------------->\ndef evaluate(model, dataloader, loss_fn, device):\n    model.eval()\n    total_loss = total_correct = total_examples = 0\n    all_predictions, all_labels, all_probabilities = [], [], []\n    progress_bar = tqdm(dataloader, desc='Evaluating', leave=False)\n    with torch.no_grad():\n        for images, labels in progress_bar:\n            images = images.to(device, non_blocking=True)\n            labels = labels.to(device, non_blocking=True)\n            outputs       = model(images)\n            loss          = loss_fn(outputs, labels)\n            probabilities = torch.softmax(outputs, dim=1)\n            predictions   = outputs.argmax(dim=1)\n            batch_size    = labels.size(0)\n            total_loss    += loss.item() * batch_size\n            total_correct += (predictions == labels).sum().item()\n            total_examples += batch_size\n            all_predictions.append(predictions.cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n            all_probabilities.append(probabilities.cpu().numpy())\n    return (total_loss / total_examples,\n            total_correct / total_examples,\n            np.concatenate(all_predictions),\n            np.concatenate(all_labels),\n            np.concatenate(all_probabilities))\n# <---------------------------- MANDATORY (don't change this) --------------------------------------\n\n\ndef evaluate_with_tta(model, dataframe, loss_fn, device, tta_tf_list, batch_size=64):\n    \"\"\"Fast TTA: φορτώνει εικόνες ΜΙΑ ΦΟΡΑ στη μνήμη, εφαρμόζει N transforms.\"\"\"\n    model.eval()\n    print('Loading images into memory...')\n    raw_images  = [Image.open(p).convert('RGB') for p in tqdm(dataframe['path'].tolist(), leave=False)]\n    true_labels = dataframe['classname'].map(class_to_idx).values\n    print(f'Loaded {len(raw_images)} images. Running {len(tta_tf_list)} TTA passes...')\n    all_probs = []\n    for t, tf in enumerate(tta_tf_list):\n        probs = []\n        for s in range(0, len(raw_images), batch_size):\n            batch = torch.stack([tf(img) for img in raw_images[s:s+batch_size]]).to(device)\n            with torch.no_grad():\n                probs.append(torch.softmax(model(batch), dim=1).cpu().numpy())\n        all_probs.append(np.concatenate(probs))\n        print(f'  Pass {t+1}/{len(tta_tf_list)} done')\n    mean_probs  = np.mean(all_probs, axis=0)\n    predictions = mean_probs.argmax(axis=1)\n    loss = -np.mean(np.log(mean_probs[np.arange(len(true_labels)), true_labels] + 1e-9))\n    acc  = (predictions == true_labels).mean()\n    return loss, acc, predictions, true_labels, mean_probs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:30.086208Z","iopub.execute_input":"2026-06-16T10:21:30.086473Z","iopub.status.idle":"2026-06-16T10:21:30.105751Z","shell.execute_reply.started":"2026-06-16T10:21:30.086451Z","shell.execute_reply":"2026-06-16T10:21:30.104910Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ================= TRAINING v6 =================\nEPOCHS         = 32\nMIXUP_OFF_LAST = 1\n\nloss_fn = nn.CrossEntropyLoss(label_smoothing=0.12)\n\nparam_groups = [\n    {'params': model.fc.parameters(),                                                           'lr': 8e-4},\n    {'params': model.layer4.parameters(),                                                       'lr': 2e-4},\n    {'params': model.layer3.parameters(),                                                       'lr': 1e-4},\n    {'params': list(model.layer1.parameters())+list(model.layer2.parameters())+\n               list(model.conv1.parameters())+list(model.bn1.parameters()),                    'lr': 5e-5},\n]\noptimizer = torch.optim.AdamW(param_groups, weight_decay=5e-4)\nscheduler = torch.optim.lr_scheduler.OneCycleLR(\n    optimizer,\n    max_lr=[g['lr'] for g in param_groups],\n    total_steps=EPOCHS * len(train_loader),\n    pct_start=0.15, div_factor=10, final_div_factor=100,\n)\nscaler = torch.amp.GradScaler('cuda') if USE_AMP else None\nema    = ModelEMA(model, decay=0.9995)\n\nbest_val_acc = 0.0\nbest_source  = ''\nhistory = {'train_loss':[], 'train_acc':[], 'valid_loss':[], 'valid_acc':[], 'valid_acc_ema':[]}\n\nfor epoch in range(EPOCHS):\n    mixup_prob = 0.0 if epoch >= EPOCHS - MIXUP_OFF_LAST else 0.85\n    train_loss, train_acc = train_one_epoch(\n        model, train_loader, loss_fn, optimizer, device,\n        scheduler=scheduler, scaler=scaler, ema=ema,\n        mixup_alpha=0.4, mixup_prob=mixup_prob, clip_grad=1.0,\n    )\n    valid_loss, valid_acc, _, _, _ = evaluate(model, valid_loader, loss_fn, device)\n    _, valid_acc_ema, _, _, _      = evaluate(ema.ema, valid_loader, loss_fn, device)\n    history['train_loss'].append(train_loss); history['train_acc'].append(train_acc)\n    history['valid_loss'].append(valid_loss); history['valid_acc'].append(valid_acc)\n    history['valid_acc_ema'].append(valid_acc_ema)\n    tag = 'NO mixup' if mixup_prob == 0 else f'mix={mixup_prob}'\n    print(f'Epoch {epoch+1:02d}/{EPOCHS} [{tag}] | loss={train_loss:.4f} train={train_acc:.4f} | val={valid_acc:.4f} | ema={valid_acc_ema:.4f}')\n    if valid_acc > best_val_acc:\n        best_val_acc = valid_acc; best_source = 'raw'\n        torch.save(model.state_dict(), '/kaggle/working/best_model.pt')\n        print(f'   -> best (raw): {best_val_acc:.4f}')\n    if valid_acc_ema > best_val_acc:\n        best_val_acc = valid_acc_ema; best_source = 'EMA'\n        torch.save(ema.ema.state_dict(), '/kaggle/working/best_model.pt')\n        print(f'   -> best (EMA): {best_val_acc:.4f}')\n\nprint(f'\\nBest val acc: {best_val_acc:.4f} ({best_source})')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T10:21:30.106692Z","iopub.execute_input":"2026-06-16T10:21:30.106981Z","iopub.status.idle":"2026-06-16T11:16:26.597768Z","shell.execute_reply.started":"2026-06-16T10:21:30.106959Z","shell.execute_reply":"2026-06-16T11:16:26.596906Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(13, 4))\naxes[0].plot(history['train_loss'], label='train'); axes[0].plot(history['valid_loss'], label='valid')\naxes[0].set_title('Loss'); axes[0].legend(); axes[0].grid(alpha=0.3)\naxes[1].plot(history['train_acc'], label='train'); axes[1].plot(history['valid_acc'], label='valid')\naxes[1].plot(history['valid_acc_ema'], label='EMA', linestyle='--')\naxes[1].set_title('Accuracy'); axes[1].legend(); axes[1].grid(alpha=0.3)\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T11:16:26.599757Z","iopub.execute_input":"2026-06-16T11:16:26.600069Z","iopub.status.idle":"2026-06-16T11:16:26.938286Z","shell.execute_reply.started":"2026-06-16T11:16:26.600033Z","shell.execute_reply":"2026-06-16T11:16:26.937667Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.load_state_dict(torch.load('/kaggle/working/best_model.pt', map_location=device))\nmodel.to(device).eval()\nprint(f'Loaded best checkpoint | val acc = {best_val_acc:.4f} ({best_source})')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T11:16:26.939881Z","iopub.execute_input":"2026-06-16T11:16:26.940313Z","iopub.status.idle":"2026-06-16T11:16:27.016386Z","shell.execute_reply.started":"2026-06-16T11:16:26.940290Z","shell.execute_reply":"2026-06-16T11:16:27.015708Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------- MANDATORY (don't change this) --------------------------------------->\ntest_loss, test_acc, test_pred, test_true, test_probs = evaluate(model, test_loader, loss_fn, device)\nprint(f'Test loss: {test_loss:.4f}')\nprint(f'Test accuracy: {test_acc:.4f}')\n# <---------------------------- MANDATORY (don't change this) --------------------------------------","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T11:16:27.017313Z","iopub.execute_input":"2026-06-16T11:16:27.017526Z","iopub.status.idle":"2026-06-16T11:16:47.580780Z","shell.execute_reply.started":"2026-06-16T11:16:27.017508Z","shell.execute_reply":"2026-06-16T11:16:47.579692Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print('Running Fast TTA x8...')\ntta_loss, tta_acc, tta_pred, tta_true, tta_probs = evaluate_with_tta(\n    model, test_df, loss_fn, device, tta_transforms, batch_size=BATCH_SIZE\n)\nprint(f'\\nStandard test acc : {test_acc:.4f}')\nprint(f'TTA x8 test acc   : {tta_acc:.4f}')\nprint(f'TTA gain          : +{(tta_acc-test_acc)*100:.2f}%')\nfinal_pred, final_true, final_acc = tta_pred, tta_true, tta_acc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T11:16:47.583353Z","iopub.execute_input":"2026-06-16T11:16:47.583860Z","iopub.status.idle":"2026-06-16T11:20:51.712520Z","shell.execute_reply.started":"2026-06-16T11:16:47.583823Z","shell.execute_reply":"2026-06-16T11:20:51.711676Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(classification_report(final_true, final_pred, target_names=CLASS_ORDER))\ncm = confusion_matrix(final_true, final_pred)\nfig, ax = plt.subplots(figsize=(10,8))\nConfusionMatrixDisplay(cm, display_labels=CLASS_ORDER).plot(ax=ax, colorbar=False)\nplt.title(f'TTA x8 Test Accuracy: {final_acc:.4f}')\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-16T11:20:51.713657Z","iopub.execute_input":"2026-06-16T11:20:51.713990Z","iopub.status.idle":"2026-06-16T11:20:52.028318Z","shell.execute_reply.started":"2026-06-16T11:20:51.713959Z","shell.execute_reply":"2026-06-16T11:20:52.027698Z"}},"outputs":[],"execution_count":null}]}