{"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,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14528683,"sourceType":"datasetVersion","datasetId":9279406}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 📌 CELL 0 - ENV & LIBRARIES\n","metadata":{}},{"cell_type":"code","source":"import os, gc, math, random, json\nimport numpy as np\nimport pandas as pd\nfrom collections import defaultdict\n\nimport cv2\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nimport timm\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import accuracy_score\n\nprint(\"Done\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:42:33.739108Z","iopub.execute_input":"2026-01-17T14:42:33.739409Z","iopub.status.idle":"2026-01-17T14:42:33.745331Z","shell.execute_reply.started":"2026-01-17T14:42:33.739384Z","shell.execute_reply":"2026-01-17T14:42:33.744635Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 1 - GLOBAL CONFIG \n","metadata":{}},{"cell_type":"code","source":"SEED = 42\nN_FOLDS = 5\nEPOCHS = 8\nNUM_CLASSES = 5\n\n# -------- ResNet --------\nRESNET_IMG = 384\nRESNET_LR = 3e-4\n\nBATCH_SIZE = 64\nNUM_WORKERS = 4\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\n\nprint(\"Done\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:42:33.746633Z","iopub.execute_input":"2026-01-17T14:42:33.746939Z","iopub.status.idle":"2026-01-17T14:42:33.761506Z","shell.execute_reply.started":"2026-01-17T14:42:33.746918Z","shell.execute_reply":"2026-01-17T14:42:33.760768Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 2 -  ROOT DATA DIR (KAGGLE) \n","metadata":{}},{"cell_type":"code","source":"\nDATA_DIR = \"/kaggle/input/cassava-leaf-disease-classification\"\n\nTRAIN_CSV = f\"{DATA_DIR}/train.csv\"\nTEST_CSV  = f\"{DATA_DIR}/sample_submission.csv\"\n\nTRAIN_IMG_DIR = f\"{DATA_DIR}/train_images\"\nTEST_IMG_DIR  = f\"{DATA_DIR}/test_images\"\n\nCKPT_DIR = \"/kaggle/working/checkpoints/resnet\"   # resnet_fold{i}_best.pth\n\nNUM_CLASSES = 5\n\nprint(\"Train images:\", len(os.listdir(TRAIN_IMG_DIR)))\nprint(\"Test images :\", len(os.listdir(TEST_IMG_DIR)))\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:42:33.787764Z","iopub.execute_input":"2026-01-17T14:42:33.788348Z","iopub.status.idle":"2026-01-17T14:42:33.799393Z","shell.execute_reply.started":"2026-01-17T14:42:33.788327Z","shell.execute_reply":"2026-01-17T14:42:33.798691Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 3 - READ DATASET","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\ntrain_df = pd.read_csv(TRAIN_CSV)\n\nprint(train_df.head())\nprint(\"Total samples:\", len(train_df))\nprint(train_df['label'].value_counts().sort_index())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:42:33.800654Z","iopub.execute_input":"2026-01-17T14:42:33.800833Z","iopub.status.idle":"2026-01-17T14:42:33.820872Z","shell.execute_reply.started":"2026-01-17T14:42:33.800816Z","shell.execute_reply":"2026-01-17T14:42:33.820348Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 4 - DATASET VISUALIZATION\n","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(6,4))\ntrain_df['label'].value_counts().sort_index().plot(kind='bar')\nplt.title('Class Distribution')\nplt.show()\n\nfig, axes = plt.subplots(1, NUM_CLASSES, figsize=(15,3))\nfor i in range(NUM_CLASSES):\n    img_id = train_df[train_df.label==i].iloc[0].image_id\n    img = Image.open(f\"{DATA_DIR}/train_images/{img_id}\")\n    axes[i].imshow(img)\n    axes[i].set_title(f\"Class {i}\")\n    axes[i].axis('off')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:42:33.836586Z","iopub.execute_input":"2026-01-17T14:42:33.836855Z","iopub.status.idle":"2026-01-17T14:42:34.289758Z","shell.execute_reply.started":"2026-01-17T14:42:33.836835Z","shell.execute_reply":"2026-01-17T14:42:34.288999Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 5 - AUGMENTATIONS\n","metadata":{}},{"cell_type":"code","source":"def get_resnet_aug():\n    return A.Compose([\n        A.RandomResizedCrop(size=(RESNET_IMG, RESNET_IMG), scale=(0.8,1.0)),\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.2),          # giảm từ 0.5 xuống 0.2\n        A.Affine(scale=(0.95,1.05), rotate=(-10,10), p=0.3),  # nhẹ hơn, ít tốn CPU\n        A.Normalize(),\n        ToTensorV2()\n    ])\n\nvalid_resnet_aug = A.Compose([\n    A.Resize(RESNET_IMG, RESNET_IMG),\n    A.Normalize(),\n    ToTensorV2()\n])\n\nprint(\"Done\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:42:34.291224Z","iopub.execute_input":"2026-01-17T14:42:34.291448Z","iopub.status.idle":"2026-01-17T14:42:34.297694Z","shell.execute_reply.started":"2026-01-17T14:42:34.291429Z","shell.execute_reply":"2026-01-17T14:42:34.297039Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 6 - DEFINE DATASET CLASS\n","metadata":{}},{"cell_type":"code","source":"class CassavaDataset(Dataset):\n    def __init__(self, df, aug):\n        self.df = df.reset_index(drop=True)\n        self.aug = aug\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img = cv2.imread(f\"{DATA_DIR}/train_images/{row.image_id}\")\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = self.aug(image=img)['image']\n        return img, row.label\n\nprint(\"Done\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:42:34.298552Z","iopub.execute_input":"2026-01-17T14:42:34.298814Z","iopub.status.idle":"2026-01-17T14:42:34.311507Z","shell.execute_reply.started":"2026-01-17T14:42:34.298792Z","shell.execute_reply":"2026-01-17T14:42:34.310971Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 7 - DATA LOADER\n","metadata":{}},{"cell_type":"code","source":"from torch.utils.data import DataLoader\n\n# ⚡ SPEED HACK (global)\ntorch.backends.cudnn.benchmark = True\n\ndef get_dataloader(\n    df,\n    transforms,\n    batch_size,\n    shuffle=True,\n    num_workers=2\n):\n    dataset = CassavaDataset(df, transforms)\n\n    return DataLoader(\n        dataset,\n        batch_size=batch_size,\n        shuffle=shuffle,\n        num_workers=num_workers,\n        pin_memory=True,\n        persistent_workers=True\n    )\nprint(\"Done\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:42:34.312383Z","iopub.execute_input":"2026-01-17T14:42:34.312896Z","iopub.status.idle":"2026-01-17T14:42:34.322749Z","shell.execute_reply.started":"2026-01-17T14:42:34.312875Z","shell.execute_reply":"2026-01-17T14:42:34.322005Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 8 - DEFINE TRAIN / VALID LOOP\n","metadata":{}},{"cell_type":"code","source":"def train_one_epoch(\n    model, loader, optimizer, criterion, device, scaler=None\n):\n    model.train()\n    \n    running_loss = 0.0\n    correct = 0\n    total = 0\n\n    pbar = tqdm(loader, desc=\"Train\", leave=False)\n\n    for images, labels in pbar:\n        images = images.to(device)\n        labels = labels.to(device)\n\n        optimizer.zero_grad(set_to_none=True)\n\n        with torch.cuda.amp.autocast(enabled=scaler is not None):\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n        if scaler:\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n        else:\n            loss.backward()\n            optimizer.step()\n\n        # ===== METRICS =====\n        running_loss += loss.item() * labels.size(0)\n        preds = outputs.argmax(dim=1)\n\n        correct += (preds == labels).sum().item()\n        total += labels.size(0)\n\n        acc = correct / total\n        avg_loss = running_loss / total\n\n        # ===== UPDATE PROGRESS BAR =====\n        pbar.set_postfix({\n            \"loss\": f\"{avg_loss:.4f}\",\n            \"acc\": f\"{acc:.4f}\"\n        })\n\n    return avg_loss, acc\n\n\n@torch.no_grad()\ndef validate_one_epoch(model, loader, criterion, device):\n    model.eval()\n\n    running_loss = 0.0\n    correct = 0\n    total = 0\n\n    pbar = tqdm(loader, desc=\"Valid\", leave=False)\n\n    for images, labels in pbar:\n        images = images.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True)\n\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n\n        running_loss += loss.item() * labels.size(0)\n        preds = outputs.argmax(dim=1)\n\n        correct += (preds == labels).sum().item()\n        total += labels.size(0)\n\n        acc = correct / total\n        avg_loss = running_loss / total\n\n        pbar.set_postfix({\n            \"val_loss\": f\"{avg_loss:.4f}\",\n            \"val_acc\": f\"{acc:.4f}\"\n        })\n\n    return avg_loss, acc\nprint(\"Done\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:42:34.324353Z","iopub.execute_input":"2026-01-17T14:42:34.324630Z","iopub.status.idle":"2026-01-17T14:42:34.334869Z","shell.execute_reply.started":"2026-01-17T14:42:34.324611Z","shell.execute_reply":"2026-01-17T14:42:34.334172Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 9 — RESNET 5-FOLD TRAINING \n","metadata":{}},{"cell_type":"code","source":"skf = StratifiedKFold(N_FOLDS, shuffle=True, random_state=SEED)\nresnet_hist = defaultdict(list)\n\nos.makedirs('checkpoints/resnet', exist_ok=True)\n\nfor fold,(tr,va) in enumerate(skf.split(train_df, train_df.label)):\n    print(f\"\\n=== ResNet Fold {fold+1} ===\")\n\n    train_loader = get_dataloader(\n        train_df.iloc[tr],\n        get_resnet_aug(),\n        BATCH_SIZE,\n        shuffle=True,\n        num_workers=NUM_WORKERS\n    )\n\n    valid_loader = get_dataloader(\n        train_df.iloc[va],\n        valid_resnet_aug,\n        BATCH_SIZE,\n        shuffle=False,\n        num_workers=NUM_WORKERS\n    )\n\n    WEIGHT_PATH = \"/kaggle/input/resnet-imagenet/resnet50d_imagenet.pth\"\n    \n    model = timm.create_model(\n        'resnet50d',\n        pretrained=False,      # ❗ KHÔNG load online\n        num_classes=NUM_CLASSES\n    ).to(DEVICE)\n    \n    state = torch.load(WEIGHT_PATH, map_location=DEVICE)\n    \n    # 🔥 REMOVE HEAD CŨ (fc)\n    state = {\n        k: v for k, v in state.items()\n        if not k.startswith(\"fc\")\n    }\n    \n    model.load_state_dict(state, strict=False)\n    \n    print(\"Loaded pretrained ResNet50d weights from file\")\n\n    crit = nn.CrossEntropyLoss()\n    opt = torch.optim.AdamW(model.parameters(), lr=RESNET_LR)\n    sch = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, 'max', patience=2)\n    scaler = GradScaler()\n\n    best_acc = 0.0\n\n    for e in range(EPOCHS):\n        tl, ta = train_one_epoch(\n            model,\n            train_loader,   # ✅ tên đúng\n            opt,\n            crit,\n            DEVICE,\n            scaler\n        )\n\n        vl, vaa = validate_one_epoch(\n            model,\n            valid_loader,   # ✅ tên đúng\n            crit,\n            DEVICE\n        )\n\n        sch.step(vaa)\n\n        print(f\"Epoch {e+1}: train_acc={ta:.4f}, val_acc={vaa:.4f}\")\n\n        if vaa > best_acc:\n            best_acc = vaa\n            print(\"Save new best model\")\n            torch.save(\n                model.state_dict(),\n                f\"checkpoints/resnet/resnet_fold{fold}_best.pth\"\n            )\n\n    del model\n    gc.collect()\n    torch.cuda.empty_cache()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:42:34.335687Z","iopub.execute_input":"2026-01-17T14:42:34.335953Z","iopub.status.idle":"2026-01-17T14:56:11.121152Z","shell.execute_reply.started":"2026-01-17T14:42:34.335925Z","shell.execute_reply":"2026-01-17T14:56:11.120329Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 📌 CELL 10 - SUBMIT TO COMPETITION","metadata":{}},{"cell_type":"code","source":"# =====================\n# LOAD DATA\n# =====================\ntrain_df = pd.read_csv(TRAIN_CSV)\ntest_df  = pd.read_csv(TEST_CSV)\nlabels = train_df.label.values\n\n# =====================\n# AUGMENTATION (NO RANDOM!)\n# =====================\nvalid_aug = A.Compose([\n    A.Resize(RESNET_IMG, RESNET_IMG),\n    A.Normalize(),\n    ToTensorV2()\n])\n\n# =====================\n# DATASET\n# =====================\nclass CassavaDataset(Dataset):\n    def __init__(self, df, img_dir, aug, is_test=False):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.aug = aug\n        self.is_test = is_test\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = f\"{self.img_dir}/{row.image_id}\"\n        img = cv2.imread(img_path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        img = self.aug(image=img)['image']\n\n        if self.is_test:\n            return img\n        return img, row.label\n\n# =====================\n# INIT STORAGE\n# =====================\noof_pred  = np.zeros((len(train_df), NUM_CLASSES), dtype=np.float32)\ntest_pred = np.zeros((len(test_df), NUM_CLASSES), dtype=np.float32)\n\n# =====================\n# STRATIFIED K-FOLD\n# =====================\nskf = StratifiedKFold(N_FOLDS, shuffle=True, random_state=SEED)\n\n# =====================\n# LOOP OVER FOLDS\n# =====================\nfor fold, (_, val_idx) in enumerate(skf.split(train_df, train_df.label)):\n    print(f\"\\n=== LOAD MODEL FOLD {fold} ===\")\n\n    # ---------- LOAD MODEL ----------\n    model = timm.create_model(\n        'resnet50d',\n        pretrained=False,\n        num_classes=NUM_CLASSES\n    ).to(DEVICE)\n\n    ckpt_path = f\"{CKPT_DIR}/resnet_fold{fold}_best.pth\"\n    model.load_state_dict(torch.load(ckpt_path, map_location=DEVICE))\n    model.eval()\n\n    # ---------- VALID OOF ----------\n    val_df = train_df.iloc[val_idx]\n    val_ds = CassavaDataset(val_df, TRAIN_IMG_DIR, valid_aug)\n    val_loader = DataLoader(\n        val_ds,\n        batch_size=BATCH_SIZE,\n        shuffle=False,\n        num_workers=NUM_WORKERS,\n        pin_memory=True\n    )\n\n    ptr = 0\n    with torch.no_grad():\n        for imgs, _ in tqdm(val_loader, leave=False):\n            imgs = imgs.to(DEVICE)\n            probs = torch.softmax(model(imgs), dim=1).cpu().numpy()\n            bs = probs.shape[0]\n            oof_pred[val_idx[ptr:ptr+bs]] += probs\n            ptr += bs\n\n    # ---------- TEST PRED ----------\n    test_ds = CassavaDataset(test_df, TEST_IMG_DIR, valid_aug, is_test=True)\n    test_loader = DataLoader(\n        test_ds,\n        batch_size=BATCH_SIZE,\n        shuffle=False,\n        num_workers=NUM_WORKERS,\n        pin_memory=True\n    )\n\n    fold_test_pred = []\n    with torch.no_grad():\n        for imgs in tqdm(test_loader, leave=False):\n            imgs = imgs.to(DEVICE)\n            probs = torch.softmax(model(imgs), dim=1).cpu().numpy()\n            fold_test_pred.append(probs)\n\n    test_pred += np.vstack(fold_test_pred)\n\n    del model\n    gc.collect()\n    torch.cuda.empty_cache()\n\n# =====================\n# AVERAGE\n# =====================\noof_pred  /= N_FOLDS\ntest_pred /= N_FOLDS\n\n# =====================\n# OOF ACC\n# =====================\noof_label = oof_pred.argmax(axis=1)\nacc = accuracy_score(labels, oof_label)\nprint(\"\\n🔥 FINAL OOF ACC:\", acc)\n\n# =====================\n# SUBMISSION\n# =====================\ntest_df['label'] = test_pred.argmax(axis=1)\ntest_df.to_csv(\"submission.csv\", index=False)\nprint(\"✅ Saved submission.csv\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-17T14:56:11.122457Z","iopub.execute_input":"2026-01-17T14:56:11.122699Z","iopub.status.idle":"2026-01-17T14:59:35.405972Z","shell.execute_reply.started":"2026-01-17T14:56:11.122667Z","shell.execute_reply":"2026-01-17T14:59:35.405378Z"}},"outputs":[],"execution_count":null}]}