{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# import torch\n# import torch.nn as nn\n# import torch.optim as optim\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from torch.cuda.amp import autocast, GradScaler\n# from tqdm import tqdm\n# import matplotlib.pyplot as plt\n# from sklearn.metrics import classification_report\n\n# # 1. CONFIGURATION\n# class CFG:\n#     seed = 42\n#     img_size = 224\n#     batch_size = 32\n#     epochs = 20\n#     lr = 2e-4\n#     min_lr = 1e-6\n#     weight_decay = 0.05\n#     model_name = 'efficientnet_b0' # EfficientNet is better for anime style features\n#     label_smoothing = 0.1\n#     tta_steps = 5\n\n# def set_seed(seed=42):\n#     random.seed(seed)\n#     np.random.seed(seed)\n#     torch.manual_seed(seed)\n#     torch.cuda.manual_seed(seed)\n#     torch.backends.cudnn.deterministic = True\n#     torch.backends.cudnn.benchmark = True # Set to True for speed boost\n\n# set_seed(CFG.seed)\n# device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# # 2. DATASET (Added Robust Image Loading)\n# class AnimeDataset(Dataset):\n#     def __init__(self, csv_file, img_dir, transform=None, is_test=False):\n#         self.data = pd.read_csv(csv_file)\n#         self.img_dir = img_dir\n#         self.transform = transform\n#         self.is_test = is_test\n        \n#     def __len__(self):\n#         return len(self.data)\n    \n#     def __getitem__(self, idx):\n#         img_name = self.data.iloc[idx, 0]\n#         img_path = os.path.join(self.img_dir, img_name)\n        \n#         try:\n#             from PIL import Image\n#             image = Image.open(img_path).convert(\"RGB\")\n#         except Exception:\n#             image = Image.new(\"RGB\", (CFG.img_size, CFG.img_size), (255, 255, 255))\n            \n#         if self.transform:\n#             image = self.transform(image)\n        \n#         if self.is_test:\n#             return image, img_name\n        \n#         label = self.data.iloc[idx, 1]\n#         return image, torch.tensor(label, dtype=torch.long)\n\n# # 3. ADVANCED AUGMENTATIONS\n# def get_transforms(mode='train'):\n#     if mode == 'train':\n#         return transforms.Compose([\n#             transforms.RandomResizedCrop(CFG.img_size, scale=(0.8, 1.0)),\n#             transforms.RandAugment(num_ops=2, magnitude=9), # Sophisticated automated augmentation\n#             transforms.RandomHorizontalFlip(),\n#             transforms.ColorJitter(0.1, 0.1, 0.1),\n#             transforms.ToTensor(),\n#             transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),\n#             transforms.RandomErasing(p=0.2)\n#         ])\n#     return transforms.Compose([\n#         transforms.Resize((CFG.img_size, CFG.img_size)),\n#         transforms.ToTensor(),\n#         transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n#     ])\n\n# # 4. MODEL (EfficientNet-B0 with modern Head)\n# class AnimeModel(nn.Module):\n#     def __init__(self, model_name=CFG.model_name, num_classes=2):\n#         super().__init__()\n#         # Load EfficientNet with better default features\n#         self.backbone = models.efficientnet_b0(weights='DEFAULT')\n        \n#         # Unfreeze the last blocks for better fine-tuning\n#         for param in list(self.backbone.parameters())[:-30]:\n#             param.requires_grad = False\n            \n#         in_features = self.backbone.classifier[1].in_features\n#         self.backbone.classifier = nn.Sequential(\n#             nn.Linear(in_features, 512),\n#             nn.BatchNorm1d(512),\n#             nn.SiLU(), # Modern activation\n#             nn.Dropout(0.4),\n#             nn.Linear(512, num_classes)\n#         )\n        \n#     def forward(self, x):\n#         return self.backbone(x)\n\n# # 5. TRAINING HELPERS\n# def train_fn(model, loader, criterion, optimizer, scheduler, scaler):\n#     model.train()\n#     total_loss, correct = 0, 0\n#     pbar = tqdm(loader, desc=\"Training\")\n    \n#     for images, labels in pbar:\n#         images, labels = images.to(device), labels.to(device)\n        \n#         with autocast(enabled=True): # Automated Mixed Precision\n#             outputs = model(images)\n#             loss = criterion(outputs, labels)\n        \n#         optimizer.zero_grad()\n#         scaler.scale(loss).backward()\n#         scaler.step(optimizer)\n#         scaler.update()\n#         scheduler.step()\n        \n#         total_loss += loss.item()\n#         _, pred = torch.max(outputs, 1)\n#         correct += (pred == labels).sum().item()\n#         pbar.set_postfix(loss=total_loss/len(loader), acc=correct/((pbar.n+1)*CFG.batch_size))\n        \n#     return total_loss/len(loader), correct/len(loader.dataset)\n\n# # 6. INFERENCE WITH TTA (Test-Time Augmentation)\n# def predict_with_tta(model, loader):\n#     model.eval()\n#     all_preds = []\n#     filenames = []\n    \n#     # Define TTA transforms\n#     tta_transform = transforms.Compose([\n#         transforms.RandomHorizontalFlip(p=1.0),\n#         transforms.ColorJitter(0.1, 0.1, 0.1),\n#     ])\n\n#     with torch.no_grad():\n#         for images, names in tqdm(loader, desc=\"Inference + TTA\"):\n#             images = images.to(device)\n#             filenames.extend(names)\n            \n#             # Step 1: Base Prediction\n#             outputs = torch.softmax(model(images), dim=1)\n            \n#             # Step 2: Augmentations for TTA\n#             for _ in range(CFG.tta_steps - 1):\n#                 # Apply small random shifts to the batch\n#                 aug_images = tta_transform(images)\n#                 outputs += torch.softmax(model(aug_images), dim=1)\n            \n#             # Average the predictions\n#             avg_outputs = outputs / CFG.tta_steps\n#             _, preds = torch.max(avg_outputs, 1)\n#             all_preds.extend(preds.cpu().numpy())\n            \n#     return filenames, all_preds\n\n# # 7. MAIN EXECUTION\n# if __name__ == \"__main__\":\n#     base_path = \"/kaggle/input/anime-gender-classification/dataset\"\n    \n#     # Loaders\n#     train_ds = AnimeDataset(f\"{base_path}/train.csv\", f\"{base_path}/train_images\", get_transforms('train'))\n#     val_ds = AnimeDataset(f\"{base_path}/val.csv\", f\"{base_path}/val_images\", get_transforms('val'))\n#     test_ds = AnimeDataset(f\"{base_path}/test.csv\", f\"{base_path}/test_images\", get_transforms('val'), is_test=True)\n    \n#     train_loader = DataLoader(train_ds, batch_size=CFG.batch_size, shuffle=True, num_workers=4)\n#     val_loader = DataLoader(val_ds, batch_size=CFG.batch_size, shuffle=False, num_workers=4)\n#     test_loader = DataLoader(test_ds, batch_size=CFG.batch_size, shuffle=False, num_workers=4)\n    \n#     model = AnimeModel().to(device)\n    \n#     # 8. OPTIMIZER & SCHEDULER (OneCycleLR is more aggressive and effective)\n#     criterion = nn.CrossEntropyLoss(label_smoothing=CFG.label_smoothing)\n#     optimizer = optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay)\n    \n#     scheduler = optim.lr_scheduler.OneCycleLR(\n#         optimizer, max_lr=CFG.lr, \n#         steps_per_epoch=len(train_loader), \n#         epochs=CFG.epochs,\n#         pct_start=0.2 # spend 20% time warming up\n#     )\n    \n#     scaler = GradScaler()\n#     best_acc = 0\n    \n#     # Training Loop\n#     for epoch in range(CFG.epochs):\n#         train_loss, train_acc = train_fn(model, train_loader, criterion, optimizer, scheduler, scaler)\n        \n#         # Quick Validation logic\n#         model.eval()\n#         val_correct = 0\n#         with torch.no_grad():\n#             for imgs, lbls in val_loader:\n#                 imgs, lbls = imgs.to(device), lbls.to(device)\n#                 outputs = model(imgs)\n#                 _, p = torch.max(outputs, 1)\n#                 val_correct += (p == lbls).sum().item()\n        \n#         val_acc = val_correct / len(val_ds)\n#         print(f\"Epoch {epoch+1} | Val Acc: {val_acc:.4f} | Train Acc: {train_acc:.4f}\")\n        \n#         if val_acc > best_acc:\n#             best_acc = val_acc\n#             torch.save(model.state_dict(), \"best_model.pth\")\n#             print(\"Model Saved!\")\n\n#     # Final Step: Inference\n#     model.load_state_dict(torch.load(\"best_model.pth\"))\n#     ids, labels = predict_with_tta(model, test_loader)\n    \n#     pd.DataFrame({'id': ids, 'label': labels}).to_csv('submission.csv', index=False)\n#     print(\"Process Complete. Submission created with TTA.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-18T12:54:47.146308Z","iopub.execute_input":"2026-01-18T12:54:47.146541Z","iopub.status.idle":"2026-01-18T12:54:47.154921Z","shell.execute_reply.started":"2026-01-18T12:54:47.146519Z","shell.execute_reply":"2026-01-18T12:54:47.153990Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import random\n# import numpy as np\n# import pandas as pd\n# import torch\n# import torch.nn as nn\n# import torch.optim as optim\n# from torch.utils.data import Dataset, DataLoader\n# from torchvision import transforms, models\n# from torch.cuda.amp import autocast, GradScaler\n# from tqdm.auto import tqdm\n# import matplotlib.pyplot as plt\n# from sklearn.metrics import classification_report, confusion_matrix\n# import warnings\n# from PIL import Image\n\n# warnings.filterwarnings(\"ignore\", category=UserWarning)\n\n\n# class CFG:\n#     # === General ===\n#     seed = 42\n#     device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n#     num_workers = min(8, os.cpu_count() or 4)\n#     pin_memory = True if torch.cuda.is_available() else False\n    \n#     # === Model ===\n#     model_name = 'efficientnet_b0'         \n#     num_classes = 2\n#     dropout_rate = 0.35\n    \n#     # === Training ===\n#     img_size = 224\n#     batch_size = 48            \n#     epochs = 25\n#     lr = 3e-4\n#     weight_decay = 0.045\n#     label_smoothing = 0.08\n    \n#     # === Scheduler ===\n#     scheduler = 'OneCycleLR'     \n#     pct_start = 0.25\n#     div_factor = 25.0\n#     final_div_factor = 1e4\n    \n#     # === Augmentation & Regularization ===\n#     mixup_alpha = 0.2          \n#     cutmix_alpha = 1.0         \n#     tta_steps = 6\n    \n#     # === Paths ===\n#     base_path = \"/kaggle/input/anime-gender-classification/dataset\"\n#     train_csv = f\"{base_path}/train.csv\"\n#     val_csv   = f\"{base_path}/val.csv\"\n#     test_csv  = f\"{base_path}/test.csv\"\n#     train_dir = f\"{base_path}/train_images\"\n#     val_dir   = f\"{base_path}/val_images\"\n#     test_dir  = f\"{base_path}/test_images\"\n\n\n# def set_seed(seed=42):\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 = True   \n\n\n# set_seed(CFG.seed)\n# print(f\"Using device: {CFG.device}\\n\")\n\n\n# # ================= DATASET =================\n# class AnimeDataset(Dataset):\n#     def __init__(self, csv_file, img_dir, transform=None, is_test=False):\n#         self.data = pd.read_csv(csv_file)\n#         self.img_dir = img_dir\n#         self.transform = transform\n#         self.is_test = is_test\n\n#     def __len__(self):\n#         return len(self.data)\n\n#     def __getitem__(self, idx):\n#         img_name = str(self.data.iloc[idx, 0])\n#         img_path = os.path.join(self.img_dir, img_name)\n\n#         try:\n#             image = Image.open(img_path).convert(\"RGB\")\n#         except Exception:\n#             # Fallback - better to log somewhere in real project\n#             image = Image.new(\"RGB\", (CFG.img_size, CFG.img_size), (240, 240, 245))\n\n#         if self.transform:\n#             image = self.transform(image)\n\n#         if self.is_test:\n#             return image, img_name\n\n#         label = int(self.data.iloc[idx, 1])\n#         return image, torch.tensor(label, dtype=torch.long)\n\n\n# # ================= TRANSFORMS =================\n# def get_transforms(mode=\"train\"):\n#     if mode == \"train\":\n#         return transforms.Compose([\n#             transforms.RandomResizedCrop(\n#                 CFG.img_size,\n#                 scale=(0.75, 1.0),\n#                 ratio=(0.9, 1.1)\n#             ),\n#             transforms.RandAugment(num_ops=2, magnitude=9),\n#             transforms.RandomHorizontalFlip(p=0.5),\n#             transforms.RandomRotation(15),\n#             transforms.ColorJitter(brightness=0.12, contrast=0.12, saturation=0.12),\n#             transforms.ToTensor(),\n#             transforms.Normalize(\n#                 mean=[0.485, 0.456, 0.406],\n#                 std=[0.229, 0.224, 0.225]\n#             ),\n#             transforms.RandomErasing(p=0.25, scale=(0.02, 0.25)),\n#         ])\n\n#     # val / test / inference\n#     return transforms.Compose([\n#         transforms.Resize((CFG.img_size, CFG.img_size), antialias=True),\n#         transforms.ToTensor(),\n#         transforms.Normalize(\n#             mean=[0.485, 0.456, 0.406],\n#             std=[0.229, 0.224, 0.225]\n#         ),\n#     ])\n\n\n# # ================= MODEL =================\n# class AnimeClassifier(nn.Module):\n#     def __init__(self, model_name=CFG.model_name, num_classes=CFG.num_classes):\n#         super().__init__()\n#         # Modern way - most models now support weights=\"DEFAULT\" / \"IMAGENET1K_V1\"\n#         self.backbone = models.efficientnet_b0(weights=\"DEFAULT\")\n\n#         # Freeze early layers (good default for small-medium dataset)\n#         for param in list(self.backbone.parameters())[:-40]:\n#             param.requires_grad = False\n\n#         in_features = self.backbone.classifier[1].in_features\n\n#         self.backbone.classifier = nn.Sequential(\n#             nn.Dropout(p=CFG.dropout_rate),\n#             nn.Linear(in_features, 576),\n#             nn.SiLU(),\n#             nn.BatchNorm1d(576),\n#             nn.Dropout(p=CFG.dropout_rate * 0.6),\n#             nn.Linear(576, num_classes)\n#         )\n\n#     def forward(self, x):\n#         return self.backbone(x)\n\n\n# # ================= LOSS with Mixup/Cutmix support =================\n# def criterion(outputs, targets, criterion_base, alpha_mixup=0.0, alpha_cutmix=0.0):\n#     if alpha_mixup > 0 and random.random() < 0.5:\n#         lam = np.random.beta(alpha_mixup, alpha_mixup)\n#         batch_size = outputs.size(0)\n#         index = torch.randperm(batch_size).to(CFG.device)\n\n#         targets_a, targets_b = targets, targets[index]\n#         outputs = lam * outputs + (1 - lam) * outputs[index]\n\n#         loss = lam * criterion_base(outputs, targets_a) + (1 - lam) * criterion_base(outputs, targets_b)\n#         return loss\n\n#     # simple cutmix is more complicated → usually people use external lib\n#     # for simplicity we skip detailed cutmix here\n\n#     return criterion_base(outputs, targets)\n\n\n# # ================= TRAIN / VALIDATE =================\n# @torch.inference_mode(False)\n# def train_one_epoch(model, loader, criterion_base, optimizer, scheduler, scaler, epoch):\n#     model.train()\n#     total_loss = 0.\n#     correct = 0\n#     total = 0\n\n#     pbar = tqdm(loader, desc=f\"Epoch {epoch+1}\", leave=False)\n\n#     for images, labels in pbar:\n#         images = images.to(CFG.device, non_blocking=True)\n#         labels = labels.to(CFG.device, non_blocking=True)\n\n#         with autocast():\n#             outputs = model(images)\n#             loss = criterion(\n#                 outputs, labels,\n#                 criterion_base,\n#                 alpha_mixup=CFG.mixup_alpha,\n#                 alpha_cutmix=CFG.cutmix_alpha\n#             )\n\n#         optimizer.zero_grad(set_to_none=True)\n#         scaler.scale(loss).backward()\n#         scaler.step(optimizer)\n#         scaler.update()\n\n#         if scheduler is not None and isinstance(scheduler, optim.lr_scheduler.OneCycleLR):\n#             scheduler.step()\n\n#         total_loss += loss.item()\n#         _, preds = outputs.max(1)\n#         correct += preds.eq(labels).sum().item()\n#         total += labels.size(0)\n\n#         pbar.set_postfix_str(\n#             f\"loss:{total_loss/(pbar.n+1):.4f} | acc:{correct/total:.4f}\"\n#         )\n\n#     return total_loss / len(loader), correct / total\n\n\n# @torch.inference_mode()\n# def validate(model, loader):\n#     model.eval()\n#     correct = 0\n#     total = 0\n#     all_preds = []\n#     all_labels = []\n\n#     for images, labels in loader:\n#         images = images.to(CFG.device, non_blocking=True)\n#         labels = labels.to(CFG.device, non_blocking=True)\n\n#         outputs = model(images)\n#         _, preds = outputs.max(1)\n\n#         correct += preds.eq(labels).sum().item()\n#         total += labels.size(0)\n\n#         all_preds.extend(preds.cpu().numpy())\n#         all_labels.extend(labels.cpu().numpy())\n\n#     acc = correct / total\n\n#     print(classification_report(all_labels, all_preds, digits=4, zero_division=0))\n\n#     return acc, all_preds, all_labels\n\n\n# # ================= TTA =================\n# @torch.inference_mode()\n# def predict_tta(model, loader, tta_steps=CFG.tta_steps):\n#     model.eval()\n#     predictions = []\n#     filenames = []\n\n#     tta_transforms = transforms.Compose([\n#         transforms.RandomHorizontalFlip(p=1.0),\n#         transforms.RandomAffine(degrees=5, translate=(0.08, 0.08), scale=(0.92, 1.08)),\n#     ])\n\n#     for images, names in tqdm(loader, desc=\"TTA Inference\"):\n#         images = images.to(CFG.device, non_blocking=True)\n#         filenames.extend(names)\n\n#         batch_preds = torch.zeros(images.size(0), CFG.num_classes, device=CFG.device)\n\n#         # original\n#         batch_preds += torch.softmax(model(images), dim=1)\n\n#         # TTA steps\n#         for _ in range(tta_steps - 1):\n#             aug_img = tta_transforms(images)\n#             batch_preds += torch.softmax(model(aug_img), dim=1)\n\n#         batch_preds /= tta_steps\n#         predictions.extend(batch_preds.argmax(1).cpu().numpy())\n\n#     return filenames, predictions\n\n\n# # ================= MAIN =================\n# def main():\n#     train_transform = get_transforms(\"train\")\n#     valid_transform = get_transforms(\"val\")\n\n#     train_ds = AnimeDataset(CFG.train_csv, CFG.train_dir, train_transform)\n#     val_ds   = AnimeDataset(CFG.val_csv,   CFG.val_dir,   valid_transform)\n#     test_ds  = AnimeDataset(CFG.test_csv,  CFG.test_dir,  valid_transform, is_test=True)\n\n#     train_loader = DataLoader(\n#         train_ds, batch_size=CFG.batch_size, shuffle=True,\n#         num_workers=CFG.num_workers, pin_memory=CFG.pin_memory\n#     )\n#     val_loader = DataLoader(\n#         val_ds, batch_size=CFG.batch_size*2, shuffle=False,\n#         num_workers=CFG.num_workers, pin_memory=CFG.pin_memory\n#     )\n#     test_loader = DataLoader(\n#         test_ds, batch_size=CFG.batch_size*2, shuffle=False,\n#         num_workers=CFG.num_workers, pin_memory=CFG.pin_memory\n#     )\n\n#     model = AnimeClassifier().to(CFG.device)\n\n#     criterion_base = nn.CrossEntropyLoss(label_smoothing=CFG.label_smoothing)\n#     optimizer = optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay)\n\n#     if CFG.scheduler == \"OneCycleLR\":\n#         scheduler = optim.lr_scheduler.OneCycleLR(\n#             optimizer,\n#             max_lr=CFG.lr,\n#             epochs=CFG.epochs,\n#             steps_per_epoch=len(train_loader),\n#             pct_start=CFG.pct_start,\n#             div_factor=CFG.div_factor,\n#             final_div_factor=CFG.final_div_factor,\n#         )\n#     else:\n#         scheduler = None\n\n#     scaler = torch.amp.GradScaler('cuda')\n\n#     best_val_acc = -1\n#     best_epoch = -1\n\n#     for epoch in range(CFG.epochs):\n#         train_loss, train_acc = train_one_epoch(\n#             model, train_loader, criterion_base, optimizer, scheduler, scaler, epoch+1\n#         )\n\n#         val_acc, _, _ = validate(model, val_loader)\n\n#         print(f\"Epoch {epoch+1:02d} | \"\n#               f\"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | \"\n#               f\"Val Acc: {val_acc:.4f}\")\n\n#         if val_acc > best_val_acc:\n#             best_val_acc = val_acc\n#             best_epoch = epoch + 1\n#             torch.save(model.state_dict(), \"best_model.pth\")\n#             print(f\"   ↑ New best @ epoch {best_epoch} - saved!\")\n\n#     print(f\"\\nBest model was at epoch {best_epoch} with Val Acc = {best_val_acc:.5f}\")\n\n#     # Final inference\n#     print(\"\\nLoading best model & doing TTA inference...\")\n#     model.load_state_dict(torch.load(\"best_model.pth\", map_location=CFG.device))\n#     ids, preds = predict_tta(model, test_loader)\n\n#     submission = pd.DataFrame({\"id\": ids, \"label\": preds})\n#     submission.to_csv(\"submission.csv\", index=False)\n#     print(\"Submission file created → submission.csv\")\n\n\n# if __name__ == \"__main__\":\n#     main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T13:14:44.146884Z","iopub.execute_input":"2026-01-18T13:14:44.147701Z","iopub.status.idle":"2026-01-18T13:15:04.203659Z","shell.execute_reply.started":"2026-01-18T13:14:44.147657Z","shell.execute_reply":"2026-01-18T13:15:04.199903Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch import amp                           # ← modern import (PyTorch ≥2.4)\n\nfrom torchvision import transforms\nfrom torchvision.transforms import v2 as transforms_v2\nfrom torchvision.models import efficientnet_b0, EfficientNet_B0_Weights\n\nfrom PIL import Image\nfrom tqdm.auto import tqdm\nimport warnings\n\nwarnings.filterwarnings(\"ignore\", category=UserWarning)\n\n# ───────────────────────────────────────────────────────────────\n#                          CONFIGURATION\n# ───────────────────────────────────────────────────────────────\nclass CFG:\n    # General\n    seed = 42\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    num_workers = min(12, os.cpu_count() or 4)\n    pin_memory = torch.cuda.is_available()\n    \n    # Model\n    model_name = \"efficientnet_b0\"\n    num_classes = 2\n    dropout_rate = 0.35\n    \n    # Training\n    img_size = 256\n    batch_size = 64                 # adjust according to your GPU memory\n    epochs = 30\n    lr = 4e-4\n    weight_decay = 0.05\n    label_smoothing = 0.08\n    \n    # Augmentation\n    mixup_alpha = 0.3\n    trivial_augment = True\n    \n    # Paths (Kaggle style)\n    BASE_PATH = \"/kaggle/input/anime-gender-classification/dataset\"\n    train_csv = f\"{BASE_PATH}/train.csv\"\n    val_csv   = f\"{BASE_PATH}/val.csv\"\n    test_csv  = f\"{BASE_PATH}/test.csv\"\n    train_dir = f\"{BASE_PATH}/train_images\"\n    val_dir   = f\"{BASE_PATH}/val_images\"\n    test_dir  = f\"{BASE_PATH}/test_images\"\n\n\ndef set_seed(seed=42):\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 = True\n\n\nset_seed(CFG.seed)\nprint(f\"Device: {CFG.device} | Workers: {CFG.num_workers}\\n\")\n\n\n# ───────────────────────────────────────────────────────────────\n#                          DATASET\n# ───────────────────────────────────────────────────────────────\nclass AnimeDataset(Dataset):\n    def __init__(self, csv_file, img_dir, transform=None, is_test=False):\n        self.data = pd.read_csv(csv_file)\n        self.img_dir = img_dir\n        self.transform = transform\n        self.is_test = is_test\n\n    def __len__(self):\n        return len(self.data)\n\n    def __getitem__(self, idx):\n        img_name = str(self.data.iloc[idx, 0])\n        img_path = os.path.join(self.img_dir, img_name)\n\n        try:\n            img = Image.open(img_path).convert(\"RGB\")\n        except Exception:\n            img = Image.new(\"RGB\", (CFG.img_size, CFG.img_size), (245, 245, 250))\n\n        if self.transform:\n            img = self.transform(img)\n\n        if self.is_test:\n            return img, img_name\n\n        label = int(self.data.iloc[idx, 1])\n        return img, torch.tensor(label, dtype=torch.long)\n\n\n# ───────────────────────────────────────────────────────────────\n#                     TRANSFORMS 2025 style\n# ───────────────────────────────────────────────────────────────\ndef get_transforms(mode=\"train\"):\n    if mode == \"train\":\n        base = [\n            transforms_v2.RandomResizedCrop(\n                size=CFG.img_size,\n                scale=(0.70, 1.0),\n                ratio=(0.85, 1.15),\n                antialias=True\n            ),\n            transforms_v2.RandomHorizontalFlip(p=0.5),\n            transforms_v2.RandomRotation(degrees=12),\n            transforms_v2.ColorJitter(brightness=0.22, contrast=0.22, saturation=0.18, hue=0.09),\n        ]\n\n        if CFG.trivial_augment:\n            base.append(transforms_v2.TrivialAugmentWide())\n\n        base.extend([\n            transforms_v2.ToImage(),\n            transforms_v2.ToDtype(torch.float32, scale=True),\n            transforms_v2.Normalize(\n                mean=[0.485, 0.456, 0.406],\n                std=[0.229, 0.224, 0.225]\n            ),\n            transforms_v2.RandomErasing(p=0.4, scale=(0.02, 0.33), ratio=(0.3, 3.3))\n        ])\n\n        return transforms_v2.Compose(base)\n\n    # val/test\n    return transforms_v2.Compose([\n        transforms_v2.Resize((CFG.img_size, CFG.img_size), antialias=True),\n        transforms_v2.ToImage(),\n        transforms_v2.ToDtype(torch.float32, scale=True),\n        transforms_v2.Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        ),\n    ])\n\n\n# ───────────────────────────────────────────────────────────────\n#                           MODEL\n# ───────────────────────────────────────────────────────────────\nclass AnimeGenderClassifier(nn.Module):\n    def __init__(self, num_classes=CFG.num_classes):\n        super().__init__()\n        self.backbone = efficientnet_b0(weights=EfficientNet_B0_Weights.DEFAULT)\n\n        # Freeze early layers\n        for param in list(self.backbone.parameters())[:-50]:\n            param.requires_grad = False\n\n        in_features = self.backbone.classifier[1].in_features\n\n        self.backbone.classifier = nn.Sequential(\n            nn.Dropout(CFG.dropout_rate),\n            nn.Linear(in_features, 1024),\n            nn.SiLU(),\n            nn.BatchNorm1d(1024),\n            nn.Dropout(CFG.dropout_rate * 0.65),\n            nn.Linear(1024, 512),\n            nn.SiLU(),\n            nn.BatchNorm1d(512),\n            nn.Dropout(0.15),\n            nn.Linear(512, num_classes)\n        )\n\n    def forward(self, x):\n        return self.backbone(x)\n\n\n# ───────────────────────────────────────────────────────────────\n#                 MIXUP + LABEL SMOOTHING LOSS\n# ───────────────────────────────────────────────────────────────\ndef mixup_criterion(pred, y_a, y_b, lam, criterion):\n    return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)\n\n\ndef train_one_epoch(model, loader, criterion, optimizer, scheduler, scaler, epoch):\n    model.train()\n    total_loss = 0.\n    correct = 0\n    total = 0\n\n    pbar = tqdm(loader, desc=f\"Epoch {epoch:02d}\")\n\n    for images, labels in pbar:\n        images = images.to(CFG.device, non_blocking=True)\n        labels = labels.to(CFG.device, non_blocking=True)\n\n        # Mixup\n        if CFG.mixup_alpha > 0 and random.random() < 0.6:\n            lam = np.random.beta(CFG.mixup_alpha, CFG.mixup_alpha)\n            index = torch.randperm(images.size(0)).to(CFG.device)\n            mixed_images = lam * images + (1 - lam) * images[index]\n            labels_a, labels_b = labels, labels[index]\n        else:\n            mixed_images = images\n            labels_a = labels_b = labels\n            lam = 1.0\n\n        with amp.autocast('cuda'):                      # ← modern syntax\n            outputs = model(mixed_images)\n            loss = mixup_criterion(outputs, labels_a, labels_b, lam, criterion)\n\n        optimizer.zero_grad(set_to_none=True)\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        if scheduler is not None:\n            scheduler.step()\n\n        total_loss += loss.item() * images.size(0)\n        _, preds = outputs.max(1)\n        correct += (preds == labels).sum().item()   # real labels for acc\n        total += labels.size(0)\n\n        pbar.set_postfix_str(f\"loss:{total_loss/total:.4f}  acc:{correct/total:.4f}\")\n\n    return total_loss / total, correct / total\n\n\n@torch.inference_mode()\ndef validate(model, loader):\n    model.eval()\n    correct = 0\n    total = 0\n\n    for images, labels in loader:\n        images = images.to(CFG.device, non_blocking=True)\n        labels = labels.to(CFG.device, non_blocking=True)\n\n        outputs = model(images)\n        _, preds = outputs.max(1)\n\n        correct += preds.eq(labels).sum().item()\n        total += labels.size(0)\n\n    return correct / total\n\n\n# ───────────────────────────────────────────────────────────────\n#                          MAIN LOOP\n# ───────────────────────────────────────────────────────────────\ndef main():\n    train_transform = get_transforms(\"train\")\n    val_transform = get_transforms(\"val\")\n\n    ds_train = AnimeDataset(CFG.train_csv, CFG.train_dir, train_transform)\n    ds_val   = AnimeDataset(CFG.val_csv,   CFG.val_dir,   val_transform)\n    ds_test  = AnimeDataset(CFG.test_csv,  CFG.test_dir,  val_transform, is_test=True)\n\n    dl_train = DataLoader(ds_train, CFG.batch_size, True,\n                          num_workers=CFG.num_workers, pin_memory=CFG.pin_memory,\n                          persistent_workers=True, drop_last=True)\n\n    dl_val = DataLoader(ds_val, CFG.batch_size * 2, False,\n                        num_workers=CFG.num_workers, pin_memory=CFG.pin_memory,\n                        persistent_workers=True)\n\n    dl_test = DataLoader(ds_test, CFG.batch_size * 2, False,\n                         num_workers=CFG.num_workers, pin_memory=CFG.pin_memory,\n                         persistent_workers=True)\n\n    model = AnimeGenderClassifier().to(CFG.device, memory_format=torch.channels_last)\n\n    # Enable TF32 on modern GPUs (Ampere+)\n    if torch.cuda.is_available() and torch.cuda.get_device_capability()[0] >= 8:\n        torch.backends.cuda.matmul.allow_tf32 = True\n        torch.backends.cudnn.allow_tf32 = True\n\n    criterion = nn.CrossEntropyLoss(label_smoothing=CFG.label_smoothing)\n    optimizer = optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay)\n\n    # Warmup → Cosine scheduler\n    warmup_steps = int(len(dl_train) * 4)  # 4 epochs warmup\n    warmup = optim.lr_scheduler.LinearLR(optimizer, start_factor=1e-6/CFG.lr, total_iters=warmup_steps)\n    cosine = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CFG.epochs - 4, eta_min=CFG.lr * 0.01)\n\n    scheduler = optim.lr_scheduler.SequentialLR(optimizer, [warmup, cosine], milestones=[warmup_steps])\n\n    scaler = amp.GradScaler('cuda')                     # ← modern syntax\n\n    best_acc = 0.0\n\n    for epoch in range(1, CFG.epochs + 1):\n        train_loss, train_acc = train_one_epoch(\n            model, dl_train, criterion, optimizer, scheduler, scaler, epoch\n        )\n\n        val_acc = validate(model, dl_val)\n\n        print(f\"Epoch {epoch:02d} | \"\n              f\"Loss: {train_loss:.4f} | Train: {train_acc:.4f} | Val: {val_acc:.4f}\")\n\n        if val_acc > best_acc + 0.001:  # small threshold to avoid too many saves\n            best_acc = val_acc\n            torch.save(model.state_dict(), \"best_model.pth\")\n            print(\"  → New best model saved!\")\n\n    print(f\"\\nBest validation accuracy: {best_acc:.5f}\")\n\n    # Final inference\n    print(\"\\nRunning inference with best model...\")\n    model.load_state_dict(torch.load(\"best_model.pth\", map_location=CFG.device))\n    model.eval()\n\n    predictions = []\n    filenames = []\n\n    with torch.inference_mode():\n        for images, names in tqdm(dl_test, desc=\"Inference\"):\n            images = images.to(CFG.device, memory_format=torch.channels_last)\n            with amp.autocast('cuda'):\n                outputs = model(images)\n            preds = outputs.argmax(dim=1).cpu().numpy()\n            predictions.extend(preds)\n            filenames.extend(names)\n\n    submission = pd.DataFrame({\"id\": filenames, \"label\": predictions})\n    submission.to_csv(\"submission.csv\", index=False)\n    print(\"Submission file created → submission.csv\")\n\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T13:16:34.511315Z","iopub.execute_input":"2026-01-18T13:16:34.512445Z","iopub.status.idle":"2026-01-18T13:34:53.210817Z","shell.execute_reply.started":"2026-01-18T13:16:34.512396Z","shell.execute_reply":"2026-01-18T13:34:53.209945Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}