{"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":33679,"databundleVersionId":3212216,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport json\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nbase_dir = \"/kaggle/input/herbarium-2022-fgvc9\"\ntrain_metadata_path = os.path.join(base_dir, \"train_metadata.json\")\ntest_metadata_path = os.path.join(base_dir, \"test_metadata.json\")\ntrain_image_dir = os.path.join(base_dir, \"train_images\")\ntest_image_dir = os.path.join(base_dir, \"test_images\")\n\nwith open(train_metadata_path, \"r\") as f:\n    train_metadata = json.load(f)\n\nwith open(test_metadata_path, \"r\") as f:\n    test_metadata = json.load(f)\n\ntrain_df = pd.DataFrame(train_metadata[\"images\"])\ntest_df = pd.DataFrame(test_metadata[\"images\"])\n\ntrain_df[\"file_path\"] = train_df[\"file_name\"].apply(lambda x: os.path.join(train_image_dir, x))\ntest_df[\"file_path\"] = test_df[\"file_name\"].apply(lambda x: os.path.join(test_image_dir, x))\n\nprint(\"TRAIN DATA OVERVIEW\")\nprint(train_df.info())\nprint(\"\\nFirst few rows:\")\nprint(train_df.head())\n\nprint(\"\\nBasic stats:\")\nprint(f\"Total training images: {len(train_df)}\")\nprint(f\"Total test images: {len(test_df)}\")\nprint(f\"Unique categories: {train_df['category_id'].nunique()}\")\n\nprint(\"\\nMissing values per column:\")\nprint(train_df.isna().sum())\n\nsample_df = train_df.sample(5, random_state=42)\n\nplt.figure(figsize=(15, 5))\nfor i, (_, row) in enumerate(sample_df.iterrows(), 1):\n    img = Image.open(row[\"file_path\"])\n    plt.subplot(1, 5, i)\n    plt.imshow(img)\n    plt.title(f\"ID: {row['id']}\\nClass: {row['category_id']}\")\n    plt.axis(\"off\")\n\nplt.suptitle(\"Sample Training Images\", fontsize=14)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport random\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\nfrom pathlib import Path\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms as T, models\nfrom sklearn.metrics import f1_score, accuracy_score\nfrom sklearn.model_selection import StratifiedShuffleSplit\nfrom sklearn.preprocessing import LabelEncoder\n\nSEED = 42\ntorch.manual_seed(SEED)\nnp.random.seed(SEED)\nrandom.seed(SEED)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\n\nBASE_DIR = Path(\"/kaggle/input/herbarium-2022-fgvc9\")\n\nTRAIN_META = BASE_DIR / 'train_metadata.json'\nTEST_META = BASE_DIR / 'test_metadata.json'\nTRAIN_IMG_DIR = BASE_DIR / 'train_images'\nTEST_IMG_DIR = BASE_DIR / 'test_images'\nSAMPLE_SUB = BASE_DIR / 'sample_submission.csv'\n\nSUBMISSION_OUT = 'submission.csv'\n\nNUM_WORKERS = 2\nIMAGE_SIZE = 224\nIMAGENET_MEAN = [0.485, 0.456, 0.406]\nIMAGENET_STD = [0.229, 0.224, 0.225]\n\ntorch.backends.cudnn.benchmark = True\n\ndef load_coco_style_pairs(meta_path, images_dir):\n    \n    with open(meta_path, 'r') as f:\n        meta = json.load(f)\n\n    rows = []\n \n    if isinstance(meta, dict) and 'annotations' in meta:\n        img_map = {img['image_id']: img['file_name'] for img in meta['images']}\n        for ann in tqdm(meta['annotations'], desc=f\"Loading {os.path.basename(meta_path)}\"):\n            image_id = ann['image_id']\n            category_id = ann['category_id']\n            file_name = img_map.get(image_id)\n            if file_name:\n                file_path = str(Path(images_dir) / file_name)\n                rows.append({\n                    'file_name': file_name,\n                    'category_id': category_id,\n                    'file_path': file_path\n                })\n\n    elif isinstance(meta, list):\n        for item in tqdm(meta, desc=f\"Loading {os.path.basename(meta_path)}\"):\n            file_name = item['file_name']\n            image_id = item['image_id']\n            file_path = str(Path(images_dir) / file_name)\n            rows.append({\n                'file_name': file_name,\n                'category_id': -1,\n                'image_id': image_id,\n                'file_path': file_path\n            })\n    else:\n        raise ValueError(f\"Unexpected JSON structure in {meta_path}\")\n\n    df = pd.DataFrame(rows)\n    print(f\"Loaded {len(df)} records from {meta_path}\")\n    return df\n\n\nclass HerbariumDataset(Dataset):\n    def __init__(self, df, label_encoder=None, transforms=None, is_test=False):\n        self.df = df.reset_index(drop=True)\n        self.transforms = transforms\n        self.is_test = is_test\n        self.le = label_encoder\n        if not is_test:\n            assert 'category_id' in df.columns\n            self.labels = self.le.transform(self.df['category_id'].astype(str).values)\n        else:\n            self.labels = None\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        path = self.df.loc[idx, 'file_path']\n        img = Image.open(path).convert('RGB')\n        if self.transforms:\n            img = self.transforms(img)\n        if self.is_test:\n            return img, self.df.loc[idx, 'file_name']\n        else:\n            label = int(self.labels[idx])\n            return img, label\n\n\ndef get_transforms(train=True):\n    if train:\n        return T.Compose([\n            T.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n            T.RandomHorizontalFlip(p=0.5),\n            T.RandomRotation(degrees=15),\n            T.ColorJitter(brightness=0.1, contrast=0.1),\n            T.ToTensor(),\n            T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n        ])\n    else:\n        return T.Compose([\n            T.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n            T.ToTensor(),\n            T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n        ])\n\n\ndef build_model(num_classes):\n    model = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.IMAGENET1K_V1)\n    in_features = model.classifier[1].in_features\n    model.classifier = nn.Sequential(\n        nn.Dropout(p=0.2, inplace=True),\n        nn.Linear(in_features, num_classes)\n    )\n    return model\n\n\ndef evaluate(model, loader, device):\n    model.eval()\n    preds, trues = [], []\n    with torch.no_grad():\n        for x, y in tqdm(loader, desc=\"Evaluating\", leave=False):\n            x, y = x.to(device), y.to(device)\n            logits = model(x)\n            pred = torch.argmax(logits, dim=1)\n            preds.extend(pred.cpu().numpy())\n            trues.extend(y.cpu().numpy())\n    return f1_score(trues, preds, average='macro'), accuracy_score(trues, preds)\n\ndef train_one_epoch(model, loader, optimizer, criterion, device):\n    model.train()\n    running_loss = 0.0\n    for x, y in tqdm(loader, desc=\"Training\", leave=False):\n        x, y = x.to(device), y.to(device)\n        optimizer.zero_grad()\n        logits = model(x)\n        loss = criterion(logits, y)\n        loss.backward()\n        optimizer.step()\n        running_loss += loss.item() * x.size(0)\n    return running_loss / len(loader.dataset)\n\n\ndef run_experiment(train_df, val_df, label_encoder, lr, batch_size, device,\n                   epochs=10, freeze_head_epochs=2):\n    num_classes = len(label_encoder.classes_)\n    train_ds = HerbariumDataset(train_df, label_encoder, get_transforms(True))\n    val_ds = HerbariumDataset(val_df, label_encoder, get_transforms(False))\n    train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=NUM_WORKERS, pin_memory=True)\n    val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True)\n\n    model = build_model(num_classes).to(device)\n    criterion = nn.CrossEntropyLoss()\n    optimizer = torch.optim.Adam(model.parameters(), lr=lr)\n    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=2)\n\n    \n    for name, param in model.named_parameters():\n        param.requires_grad = False\n    for param in model.classifier.parameters():\n        param.requires_grad = True\n\n    best_f1, best_state = -1.0, None\n    patience, no_improve = 3, 0\n\n    for epoch in range(1, epochs + 1):\n        if epoch == freeze_head_epochs + 1:\n            for param in model.parameters():\n                param.requires_grad = True\n        train_loss = train_one_epoch(model, train_loader, optimizer, criterion, device)\n        val_f1, val_acc = evaluate(model, val_loader, device)\n        scheduler.step(val_f1)\n\n        print(f\"Epoch {epoch}/{epochs} | loss={train_loss:.4f} | val_f1={val_f1:.4f} | val_acc={val_acc:.4f}\")\n\n        if val_f1 > best_f1:\n            best_f1 = val_f1\n            best_state = {k: v.cpu() for k, v in model.state_dict().items()}\n            torch.save(best_state, 'best_model.pth')\n            print(\"Saved best_model.pth\")\n            no_improve = 0\n        else:\n            no_improve += 1\n            if no_improve >= patience:\n                print(\"Early stopping.\")\n                break\n\n    return best_f1, best_state\n\n\ndef main():\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    print(\"Device:\", device)\n\n    train_df = load_coco_style_pairs(TRAIN_META, TRAIN_IMG_DIR)\n    test_df = load_coco_style_pairs(TEST_META, TEST_IMG_DIR)\n\n    le = LabelEncoder()\n    train_df['category_id'] = train_df['category_id'].astype(str)\n    le.fit(train_df['category_id'])\n\n    splitter = StratifiedShuffleSplit(n_splits=1, test_size=0.1, random_state=SEED)\n    train_idx, val_idx = next(splitter.split(train_df, train_df['category_id']))\n    df_train = train_df.iloc[train_idx].reset_index(drop=True)\n    df_val = train_df.iloc[val_idx].reset_index(drop=True)\n\n    print(f\"Train: {len(df_train)} | Val: {len(df_val)} | Classes: {len(le.classes_)}\")\n\n    best_f1, best_state = run_experiment(df_train, df_val, le, lr=1e-3, batch_size=32, device=device)\n    print(f\"Best Val F1 = {best_f1:.4f}\")\n\n    full_df = pd.concat([df_train, df_val]).reset_index(drop=True)\n    num_classes = len(le.classes_)\n    full_ds = HerbariumDataset(full_df, le, get_transforms(True))\n    full_loader = DataLoader(full_ds, batch_size=32, shuffle=True, num_workers=NUM_WORKERS, pin_memory=True)\n\n    model = build_model(num_classes).to(device)\n    model.load_state_dict(torch.load('best_model.pth'))\n    optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\n    criterion = nn.CrossEntropyLoss()\n\n    print(\"\\nRetraining full model...\")\n    for epoch in range(3):\n        loss = train_one_epoch(model, full_loader, optimizer, criterion, device)\n        print(f\"Full epoch {epoch+1}/3 | loss={loss:.4f}\")\n\n    torch.save(model.state_dict(), 'final_model.pth')\n    print(\"Saved final_model.pth\")\n\n \n    test_ds = HerbariumDataset(test_df, le, get_transforms(False), is_test=True)\n    test_loader = DataLoader(test_ds, batch_size=32, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True)\n    model.eval()\n\n    preds, filenames = [], []\n    with torch.no_grad():\n        for x, fnames in tqdm(test_loader, desc=\"Predicting\"):\n            x = x.to(device)\n            logits = model(x)\n            pred = torch.argmax(logits, dim=1).cpu().numpy()\n            preds.extend(pred)\n            filenames.extend(fnames)\n\n    pred_labels = le.inverse_transform(preds)\n    pred_labels = [int(x) for x in pred_labels]\n\n    submission = pd.DataFrame({'Id': filenames, 'Predicted': pred_labels})\n    sample = pd.read_csv(SAMPLE_SUB)\n    submission = sample[['Id']].merge(submission, on='Id', how='left')\n    submission.to_csv(SUBMISSION_OUT, index=False)\n    print(f\"Submission saved as {SUBMISSION_OUT}\")\n\nif __name__ == \"__main__\":\n    main()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}