{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":108418,"databundleVersionId":13147963,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Imports and setup","metadata":{}},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision import datasets, transforms, models\nfrom torch.utils.data import DataLoader, Dataset, Subset\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import classification_report, confusion_matrix\nimport seaborn as sns\nfrom collections import Counter\nimport warnings\nwarnings.filterwarnings('ignore')","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:27:58.117756Z","iopub.status.busy":"2025-07-29T14:27:58.117105Z","iopub.status.idle":"2025-07-29T14:28:07.173647Z","shell.execute_reply":"2025-07-29T14:28:07.173072Z","shell.execute_reply.started":"2025-07-29T14:27:58.117731Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset Loading and Analysis","metadata":{}},{"cell_type":"code","source":"TRAIN_IMAGES_PATH = '/kaggle/input/binary-biplob-can-you-guess-the-chess-opening/chess_dataset/train_images'\nTEST_IMAGES_PATH = '/kaggle/input/binary-biplob-can-you-guess-the-chess-opening/chess_dataset/test_images'\nTRAIN_CSV = '/kaggle/input/binary-biplob-can-you-guess-the-chess-opening/chess_dataset/train.csv'\nTEST_CSV = '/kaggle/input/binary-biplob-can-you-guess-the-chess-opening/chess_dataset/test.csv'\n\n# Load metadata\ntrain_df = pd.read_csv(TRAIN_CSV)\ntest_df = pd.read_csv(TEST_CSV)","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:07.174934Z","iopub.status.busy":"2025-07-29T14:28:07.174561Z","iopub.status.idle":"2025-07-29T14:28:07.213994Z","shell.execute_reply":"2025-07-29T14:28:07.213269Z","shell.execute_reply.started":"2025-07-29T14:28:07.174897Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"📊 Training samples: {len(train_df)}\")\nprint(f\"📊 Test samples: {len(test_df)}\")\nprint(f\"📁 Training images folder: {len(os.listdir(TRAIN_IMAGES_PATH))} files\")\nprint(f\"📁 Test images folder: {len(os.listdir(TEST_IMAGES_PATH))} files\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:07.215004Z","iopub.status.busy":"2025-07-29T14:28:07.214747Z","iopub.status.idle":"2025-07-29T14:28:07.305577Z","shell.execute_reply":"2025-07-29T14:28:07.304974Z","shell.execute_reply.started":"2025-07-29T14:28:07.214982Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Analyze dataset structure\nprint(\"🔍 Training DataFrame Info:\")\nprint(train_df.head())\nprint(\"\\n📋 Columns:\", train_df.columns.tolist())\nprint(\"\\n📊 ECO Volume Distribution:\")\neco_counts = train_df['eco_volume'].value_counts().sort_index()\nprint(eco_counts)\nprint(f\"\\n🎯 Number of classes: {train_df['eco_volume'].nunique()}\")\nprint(f\"📝 Classes: {sorted(train_df['eco_volume'].unique())}\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:09.707657Z","iopub.status.busy":"2025-07-29T14:28:09.707397Z","iopub.status.idle":"2025-07-29T14:28:09.733920Z","shell.execute_reply":"2025-07-29T14:28:09.733255Z","shell.execute_reply.started":"2025-07-29T14:28:09.707637Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Visualize class distribution","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(10, 6))\neco_counts.plot(kind='bar', color=['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'])\nplt.title('ECO Volume Distribution in Training Set', fontsize=16, fontweight='bold')\nplt.xlabel('ECO Volume', fontsize=12)\nplt.ylabel('Number of Samples', fontsize=12)\nplt.xticks(rotation=0)\nplt.grid(axis='y', alpha=0.3)\n\n# Add value labels on bars\nfor i, v in enumerate(eco_counts.values):\n    plt.text(i, v + len(train_df) * 0.01, str(v), ha='center', fontweight='bold')\n\nplt.tight_layout()\nplt.show()\n\n# Check for class imbalance\nimbalance_ratio = eco_counts.max() / eco_counts.min()\nprint(f\"\\n⚖️ Class imbalance ratio: {imbalance_ratio:.2f}\")\nif imbalance_ratio > 2:\n    print(\"⚠️ Significant class imbalance detected - consider weighted loss or data balancing\")\nelse:\n    print(\"✅ Classes are reasonably balanced\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:14.530612Z","iopub.status.busy":"2025-07-29T14:28:14.530354Z","iopub.status.idle":"2025-07-29T14:28:14.848098Z","shell.execute_reply":"2025-07-29T14:28:14.847236Z","shell.execute_reply.started":"2025-07-29T14:28:14.530594Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Display sample images from each ECO volume","metadata":{}},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 5, figsize=(20, 4))\neco_volumes = sorted(train_df['eco_volume'].unique())\n\nfor idx, eco in enumerate(eco_volumes):\n    # Get a sample image for this ECO volume\n    sample = train_df[train_df['eco_volume'] == eco].iloc[0]\n    # img_path = os.path.join(TRAIN_IMAGES_PATH, sample['image_id'])\n    img_path = os.path.join(TRAIN_IMAGES_PATH, f\"{sample['image_id']}.jpg\")\n\n    \n    if os.path.exists(img_path):\n        img = Image.open(img_path)\n        axes[idx].imshow(img)\n        axes[idx].set_title(f'ECO Volume {eco}\\n{sample[\"opening_name\"][:30]}...', \n                           fontsize=10, fontweight='bold')\n        axes[idx].axis('off')\n    else:\n        axes[idx].text(0.5, 0.5, f'Image not found\\nECO {eco}', \n                      ha='center', va='center', transform=axes[idx].transAxes)\n        axes[idx].axis('off')\n\nplt.suptitle('Sample Chessboard Images by ECO Volume', fontsize=16, fontweight='bold')\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:17.754370Z","iopub.status.busy":"2025-07-29T14:28:17.754089Z","iopub.status.idle":"2025-07-29T14:28:17.906425Z","shell.execute_reply":"2025-07-29T14:28:17.905854Z","shell.execute_reply.started":"2025-07-29T14:28:17.754351Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Preparing and Validating Dataset","metadata":{}},{"cell_type":"code","source":"class ChessECODataset(Dataset):\n    def __init__(self, df, images_path, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.images_path = images_path\n        self.transform = transform\n        \n        # Create label mapping\n        self.eco_to_idx = {eco: idx for idx, eco in enumerate(sorted(df['eco_volume'].unique()))}\n        self.idx_to_eco = {idx: eco for eco, idx in self.eco_to_idx.items()}\n        \n        print(f\"📋 ECO to Index mapping: {self.eco_to_idx}\")\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_id = row['image_id']\n        \n        # Try to load image with smart extension detection\n        img_path = None\n        possible_extensions = ['', '.jpg', '.jpeg', '.png', '.bmp', '.tiff']\n        \n        for ext in possible_extensions:\n            test_path = os.path.join(self.images_path, image_id + ext)\n            if os.path.exists(test_path):\n                img_path = test_path\n                break\n        \n        if img_path is None:\n            print(f\"⚠️ Image not found for any extension: {image_id}\")\n            # Return a blank image as fallback\n            image = Image.new('RGB', (224, 224), color='white')\n        else:\n            try:\n                image = Image.open(img_path).convert('RGB')\n            except Exception as e:\n                print(f\"⚠️ Error loading image {img_path}: {e}\")\n                # Return a blank image as fallback\n                image = Image.new('RGB', (224, 224), color='white')\n        \n        # Apply transforms\n        if self.transform:\n            image = self.transform(image)\n        \n        # Get label\n        label = self.eco_to_idx[row['eco_volume']]\n        \n        return image, label\n    \n    def get_class_weights(self):\n        \"\"\"Calculate class weights for handling imbalanced data\"\"\"\n        eco_counts = self.df['eco_volume'].value_counts()\n        total_samples = len(self.df)\n        weights = []\n        \n        for eco in sorted(self.eco_to_idx.keys()):\n            weight = total_samples / (len(self.eco_to_idx) * eco_counts[eco])\n            weights.append(weight)\n        \n        return torch.FloatTensor(weights)\n\nprint(\"✅ ChessECODataset class defined\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:20.273766Z","iopub.status.busy":"2025-07-29T14:28:20.273220Z","iopub.status.idle":"2025-07-29T14:28:20.284225Z","shell.execute_reply":"2025-07-29T14:28:20.283590Z","shell.execute_reply.started":"2025-07-29T14:28:20.273744Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Dataset validation - Check image files and extensions\ndef validate_dataset_files(df, images_path, sample_size=10):\n    \"\"\"Validate that image files exist and check their extensions\"\"\"\n    print(f\"🔍 Validating dataset files in: {images_path}\")\n    \n    if not os.path.exists(images_path):\n        print(f\"❌ Directory does not exist: {images_path}\")\n        return\n    \n    # Get all files in directory\n    all_files = os.listdir(images_path)\n    image_extensions = {}\n    \n    # Count file extensions\n    for file in all_files:\n        ext = os.path.splitext(file)[1].lower()\n        if ext in ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']:\n            image_extensions[ext] = image_extensions.get(ext, 0) + 1\n    \n    print(f\"📁 Total files in directory: {len(all_files)}\")\n    print(f\"🖼️ Image files by extension: {image_extensions}\")\n    \n    # Check a sample of image_ids from DataFrame\n    sample_df = df.head(sample_size)\n    found_count = 0\n    missing_count = 0\n    \n    print(f\"\\n🧪 Checking {len(sample_df)} sample image IDs:\")\n    for idx, row in sample_df.iterrows():\n        image_id = row['image_id']\n        \n        # Try direct path first\n        direct_path = os.path.join(images_path, image_id)\n        if os.path.exists(direct_path):\n            found_count += 1\n            print(f\"✅ Found: {image_id}\")\n            continue\n        \n        # Try with extensions\n        found = False\n        for ext in ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']:\n            ext_path = os.path.join(images_path, image_id + ext)\n            if os.path.exists(ext_path):\n                found_count += 1\n                print(f\"✅ Found with extension: {image_id} -> {image_id + ext}\")\n                found = True\n                break\n        \n        if not found:\n            missing_count += 1\n            print(f\"❌ Missing: {image_id}\")\n    \n    print(f\"\\n📊 Summary:\")\n    print(f\"  Found: {found_count}/{len(sample_df)}\")\n    print(f\"  Missing: {missing_count}/{len(sample_df)}\")\n    \n    # Show some actual filenames for reference\n    print(f\"\\n📋 Sample actual filenames:\")\n    for i, file in enumerate(all_files[:5]):\n        print(f\"  {file}\")\n    if len(all_files) > 5:\n        print(f\"  ... and {len(all_files) - 5} more files\")\n\n# Validate both training and test datasets\nprint(\"🔍 Validating Training Dataset:\")\nvalidate_dataset_files(train_df, TRAIN_IMAGES_PATH)\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"🔍 Validating Test Dataset:\")\nvalidate_dataset_files(test_df, TEST_IMAGES_PATH)","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:23.344030Z","iopub.status.busy":"2025-07-29T14:28:23.343717Z","iopub.status.idle":"2025-07-29T14:28:23.596893Z","shell.execute_reply":"2025-07-29T14:28:23.595956Z","shell.execute_reply.started":"2025-07-29T14:28:23.344009Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# GPU configuration","metadata":{}},{"cell_type":"code","source":"# Device configuration\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"🖥️ Using device: {device}\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Augmentation","metadata":{}},{"cell_type":"code","source":"# Custom chess rotation transform (❌ DO NOT USE if orientation matters)\nclass ChessRandomRotation:\n    def __init__(self, p=0.5):\n        self.p = p\n        self.angles = [0, 90, 180, 270]\n    \n    def __call__(self, img):\n        if torch.rand(1) < self.p:\n            angle = int(np.random.choice(self.angles))\n            return transforms.functional.rotate(img, angle)\n        return img\n\n# === TRAIN TRANSFORMS ===\ntrain_transform = transforms.Compose([\n    transforms.Resize((256, 256)),             \n    transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),\n\n    transforms.RandomHorizontalFlip(0.5),  # ✅ Safe for chessboards\n\n    transforms.ColorJitter(                # ✅ Safe — simulate lighting variations\n        brightness=0.2,\n        contrast=0.2,\n        saturation=0.1,\n        hue=0.05\n    ),\n    transforms.RandomApply([\n        transforms.GaussianBlur(3, sigma=(0.1, 1.0))\n    ], p=0.1),                             # ✅ Optional noise blur\n    transforms.ToTensor(),\n    transforms.Normalize(                 # ✅ Matches pretrained model expectations\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])\n","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_transform = transforms.Compose([\n    transforms.Resize((256, 256)),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tta_transforms = [\n    # Original image\n    transforms.Compose([\n        transforms.Resize((256, 256)),\n        transforms.CenterCrop(224),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                             std=[0.229, 0.224, 0.225])\n    ]),\n\n    # Horizontal flip — ✅ valid chess symmetry\n    transforms.Compose([\n        transforms.Resize((256, 256)),\n        transforms.CenterCrop(224),\n        transforms.RandomHorizontalFlip(p=1.0),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=[0.485, 0.456, 0.406],\n                             std=[0.229, 0.224, 0.225])\n    ]),\n]","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#  Train Validation Split","metadata":{}},{"cell_type":"code","source":"# Create datasets\ntrain_dataset_full = ChessECODataset(train_df, TRAIN_IMAGES_PATH, transform=train_transform)\nval_dataset_full = ChessECODataset(train_df, TRAIN_IMAGES_PATH, transform=val_transform)\n\n# Stratified train/validation split\nfrom sklearn.model_selection import train_test_split\n\n# Create stratified split indices\ntrain_indices, val_indices = train_test_split(\n    range(len(train_df)),\n    test_size=0.2,\n    stratify=train_df['eco_volume'],\n    random_state=42\n)\n\n# # Create subset datasets\n# train_dataset = Subset(train_dataset_full, train_indices)\n# val_dataset = Subset(val_dataset_full, val_indices)\n\n# Step 2: Use split DataFrames instead of Subset\ntrain_df_split = train_df.iloc[train_indices].reset_index(drop=True)\nval_df_split = train_df.iloc[val_indices].reset_index(drop=True)\n\n# Step 3: Directly create datasets\ntrain_dataset = ChessECODataset(train_df_split, TRAIN_IMAGES_PATH, transform=train_transform)\nval_dataset = ChessECODataset(val_df_split, TRAIN_IMAGES_PATH, transform=val_transform)\n\n# Calculate class weights for loss function\nclass_weights = train_dataset_full.get_class_weights().to(device)\n\nprint(f\"📊 Training samples: {len(train_dataset)}\")\nprint(f\"📊 Validation samples: {len(val_dataset)}\")\nprint(f\"⚖️ Class weights: {class_weights.cpu().numpy()}\")\n\n# Verify stratification\ntrain_eco_dist = train_df.iloc[train_indices]['eco_volume'].value_counts().sort_index()\nval_eco_dist = train_df.iloc[val_indices]['eco_volume'].value_counts().sort_index()\n\nprint(\"\\n📈 Training set distribution:\")\nprint(train_eco_dist)\nprint(\"\\n📈 Validation set distribution:\")\nprint(val_eco_dist)","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:31.707726Z","iopub.status.busy":"2025-07-29T14:28:31.707072Z","iopub.status.idle":"2025-07-29T14:28:31.933688Z","shell.execute_reply":"2025-07-29T14:28:31.933109Z","shell.execute_reply.started":"2025-07-29T14:28:31.707705Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Create data loaders","metadata":{}},{"cell_type":"code","source":"BATCH_SIZE = 32\nNUM_WORKERS = 4\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=NUM_WORKERS,\n    pin_memory=True,\n    drop_last=True  # For stable batch normalization\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=NUM_WORKERS,\n    pin_memory=True\n)\n\nprint(f\"🔄 Data loaders created with batch size: {BATCH_SIZE}\")\nprint(f\"📦 Training batches: {len(train_loader)}\")\nprint(f\"📦 Validation batches: {len(val_loader)}\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:34.485260Z","iopub.status.busy":"2025-07-29T14:28:34.484995Z","iopub.status.idle":"2025-07-29T14:28:34.490715Z","shell.execute_reply":"2025-07-29T14:28:34.489928Z","shell.execute_reply.started":"2025-07-29T14:28:34.485240Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Load EfficientNet-B3 model","metadata":{}},{"cell_type":"code","source":"from torchvision.models import efficientnet_b3\n\n# Load pre-trained model\nmodel = efficientnet_b3(pretrained=True)\n\n# Modify classifier for 5-class chess ECO classification\nnum_classes = len(train_dataset_full.eco_to_idx)\nnum_features = model.classifier[1].in_features\n\nmodel.classifier = nn.Sequential(\n    nn.Dropout(0.3),\n    nn.Linear(num_features, 512),\n    nn.ReLU(),\n    nn.Dropout(0.3),\n    nn.Linear(512, 256),\n    nn.ReLU(),\n    nn.Dropout(0.2),\n    nn.Linear(256, num_classes)\n)\n# model.classifier = nn.Sequential(\n#     nn.Dropout(0.4),\n#     nn.Linear(num_features, 256),\n#     nn.GELU(),\n#     nn.LayerNorm(256),\n#     nn.Dropout(0.3),\n#     nn.Linear(256, num_classes)\n# )\n\n\nmodel = model.to(device)\n\n# Model summary\ntotal_params = sum(p.numel() for p in model.parameters())\ntrainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n\nprint(f\"🏗️ Model: EfficientNet-B3\")\nprint(f\"📊 Total parameters: {total_params:,}\")\nprint(f\"🎯 Trainable parameters: {trainable_params:,}\")\nprint(f\"🔢 Number of classes: {num_classes}\")\nprint(f\"📝 Classes: {list(train_dataset_full.eco_to_idx.keys())}\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:36.875433Z","iopub.status.busy":"2025-07-29T14:28:36.875171Z","iopub.status.idle":"2025-07-29T14:28:37.619598Z","shell.execute_reply":"2025-07-29T14:28:37.619035Z","shell.execute_reply.started":"2025-07-29T14:28:36.875413Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Loss function with class weights to handle imbalance","metadata":{}},{"cell_type":"code","source":"criterion = nn.CrossEntropyLoss(weight=class_weights, label_smoothing=0.0)\n\n# Optimizer - AdamW with weight decay\noptimizer = optim.AdamW(\n    model.parameters(),\n    lr=0.0005,\n    weight_decay=0.01,\n    betas=(0.9, 0.999)\n)\n\n# Learning rate scheduler\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nscheduler = ReduceLROnPlateau(\n    optimizer,\n    mode='max',\n    factor=0.3,\n    patience=3,\n    verbose=True,\n    min_lr=1e-6\n)\n\n# from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts\n\n# scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=10, T_mult=2)\n\n# Training parameters\nEPOCHS = 50#100\nPATIENCE = 15\nSAVE_PATH = 'best_chess_eco_model.pth'","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:28:40.228435Z","iopub.status.busy":"2025-07-29T14:28:40.228204Z","iopub.status.idle":"2025-07-29T14:28:40.236141Z","shell.execute_reply":"2025-07-29T14:28:40.235244Z","shell.execute_reply.started":"2025-07-29T14:28:40.228418Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Verify dataset paths before training","metadata":{}},{"cell_type":"code","source":"import os\n\nprint(\"📍 Checking dataset paths...\")\npaths_to_check = [\n    ('Training images', TRAIN_IMAGES_PATH),\n    ('Test images', TEST_IMAGES_PATH),\n    ('Training CSV', TRAIN_CSV),\n    ('Test CSV', TEST_CSV)\n]\n\nall_paths_exist = True\nfor name, path in paths_to_check:\n    if os.path.exists(path):\n        if os.path.isdir(path):\n            count = len(os.listdir(path))\n            print(f\"✅ {name}: {path} ({count} files)\")\n        else:\n            print(f\"✅ {name}: {path}\")\n    else:\n        print(f\"❌ {name}: {path} (NOT FOUND)\")\n        all_paths_exist = False\n\nif all_paths_exist:\n    print(\"\\n🎉 All paths are valid! Ready to proceed with training.\")\nelse:\n    print(\"\\n⚠️ Some paths are missing. Please update the file paths in cell 3.\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:29:01.963423Z","iopub.status.busy":"2025-07-29T14:29:01.962769Z","iopub.status.idle":"2025-07-29T14:29:01.974345Z","shell.execute_reply":"2025-07-29T14:29:01.973636Z","shell.execute_reply.started":"2025-07-29T14:29:01.963400Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training tracking","metadata":{}},{"cell_type":"code","source":"train_losses = []\ntrain_accuracies = []\nval_losses = []\nval_accuracies = []\nbest_val_acc = 0.0\npatience_counter = 0\n\nprint(\"🚀 Starting training...\\n\")\n\nfor epoch in range(EPOCHS):\n    # Training phase\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    for batch_idx, (images, labels) in enumerate(train_loader):\n        images, labels = images.to(device), labels.to(device)\n        \n        # Forward pass\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        \n        # Backward pass\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n        \n        # Statistics\n        running_loss += loss.item()\n        _, predicted = torch.max(outputs.data, 1)\n        total += labels.size(0)\n        correct += (predicted == labels).sum().item()\n        \n        # Progress logging\n        if batch_idx % 20 == 0:\n            print(f'Epoch {epoch+1}/{EPOCHS}, Batch {batch_idx}/{len(train_loader)}, Loss: {loss.item():.4f}')\n    \n    train_acc = 100 * correct / total\n    avg_train_loss = running_loss / len(train_loader)\n    \n    # Validation phase\n    model.eval()\n    val_correct = 0\n    val_total = 0\n    val_running_loss = 0.0\n    \n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n            val_running_loss += loss.item()\n            _, predicted = torch.max(outputs, 1)\n            val_total += labels.size(0)\n            val_correct += (predicted == labels).sum().item()\n    \n    val_acc = 100 * val_correct / val_total\n    avg_val_loss = val_running_loss / len(val_loader)\n    \n    # Record metrics\n    train_losses.append(avg_train_loss)\n    train_accuracies.append(train_acc)\n    val_losses.append(avg_val_loss)\n    val_accuracies.append(val_acc)\n    \n    # Learning rate scheduling\n    scheduler.step(val_acc)\n    \n    # Print epoch results\n    print(f\"\\n📊 Epoch {epoch+1}/{EPOCHS} Results:\")\n    print(f\"  Train Loss: {avg_train_loss:.4f}, Train Acc: {train_acc:.2f}%\")\n    print(f\"  Val Loss: {avg_val_loss:.4f}, Val Acc: {val_acc:.2f}%\")\n    print(f\"  Learning Rate: {optimizer.param_groups[0]['lr']:.6f}\")\n    \n    # Early stopping and model saving\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        patience_counter = 0\n        \n        # Save best model\n        torch.save({\n            'epoch': epoch,\n            'model_state_dict': model.state_dict(),\n            'optimizer_state_dict': optimizer.state_dict(),\n            'val_acc': val_acc,\n            'eco_to_idx': train_dataset_full.eco_to_idx,\n            'idx_to_eco': train_dataset_full.idx_to_eco\n        }, SAVE_PATH)\n        \n        print(f\"  ✅ New best validation accuracy: {val_acc:.2f}% - Model saved!\")\n    else:\n        patience_counter += 1\n        print(f\"  ⏰ No improvement. Patience: {patience_counter}/{PATIENCE}\")\n    \n    if patience_counter >= PATIENCE:\n        print(f\"\\n🛑 Early stopping triggered after {epoch+1} epochs\")\n        break\n    \n    print(\"-\" * 60)\n\nprint(f\"\\n🎉 Training completed! Best validation accuracy: {best_val_acc:.2f}%\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:29:08.268623Z","iopub.status.busy":"2025-07-29T14:29:08.267944Z","iopub.status.idle":"2025-07-29T14:41:45.656628Z","shell.execute_reply":"2025-07-29T14:41:45.655860Z","shell.execute_reply.started":"2025-07-29T14:29:08.268599Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Plot training progress","metadata":{}},{"cell_type":"code","source":"epochs_completed = len(train_losses)\n\nfig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10))\n\n# Loss plots\nax1.plot(range(1, epochs_completed + 1), train_losses, 'b-', label='Training Loss', linewidth=2)\nax1.plot(range(1, epochs_completed + 1), val_losses, 'r-', label='Validation Loss', linewidth=2)\nax1.set_title('Training and Validation Loss', fontsize=14, fontweight='bold')\nax1.set_xlabel('Epoch')\nax1.set_ylabel('Loss')\nax1.legend()\nax1.grid(True, alpha=0.3)\n\n# Accuracy plots\nax2.plot(range(1, epochs_completed + 1), train_accuracies, 'b-', label='Training Accuracy', linewidth=2)\nax2.plot(range(1, epochs_completed + 1), val_accuracies, 'r-', label='Validation Accuracy', linewidth=2)\nax2.set_title('Training and Validation Accuracy', fontsize=14, fontweight='bold')\nax2.set_xlabel('Epoch')\nax2.set_ylabel('Accuracy (%)')\nax2.legend()\nax2.grid(True, alpha=0.3)\n\n# Learning rate plot (if available)\nax3.plot(range(1, epochs_completed + 1), [optimizer.param_groups[0]['lr']] * epochs_completed, 'g-', linewidth=2)\nax3.set_title('Learning Rate Schedule', fontsize=14, fontweight='bold')\nax3.set_xlabel('Epoch')\nax3.set_ylabel('Learning Rate')\nax3.set_yscale('log')\nax3.grid(True, alpha=0.3)\n\n# Training summary\nax4.axis('off')\nsummary_text = f\"\"\"\n📊 Training Summary\n\n🎯 Best Validation Accuracy: {best_val_acc:.2f}%\n📈 Final Training Accuracy: {train_accuracies[-1]:.2f}%\n📉 Final Validation Loss: {val_losses[-1]:.4f}\n🔄 Epochs Completed: {epochs_completed}\n⏰ Early Stopping: {'Yes' if patience_counter >= PATIENCE else 'No'}\n💾 Model Saved: {SAVE_PATH}\n\n🏆 Model: EfficientNet-B3\n📚 Classes: {num_classes} ECO Volumes\n🔢 Parameters: {total_params:,}\n\"\"\"\nax4.text(0.1, 0.9, summary_text, transform=ax4.transAxes, fontsize=11, \n         verticalalignment='top', fontfamily='monospace')\n\nplt.tight_layout()\nplt.savefig('chess_training_progress.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:41:45.658358Z","iopub.status.busy":"2025-07-29T14:41:45.658126Z","iopub.status.idle":"2025-07-29T14:41:47.730544Z","shell.execute_reply":"2025-07-29T14:41:47.729839Z","shell.execute_reply.started":"2025-07-29T14:41:45.658336Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Load best model","metadata":{}},{"cell_type":"code","source":"checkpoint = torch.load(SAVE_PATH)\nmodel.load_state_dict(checkpoint['model_state_dict'])\nprint(f\"✅ Loaded best model with validation accuracy: {checkpoint['val_acc']:.2f}%\")\n\n# Detailed validation evaluation\nmodel.eval()\nall_predictions = []\nall_labels = []\nall_probabilities = []\n\nwith torch.no_grad():\n    for images, labels in val_loader:\n        images, labels = images.to(device), labels.to(device)\n        outputs = model(images)\n        probabilities = torch.softmax(outputs, dim=1)\n        _, predicted = torch.max(outputs, 1)\n        \n        all_predictions.extend(predicted.cpu().numpy())\n        all_labels.extend(labels.cpu().numpy())\n        all_probabilities.extend(probabilities.cpu().numpy())\n\n# Convert to numpy arrays\nall_predictions = np.array(all_predictions)\nall_labels = np.array(all_labels)\nall_probabilities = np.array(all_probabilities)\n\n# Calculate accuracy\naccuracy = (all_predictions == all_labels).mean()\nprint(f\"\\n🎯 Validation Accuracy: {accuracy:.4f} ({accuracy*100:.2f}%)\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:41:47.731722Z","iopub.status.busy":"2025-07-29T14:41:47.731485Z","iopub.status.idle":"2025-07-29T14:41:50.162569Z","shell.execute_reply":"2025-07-29T14:41:50.161894Z","shell.execute_reply.started":"2025-07-29T14:41:47.731704Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Classification report","metadata":{}},{"cell_type":"code","source":"eco_names = [train_dataset_full.idx_to_eco[i] for i in range(num_classes)]\n\nprint(\"📋 Classification Report:\")\nprint(classification_report(all_labels, all_predictions, target_names=eco_names, digits=4))\n\n# Confusion Matrix\ncm = confusion_matrix(all_labels, all_predictions)\n\nplt.figure(figsize=(10, 8))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n            xticklabels=eco_names, yticklabels=eco_names)\nplt.title('Confusion Matrix - Chess ECO Classification', fontsize=16, fontweight='bold')\nplt.xlabel('Predicted ECO Volume', fontsize=12)\nplt.ylabel('True ECO Volume', fontsize=12)\nplt.tight_layout()\nplt.savefig('chess_confusion_matrix.png', dpi=300, bbox_inches='tight')\nplt.show()\n\n# Per-class accuracy\nclass_accuracies = cm.diagonal() / cm.sum(axis=1)\nprint(\"\\n📊 Per-class Accuracies:\")\nfor i, eco in enumerate(eco_names):\n    print(f\"  ECO {eco}: {class_accuracies[i]:.4f} ({class_accuracies[i]*100:.2f}%)\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:41:50.164925Z","iopub.status.busy":"2025-07-29T14:41:50.164671Z","iopub.status.idle":"2025-07-29T14:41:50.920853Z","shell.execute_reply":"2025-07-29T14:41:50.920071Z","shell.execute_reply.started":"2025-07-29T14:41:50.164897Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Create test dataset for TTA","metadata":{}},{"cell_type":"code","source":"class TestDatasetTTA(Dataset):\n    def __init__(self, df, images_path, tta_transforms):\n        self.df = df.reset_index(drop=True)\n        self.images_path = images_path\n        self.tta_transforms = tta_transforms\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        image_id = row['image_id']\n        \n        # Smart file loading with extension detection\n        img_path = None\n        possible_extensions = ['', '.jpg', '.jpeg', '.png', '.bmp', '.tiff']\n        \n        for ext in possible_extensions:\n            test_path = os.path.join(self.images_path, image_id + ext)\n            if os.path.exists(test_path):\n                img_path = test_path\n                break\n        \n        if img_path is None:\n            # If still no valid path found, list available files and try to match\n            available_files = os.listdir(self.images_path)\n            base_name = os.path.splitext(image_id)[0]  # Remove any existing extension\n            \n            # Try to find a matching file\n            matching_file = None\n            for file in available_files:\n                if os.path.splitext(file)[0] == base_name:\n                    matching_file = file\n                    break\n            \n            if matching_file:\n                img_path = os.path.join(self.images_path, matching_file)\n        \n        # Load the image\n        try:\n            if img_path and os.path.exists(img_path):\n                image = Image.open(img_path).convert('RGB')\n            else:\n                print(f\"⚠️ No image found for {image_id} in {self.images_path}\")\n                image = Image.new('RGB', (224, 224), color='white')\n        except Exception as e:\n            print(f\"⚠️ Error loading test image {img_path}: {e}\")\n            image = Image.new('RGB', (224, 224), color='white')\n        \n        # Apply all TTA transforms\n        tta_images = []\n        for transform in self.tta_transforms:\n            tta_images.append(transform(image))\n        \n        return tta_images, row['image_id']\n\n# Create test dataset with TTA\ntest_dataset_tta = TestDatasetTTA(test_df, TEST_IMAGES_PATH, tta_transforms)\ntest_loader_tta = DataLoader(test_dataset_tta, batch_size=8, shuffle=False, num_workers=2)\n\nprint(f\"🧪 Test dataset with TTA created: {len(test_dataset_tta)} samples\")\nprint(f\"🔄 Number of TTA transforms: {len(tta_transforms)}\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:45:43.258462Z","iopub.status.busy":"2025-07-29T14:45:43.257732Z","iopub.status.idle":"2025-07-29T14:45:43.268752Z","shell.execute_reply":"2025-07-29T14:45:43.267994Z","shell.execute_reply.started":"2025-07-29T14:45:43.258439Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Inference with Test Time Augmentation\nmodel.eval()\ntest_results = []\n\nprint(\"🔮 Starting test inference with TTA...\")\n\nwith torch.no_grad():\n    for batch_idx, (tta_images_list, image_ids) in enumerate(test_loader_tta):\n        batch_predictions = []\n        \n        for img_idx in range(len(image_ids)):\n            # Collect predictions from all TTA transforms\n            tta_predictions = []\n            \n            for tta_idx in range(len(tta_transforms)):\n                img = tta_images_list[tta_idx][img_idx].unsqueeze(0).to(device)\n                output = model(img)\n                prob = torch.softmax(output, dim=1)\n                tta_predictions.append(prob.cpu())\n            \n            # Average predictions across all TTA transforms\n            avg_prediction = torch.mean(torch.stack(tta_predictions), dim=0)\n            pred_class = torch.argmax(avg_prediction, dim=1).item()\n            confidence = avg_prediction[0][pred_class].item()\n            \n            # Convert prediction back to ECO volume\n            eco_volume = train_dataset_full.idx_to_eco[pred_class]\n            \n            test_results.append({\n                'image_id': image_ids[img_idx],\n                'eco_volume': eco_volume,\n                'confidence': confidence\n            })\n        \n        if batch_idx % 10 == 0:\n            print(f\"Processed {batch_idx * test_loader_tta.batch_size}/{len(test_dataset_tta)} images\")\n\nprint(f\"\\n✅ Test inference completed! Generated predictions for {len(test_results)} images\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:45:45.526108Z","iopub.status.busy":"2025-07-29T14:45:45.525441Z","iopub.status.idle":"2025-07-29T14:46:16.592592Z","shell.execute_reply":"2025-07-29T14:46:16.591629Z","shell.execute_reply.started":"2025-07-29T14:45:45.526083Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create submission DataFrame\nsubmission_df = pd.DataFrame(test_results)\n\n# Ensure all test images are included\nsubmission_df = submission_df.merge(test_df[['image_id']], on='image_id', how='right')\n\n# Check for any missing predictions\nmissing_predictions = submission_df['eco_volume'].isna().sum()\nif missing_predictions > 0:\n    print(f\"⚠️ Warning: {missing_predictions} missing predictions detected\")\n    # Fill missing predictions with most common class\n    most_common_eco = submission_df['eco_volume'].mode()[0]\n    submission_df['eco_volume'].fillna(most_common_eco, inplace=True)\n    print(f\"Filled missing predictions with most common class: {most_common_eco}\")\n\n# Create final submission file\nfinal_submission = submission_df[['image_id', 'eco_volume']].copy()\nfinal_submission.to_csv('chess_eco_submission.csv', index=False)\n\nprint(f\"\\n📄 Submission file created: chess_eco_submission.csv\")\nprint(f\"📊 Submission shape: {final_submission.shape}\")\nprint(f\"\\n📈 Prediction distribution:\")\nprint(final_submission['eco_volume'].value_counts().sort_index())\n\n# Display sample predictions\nprint(f\"\\n🔍 Sample predictions:\")\nsample_with_confidence = submission_df[['image_id', 'eco_volume', 'confidence']].head(10)\nprint(sample_with_confidence.to_string(index=False))\n\nprint(f\"\\n✅ Ready for submission!\")\nprint(f\"📋 Final validation accuracy: {best_val_acc:.2f}%\")\nprint(f\"🎯 Model: EfficientNet-B3 with TTA\")\nprint(f\"📁 Submission file: chess_eco_submission.csv\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:46:20.811881Z","iopub.status.busy":"2025-07-29T14:46:20.811345Z","iopub.status.idle":"2025-07-29T14:46:20.831413Z","shell.execute_reply":"2025-07-29T14:46:20.830835Z","shell.execute_reply.started":"2025-07-29T14:46:20.811852Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Confidence distribution analysis\nplt.figure(figsize=(15, 5))\n\n# Overall confidence distribution\nplt.subplot(1, 3, 1)\nplt.hist(submission_df['confidence'].dropna(), bins=30, alpha=0.7, color='skyblue', edgecolor='black')\nplt.title('Prediction Confidence Distribution', fontweight='bold')\nplt.xlabel('Confidence Score')\nplt.ylabel('Frequency')\nplt.grid(True, alpha=0.3)\n\n# Confidence by ECO volume\nplt.subplot(1, 3, 2)\nfor eco in sorted(submission_df['eco_volume'].dropna().unique()):\n    eco_confidences = submission_df[submission_df['eco_volume'] == eco]['confidence'].dropna()\n    plt.hist(eco_confidences, alpha=0.6, label=f'ECO {eco}', bins=20)\nplt.title('Confidence by ECO Volume', fontweight='bold')\nplt.xlabel('Confidence Score')\nplt.ylabel('Frequency')\nplt.legend()\nplt.grid(True, alpha=0.3)\n\n# Average confidence by ECO volume\nplt.subplot(1, 3, 3)\navg_confidence = submission_df.groupby('eco_volume')['confidence'].mean().sort_index()\nbars = plt.bar(avg_confidence.index, avg_confidence.values, \n               color=['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'])\nplt.title('Average Confidence by ECO Volume', fontweight='bold')\nplt.xlabel('ECO Volume')\nplt.ylabel('Average Confidence')\nplt.ylim(0, 1)\n\n# Add value labels on bars\nfor bar, val in zip(bars, avg_confidence.values):\n    plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01, \n             f'{val:.3f}', ha='center', fontweight='bold')\n\nplt.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.savefig('chess_confidence_analysis.png', dpi=300, bbox_inches='tight')\nplt.show()\n\n# Summary statistics\nprint(\"📊 Confidence Statistics:\")\nprint(f\"  Mean confidence: {submission_df['confidence'].mean():.4f}\")\nprint(f\"  Median confidence: {submission_df['confidence'].median():.4f}\")\nprint(f\"  Min confidence: {submission_df['confidence'].min():.4f}\")\nprint(f\"  Max confidence: {submission_df['confidence'].max():.4f}\")\nprint(f\"  Std confidence: {submission_df['confidence'].std():.4f}\")\n\n# High confidence predictions\nhigh_conf_threshold = 0.9\nhigh_conf_predictions = submission_df[submission_df['confidence'] > high_conf_threshold]\nprint(f\"\\n🎯 High confidence predictions (>{high_conf_threshold}): {len(high_conf_predictions)} ({len(high_conf_predictions)/len(submission_df)*100:.1f}%)\")\n\n# Low confidence predictions\nlow_conf_threshold = 0.5\nlow_conf_predictions = submission_df[submission_df['confidence'] < low_conf_threshold]\nprint(f\"⚠️ Low confidence predictions (<{low_conf_threshold}): {len(low_conf_predictions)} ({len(low_conf_predictions)/len(submission_df)*100:.1f}%)\")","metadata":{"execution":{"iopub.execute_input":"2025-07-29T14:46:24.018176Z","iopub.status.busy":"2025-07-29T14:46:24.017912Z","iopub.status.idle":"2025-07-29T14:46:25.657932Z","shell.execute_reply":"2025-07-29T14:46:25.657266Z","shell.execute_reply.started":"2025-07-29T14:46:24.018155Z"},"trusted":true},"outputs":[],"execution_count":null}]}