{"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":108418,"databundleVersionId":13147963,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q timm albumentations\n\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\nfrom sklearn.model_selection import StratifiedKFold\nimport timm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm.auto import tqdm\n\n# ==== CONFIGURATION ====\nclass CFG:\n    seed = 42\n    img_size = 224\n    model_name = 'tf_efficientnetv2_s_in21ft1k'\n    train_csv = '/kaggle/input/binary-biplob-can-you-guess-the-chess-opening/chess_dataset/train.csv'\n    test_csv = '/kaggle/input/binary-biplob-can-you-guess-the-chess-opening/chess_dataset/test.csv'\n    train_path = '/kaggle/input/binary-biplob-can-you-guess-the-chess-opening/chess_dataset/train_images'\n    test_path = '/kaggle/input/binary-biplob-can-you-guess-the-chess-opening/chess_dataset/test_images'\n    batch_size = 32\n    num_workers = 2\n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n    epochs = 10   # can increase to 15–20 for final submission\n    lr = 1e-4\n    folds = 5\n\n# ==== SEED ====\ndef seed_everything(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n\nseed_everything(CFG.seed)\n\n# ==== LOAD TRAIN DATA ====\ntrain_df = pd.read_csv(CFG.train_csv)\nlabel_map = {label: idx for idx, label in enumerate(sorted(train_df.eco_volume.unique()))}\ninv_label_map = {v: k for k, v in label_map.items()}\ntrain_df['label'] = train_df.eco_volume.map(label_map)\ntrain_df['image'] = train_df['image_id'].apply(lambda x: os.path.join(CFG.train_path, f\"{x}.jpg\"))\n\n# ==== DATASET ====\nclass ChessDataset(Dataset):\n    def __init__(self, df, transform=None, test=False):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n        self.test = test\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        image_path = self.df.loc[idx, 'image']\n        image = Image.open(image_path).convert('RGB')\n        image = np.array(image)\n\n        if self.transform:\n            image = self.transform(image=image)['image']\n\n        if self.test:\n            return image, self.df.loc[idx, 'image_id']\n        else:\n            label = self.df.loc[idx, 'label']\n            return image, label\n\n# ==== AUGMENTATIONS ====\ntrain_transform = A.Compose([\n    A.Resize(CFG.img_size, CFG.img_size),\n    A.HorizontalFlip(p=0.5),\n    A.RandomBrightnessContrast(p=0.2),\n    A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=15, p=0.5),\n    A.Normalize(),\n    ToTensorV2(),\n])\n\nval_transform = A.Compose([\n    A.Resize(CFG.img_size, CFG.img_size),\n    A.Normalize(),\n    ToTensorV2(),\n])\n\n# ==== MODEL ====\nclass ChessClassifier(nn.Module):\n    def __init__(self, model_name, n_classes):\n        super().__init__()\n        self.backbone = timm.create_model(model_name, pretrained=True)\n        in_features = self.backbone.classifier.in_features\n        self.backbone.classifier = nn.Linear(in_features, n_classes)\n\n    def forward(self, x):\n        return self.backbone(x)\n\n# ==== TRAIN & EVAL ====\ndef train_fn(model, loader, optimizer, criterion):\n    model.train()\n    total_loss = 0\n    for x, y in tqdm(loader, leave=False):\n        x, y = x.to(CFG.device), y.to(CFG.device)\n        optimizer.zero_grad()\n        output = model(x)\n        loss = criterion(output, y)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n    return total_loss / len(loader)\n\ndef eval_fn(model, loader):\n    model.eval()\n    correct, total = 0, 0\n    with torch.no_grad():\n        for x, y in loader:\n            x, y = x.to(CFG.device), y.to(CFG.device)\n            output = model(x)\n            preds = output.argmax(1)\n            correct += (preds == y).sum().item()\n            total += y.size(0)\n    return correct / total\n\n# ==== TRAINING LOOP ====\nskf = StratifiedKFold(n_splits=CFG.folds, shuffle=True, random_state=CFG.seed)\nbest_models = []\n\nfor fold, (train_idx, val_idx) in enumerate(skf.split(train_df, train_df.label)):\n    print(f\"\\n===== Fold {fold + 1} =====\")\n    train_data = train_df.iloc[train_idx]\n    val_data = train_df.iloc[val_idx]\n\n    train_ds = ChessDataset(train_data, transform=train_transform)\n    val_ds = ChessDataset(val_data, transform=val_transform)\n\n    train_loader = DataLoader(train_ds, batch_size=CFG.batch_size, shuffle=True, num_workers=CFG.num_workers)\n    val_loader = DataLoader(val_ds, batch_size=CFG.batch_size, num_workers=CFG.num_workers)\n\n    model = ChessClassifier(CFG.model_name, len(label_map)).to(CFG.device)\n    optimizer = torch.optim.AdamW(model.parameters(), lr=CFG.lr)\n    criterion = nn.CrossEntropyLoss()\n\n    best_acc = 0\n    for epoch in range(CFG.epochs):\n        train_loss = train_fn(model, train_loader, optimizer, criterion)\n        val_acc = eval_fn(model, val_loader)\n        print(f\"Epoch {epoch+1} | Train Loss: {train_loss:.4f} | Val Acc: {val_acc:.4f}\")\n        if val_acc > best_acc:\n            best_acc = val_acc\n            torch.save(model.state_dict(), f'model_fold{fold}.pth')\n    best_models.append(f'model_fold{fold}.pth')\n\n# ==== TEST PREDICTION ====\ntest_df = pd.read_csv(CFG.test_csv)\ntest_df['image'] = test_df['image_id'].apply(lambda x: os.path.join(CFG.test_path, f\"{x}.jpg\"))\ntest_ds = ChessDataset(test_df, transform=val_transform, test=True)\ntest_loader = DataLoader(test_ds, batch_size=CFG.batch_size)\n\nfinal_preds = []\n\nfor fold_model in best_models:\n    model = ChessClassifier(CFG.model_name, len(label_map)).to(CFG.device)\n    model.load_state_dict(torch.load(fold_model))\n    model.eval()\n\n    fold_preds = []\n    with torch.no_grad():\n        for x, _ in test_loader:\n            x = x.to(CFG.device)\n            output = model(x)\n            fold_preds.append(output.softmax(1).cpu().numpy())\n\n    fold_preds = np.concatenate(fold_preds, axis=0)\n    final_preds.append(fold_preds)\n\nmean_preds = np.mean(final_preds, axis=0)\npred_labels = mean_preds.argmax(1)\npred_labels = [inv_label_map[p] for p in pred_labels]\n\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-30T03:15:18.127886Z","iopub.execute_input":"2025-07-30T03:15:18.128183Z","iopub.status.idle":"2025-07-30T03:51:05.379348Z","shell.execute_reply.started":"2025-07-30T03:15:18.128139Z","shell.execute_reply":"2025-07-30T03:51:05.378201Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==== SUBMISSION ====\nsubmission = pd.DataFrame({\n    \"image_id\": test_df['image_id'],\n    \"eco_volume\": pred_labels\n})\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(\"✅ submission.csv saved!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-30T03:51:05.746718Z","iopub.execute_input":"2025-07-30T03:51:05.747265Z","iopub.status.idle":"2025-07-30T03:51:05.753428Z","shell.execute_reply.started":"2025-07-30T03:51:05.747242Z","shell.execute_reply":"2025-07-30T03:51:05.752889Z"}},"outputs":[],"execution_count":null}]}