{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":13836,"databundleVersionId":1718836,"sourceType":"competition"}],"dockerImageVersionId":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Cassava Leaf Disease Classification\n**Assignment Submission**\n\n**Team Members:**\n1. Fares Mahmoud\n2. Youssef Dewidar\n3. Abdelfatah Elsabagh\n\n**Objective:**\nTo classify cassava leaf images into 5 categories.\nWe compare two different techniques:\n1.  **Custom CNN (trained from scratch)** as a baseline.\n2.  **ResNet50 (Transfer Learning)** for improved accuracy.\n\n**Dataset Split:**\n* **Training:** 70%\n* **Validation:** 20%\n* **Test:** 10%","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"import os\nimport random\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport cv2\nfrom tqdm.auto import tqdm\n\n# Scikit-learn\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix\n\n# PyTorch\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as transforms\nfrom torchvision import models\n\n# Albumentations\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# --- CONFIGURATION ---\nCONFIG = {\n    'seed': 42,\n    'img_size': 224,\n    'batch_size': 32,\n    'epochs': 10,\n    'learning_rate': 1e-4,\n    'num_classes': 5,\n    'device': torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n}\n\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(CONFIG['seed'])\nprint(f\"Using Device: {CONFIG['device']}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- DATA SPLITTING (70/20/10) ---\nBASE_DIR = '/kaggle/input/cassava-leaf-disease-classification'\nTRAIN_IMG_PATH = os.path.join(BASE_DIR, 'train_images')\ndf = pd.read_csv(os.path.join(BASE_DIR, 'train.csv'))\n\n# 1. Split 70% Train, 30% Temp\ntrain_df, temp_df = train_test_split(\n    df, test_size=0.30, stratify=df['label'], random_state=CONFIG['seed']\n)\n\n# 2. Split Temp into 20% Val and 10% Test\n# (0.3333 of 30% is approx 10%)\nval_df, test_df = train_test_split(\n    temp_df, test_size=0.3333, stratify=temp_df['label'], random_state=CONFIG['seed']\n)\n\ntrain_df = train_df.reset_index(drop=True)\nval_df = val_df.reset_index(drop=True)\ntest_df = test_df.reset_index(drop=True)\n\nprint(f\"Train: {len(train_df)} | Val: {len(val_df)} | Test: {len(test_df)}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- DATASET & AUGMENTATIONS ---\nclass CassavaDataset(Dataset):\n    def __init__(self, df, img_dir, transforms=None):\n        self.df = df\n        self.img_dir = img_dir\n        self.transforms = transforms\n        \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, index):\n        row = self.df.iloc[index]\n        img_path = os.path.join(self.img_dir, row['image_id'])\n        image = cv2.imread(img_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n        \n        if self.transforms:\n            image = self.transforms(image=image)['image']\n            \n        return image, torch.tensor(row['label'], dtype=torch.long)\n\ntrain_transforms = A.Compose([\n    A.Resize(CONFIG['img_size'], CONFIG['img_size']),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.5),\n    A.Rotate(limit=30, p=0.5),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\nvalid_transforms = A.Compose([\n    A.Resize(CONFIG['img_size'], CONFIG['img_size']),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\ntrain_ds = CassavaDataset(train_df, TRAIN_IMG_PATH, train_transforms)\nval_ds = CassavaDataset(val_df, TRAIN_IMG_PATH, valid_transforms)\ntest_ds = CassavaDataset(test_df, TRAIN_IMG_PATH, valid_transforms)\n\ntrain_loader = DataLoader(train_ds, batch_size=CONFIG['batch_size'], shuffle=True, num_workers=0)\nval_loader = DataLoader(val_ds, batch_size=CONFIG['batch_size'], shuffle=False, num_workers=0)\ntest_loader = DataLoader(test_ds, batch_size=CONFIG['batch_size'], shuffle=False, num_workers=0)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Technique 1: Custom CNN (From Scratch)\nWe build a simple Convolutional Neural Network with 4 convolutional blocks. This serves as our baseline experiment.","metadata":{}},{"cell_type":"code","source":"class CustomCNN(nn.Module):\n    def __init__(self, num_classes=5):\n        super(CustomCNN, self).__init__()\n        # 4 Convolutional Blocks\n        self.features = nn.Sequential(\n            # Block 1\n            nn.Conv2d(3, 32, kernel_size=3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2), # 112x112\n            \n            # Block 2\n            nn.Conv2d(32, 64, kernel_size=3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2), # 56x56\n            \n            # Block 3\n            nn.Conv2d(64, 128, kernel_size=3, padding=1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2), # 28x28\n            \n            # Block 4\n            nn.Conv2d(128, 256, kernel_size=3, padding=1),\n            nn.BatchNorm2d(256),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2), # 14x14\n        )\n        \n        self.classifier = nn.Sequential(\n            nn.Flatten(),\n            nn.Dropout(0.5),\n            nn.Linear(256 * 14 * 14, 512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, num_classes)\n        )\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.classifier(x)\n        return x","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Technique 2: Transfer Learning (ResNet50)\nWe use a ResNet50 model pre-trained on ImageNet. This typically performs better as it has already learned feature extraction from millions of images.","metadata":{}},{"cell_type":"code","source":"class ResNetTransfer(nn.Module):\n    def __init__(self, num_classes=5):\n        super(ResNetTransfer, self).__init__()\n        self.backbone = models.resnet50(pretrained=True)\n        \n        # Replace FC layer\n        in_features = self.backbone.fc.in_features\n        self.backbone.fc = nn.Sequential(\n            nn.Linear(in_features, 512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, num_classes)\n        )\n        \n    def forward(self, x):\n        return self.backbone(x)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- UNIVERSAL TRAINING FUNCTION ---\ndef train_model(model_class, model_name, epochs=8):\n    print(f\"\\n🚀 STARTING TRAINING: {model_name}\")\n    print(\"=\"*40)\n    \n    model = model_class(num_classes=5).to(CONFIG['device'])\n    criterion = nn.CrossEntropyLoss()\n    optimizer = optim.Adam(model.parameters(), lr=CONFIG['learning_rate'])\n    \n    best_acc = 0.0\n    history = {'train_acc': [], 'val_acc': []}\n    \n    for epoch in range(epochs):\n        # Train\n        model.train()\n        train_correct = 0\n        train_total = 0\n        \n        for images, labels in tqdm(train_loader, desc=f\"Epoch {epoch+1}\", leave=False):\n            images, labels = images.to(CONFIG['device']), labels.to(CONFIG['device'])\n            optimizer.zero_grad()\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            optimizer.step()\n            \n            _, preds = outputs.max(1)\n            train_correct += (preds == labels).sum().item()\n            train_total += labels.size(0)\n            \n        train_acc = 100. * train_correct / train_total\n        \n        # Validate\n        model.eval()\n        val_correct = 0\n        val_total = 0\n        with torch.no_grad():\n            for images, labels in val_loader:\n                images, labels = images.to(CONFIG['device']), labels.to(CONFIG['device'])\n                outputs = model(images)\n                _, preds = outputs.max(1)\n                val_correct += (preds == labels).sum().item()\n                val_total += labels.size(0)\n        \n        val_acc = 100. * val_correct / val_total\n        \n        print(f\"Epoch {epoch+1} | Train Acc: {train_acc:.2f}% | Val Acc: {val_acc:.2f}%\")\n        \n        history['train_acc'].append(train_acc)\n        history['val_acc'].append(val_acc)\n        \n        if val_acc > best_acc:\n            best_acc = val_acc\n            torch.save(model.state_dict(), f'best_{model_name}.pth')\n            \n    return history, best_acc\n\n# 1. Train Custom CNN\nhist_custom, acc_custom = train_model(CustomCNN, \"CustomCNN\", epochs=10)\n\n# 2. Train ResNet50\nhist_resnet, acc_resnet = train_model(ResNetTransfer, \"ResNet50\", epochs=8)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- COMPARISON & RESULTS ---\n\nprint(\"\\n🏆 FINAL COMPARISON\")\nprint(\"=\"*40)\nprint(f\"1. Custom CNN Validation Accuracy: {acc_custom:.2f}%\")\nprint(f\"2. ResNet50 Validation Accuracy:   {acc_resnet:.2f}%\")\n\n# Plotting Comparison\nplt.figure(figsize=(10, 5))\nplt.plot(hist_custom['val_acc'], label=f'Custom CNN (Best: {acc_custom:.1f}%)', marker='o')\nplt.plot(hist_resnet['val_acc'], label=f'ResNet50 (Best: {acc_resnet:.1f}%)', marker='o')\nplt.title('Validation Accuracy Comparison')\nplt.xlabel('Epochs')\nplt.ylabel('Accuracy %')\nplt.legend()\nplt.grid(True)\nplt.show()\n\n# --- FINAL TEST EVALUATION (Using the better model) ---\nprint(\"\\n📝 Evaluating Best Model on Test Set (10% split)...\")\nbest_model_name = \"ResNet50\" if acc_resnet > acc_custom else \"CustomCNN\"\nmodel_class = ResNetTransfer if acc_resnet > acc_custom else CustomCNN\n\nfinal_model = model_class().to(CONFIG['device'])\nfinal_model.load_state_dict(torch.load(f'best_{best_model_name}.pth'))\nfinal_model.eval()\n\nall_preds = []\nall_labels = []\n\nwith torch.no_grad():\n    for images, labels in test_loader:\n        images = images.to(CONFIG['device'])\n        outputs = final_model(images)\n        _, preds = outputs.max(1)\n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(labels.cpu().numpy())\n\nprint(f\"Selected Model: {best_model_name}\")\nprint(classification_report(all_labels, all_preds))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- KAGGLE SUBMISSION ---\nimport glob\n\ndef predict_image(image_path, model, transform):\n    image = cv2.imread(image_path)\n    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n    \n    # Apply the same validation transforms\n    if transform:\n        augmented = transform(image=image)\n        image = augmented['image']\n    \n    # Add batch dimension (3, 224, 224) -> (1, 3, 224, 224)\n    image = image.unsqueeze(0).to(CONFIG['device'])\n    \n    with torch.no_grad():\n        output = model(image)\n        _, predicted = torch.max(output, 1)\n        \n    return predicted.item()\n\n# Load Test Images (Kaggle Evaluation Images)\ntest_files = glob.glob(os.path.join(BASE_DIR, 'test_images', '*.jpg'))\nsubmission = {'image_id': [], 'label': []}\n\nprint(f\"📝 Generating predictions for {len(test_files)} images...\")\n\n# 3. Predict Loop\nfor img_path in test_files:\n    img_id = os.path.basename(img_path)\n    pred_label = predict_image(img_path, final_model, valid_transforms)\n    \n    submission['image_id'].append(img_id)\n    submission['label'].append(pred_label)\n\n# 4. Save CSV\nsub_df = pd.DataFrame(submission)\nsub_df.to_csv('submission.csv', index=False)\n\nprint(\"✅ 'submission.csv' saved!\")\nprint(sub_df.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}