{"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":"none","dataSources":[{"sourceId":127713,"databundleVersionId":15291341,"sourceType":"competition"}],"dockerImageVersionId":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"27f256dd","cell_type":"markdown","source":"# Anime Gender Classification","metadata":{}},{"id":"e790a842","cell_type":"code","source":"import os\nimport random\nfrom pathlib import Path\n\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torchvision.models import resnet18, ResNet18_Weights","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T00:57:17.997378Z","iopub.execute_input":"2026-02-04T00:57:17.998573Z","iopub.status.idle":"2026-02-04T00:57:33.034526Z","shell.execute_reply.started":"2026-02-04T00:57:17.998530Z","shell.execute_reply":"2026-02-04T00:57:33.033427Z"}},"outputs":[],"execution_count":null},{"id":"e7c00582","cell_type":"code","source":"def is_kaggle() -> bool:\n    return bool(os.environ.get(\"KAGGLE_KERNEL_RUN_TYPE\") or os.environ.get(\"KAGGLE_URL_BASE\"))\n\n\ndef find_data_root(project_root: Path) -> Path:\n    candidates = []\n    if is_kaggle():\n        candidates.extend(\n            [\n                Path(\"/kaggle/input/anime-gender-classification\"),\n                Path(\"/kaggle/input/anime-gender-classification-dataset\"),\n                Path(\"/kaggle/input\"),\n            ]\n        )\n    candidates.append(project_root / \"dataset\")\n    candidates.append(project_root)\n\n    for base in candidates:\n        if not base.exists():\n            continue\n        for sub in [base, base / \"dataset\"]:\n            if (sub / \"train.csv\").exists():\n                return sub\n\n        for child in base.iterdir():\n            if child.is_dir() and (child / \"train.csv\").exists():\n                return child\n\n    raise FileNotFoundError(\"Could not locate dataset root with train.csv.\")\n\n\ndef set_seed(seed: int):\n    random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\n\ndef build_transforms(img_size: int):\n    train_tfms = transforms.Compose(\n        [\n            transforms.RandomResizedCrop(img_size, scale=(0.8, 1.0)),\n            transforms.RandomHorizontalFlip(),\n            transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),\n            transforms.ToTensor(),\n            transforms.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n        ]\n    )\n    val_tfms = transforms.Compose(\n        [\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    )\n    return train_tfms, val_tfms\n\n\nclass AnimeGenderDataset(Dataset):\n    def __init__(self, csv_path: Path, images_dir: Path, transform=None, has_labels=True):\n        self.df = pd.read_csv(csv_path)\n        self.images_dir = images_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):\n        row = self.df.iloc[idx]\n        img_path = self.images_dir / row[\"id\"]\n        image = Image.open(img_path).convert(\"RGB\")\n        if self.transform:\n            image = self.transform(image)\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-04T00:57:33.036811Z","iopub.execute_input":"2026-02-04T00:57:33.037809Z","iopub.status.idle":"2026-02-04T00:57:33.052900Z","shell.execute_reply.started":"2026-02-04T00:57:33.037774Z","shell.execute_reply":"2026-02-04T00:57:33.051808Z"}},"outputs":[],"execution_count":null},{"id":"ad62c39f","cell_type":"code","source":"# Config\nEPOCHS = 5\nBATCH_SIZE = 32\nIMG_SIZE = 224\nLR = 3e-4\nSEED = 42\nNUM_WORKERS = 2\n\nset_seed(SEED)\n\nproject_root = Path.cwd()\nif (project_root / \"Notebooks\").exists():\n    project_root = project_root\nelse:\n    project_root = Path(__file__).resolve().parents[1] if \"__file__\" in globals() else Path.cwd()\n\ndata_root = find_data_root(project_root)\ntrain_csv = data_root / \"train.csv\"\nval_csv = data_root / \"val.csv\"\ntest_csv = data_root / \"test.csv\"\nsubmission_format = data_root / \"submission_format.csv\"\n\ntrain_images = data_root / \"train_images\"\nval_images = data_root / \"val_images\"\ntest_images = data_root / \"test_images\"\n\noutput_dir = Path(\"/kaggle/working\") if is_kaggle() else (project_root / \"outputs\")\noutput_dir.mkdir(parents=True, exist_ok=True)\n\ntrain_tfms, val_tfms = build_transforms(IMG_SIZE)\n\ntrain_ds = AnimeGenderDataset(train_csv, train_images, transform=train_tfms, has_labels=True)\nval_ds = AnimeGenderDataset(val_csv, val_images, transform=val_tfms, has_labels=True)\ntest_ds = AnimeGenderDataset(test_csv, test_images, transform=val_tfms, has_labels=False)\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS, pin_memory=True)\nval_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True)\ntest_loader = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Data root:\", data_root)\nprint(\"Device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T00:57:33.054551Z","iopub.execute_input":"2026-02-04T00:57:33.055363Z","iopub.status.idle":"2026-02-04T00:57:33.178001Z","shell.execute_reply.started":"2026-02-04T00:57:33.055309Z","shell.execute_reply":"2026-02-04T00:57:33.177029Z"}},"outputs":[],"execution_count":null},{"id":"59f70866","cell_type":"code","source":"def train_one_epoch(model, loader, criterion, optimizer, device):\n    model.train()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    for images, labels in loader:\n        images = images.to(device)\n        labels = labels.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\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    return running_loss / total, correct / total\n\n\n@torch.no_grad()\ndef evaluate(model, loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    correct = 0\n    total = 0\n    for images, labels in loader:\n        images = images.to(device)\n        labels = labels.to(device)\n        outputs = model(images)\n        loss = criterion(outputs, labels)\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    return running_loss / total, correct / total\n\n\ntry:\n    weights = ResNet18_Weights.IMAGENET1K_V1\n    model = resnet18(weights=weights)\nexcept Exception:\n    model = resnet18(weights=None)\n\nmodel.fc = nn.Linear(model.fc.in_features, 2)\nmodel = model.to(device)\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=LR)\n\nbest_acc = 0.0\nbest_path = output_dir / \"best.pt\"\n\nfor epoch in range(EPOCHS):\n    train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device)\n    val_loss, val_acc = evaluate(model, val_loader, criterion, device)\n    print(\n        f\"Epoch {epoch+1}/{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\n    if val_acc > best_acc:\n        best_acc = val_acc\n        torch.save(model.state_dict(), best_path)\n\nif best_path.exists():\n    model.load_state_dict(torch.load(best_path, map_location=device))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T00:57:33.180119Z","iopub.execute_input":"2026-02-04T00:57:33.180498Z","iopub.status.idle":"2026-02-04T01:54:22.822514Z","shell.execute_reply.started":"2026-02-04T00:57:33.180469Z","shell.execute_reply":"2026-02-04T01:54:22.821391Z"}},"outputs":[],"execution_count":null},{"id":"9264fe53","cell_type":"code","source":"@torch.no_grad()\ndef predict(model, loader, device):\n    model.eval()\n    ids = []\n    labels = []\n    for images, id_list in loader:\n        images = images.to(device)\n        outputs = model(images)\n        preds = outputs.argmax(dim=1).cpu().numpy().tolist()\n        ids.extend(id_list)\n        labels.extend(preds)\n    return ids, labels\n\n\nids, preds = predict(model, test_loader, device)\nsubmission = pd.DataFrame({\"id\": ids, \"label\": preds})\n\nif submission_format.exists():\n    order = pd.read_csv(submission_format)[\"id\"].tolist()\n    submission = submission.set_index(\"id\").loc[order].reset_index()\n\nsubmission_path = output_dir / \"submission.csv\"\nsubmission.to_csv(submission_path, index=False)\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T01:54:22.824103Z","iopub.execute_input":"2026-02-04T01:54:22.824437Z","iopub.status.idle":"2026-02-04T01:54:50.985091Z","shell.execute_reply.started":"2026-02-04T01:54:22.824400Z","shell.execute_reply":"2026-02-04T01:54:50.983817Z"}},"outputs":[],"execution_count":null}]}