{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport random\n\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\nfrom torch.utils.data import DataLoader, Dataset\nfrom torchvision import models, transforms\n\nDATA_DIR = \"/kaggle/input/competitions/classify-leaves\"\nOUTPUT_DIR = \"/kaggle/working\"\n\nTRAIN_CSV = os.path.join(DATA_DIR, \"train.csv\")\nTEST_CSV = os.path.join(DATA_DIR, \"test.csv\")\nBEST_MODEL_PATH = os.path.join(OUTPUT_DIR, \"best_model.pth\")\nSUBMISSION_PATH = os.path.join(OUTPUT_DIR, \"submission.csv\")\n\nBATCH_SIZE = 64\nEPOCHS = 10\nLR = 1e-4\nVAL_RATIO = 0.2\nSEED = 42\nNUM_WORKERS = 2\n\n\ndef seed_everything(seed):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\n\nclass LeavesDataset(Dataset):\n    def __init__(self, dataframe, data_dir, transform=None, is_test=False):\n        self.dataframe = dataframe.reset_index(drop=True)\n        self.data_dir = data_dir\n        self.transform = transform\n        self.is_test = is_test\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def __getitem__(self, idx):\n        row = self.dataframe.iloc[idx]\n        image_name = row[\"image\"]\n        image_path = os.path.join(self.data_dir, image_name)\n\n        image = Image.open(image_path).convert(\"RGB\")\n        if self.transform is not None:\n            image = self.transform(image)\n        if self.is_test:\n            return image, image_name\n        label = int(row[\"label_idx\"])\n        return image, label\n\n\ndef get_transforms():\n    imagenet_mean = [0.485, 0.456, 0.406]\n    imagenet_std = [0.229, 0.224, 0.225]\n\n    train_transform = transforms.Compose(\n        [\n            transforms.Resize(256),\n            transforms.RandomCrop(224),\n            transforms.RandomHorizontalFlip(),\n            transforms.ToTensor(),\n            transforms.Normalize(imagenet_mean, imagenet_std),\n        ]\n    )\n\n    eval_transform = transforms.Compose(\n        [\n            transforms.Resize(256),\n            transforms.CenterCrop(224),\n            transforms.ToTensor(),\n            transforms.Normalize(imagenet_mean, imagenet_std),\n        ]\n    )\n\n    return train_transform, eval_transform\n\n\ndef build_model(num_classes):\n    try:\n        weights = models.ResNet18_Weights.DEFAULT\n        model = models.resnet18(weights=weights)\n    except Exception as exc:\n        raise RuntimeError(\n            \"Failed to load torchvision ResNet18 pretrained weights. \"\n            \"On Kaggle, enable internet or attach a dataset/cache containing the weights.\"\n        ) from exc\n\n    in_features = model.fc.in_features\n    model.fc = nn.Linear(in_features, num_classes)\n    return model\n\n\ndef train_one_epoch(model, dataloader, criterion, optimizer, device):\n    model.train()\n    running_loss = 0.0\n\n    for images, labels in dataloader:\n        images = images.to(device)\n        labels = labels.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * images.size(0)\n\n    return running_loss / len(dataloader.dataset)\n\n\ndef validate(model, dataloader, device):\n    model.eval()\n    correct = 0\n    total = 0\n\n    with torch.no_grad():\n        for images, labels in dataloader:\n            images = images.to(device)\n            labels = labels.to(device)\n\n            outputs = model(images)\n            preds = outputs.argmax(dim=1)\n            correct += (preds == labels).sum().item()\n            total += labels.size(0)\n\n    return correct / total if total > 0 else 0.0\n\n\ndef predict(model, dataloader, idx_to_label, device):\n    model.eval()\n    image_names = []\n    labels = []\n\n    with torch.no_grad():\n        for images, batch_image_names in dataloader:\n            images = images.to(device)\n            outputs = model(images)\n            preds = outputs.argmax(dim=1).cpu().numpy()\n\n            image_names.extend(batch_image_names)\n            labels.extend([idx_to_label[int(pred)] for pred in preds])\n\n    return pd.DataFrame({\"image\": image_names, \"label\": labels})\n\n\ndef main():\n    seed_everything(SEED)\n    os.makedirs(OUTPUT_DIR, exist_ok=True)\n\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    print(f\"Using device: {device}\")\n\n    train_df = pd.read_csv(TRAIN_CSV)\n    test_df = pd.read_csv(TEST_CSV)\n\n    required_train_columns = {\"image\", \"label\"}\n    if not required_train_columns.issubset(train_df.columns):\n        raise ValueError(\"train.csv must contain image and label columns.\")\n    if \"image\" not in test_df.columns:\n        raise ValueError(\"test.csv must contain an image column.\")\n\n    class_names = sorted(train_df[\"label\"].unique())\n    label_to_idx = {label: idx for idx, label in enumerate(class_names)}\n    idx_to_label = {idx: label for label, idx in label_to_idx.items()}\n    train_df[\"label_idx\"] = train_df[\"label\"].map(label_to_idx)\n\n    train_split, val_split = train_test_split(\n        train_df,\n        test_size=VAL_RATIO,\n        random_state=SEED,\n        stratify=train_df[\"label_idx\"],\n    )\n    train_transform, eval_transform = get_transforms()\n\n    train_dataset = LeavesDataset(\n        train_split,\n        DATA_DIR,\n        transform=train_transform,\n        is_test=False,\n    )\n    val_dataset = LeavesDataset(\n        val_split,\n        DATA_DIR,\n        transform=eval_transform,\n        is_test=False,\n    )\n    test_dataset = LeavesDataset(\n        test_df,\n        DATA_DIR,\n        transform=eval_transform,\n        is_test=True,\n    )\n\n    pin_memory = device.type == \"cuda\"\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=BATCH_SIZE,\n        shuffle=True,\n        num_workers=NUM_WORKERS,\n        pin_memory=pin_memory,\n    )\n    val_loader = DataLoader(\n        val_dataset,\n        batch_size=BATCH_SIZE,\n        shuffle=False,\n        num_workers=NUM_WORKERS,\n        pin_memory=pin_memory,\n    )\n    test_loader = DataLoader(\n        test_dataset,\n        batch_size=BATCH_SIZE,\n        shuffle=False,\n        num_workers=NUM_WORKERS,\n        pin_memory=pin_memory,\n    )\n\n    model = build_model(num_classes=len(class_names)).to(device)\n    criterion = nn.CrossEntropyLoss()\n    optimizer = optim.Adam(model.parameters(), lr=LR)\n    best_val_acc = -1.0\n\n    for epoch in range(1, EPOCHS + 1):\n        train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device)\n        val_acc = validate(model, val_loader, device)\n\n        print(\n            f\"Epoch {epoch}/{EPOCHS} | \"\n            f\"Train Loss: {train_loss:.4f} | \"\n            f\"Val Acc: {val_acc:.4f}\"\n        )\n\n        if val_acc > best_val_acc:\n            best_val_acc = val_acc\n            torch.save(\n                {\n                    \"model_state_dict\": model.state_dict(),\n                    \"label_to_idx\": label_to_idx,\n                    \"idx_to_label\": idx_to_label,\n                    \"best_val_acc\": best_val_acc,\n                },\n                BEST_MODEL_PATH,\n            )\n            print(f\"Saved best model to {BEST_MODEL_PATH}\")\n\n    checkpoint = torch.load(BEST_MODEL_PATH, map_location=device)\n    model.load_state_dict(checkpoint[\"model_state_dict\"])\n\n    submission = predict(model, test_loader, idx_to_label, device)\n    submission.to_csv(SUBMISSION_PATH, index=False)\n    print(f\"Best Val Acc: {best_val_acc:.4f}\")\n    print(f\"Saved submission to {SUBMISSION_PATH}\")\n\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T08:49:23.136650Z","iopub.execute_input":"2026-06-06T08:49:23.137170Z","iopub.status.idle":"2026-06-06T09:01:06.818673Z","shell.execute_reply.started":"2026-06-06T08:49:23.137141Z","shell.execute_reply":"2026-06-06T09:01:06.817746Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\n\nprint(os.listdir(\"/kaggle/working\"))\n\nsub = pd.read_csv(\"/kaggle/working/submission.csv\")\nprint(sub.head())\nprint(sub.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T09:03:53.464255Z","iopub.execute_input":"2026-06-06T09:03:53.464956Z","iopub.status.idle":"2026-06-06T09:03:53.487434Z","shell.execute_reply.started":"2026-06-06T09:03:53.464861Z","shell.execute_reply":"2026-06-06T09:03:53.486695Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from IPython.display import FileLink\n\nFileLink(\"/kaggle/working/submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-06T09:04:19.753956Z","iopub.execute_input":"2026-06-06T09:04:19.754385Z","iopub.status.idle":"2026-06-06T09:04:19.760950Z","shell.execute_reply.started":"2026-06-06T09:04:19.754356Z","shell.execute_reply":"2026-06-06T09:04:19.760087Z"}},"outputs":[],"execution_count":null}]}