{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":127713,"databundleVersionId":15291341,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"4274a56c","cell_type":"markdown","source":"# Anime Gender Classification","metadata":{}},{"id":"7c9bcbe7","cell_type":"code","source":"import os\nimport gc\nimport random\nimport warnings\nfrom pathlib import Path\nfrom typing import Optional, Tuple, List\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import GradScaler, autocast\nfrom torch.optim.lr_scheduler import CosineAnnealingWarmRestarts\n\nimport timm\nfrom torchvision import transforms\n\nwarnings.filterwarnings(\"ignore\")\n\n# Check environment\ndef is_kaggle() -> bool:\n    return bool(os.environ.get(\"KAGGLE_KERNEL_RUN_TYPE\") or os.environ.get(\"KAGGLE_URL_BASE\"))\n\nprint(f\"PyTorch: {torch.__version__}\")\nprint(f\"CUDA available: {torch.cuda.is_available()}\")\nprint(f\"GPU count: {torch.cuda.device_count()}\")\nif torch.cuda.is_available():\n    for i in range(torch.cuda.device_count()):\n        print(f\"  GPU {i}: {torch.cuda.get_device_name(i)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T04:54:37.671404Z","iopub.execute_input":"2026-02-04T04:54:37.672200Z","iopub.status.idle":"2026-02-04T04:54:56.432802Z","shell.execute_reply.started":"2026-02-04T04:54:37.672165Z","shell.execute_reply":"2026-02-04T04:54:56.431848Z"}},"outputs":[],"execution_count":null},{"id":"6c3a8b94","cell_type":"code","source":"# =============================================================================\n# Configuration - Optimized for dual T4 GPUs\n# =============================================================================\n\n# Set to True for quick local testing (fewer epochs, single fold)\nQUICK_RUN = False\n\nclass CFG:\n    # Model\n    model_name = \"tf_efficientnet_b4_ns\"  # ImageNet pretrained, strong baseline\n    pretrained = True\n    num_classes = 2\n    \n    # Training\n    epochs = 2 if QUICK_RUN else 15\n    batch_size = 16 if QUICK_RUN else 32  # Per GPU, effective = 64 with 2 GPUs\n    accumulation_steps = 1\n    lr = 2e-4\n    weight_decay = 1e-4\n    \n    # Data\n    img_size = 224 if QUICK_RUN else 380  # EfficientNet-B4 optimal at 380\n    num_workers = 0 if not is_kaggle() else 4  # macOS fix for local\n    \n    # Advanced\n    use_amp = True  # Mixed precision\n    use_tta = not QUICK_RUN  # Test Time Augmentation\n    tta_transforms = 5  # Number of TTA variants\n    n_folds = 5  # K-fold CV\n    train_folds = [0] if QUICK_RUN else [0, 1, 2, 3, 4]  # Which folds to train\n    \n    # Augmentation\n    mixup_alpha = 0.4\n    cutmix_alpha = 1.0\n    mixup_prob = 0.5\n    \n    # Seed\n    seed = 42\n    \n    # Paths (auto-detected)\n    data_root = None\n    output_dir = None\n\nprint(f\"Mode: {'QUICK_RUN (local test)' if QUICK_RUN else 'FULL (competition)'}\")\n\ndef set_seed(seed: int = 42):\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.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(CFG.seed)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T04:54:56.435064Z","iopub.execute_input":"2026-02-04T04:54:56.435367Z","iopub.status.idle":"2026-02-04T04:54:56.450373Z","shell.execute_reply.started":"2026-02-04T04:54:56.435340Z","shell.execute_reply":"2026-02-04T04:54:56.449482Z"}},"outputs":[],"execution_count":null},{"id":"eead8746","cell_type":"code","source":"# =============================================================================\n# Data Discovery - Auto-detect Kaggle or Local\n# =============================================================================\ndef find_data_root() -> Path:\n    \"\"\"Locate dataset root (contains train.csv). Works for local and Kaggle.\"\"\"\n    # Kaggle paths\n    if is_kaggle():\n        kaggle_input = Path(\"/kaggle/input\")\n        for name in [\"anime-gender-classification\", \"anime-gender-classification-dataset\"]:\n            d = kaggle_input / name\n            if d.exists():\n                for sub in [d, d / \"dataset\"]:\n                    if (sub / \"train.csv\").exists():\n                        return sub\n        # Scan all input folders\n        if kaggle_input.exists():\n            for d in kaggle_input.iterdir():\n                if d.is_dir():\n                    for sub in [d, d / \"dataset\"]:\n                        if (sub / \"train.csv\").exists():\n                            return sub\n    \n    # Local paths\n    cwd = Path.cwd()\n    for r in [cwd, cwd.parent, cwd.parent.parent]:\n        for p in [r / \"dataset\", r]:\n            if (p / \"train.csv\").exists():\n                return p\n    \n    raise FileNotFoundError(\"Could not locate dataset. Add data to Kaggle or check local dataset/ folder.\")\n\n# Setup paths\nCFG.data_root = find_data_root()\nCFG.output_dir = Path(\"/kaggle/working\") if is_kaggle() else (CFG.data_root.parent / \"outputs\")\nCFG.output_dir.mkdir(parents=True, exist_ok=True)\n\nprint(f\"Data root: {CFG.data_root}\")\nprint(f\"Output dir: {CFG.output_dir}\")\n\n# Load CSVs\ntrain_df = pd.read_csv(CFG.data_root / \"train.csv\")\nval_df = pd.read_csv(CFG.data_root / \"val.csv\")\ntest_df = pd.read_csv(CFG.data_root / \"test.csv\")\n\n# Combine train + val for K-fold (use all labeled data)\nfull_df = pd.concat([train_df, val_df], ignore_index=True)\nprint(f\"Total labeled samples: {len(full_df)}\")\nprint(f\"Test samples: {len(test_df)}\")\nprint(f\"Label distribution:\\n{full_df['label'].value_counts()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T04:54:56.451387Z","iopub.execute_input":"2026-02-04T04:54:56.451701Z","iopub.status.idle":"2026-02-04T04:54:56.555583Z","shell.execute_reply.started":"2026-02-04T04:54:56.451676Z","shell.execute_reply":"2026-02-04T04:54:56.554702Z"}},"outputs":[],"execution_count":null},{"id":"b4c82e24","cell_type":"code","source":"# =============================================================================\n# K-Fold Split (Stratified)\n# =============================================================================\nfrom sklearn.model_selection import StratifiedKFold\n\nskf = StratifiedKFold(n_splits=CFG.n_folds, shuffle=True, random_state=CFG.seed)\nfull_df[\"fold\"] = -1\n\nfor fold, (_, val_idx) in enumerate(skf.split(full_df, full_df[\"label\"])):\n    full_df.loc[val_idx, \"fold\"] = fold\n\nprint(\"Fold distribution:\")\nprint(full_df.groupby([\"fold\", \"label\"]).size().unstack())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T04:54:56.556700Z","iopub.execute_input":"2026-02-04T04:54:56.557137Z","iopub.status.idle":"2026-02-04T04:54:58.423279Z","shell.execute_reply.started":"2026-02-04T04:54:56.557108Z","shell.execute_reply":"2026-02-04T04:54:58.422316Z"}},"outputs":[],"execution_count":null},{"id":"75d7e484","cell_type":"code","source":"# =============================================================================\n# Dataset and Augmentations\n# =============================================================================\ndef get_train_transforms(img_size: int):\n    return transforms.Compose([\n        transforms.RandomResizedCrop(img_size, scale=(0.8, 1.0), ratio=(0.9, 1.1)),\n        transforms.RandomHorizontalFlip(p=0.5),\n        transforms.RandomVerticalFlip(p=0.1),\n        transforms.RandomRotation(15),\n        transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.1),\n        transforms.RandomGrayscale(p=0.05),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        transforms.RandomErasing(p=0.3, scale=(0.02, 0.2)),\n    ])\n\ndef get_val_transforms(img_size: int):\n    return transforms.Compose([\n        transforms.Resize(img_size + 32),\n        transforms.CenterCrop(img_size),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ])\n\ndef get_tta_transforms(img_size: int) -> List[transforms.Compose]:\n    \"\"\"Multiple transforms for Test Time Augmentation\"\"\"\n    base_norm = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n    return [\n        # Original\n        transforms.Compose([\n            transforms.Resize(img_size + 32),\n            transforms.CenterCrop(img_size),\n            transforms.ToTensor(),\n            base_norm,\n        ]),\n        # Horizontal flip\n        transforms.Compose([\n            transforms.Resize(img_size + 32),\n            transforms.CenterCrop(img_size),\n            transforms.RandomHorizontalFlip(p=1.0),\n            transforms.ToTensor(),\n            base_norm,\n        ]),\n        # Slight rotation\n        transforms.Compose([\n            transforms.Resize(img_size + 32),\n            transforms.CenterCrop(img_size),\n            transforms.RandomRotation((5, 5)),\n            transforms.ToTensor(),\n            base_norm,\n        ]),\n        # Scale up\n        transforms.Compose([\n            transforms.Resize(img_size + 64),\n            transforms.CenterCrop(img_size),\n            transforms.ToTensor(),\n            base_norm,\n        ]),\n        # Scale down\n        transforms.Compose([\n            transforms.Resize(img_size),\n            transforms.CenterCrop(img_size - 32),\n            transforms.Resize(img_size),\n            transforms.ToTensor(),\n            base_norm,\n        ]),\n    ]\n\nclass AnimeDataset(Dataset):\n    def __init__(self, df: pd.DataFrame, img_dir: Path, transform=None, has_labels: bool = True):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n        self.has_labels = has_labels\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx: int):\n        row = self.df.iloc[idx]\n        img_path = self.img_dir / row[\"id\"]\n        image = Image.open(img_path).convert(\"RGB\")\n        \n        if self.transform:\n            image = self.transform(image)\n        \n        if self.has_labels:\n            label = int(row[\"label\"])\n            return image, label\n        return image, row[\"id\"]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T04:54:58.424619Z","iopub.execute_input":"2026-02-04T04:54:58.425899Z","iopub.status.idle":"2026-02-04T04:54:58.439673Z","shell.execute_reply.started":"2026-02-04T04:54:58.425850Z","shell.execute_reply":"2026-02-04T04:54:58.438756Z"}},"outputs":[],"execution_count":null},{"id":"1147196b","cell_type":"code","source":"# =============================================================================\n# MixUp / CutMix Augmentation\n# =============================================================================\ndef mixup_data(x, y, alpha=0.4):\n    \"\"\"MixUp augmentation\"\"\"\n    if alpha > 0:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1\n    \n    batch_size = x.size(0)\n    index = torch.randperm(batch_size).to(x.device)\n    \n    mixed_x = lam * x + (1 - lam) * x[index]\n    y_a, y_b = y, y[index]\n    return mixed_x, y_a, y_b, lam\n\ndef mixup_criterion(criterion, pred, y_a, y_b, lam):\n    return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)\n\n# =============================================================================\n# Model with Multi-GPU Support\n# =============================================================================\ndef create_model(model_name: str, num_classes: int, pretrained: bool = True):\n    \"\"\"Create model using timm with ImageNet pretrained weights\"\"\"\n    model = timm.create_model(\n        model_name,\n        pretrained=pretrained,\n        num_classes=num_classes,\n        drop_rate=0.3,\n        drop_path_rate=0.2,\n    )\n    return model\n\ndef prepare_model_for_training(model, device):\n    \"\"\"Wrap model with DataParallel if multiple GPUs available\"\"\"\n    model = model.to(device)\n    \n    if torch.cuda.device_count() > 1:\n        print(f\"Using {torch.cuda.device_count()} GPUs with DataParallel\")\n        model = nn.DataParallel(model)\n    \n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T04:54:58.440949Z","iopub.execute_input":"2026-02-04T04:54:58.441350Z","iopub.status.idle":"2026-02-04T04:54:58.465963Z","shell.execute_reply.started":"2026-02-04T04:54:58.441312Z","shell.execute_reply":"2026-02-04T04:54:58.465025Z"}},"outputs":[],"execution_count":null},{"id":"e05b9f8e","cell_type":"code","source":"# =============================================================================\n# Training Functions with AMP\n# =============================================================================\ndef train_one_epoch(model, loader, criterion, optimizer, scheduler, scaler, device, epoch):\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    pbar = tqdm(loader, desc=f\"Train Epoch {epoch+1}\")\n    for step, (images, labels) in enumerate(pbar):\n        images = images.to(device)\n        labels = labels.to(device)\n        \n        # MixUp with probability\n        use_mixup = np.random.rand() < CFG.mixup_prob\n        if use_mixup:\n            images, labels_a, labels_b, lam = mixup_data(images, labels, CFG.mixup_alpha)\n        \n        optimizer.zero_grad()\n        \n        # Mixed precision forward\n        with autocast(enabled=CFG.use_amp):\n            outputs = model(images)\n            if use_mixup:\n                loss = mixup_criterion(criterion, outputs, labels_a, labels_b, lam)\n            else:\n                loss = criterion(outputs, labels)\n        \n        # Backward with gradient scaling\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        # Update scheduler (per step for CosineAnnealing)\n        if scheduler is not None:\n            scheduler.step(epoch + step / len(loader))\n        \n        running_loss += loss.item() * images.size(0)\n        preds = outputs.argmax(dim=1)\n        if not use_mixup:\n            correct += (preds == labels).sum().item()\n            total += labels.size(0)\n        \n        # Update progress bar\n        current_lr = optimizer.param_groups[0][\"lr\"]\n        pbar.set_postfix({\"loss\": loss.item(), \"lr\": f\"{current_lr:.2e}\"})\n    \n    epoch_loss = running_loss / len(loader.dataset)\n    epoch_acc = correct / total if total > 0 else 0.0\n    return epoch_loss, epoch_acc\n\n@torch.no_grad()\ndef validate(model, loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    \n    for images, labels in tqdm(loader, desc=\"Validating\"):\n        images = images.to(device)\n        labels = labels.to(device)\n        \n        with autocast(enabled=CFG.use_amp):\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n        \n        running_loss += loss.item() * images.size(0)\n        preds = outputs.argmax(dim=1)\n        correct += (preds == labels).sum().item()\n        total += labels.size(0)\n    \n    epoch_loss = running_loss / total\n    epoch_acc = correct / total\n    return epoch_loss, epoch_acc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T04:54:58.468750Z","iopub.execute_input":"2026-02-04T04:54:58.469188Z","iopub.status.idle":"2026-02-04T04:54:58.487579Z","shell.execute_reply.started":"2026-02-04T04:54:58.469154Z","shell.execute_reply":"2026-02-04T04:54:58.486593Z"}},"outputs":[],"execution_count":null},{"id":"f4ea1fe7","cell_type":"code","source":"# =============================================================================\n# K-Fold Training Loop\n# =============================================================================\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Device: {device}\")\n\n# Determine image directories\n# Check if images are in split folders or single folder\nif (CFG.data_root / \"train_images\").exists():\n    train_img_dir = CFG.data_root / \"train_images\"\n    val_img_dir = CFG.data_root / \"val_images\"\n    test_img_dir = CFG.data_root / \"test_images\"\n    # For combined df, we need to handle both directories\n    # Create a mapping function\n    def get_img_dir(img_id: str) -> Path:\n        if (train_img_dir / img_id).exists():\n            return train_img_dir\n        elif (val_img_dir / img_id).exists():\n            return val_img_dir\n        return train_img_dir  # fallback\nelse:\n    train_img_dir = CFG.data_root / \"images\"\n    val_img_dir = train_img_dir\n    test_img_dir = train_img_dir\n    def get_img_dir(img_id: str) -> Path:\n        return train_img_dir\n\nprint(f\"Train images: {train_img_dir}\")\nprint(f\"Test images: {test_img_dir}\")\n\n# Store OOF predictions and models\noof_preds = np.zeros((len(full_df), CFG.num_classes))\nbest_scores = []\n\nfor fold in CFG.train_folds:\n    print(f\"\\n{'='*50}\")\n    print(f\"FOLD {fold}\")\n    print(f\"{'='*50}\")\n    \n    # Split data\n    train_fold_df = full_df[full_df[\"fold\"] != fold].reset_index(drop=True)\n    val_fold_df = full_df[full_df[\"fold\"] == fold].reset_index(drop=True)\n    \n    print(f\"Train: {len(train_fold_df)}, Val: {len(val_fold_df)}\")\n    \n    # Create datasets - handle split directories\n    class FoldDataset(Dataset):\n        def __init__(self, df, transform, has_labels=True):\n            self.df = df.reset_index(drop=True)\n            self.transform = transform\n            self.has_labels = has_labels\n        \n        def __len__(self):\n            return len(self.df)\n        \n        def __getitem__(self, idx):\n            row = self.df.iloc[idx]\n            img_id = row[\"id\"]\n            # Try train_images first, then val_images\n            img_path = train_img_dir / img_id\n            if not img_path.exists():\n                img_path = val_img_dir / img_id\n            image = Image.open(img_path).convert(\"RGB\")\n            if self.transform:\n                image = self.transform(image)\n            if self.has_labels:\n                return image, int(row[\"label\"])\n            return image, img_id\n    \n    train_ds = FoldDataset(train_fold_df, get_train_transforms(CFG.img_size))\n    val_ds = FoldDataset(val_fold_df, get_val_transforms(CFG.img_size))\n    \n    # DataLoaders with multi-GPU batch size\n    effective_batch = CFG.batch_size * max(1, torch.cuda.device_count())\n    train_loader = DataLoader(train_ds, batch_size=effective_batch, shuffle=True, \n                              num_workers=CFG.num_workers, pin_memory=True, drop_last=True)\n    val_loader = DataLoader(val_ds, batch_size=effective_batch, shuffle=False,\n                            num_workers=CFG.num_workers, pin_memory=True)\n    \n    # Create model\n    model = create_model(CFG.model_name, CFG.num_classes, CFG.pretrained)\n    model = prepare_model_for_training(model, device)\n    \n    # Loss, optimizer, scheduler\n    criterion = nn.CrossEntropyLoss(label_smoothing=0.1)\n    optimizer = torch.optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay)\n    scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=CFG.epochs, T_mult=1, eta_min=1e-7)\n    scaler = GradScaler(enabled=CFG.use_amp)\n    \n    # Training\n    best_acc = 0.0\n    best_model_path = CFG.output_dir / f\"best_fold{fold}.pt\"\n    \n    for epoch in range(CFG.epochs):\n        train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, \n                                                 scheduler, scaler, device, epoch)\n        val_loss, val_acc = validate(model, val_loader, criterion, device)\n        \n        print(f\"Epoch {epoch+1}/{CFG.epochs} - \"\n              f\"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.4f} - \"\n              f\"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.4f}\")\n        \n        if val_acc > best_acc:\n            best_acc = val_acc\n            # Save model state (unwrap DataParallel if needed)\n            state_dict = model.module.state_dict() if hasattr(model, \"module\") else model.state_dict()\n            torch.save(state_dict, best_model_path)\n            print(f\"  -> New best! Saved to {best_model_path}\")\n    \n    best_scores.append(best_acc)\n    print(f\"Fold {fold} Best Accuracy: {best_acc:.4f}\")\n    \n    # Clean up\n    del model, optimizer, scheduler, scaler\n    gc.collect()\n    torch.cuda.empty_cache()\n\nprint(f\"\\n{'='*50}\")\nprint(f\"CV Score: {np.mean(best_scores):.4f} (+/- {np.std(best_scores):.4f})\")\nprint(f\"{'='*50}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T04:54:58.488859Z","iopub.execute_input":"2026-02-04T04:54:58.489740Z","iopub.status.idle":"2026-02-04T07:51:10.178536Z","shell.execute_reply.started":"2026-02-04T04:54:58.489707Z","shell.execute_reply":"2026-02-04T07:51:10.177705Z"}},"outputs":[],"execution_count":null},{"id":"78d5a6e4","cell_type":"code","source":"# =============================================================================\n# Inference with TTA and Ensemble\n# =============================================================================\n@torch.no_grad()\ndef predict_with_tta(model, img_dir: Path, test_df: pd.DataFrame, device, use_tta: bool = True):\n    \"\"\"Predict with Test Time Augmentation\"\"\"\n    model.eval()\n    \n    tta_tfms = get_tta_transforms(CFG.img_size) if use_tta else [get_val_transforms(CFG.img_size)]\n    \n    all_preds = []\n    \n    for tfm in tta_tfms:\n        test_ds = AnimeDataset(test_df, img_dir, transform=tfm, has_labels=False)\n        test_loader = DataLoader(test_ds, batch_size=CFG.batch_size * 2, shuffle=False,\n                                 num_workers=CFG.num_workers, pin_memory=True)\n        \n        preds = []\n        for images, _ in tqdm(test_loader, desc=\"TTA Inference\", leave=False):\n            images = images.to(device)\n            with autocast(enabled=CFG.use_amp):\n                outputs = model(images)\n                probs = F.softmax(outputs, dim=1)\n            preds.append(probs.cpu().numpy())\n        \n        all_preds.append(np.concatenate(preds, axis=0))\n    \n    # Average TTA predictions\n    avg_preds = np.mean(all_preds, axis=0)\n    return avg_preds\n\n# Ensemble predictions from all folds\nprint(\"Running inference with TTA ensemble...\")\nensemble_preds = np.zeros((len(test_df), CFG.num_classes))\n\nfor fold in CFG.train_folds:\n    model_path = CFG.output_dir / f\"best_fold{fold}.pt\"\n    if not model_path.exists():\n        print(f\"Warning: Model for fold {fold} not found, skipping\")\n        continue\n    \n    print(f\"Loading fold {fold} model...\")\n    model = create_model(CFG.model_name, CFG.num_classes, pretrained=False)\n    model.load_state_dict(torch.load(model_path, map_location=device))\n    model = model.to(device)\n    model.eval()\n    \n    fold_preds = predict_with_tta(model, test_img_dir, test_df, device, use_tta=CFG.use_tta)\n    ensemble_preds += fold_preds\n    \n    del model\n    gc.collect()\n    torch.cuda.empty_cache()\n\n# Average across folds\nensemble_preds /= len(CFG.train_folds)\nfinal_labels = ensemble_preds.argmax(axis=1)\n\nprint(f\"Prediction distribution: 0={np.sum(final_labels==0)}, 1={np.sum(final_labels==1)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T07:51:10.180359Z","iopub.execute_input":"2026-02-04T07:51:10.180657Z","iopub.status.idle":"2026-02-04T08:02:24.543268Z","shell.execute_reply.started":"2026-02-04T07:51:10.180632Z","shell.execute_reply":"2026-02-04T08:02:24.542438Z"}},"outputs":[],"execution_count":null},{"id":"1a864b48","cell_type":"code","source":"# =============================================================================\n# Generate Submission\n# =============================================================================\nsubmission = pd.DataFrame({\n    \"id\": test_df[\"id\"],\n    \"label\": final_labels\n})\n\n# Match submission format if provided\nsubmission_format_path = CFG.data_root / \"submission_format.csv\"\nif submission_format_path.exists():\n    fmt = pd.read_csv(submission_format_path)\n    submission = submission.set_index(\"id\").loc[fmt[\"id\"]].reset_index()\n\n# Save\nsubmission_path = CFG.output_dir / \"submission.csv\"\nsubmission.to_csv(submission_path, index=False)\n\nprint(f\"\\nSubmission saved to: {submission_path}\")\nprint(f\"Shape: {submission.shape}\")\nprint(submission.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T08:02:24.544504Z","iopub.execute_input":"2026-02-04T08:02:24.544806Z","iopub.status.idle":"2026-02-04T08:02:24.573502Z","shell.execute_reply.started":"2026-02-04T08:02:24.544781Z","shell.execute_reply":"2026-02-04T08:02:24.572903Z"}},"outputs":[],"execution_count":null},{"id":"89eac3cb","cell_type":"code","source":"# =============================================================================\n# Summary\n# =============================================================================\nprint(f\"\"\"\n{'='*60}\nTRAINING COMPLETE\n{'='*60}\n\nModel: {CFG.model_name}\nImage Size: {CFG.img_size}\nEpochs: {CFG.epochs}\nBatch Size: {CFG.batch_size} x {torch.cuda.device_count()} GPUs = {CFG.batch_size * max(1, torch.cuda.device_count())}\n\nCV Scores: {best_scores}\nMean CV: {np.mean(best_scores):.4f} (+/- {np.std(best_scores):.4f})\n\nTTA: {'Enabled' if CFG.use_tta else 'Disabled'} ({len(get_tta_transforms(CFG.img_size))} variants)\nEnsemble: {len(CFG.train_folds)} folds\n\nSubmission: {submission_path}\n{'='*60}\n\"\"\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T08:02:24.574407Z","iopub.execute_input":"2026-02-04T08:02:24.574701Z","iopub.status.idle":"2026-02-04T08:02:24.580352Z","shell.execute_reply.started":"2026-02-04T08:02:24.574681Z","shell.execute_reply":"2026-02-04T08:02:24.579670Z"}},"outputs":[],"execution_count":null}]}