{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":13836,"databundleVersionId":1718836,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<div style=\"background-color:#005f73; padding:20px; border-radius:10px; border: 2px solid #0a9396\">\n    <h1 style=\"color:white; text-align:center; font-family:Verdana;\">\n        🌱 CLASSIFICATION OF CASSAVA LEAF DISEASES\n    </h1>\n    <h3 style=\"color:#e9d8a6; text-align:center;\">\n        Comparative Analysis: CNN (ResNet, EfficientNet) vs Vision Transformer (ViT)\n    </h3>\n    <p style=\"color:white; text-align:center; font-style:italic;\">\n        <b>Author:</b> Jane Chrestella Marutotamtama | <b>NIM:</b> 2602683244\n    </p>\n</div>","metadata":{}},{"cell_type":"code","source":"import cv2, time, random, timm, json\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast, GradScaler\nfrom torchvision import transforms\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import accuracy_score, f1_score, confusion_matrix\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport matplotlib.pyplot as plt\nimport matplotlib.image as mpimg\nimport seaborn as sns\nsns.set_theme(style=\"whitegrid\", context=\"paper\", font_scale=1.1)\n\nfrom tensorflow.keras.models import Sequential, Model\nfrom tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization, Input, GlobalAveragePooling2D\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau, EarlyStopping\nimport warnings\nwarnings.filterwarnings('ignore')\nimport os\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#     for filename in filenames:\n#         print(os.path.join(dirname, filename))\n\ndata_path = Path('/kaggle/input/cassava-leaf-disease-classification/')\ntrain_df = pd.read_csv(data_path/'train.csv')\n\n# Load label map\nwith open(data_path/'label_num_to_disease_map.json') as f:\n    label_map = json.load(f)\n\n# Convert label numbers to strings before mapping\ntrain_df['label_name'] = train_df['label'].apply(lambda x: label_map[str(x)])\ntrain_df['filepath'] = train_df['image_id'].apply(lambda x: str(data_path/'train_images'/x))\nnum_classes = train_df['label'].nunique()\n\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T11:52:36.285629Z","iopub.execute_input":"2025-12-17T11:52:36.286263Z","iopub.status.idle":"2025-12-17T11:52:36.418273Z","shell.execute_reply.started":"2025-12-17T11:52:36.286236Z","shell.execute_reply":"2025-12-17T11:52:36.417686Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(8,5))\nsns.countplot(\n    x='label_name', \n    data=train_df, \n    order=train_df['label_name'].value_counts().index,\n    palette='viridis'\n)\n\nplt.title('Distribution of Cassava Leaf Disease Classes', fontsize=14)\nplt.xlabel('Disease Type', fontsize=12)\nplt.ylabel('Number of Images', fontsize=12)\nplt.xticks(rotation=30, ha='right')\nplt.show ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T11:52:37.573270Z","iopub.execute_input":"2025-12-17T11:52:37.574000Z","iopub.status.idle":"2025-12-17T11:52:37.871144Z","shell.execute_reply.started":"2025-12-17T11:52:37.573951Z","shell.execute_reply":"2025-12-17T11:52:37.870417Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dari grafik di atas, terlihat ketidakseimbangan kelas yang ekstrem (Severe Class Imbalance). Penyakit CMD mendominasi 61.5% data, sedangkan CBB hanya 5%. Oleh karena itu, kita akan menggunakan Stratified K-Fold agar validasi adil.","metadata":{}},{"cell_type":"code","source":"def show_samples(df, n=10):\n    sample = df.sample(n)\n    plt.figure(figsize=(12,8))\n    for i,(fp,lab) in enumerate(zip(sample['filepath'], sample['label'])):\n        plt.subplot(2, n//2, i+1); plt.imshow(plt.imread(fp)); plt.axis('off')\n        plt.title(f\"{lab}: {label_map[str(lab)]}\")\n    plt.tight_layout(); plt.show()\n\nshow_samples(train_df, n=8)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T11:52:40.082497Z","iopub.execute_input":"2025-12-17T11:52:40.082768Z","iopub.status.idle":"2025-12-17T11:52:41.867439Z","shell.execute_reply.started":"2025-12-17T11:52:40.082745Z","shell.execute_reply":"2025-12-17T11:52:41.866491Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Main Program\n## Berikut adalah konfigurasi hyperparameter untuk eksperimen. Kita menggunakan ukuran gambar 224x224 untuk efisiensi dan Batch Size 32 untuk memaksimalkan GPU T4.","metadata":{}},{"cell_type":"code","source":"# --- KONFIGURASI UTAMA (CFG) ---\nclass CFG:\n    seed = 42\n    models_to_compare = ['resnet50', 'tf_efficientnet_b0_ns', 'vit_base_patch16_224']\n    img_size = 224      # Ukuran gambar input\n    num_classes = 5     # Jumlah kategori penyakit (0-4)\n    batch_size = 32     # Sesuaikan dengan VRAM\n    epochs = 25          # Jumlah putaran training\n    patience = 3\n    lr = 1e-4           # Learning Rate\n    weight_decay = 1e-6\n    num_workers = 0 #4     # Sesuai core CPU laptop\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    target_size = 5     # Sama dengan num_classes\n    label_smoothing = 0.1\n    \n# Kunci Randomness agar hasil bisa diulang (Reproducible)\ndef seed_everything(seed):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nseed_everything(CFG.seed)\nprint(f\"Device yang digunakan: {CFG.device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T11:52:45.925746Z","iopub.execute_input":"2025-12-17T11:52:45.926390Z","iopub.status.idle":"2025-12-17T11:52:46.009208Z","shell.execute_reply.started":"2025-12-17T11:52:45.926362Z","shell.execute_reply":"2025-12-17T11:52:46.008473Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- AUGMENTASI DATA ---\ndef get_transforms(data):\n    if data == 'train':\n        return A.Compose([\n            A.Resize(CFG.img_size, CFG.img_size),\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=0.5),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2(),\n        ])\n    elif data == 'valid':\n        return A.Compose([\n            A.Resize(CFG.img_size, CFG.img_size),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2(),\n        ])\n\n# --- DATASET CLASS ---\nclass CassavaDataset(Dataset):\n    def __init__(self, df, transform=None):\n        self.df = df\n        self.file_paths = df['path'].values \n        self.labels = df['label'].values\n        self.transform = transform\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        file_path = self.file_paths[idx]\n        image = cv2.imread(file_path)\n        if image is None:\n            image = np.zeros((CFG.img_size, CFG.img_size, 3), dtype=np.uint8)\n        else:\n            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n            \n        if self.transform:\n            augmented = self.transform(image=image)\n            image = augmented['image']\n            \n        label = torch.tensor(self.labels[idx]).long()\n        return image, label\n\n# --- FUNGSI TRAIN & VALID ---\ndef train_one_epoch(model, optimizer, scheduler, dataloader, device, epoch):\n    model.train()\n    scaler = GradScaler()\n    dataset_size = 0\n    running_loss = 0.0\n    criterion = nn.CrossEntropyLoss(label_smoothing=CFG.label_smoothing)\n    \n    for step, (images, labels) in enumerate(dataloader):\n        images = images.to(device)\n        labels = labels.to(device)\n        batch_size = images.size(0)\n        \n        with autocast():\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        optimizer.zero_grad()\n        \n        running_loss += (loss.item() * batch_size)\n        dataset_size += batch_size\n\n    if scheduler is not None:\n        scheduler.step()\n        \n    return running_loss / dataset_size\n\ndef valid_one_epoch(model, dataloader, device):\n    model.eval()\n    running_loss = 0.0\n    dataset_size = 0\n    preds = []\n    valid_labels = []\n    \n    criterion = nn.CrossEntropyLoss() \n    \n    with torch.no_grad():\n        for images, labels in dataloader:\n            images = images.to(device)\n            labels = labels.to(device)\n            batch_size = images.size(0)\n            \n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n            running_loss += (loss.item() * batch_size)\n            dataset_size += batch_size\n            \n            preds.append(outputs.softmax(1).detach().cpu().numpy())\n            valid_labels.append(labels.detach().cpu().numpy())\n            \n    preds = np.concatenate(preds, axis=0)\n    valid_labels = np.concatenate(valid_labels, axis=0)\n    pred_classes = preds.argmax(1)\n    acc = accuracy_score(valid_labels, pred_classes)\n    f1 = f1_score(valid_labels, pred_classes, average='macro')\n    \n    return running_loss / dataset_size, acc, f1\n\n\n# --- FUNGSI VISUALISASI ---\ndef plot_history(all_histories):\n    plt.figure(figsize=(18, 5))\n    \n    # Plot Accuracy\n    plt.subplot(1, 3, 1)\n    for model_name, history in all_histories.items():\n        plt.plot(history['val_acc'], label=f'{model_name}', marker='o')\n    plt.title('Validation Accuracy')\n    plt.xlabel('Epoch')\n    plt.legend()\n    plt.grid(True, alpha=0.3)\n\n    # Plot F1 Score (NEW)\n    plt.subplot(1, 3, 2)\n    for model_name, history in all_histories.items():\n        plt.plot(history['val_f1'], label=f'{model_name}', marker='x', linestyle='--')\n    plt.title('Validation F1 Score (Macro)')\n    plt.xlabel('Epoch')\n    plt.legend()\n    plt.grid(True, alpha=0.3)\n\n    # Plot Loss\n    plt.subplot(1, 3, 3)\n    for model_name, history in all_histories.items():\n        plt.plot(history['val_loss'], label=f'{model_name}')\n    plt.title('Validation Loss')\n    plt.xlabel('Epoch')\n    plt.legend()\n    plt.grid(True, alpha=0.3)\n    \n    plt.tight_layout()\n    plt.show()\n\nclass EarlyStopping:\n    def __init__(self, patience=5, verbose=False, delta=0, path='checkpoint.pth', trace_func=print):\n        \"\"\"\n        Args:\n            patience (int): Berapa epoch harus menunggu setelah last time score improved.\n            verbose (bool): Jika True, cetak pesan setiap kali validasi membaik.\n            delta (float): Minimum perubahan agar dianggap sebagai perbaikan.\n            path (str): Nama file untuk menyimpan model terbaik.\n            trace_func (function): Fungsi untuk print output (default: print).\n        \"\"\"\n        self.patience = patience\n        self.verbose = verbose\n        self.counter = 0\n        self.best_score = None\n        self.early_stop = False\n        self.val_loss_min = np.inf\n        self.delta = delta\n        self.path = path\n        self.trace_func = trace_func\n\n    def __call__(self, val_metric, model):\n        # val_metric disini adalah F1 Score\n        score = val_metric\n\n        if self.best_score is None:\n            self.best_score = score\n            self.save_checkpoint(val_metric, model)\n        elif score < self.best_score + self.delta:\n            self.counter += 1\n            if self.verbose:\n                print(f'EarlyStopping counter: {self.counter} out of {self.patience}')\n            if self.counter >= self.patience:\n                self.early_stop = True\n        else:\n            self.best_score = score\n            self.save_checkpoint(val_metric, model)\n            self.counter = 0\n\n    def save_checkpoint(self, val_metric, model):\n        if self.verbose:\n            print(f'Metric improved ({self.best_score:.4f} --> {val_metric:.4f}). Saving model ...')\n        torch.save(model.state_dict(), self.path)\n\n# --- MODEL WRAPPER ---\nclass UniversalModel(nn.Module):\n    def __init__(self, model_name, num_classes=5, pretrained=True):\n        super().__init__()\n        self.model = timm.create_model(model_name, pretrained=pretrained, num_classes=num_classes)\n        \n        # Logika dinamis untuk mengganti classifier head\n        if 'resnet' in model_name:\n            self.model.fc = nn.Linear(self.model.fc.in_features, num_classes)\n        elif 'efficientnet' in model_name:\n            self.model.classifier = nn.Linear(self.model.classifier.in_features, num_classes)\n        elif 'vit' in model_name:\n            self.model.head = nn.Linear(self.model.head.in_features, num_classes)\n            \n    def forward(self, x):\n        return self.model(x)\n\n# --- TRAINING ENGINE ---\ndef train_model(model_name, train_loader, valid_loader):\n    print(f\"\\n⚡ START TRAINING (Metric: F1 Macro): {model_name} ⚡\")\n    model = UniversalModel(model_name, num_classes=CFG.num_classes).to(CFG.device)\n    optimizer = optim.AdamW(model.parameters(), lr=CFG.lr)\n    scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer, T_0=CFG.epochs, T_mult=1, eta_min=1e-6, last_epoch=-1)\n    \n    history = {'train_loss': [], 'val_loss': [], 'val_acc': [], 'val_f1': []}\n    \n    # Early Stopping sekarang memantau F1 Score\n    early_stopping = EarlyStopping(patience=CFG.patience, verbose=True, path=f\"{model_name}_best.pth\")\n    \n    for epoch in range(CFG.epochs):\n        start_time = time.time()\n        train_loss = train_one_epoch(model, optimizer, scheduler, train_loader, CFG.device, epoch)\n        val_loss, val_acc, val_f1 = valid_one_epoch(model, valid_loader, CFG.device)\n        \n        history['train_loss'].append(train_loss)\n        history['val_loss'].append(val_loss)\n        history['val_acc'].append(val_acc)\n        history['val_f1'].append(val_f1)\n        \n        elapsed = time.time() - start_time\n        print(f\"Epoch {epoch+1} | Loss: {train_loss:.4f} | Val Acc: {val_acc:.4f} | Val F1: {val_f1:.4f}\")\n        early_stopping(val_f1, model)\n        \n        if early_stopping.early_stop:\n            print(\"🛑 Early stopping triggered!\")\n            break\n            \n    model.load_state_dict(torch.load(f\"{model_name}_best.pth\"))\n    # Kembalikan Best F1 Score\n    return early_stopping.best_score, history, model\n\n# --- MAIN EXECUTION ---\nif __name__ == \"__main__\":\n    print(\"=== 1. Mempersiapkan Data ===\")\n    base_image_dir = data_path/'train_images/' \n    train_df = pd.read_csv(data_path/'train.csv')\n    \n    # Buat kolom 'path' (Full absolute path ke gambar)\n    train_df['path'] = train_df['image_id'].apply(lambda x: os.path.join(base_image_dir, x))\n    \n    # Cek apakah path valid (Debugging)\n    if not os.path.exists(train_df['path'][0]):\n        print(f\"❌ PERINGATAN: Gambar tidak ditemukan di {train_df['path'][0]}\")\n        print(\"Cek apakah folder 'dataset' sudah benar hasil ekstraknya.\")\n    else:\n        print(f\"✅ Path Valid. Contoh: {train_df['path'][0]}\")\n\n    base_image_dir = data_path/'train_images/' \n    train_df['path'] = train_df['image_id'].apply(lambda x: os.path.join(base_image_dir, x))\n\n    # 3. Split Data (Stratified K-Fold)\n    folds = train_df.copy()\n    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=CFG.seed)\n    for n, (train_index, val_index) in enumerate(skf.split(folds, folds['label'])):\n        folds.loc[val_index, 'fold'] = int(n)\n    \n    fold_to_train = 0\n    trn_idx = folds[folds['fold'] != fold_to_train].index\n    val_idx = folds[folds['fold'] == fold_to_train].index\n    train_data = folds.loc[trn_idx].reset_index(drop=True)\n    valid_data = folds.loc[val_idx].reset_index(drop=True)\n    \n    train_dataset = CassavaDataset(train_data, transform=get_transforms('train'))\n    valid_dataset = CassavaDataset(valid_data, transform=get_transforms('valid'))\n    \n    train_loader = DataLoader(train_dataset, batch_size=CFG.batch_size, shuffle=True, \n                              num_workers=CFG.num_workers, pin_memory=True)\n    valid_loader = DataLoader(valid_dataset, batch_size=CFG.batch_size, shuffle=False, \n                              num_workers=CFG.num_workers, pin_memory=True)\n    \n    # --- LOOP KOMPARASI MODEL ---\n    all_histories = {}\n    best_models = {}\n    results = []\n    \n    print(f\"🚀 Memulai Komparasi {len(CFG.models_to_compare)} Model pada Device: {CFG.device}\")\n    \n    for model_name in CFG.models_to_compare:\n        acc, hist, trained_model = train_model(model_name, train_loader, valid_loader)\n        all_histories[model_name] = hist\n        best_models[model_name] = trained_model\n        results.append({\n            'Model Architecture': model_name,\n            'Best Validation Accuracy': acc,\n            'Image Size': CFG.img_size,\n            'Epochs': CFG.epochs\n        })\n        torch.cuda.empty_cache() # Bersihkan VRAM","metadata":{"execution":{"iopub.status.busy":"2025-12-17T11:52:48.523799Z","iopub.execute_input":"2025-12-17T11:52:48.524113Z","iopub.status.idle":"2025-12-17T14:18:38.838893Z","shell.execute_reply.started":"2025-12-17T11:52:48.524091Z","shell.execute_reply":"2025-12-17T14:18:38.838049Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def unnormalize(tensor):\n    \"\"\"Mengembalikan gambar tensor yang ternormalisasi ke warna aslinya\"\"\"\n    mean = np.array([0.485, 0.456, 0.406])\n    std = np.array([0.229, 0.224, 0.225])\n    \n    # Pindah ke CPU, ubah dimensi (C, H, W) -> (H, W, C)\n    img = tensor.cpu().numpy().transpose((1, 2, 0))\n    \n    # Denormalisasi\n    img = std * img + mean\n    return np.clip(img, 0, 1)\n\n# --- 2. Definisi Fungsi Visualisasi ---\ndef visualize_predictions(model, loader, device, label_map, num_images=10):\n    \"\"\"Menampilkan grid gambar prediksi vs label asli\"\"\"\n    model.eval()\n    images_shown = 0\n    plt.figure(figsize=(15, 8))\n    \n    with torch.no_grad():\n        for images, labels in loader:\n            images = images.to(device)\n            labels = labels.to(device)\n            outputs = model(images)\n            _, preds = torch.max(outputs, 1)\n            \n            for j in range(images.size(0)):\n                if images_shown >= num_images: break\n                \n                ax = plt.subplot(2, 5, images_shown + 1)\n                \n                # Panggil fungsi unnormalize yang sudah didefinisikan di atas\n                img = unnormalize(images[j])\n                plt.imshow(img)\n                \n                true_lb = label_map[str(labels[j].item())]\n                pred_lb = label_map[str(preds[j].item())]\n                \n                # Warna teks: Hijau (Benar), Merah (Salah)\n                color = 'green' if labels[j] == preds[j] else 'red'\n                \n                ax.set_title(f\"True: {true_lb}\\nPred: {pred_lb}\", color=color, fontsize=9, fontweight='bold')\n                plt.axis('off')\n                images_shown += 1\n            \n            if images_shown >= num_images: break\n            \n    plt.tight_layout()\n    plt.show()\n\n# --- TAMPILKAN HASIL AKHIR ---\nprint(\"\\n\\n\" + \"=\"*40)\nprint(\"       HASIL AKHIR KOMPARASI\")\nprint(\"=\"*40)\n\nresults_df = pd.DataFrame(results)\nresults_df = results_df.sort_values(by='Best Validation Accuracy', ascending=False).reset_index(drop=True)\n\nprint(results_df)\n\n# --- OUTPUT VISUALISASI ---\nprint(\"\\n\" + \"=\"*40)\nprint(\"📊 HASIL AKHIR & VISUALISASI\")\nprint(\"=\"*40)\n\n# 1. Tabel Skor\ndf_res = pd.DataFrame(results).sort_values(by='Best Validation Accuracy', ascending=False)\nprint(df_res)\n\n# 2. Grafik Chart\nplot_history(all_histories)\n\n# 3. Contoh Gambar Prediksi (dari Model Terbaik)\nbest_model_name = df_res.iloc[0]['Model Architecture']\nprint(f\"🖼️ Menampilkan Prediksi Model Terbaik ({best_model_name})...\")\nvisualize_predictions(best_models[best_model_name], valid_loader, CFG.device, label_map)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-17T14:36:19.751787Z","iopub.execute_input":"2025-12-17T14:36:19.752173Z","iopub.status.idle":"2025-12-17T14:36:22.863369Z","shell.execute_reply.started":"2025-12-17T14:36:19.752148Z","shell.execute_reply":"2025-12-17T14:36:22.862196Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}