{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":13836,"databundleVersionId":1718836,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 📌 CELL 1 — Import & cấu hình chung\n","metadata":{}},{"cell_type":"code","source":"# ===============================\n# 1) IMPORTS\n# ===============================\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nimport timm\n\nprint(\"[INFO] Imports loaded.\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:14.099330Z","iopub.execute_input":"2026-01-07T08:47:14.099736Z","iopub.status.idle":"2026-01-07T08:47:14.105722Z","shell.execute_reply.started":"2026-01-07T08:47:14.099693Z","shell.execute_reply":"2026-01-07T08:47:14.104957Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 2 — Đường dẫn dataset (chuẩn Kaggle)","metadata":{}},{"cell_type":"code","source":"ROOT = \"/kaggle/input/cassava-leaf-disease-classification\"\nCSV_PATH = os.path.join(ROOT, \"train.csv\")\nIMG_DIR = os.path.join(ROOT, \"train_images\")\n\nTEST_DIR = \"/kaggle/input/cassava-leaf-disease-classification/test_images\"\nSAMPLE_SUB = os.path.join(ROOT, \"sample_submission.csv\")\n\nWEIGHT_PATH = \"/kaggle/working/best_efficientnet_b3.pth\"\n\nprint(\"[INFO] Paths ready.\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:14.109044Z","iopub.execute_input":"2026-01-07T08:47:14.109314Z","iopub.status.idle":"2026-01-07T08:47:14.142668Z","shell.execute_reply.started":"2026-01-07T08:47:14.109292Z","shell.execute_reply":"2026-01-07T08:47:14.142020Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ndf = pd.read_csv(CSV_PATH)\n\nprint(df.head())\n\nclass_counts = df['label'].value_counts().sort_index()\n\nprint(\"\\nSố mẫu mỗi lớp:\")\nprint(class_counts)\n\nprint(\"\\nTổng ảnh:\", len(df))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:14.143861Z","iopub.execute_input":"2026-01-07T08:47:14.144216Z","iopub.status.idle":"2026-01-07T08:47:14.179744Z","shell.execute_reply.started":"2026-01-07T08:47:14.144193Z","shell.execute_reply":"2026-01-07T08:47:14.179165Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport cv2\nimport matplotlib.pyplot as plt\n\nlabel_map_path = os.path.join(ROOT, \"label_num_to_disease_map.json\")\n\nimport json\nwith open(label_map_path, \"r\") as f:\n    label_map = json.load(f)\n\nprint(label_map)\n\nfor label in sorted(df['label'].unique()):\n    subset = df[df['label'] == label].sample(1, random_state=42)\n\n    print(f\"\\nClass {label} — {label_map[str(label)]}\")\n\n    for _, row in subset.iterrows():\n        img_path = os.path.join(IMG_DIR, row[\"image_id\"])\n\n        img = cv2.imread(img_path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n        plt.imshow(img)\n        plt.title(f\"Class {label} — {label_map[str(label)]}\")\n        plt.axis(\"off\")\n        plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:14.180628Z","iopub.execute_input":"2026-01-07T08:47:14.180869Z","iopub.status.idle":"2026-01-07T08:47:15.051175Z","shell.execute_reply.started":"2026-01-07T08:47:14.180846Z","shell.execute_reply":"2026-01-07T08:47:15.050369Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 3 — Hyperparameters + seed","metadata":{}},{"cell_type":"code","source":"SEED = 42\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nBATCH_SIZE = 64\nFREEZE_EPOCHS = 5          # phase 1\nFINETUNE_EPOCHS = 30       # phase 2 + 3\nLR = 1e-3\nVAL_SPLIT = 0.15\nAMP = True\nEMA_DECAY = 0.999\n\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\n\nset_seed(SEED)\nprint(f\"[INFO] Using device: {DEVICE}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:15.053364Z","iopub.execute_input":"2026-01-07T08:47:15.053588Z","iopub.status.idle":"2026-01-07T08:47:15.061306Z","shell.execute_reply.started":"2026-01-07T08:47:15.053567Z","shell.execute_reply":"2026-01-07T08:47:15.060611Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 4 — Dataset class","metadata":{}},{"cell_type":"code","source":"class CassavaDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df.values\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        image_id, label = self.df[idx]\n        img_path = os.path.join(IMG_DIR, image_id)\n\n        image = np.array(Image.open(img_path).convert(\"RGB\"))\n\n        if self.transform:\n            image = self.transform(image=image)[\"image\"]\n\n        return image, torch.tensor(label, dtype=torch.long)\n\n\nprint(\"[INFO] Dataset class ready.\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:15.062159Z","iopub.execute_input":"2026-01-07T08:47:15.062412Z","iopub.status.idle":"2026-01-07T08:47:15.085671Z","shell.execute_reply.started":"2026-01-07T08:47:15.062391Z","shell.execute_reply":"2026-01-07T08:47:15.084959Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 5 — Augmentation + chia train/val","metadata":{}},{"cell_type":"code","source":"\n# train_tf = A.Compose([\n#     A.RandomResizedCrop(\n#         size=(300, 300),\n#         scale=(0.9, 1.0),\n#         ratio=(0.95, 1.05),\n#         p=1.0\n#     ),\n\n#     A.HorizontalFlip(p=0.4),\n\n#     A.RandomBrightnessContrast(\n#         brightness_limit=0.12,\n#         contrast_limit=0.12,\n#         p=0.4\n#     ),\n\n#     A.Affine(\n#         scale=(0.98, 1.02),\n#         translate_percent=(0.0, 0.03),\n#         rotate=(-7, 7),\n#         p=0.3\n#     ),\n\n#     A.GaussianBlur(blur_limit=(3, 3), p=0.15),\n\n#     A.Normalize(),\n#     ToTensorV2()\n# ])\n\ntrain_tf = A.Compose([\n    A.RandomResizedCrop(size=(300, 300), scale=(0.8, 1.0), p=1.0),\n\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.15),\n\n    A.ColorJitter(0.2, 0.2, 0.15, 0.02, p=0.7),\n\n    A.GaussNoise(var_limit=(10, 25), p=0.5),\n    A.Sharpen(alpha=(0.1, 0.4), p=0.4),\n\n    A.CoarseDropout(\n        max_holes=12,\n        max_height=50,\n        max_width=50,\n        p=0.6\n    ),\n\n    A.Normalize(),\n    ToTensorV2(),\n])\n\nval_tf = A.Compose([\n    A.Resize(300, 300),\n    A.Normalize(),\n    ToTensorV2()\n])\n\ndef cutmix(imgs, labels, alpha=1.0):\n    lam = np.random.beta(alpha, alpha)\n    batch = imgs.size(0)\n\n    idx = torch.randperm(batch)\n\n    bbx1 = np.random.randint(0, imgs.size(2))\n    bby1 = np.random.randint(0, imgs.size(3))\n\n    cut_w = int(imgs.size(2) * np.sqrt(1 - lam))\n    cut_h = int(imgs.size(3) * np.sqrt(1 - lam))\n\n    x1 = np.clip(bbx1 - cut_w // 2, 0, imgs.size(2))\n    y1 = np.clip(bby1 - cut_h // 2, 0, imgs.size(3))\n    x2 = np.clip(bbx1 + cut_w // 2, 0, imgs.size(2))\n    y2 = np.clip(bby1 + cut_h // 2, 0, imgs.size(3))\n\n    imgs[:, :, x1:x2, y1:y2] = imgs[idx, :, x1:x2, y1:y2]\n    lam = 1 - ((x2 - x1) * (y2 - y1) / (imgs.size(-1) * imgs.size(-2)))\n\n    return imgs, labels, labels[idx], lam\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:15.086549Z","iopub.execute_input":"2026-01-07T08:47:15.086837Z","iopub.status.idle":"2026-01-07T08:47:15.111706Z","shell.execute_reply.started":"2026-01-07T08:47:15.086805Z","shell.execute_reply":"2026-01-07T08:47:15.110952Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = pd.read_csv(CSV_PATH)\n\nfrom sklearn.model_selection import StratifiedShuffleSplit\n\nsplitter = StratifiedShuffleSplit(\n    n_splits=1,\n    test_size=VAL_SPLIT,\n    random_state=42\n)\n\nfor train_idx, val_idx in splitter.split(df, df[\"label\"]):\n    train_df = df.iloc[train_idx]\n    val_df = df.iloc[val_idx]\n\nprint(train_df[\"label\"].value_counts())\nprint(val_df[\"label\"].value_counts())\n\ntrain_dataset = CassavaDataset(train_df, train_tf)\nval_dataset   = CassavaDataset(val_df, val_tf)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:15.112678Z","iopub.execute_input":"2026-01-07T08:47:15.113000Z","iopub.status.idle":"2026-01-07T08:47:15.149573Z","shell.execute_reply.started":"2026-01-07T08:47:15.112967Z","shell.execute_reply":"2026-01-07T08:47:15.148701Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 6 — DataLoader (tối ưu tốc độ)","metadata":{}},{"cell_type":"code","source":"train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,\n                          num_workers=4, pin_memory=False)\n\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False,\n                        num_workers=4, pin_memory=False)\n\nprint(\"[INFO] DataLoaders ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:15.150539Z","iopub.execute_input":"2026-01-07T08:47:15.150814Z","iopub.status.idle":"2026-01-07T08:47:15.155924Z","shell.execute_reply.started":"2026-01-07T08:47:15.150791Z","shell.execute_reply":"2026-01-07T08:47:15.155157Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 7 — Model + Optimizer + Scheduler","metadata":{}},{"cell_type":"code","source":"import timm\nimport torch\nimport torch.nn as nn\nimport numpy as np\nfrom sklearn.utils.class_weight import compute_class_weight\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nAMP = True\nLR = 1e-3\n\nmodel = timm.create_model(\n    \"seresnext50_32x4d\",\n    pretrained=True,\n    num_classes=5\n).to(DEVICE)\n\ndef init_head(m):\n    if isinstance(m, nn.Linear):\n        nn.init.kaiming_normal_(m.weight, nonlinearity=\"relu\")\n        if m.bias is not None:\n            nn.init.zeros_(m.bias)\n\nmodel.get_classifier().apply(init_head)\n\n# ---------- FREEZE BACKBONE (phase 1) ----------\nfor name, p in model.named_parameters():\n    if \"fc\" not in name and \"classifier\" not in name:\n        p.requires_grad = False\n\n\n# ===============================\n# 2) CLASS WEIGHTS\n# ===============================\n\nlabels = df[\"label\"].values\n\nclass_weights = compute_class_weight(\n    class_weight=\"balanced\",\n    classes=np.unique(labels),\n    y=labels\n)\n\nclass_weights = torch.tensor(class_weights, dtype=torch.float32).to(DEVICE)\n\ncriterion = nn.CrossEntropyLoss(\n    weight=class_weights,\n    label_smoothing=0.05\n)\n\n# ===============================\n# 3) OPTIMIZER + SCHEDULER\n# ===============================\n\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=LR,\n    weight_decay=1e-4\n)\n\ncosine = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(\n    optimizer,\n    T_0=5,\n)\n\nplateau = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer,\n    mode=\"min\",\n    patience=2,\n    factor=0.3\n)\n\nscaler = torch.cuda.amp.GradScaler(enabled=AMP)\n\n# ---------- EMA ----------\nimport copy\n\nema_model = copy.deepcopy(model)\nfor p in ema_model.parameters():\n    p.requires_grad = False\n\ndef update_ema():\n    with torch.no_grad():\n        for p, p_ema in zip(model.parameters(), ema_model.parameters()):\n            p_ema.data.mul_(EMA_DECAY).add_(p.data * (1 - EMA_DECAY))\n\nprint(\"[INFO] Training setup ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:15.156863Z","iopub.execute_input":"2026-01-07T08:47:15.157098Z","iopub.status.idle":"2026-01-07T08:47:15.845908Z","shell.execute_reply.started":"2026-01-07T08:47:15.157077Z","shell.execute_reply":"2026-01-07T08:47:15.845079Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 8 — Hàm train & validate","metadata":{}},{"cell_type":"code","source":"\ndef run_epoch(loader, training=True, use_cutmix=False):\n    total_loss, correct, total = 0, 0, 0\n\n    model.train() if training else model.eval()\n\n    pbar = tqdm(loader)\n\n    with torch.set_grad_enabled(training):\n        for imgs, labels in pbar:\n            imgs, labels = imgs.to(DEVICE), labels.to(DEVICE)\n\n            if training:\n                optimizer.zero_grad()\n\n            if training and use_cutmix and np.random.rand() < 0.5:\n                imgs, y1, y2, lam = cutmix(imgs, labels)\n                with torch.cuda.amp.autocast(enabled=AMP):\n                    outputs = model(imgs)\n                    loss = lam * criterion(outputs, y1) + (1 - lam) * criterion(outputs, y2)\n            else:\n                with torch.cuda.amp.autocast(enabled=AMP):\n                    outputs = model(imgs)\n                    loss = criterion(outputs, labels)\n\n            if training:\n                scaler.scale(loss).backward()\n                scaler.step(optimizer)\n                scaler.update()\n                update_ema()\n\n            total_loss += loss.item() * imgs.size(0)\n            preds = outputs.argmax(1)\n            correct += (preds == labels).sum().item()\n            total += labels.size(0)\n\n    return total_loss/total, correct/total\n\nprint(\"[INFO] Defining 1 training epoch\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:15.847953Z","iopub.execute_input":"2026-01-07T08:47:15.848329Z","iopub.status.idle":"2026-01-07T08:47:15.856521Z","shell.execute_reply.started":"2026-01-07T08:47:15.848299Z","shell.execute_reply":"2026-01-07T08:47:15.855921Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 9 — Training loop","metadata":{}},{"cell_type":"code","source":"history = {\n    \"train_loss\": [], \"val_loss\": [],\n    \"train_acc\":  [], \"val_acc\": []\n}\n\nbest_acc = 0\n\n\n# ============================\n# 🔹 PHASE 1 — FREEZE BACKBONE\n# ============================\nprint(\"\\n====== PHASE 1: FREEZE BACKBONE ======\\n\")\n\nfor epoch in range(FREEZE_EPOCHS):\n\n    tr_loss, tr_acc = run_epoch(train_loader, True, use_cutmix=True)\n    va_loss, va_acc = run_epoch(val_loader, False)\n\n    cosine.step()\n\n    history[\"train_loss\"].append(tr_loss)\n    history[\"val_loss\"].append(va_loss)\n    history[\"train_acc\"].append(tr_acc)\n    history[\"val_acc\"].append(va_acc)\n\n    print(f\"[P1][Epoch {epoch+1}/{FREEZE_EPOCHS}] \"\n          f\"Train: loss={tr_loss:.4f}, acc={tr_acc:.4f} | \"\n          f\"Val: loss={va_loss:.4f}, acc={va_acc:.4f}\")\n\n    if va_acc > best_acc:\n        best_acc = va_acc\n        torch.save(ema_model.state_dict(), WEIGHT_PATH)\n        print(f\"🔥 New best (P1) — acc={best_acc:.4f}\")\n\n\n# ===============================\n# 🔹 PHASE 2/3 — UNFREEZE + FINETUNE\n# ===============================\nprint(\"\\n====== PHASE 2/3: UNFREEZE & FINETUNE ======\\n\")\n\nfor p in model.parameters():\n    p.requires_grad = True\n\noptimizer.param_groups[0][\"lr\"] = 1e-4\n\n\nfor epoch in range(FINETUNE_EPOCHS):\n\n    tr_loss, tr_acc = run_epoch(train_loader, True, use_cutmix=True)\n    va_loss, va_acc = run_epoch(val_loader, False)\n\n    plateau.step(va_loss)\n\n    history[\"train_loss\"].append(tr_loss)\n    history[\"val_loss\"].append(va_loss)\n    history[\"train_acc\"].append(tr_acc)\n    history[\"val_acc\"].append(va_acc)\n\n    print(f\"[P2][Epoch {epoch+1}/{FINETUNE_EPOCHS}] \"\n          f\"Train: loss={tr_loss:.4f}, acc={tr_acc:.4f} | \"\n          f\"Val: loss={va_loss:.4f}, acc={va_acc:.4f}\")\n\n    if va_acc > best_acc:\n        best_acc = va_acc\n        torch.save(ema_model.state_dict(), WEIGHT_PATH)\n        print(f\"🔥 New best (P2) — acc={best_acc:.4f}\")\n\n\nprint(f\"\\n🎯 Done — BEST VAL ACC = {best_acc:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-07T08:47:15.857427Z","iopub.execute_input":"2026-01-07T08:47:15.857720Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(12,4))\n\n# ---- Loss ----\nplt.subplot(1,2,1)\nplt.plot(history[\"train_loss\"], label=\"Train loss\")\nplt.plot(history[\"val_loss\"], label=\"Val loss\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.title(\"Loss\")\n\n# ---- Accuracy ----\nplt.subplot(1,2,2)\nplt.plot(history[\"train_acc\"], label=\"Train acc\")\nplt.plot(history[\"val_acc\"], label=\"Val acc\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy\")\nplt.legend()\nplt.title(\"Accuracy\")\n\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 10 — Load weight của best model","metadata":{}},{"cell_type":"code","source":"print(f\"\\n[INFO] Loading weights: {WEIGHT_PATH}\")\n\nmodel.load_state_dict(\n    torch.load(WEIGHT_PATH, map_location=DEVICE)\n)\nmodel.eval()\n\nprint(\"[INFO] Model loaded successfully!\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌CELL 11 - Load Testset\n","metadata":{}},{"cell_type":"code","source":"class CassavaTestDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        image_id = self.df.iloc[idx].image_id\n        path = os.path.join(TEST_DIR, image_id)\n\n        image = np.array(Image.open(path).convert(\"RGB\"))\n\n        if self.transform:\n            image = self.transform(image=image)[\"image\"]\n\n        return image_id, image\n\n\ntest_tf = A.Compose([\n    A.Resize(300, 300),\n    A.Normalize(),\n    ToTensorV2()\n])\n\nsample_sub = pd.read_csv(SAMPLE_SUB)\ntest_dataset = CassavaTestDataset(sample_sub, test_tf)\n\ntest_loader = DataLoader(test_dataset, batch_size=64,\n                         shuffle=False, num_workers=2)\n\npreds = []\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌CELL 12 - Predicting","metadata":{}},{"cell_type":"code","source":"tta_tf = [\n    A.Compose([A.Resize(300,300), A.Normalize(), ToTensorV2()]),\n    A.Compose([A.Resize(300,300), A.HorizontalFlip(p=1), A.Normalize(), ToTensorV2()])\n]\n\ndef tta_predict(img):\n    preds = []\n    with torch.no_grad():\n        for tf in tta_tf:\n            x = tf(image=img)[\"image\"].unsqueeze(0).to(DEVICE)\n            preds.append(model(x))\n    return torch.mean(torch.stack(preds), dim=0)\n    \nwith torch.no_grad():\n    for image_ids, images in tqdm(test_loader):\n        batch_preds = []\n        for img in images:\n            img_np = img.permute(1,2,0).cpu().numpy()\n            p = tta_predict(img_np)\n            batch_preds.append(p)\n\n        batch_preds = torch.cat(batch_preds, dim=0)\n        labels = batch_preds.argmax(1).cpu().numpy()\n\n        for img, lab in zip(image_ids, labels):\n            preds.append((img, int(lab)))\n\n\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌CELL 13 - Submission","metadata":{}},{"cell_type":"code","source":"submission = pd.DataFrame(preds, columns=[\"image_id\", \"label\"])\nsubmission.to_csv(\"/kaggle/working/submission.csv\", index=False)\n\nprint(\"\\n🎯 DONE — saved -> /kaggle/working/submission.csv\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}