{"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":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"from IPython.display import YouTubeVideo\nYouTubeVideo(\"kNmaKyMsr64\", width=600, height=350)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:07:51.649451Z","iopub.execute_input":"2026-01-28T01:07:51.649807Z","iopub.status.idle":"2026-01-28T01:07:51.689577Z","shell.execute_reply.started":"2026-01-28T01:07:51.649783Z","shell.execute_reply":"2026-01-28T01:07:51.688881Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, math, random, glob, warnings\nwarnings.filterwarnings(\"ignore\")\n\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\ntry:\n    import timm\n    from timm.utils import ModelEmaV2\nexcept Exception as e:\n    raise RuntimeError(\"lmao.\") from e\n\nUSE_ALBU = True\ntry:\n    import cv2\n    import albumentations as A\n    from albumentations.pytorch import ToTensorV2\nexcept Exception:\n    USE_ALBU = False\n    from PIL import Image\n    from torchvision import transforms as T\n\nfrom tqdm.auto import tqdm\n\nclass Config:\n    seed = 35571\n    img_size = 256\n    batch_size = 64\n    epochs = 30\n\n    model_name = \"convnext_base\"\n    pretrained = True\n\n    lr = 3e-4\n    weight_decay = 0.05\n    warmup_frac = 0.08 \n\n    num_classes = 2\n    drop_rate = 0.25\n\n    # regularization\n    label_smooth = 0.05\n    mixup_p = 0.00\n    cutmix_p = 0.00\n    mix_alpha = 0.0       \n    cutmix_alpha = 1.0  \n\n    # EMA\n    use_ema = True\n    ema_decay = 0.999\n\n    # speed\n    num_workers = min(8, os.cpu_count() or 2)\n    use_amp = torch.cuda.is_available()\n    channels_last = True\n\n    base = \"/kaggle/input/anime-gender-classification/dataset\"\n\n\ndef seed_everything(seed: int):\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.benchmark = True\n\nseed_everything(Config.seed)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device, \"| Albumentations:\", USE_ALBU)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:07:51.690793Z","iopub.execute_input":"2026-01-28T01:07:51.691035Z","iopub.status.idle":"2026-01-28T01:08:05.247140Z","shell.execute_reply.started":"2026-01-28T01:07:51.691013Z","shell.execute_reply":"2026-01-28T01:08:05.246401Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def find_dataset_root(preferred: str) -> str:\n    if os.path.exists(os.path.join(preferred, \"train.csv\")):\n        return preferred\n    candidates = glob.glob(\"/kaggle/input/**/train.csv\", recursive=True)\n    if not candidates:\n        raise FileNotFoundError(\"Could not find train.csv under /kaggle/input. Check dataset mount.\")\n    return os.path.dirname(candidates[0])\n\nROOT = find_dataset_root(Config.base)\nprint(\"DATA ROOT:\", ROOT)\n\nTRAIN_CSV = os.path.join(ROOT, \"train.csv\")\nVAL_CSV   = os.path.join(ROOT, \"val.csv\")\nTEST_CSV  = os.path.join(ROOT, \"test.csv\")\n\nTRAIN_DIR = os.path.join(ROOT, \"train_images\")\nVAL_DIR   = os.path.join(ROOT, \"val_images\")\nTEST_DIR  = os.path.join(ROOT, \"test_images\")\n\n\ndef read_labels(path: str, has_label: bool):\n    df = pd.read_csv(path)\n    cols = [c.strip().lower() for c in df.columns]\n    df.columns = cols\n    if \"id\" not in df.columns:\n        df = df.rename(columns={df.columns[0]: \"id\"})\n    if has_label and \"label\" not in df.columns:\n        df = df.rename(columns={df.columns[1]: \"label\"})\n    return df\n\ndf_tr = read_labels(TRAIN_CSV, has_label=True)\ndf_va = read_labels(VAL_CSV,   has_label=True)\ndf_te = read_labels(TEST_CSV,  has_label=False)\n\nprint(\"train:\", df_tr.shape, \"val:\", df_va.shape, \"test:\", df_te.shape)\nprint(\"train label counts:\", df_tr[\"label\"].value_counts().to_dict())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:08:05.248232Z","iopub.execute_input":"2026-01-28T01:08:05.248652Z","iopub.status.idle":"2026-01-28T01:08:05.318666Z","shell.execute_reply.started":"2026-01-28T01:08:05.248628Z","shell.execute_reply":"2026-01-28T01:08:05.317851Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMAGENET_MEAN = (0.485, 0.456, 0.406)\nIMAGENET_STD  = (0.229, 0.224, 0.225)\n\nif USE_ALBU:\n    def build_tfms(train: bool):\n        if train:\n            return A.Compose([\n                A.RandomResizedCrop(size=(Config.img_size, Config.img_size), scale=(0.72, 1.0), ratio=(0.85, 1.15)),\n                A.HorizontalFlip(p=0.5),\n                # A.ShiftScaleRotate(shift_limit=0.03, scale_limit=0.08, rotate_limit=12, border_mode=cv2.BORDER_REFLECT_101, p=0.7),\n                A.ColorJitter(brightness=0.18, contrast=0.18, saturation=0.14, hue=0.05, p=0.6),\n                # A.GaussNoise(var_limit=(2.0, 18.0), p=0.25),\n                # A.CoarseDropout(max_holes=1, max_height=int(Config.img_size*0.25), max_width=int(Config.img_size*0.25), p=0.25),\n                A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n                ToTensorV2(),\n            ])\n        else:\n            return A.Compose([\n                A.Resize(Config.img_size, Config.img_size),\n                A.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n                ToTensorV2(),\n            ])\nelse:\n    def build_tfms(train: bool):\n        if train:\n            return T.Compose([\n                T.Resize((Config.img_size, Config.img_size)),\n                T.RandomHorizontalFlip(0.5),\n                T.RandomRotation(12),\n                T.ColorJitter(0.18, 0.18, 0.14, 0.05),\n                T.ToTensor(),\n                T.Normalize(IMAGENET_MEAN, IMAGENET_STD),\n            ])\n        else:\n            return T.Compose([\n                T.Resize((Config.img_size, Config.img_size)),\n                T.ToTensor(),\n                T.Normalize(IMAGENET_MEAN, IMAGENET_STD),\n            ])\n\ntfm_train = build_tfms(train=True)\ntfm_eval  = build_tfms(train=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:08:05.319708Z","iopub.execute_input":"2026-01-28T01:08:05.319999Z","iopub.status.idle":"2026-01-28T01:08:05.332749Z","shell.execute_reply.started":"2026-01-28T01:08:05.319975Z","shell.execute_reply":"2026-01-28T01:08:05.332139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class AnimePortraits(Dataset):\n    def __init__(self, df: pd.DataFrame, img_dir: str, train: bool, has_label: bool):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.train = train\n        self.has_label = has_label\n        self.tfm = tfm_train if train else tfm_eval\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load(self, path: str):\n        if USE_ALBU:\n            img = cv2.imread(path)\n            if img is None:\n                img = np.full((Config.img_size, Config.img_size, 3), 245, dtype=np.uint8)\n            else:\n                img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            return img\n        else:\n            try:\n                return Image.open(path).convert(\"RGB\")\n            except Exception:\n                return Image.new(\"RGB\", (Config.img_size, Config.img_size), (245, 245, 245))\n\n    def __getitem__(self, i):\n        img_id = str(self.df.loc[i, \"id\"])\n        path = os.path.join(self.img_dir, img_id)\n\n        img = self._load(path)\n        if USE_ALBU:\n            x = self.tfm(image=img)[\"image\"]\n        else:\n            x = self.tfm(img)\n\n        if self.has_label:\n            y = int(self.df.loc[i, \"label\"])\n            return x, y\n        return x, img_id","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:08:05.335088Z","iopub.execute_input":"2026-01-28T01:08:05.335371Z","iopub.status.idle":"2026-01-28T01:08:05.351923Z","shell.execute_reply.started":"2026-01-28T01:08:05.335329Z","shell.execute_reply":"2026-01-28T01:08:05.351093Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def one_hot(y, num_classes: int, smoothing: float = 0.0, device=None):\n    y = torch.as_tensor(y, device=device)\n    oh = F.one_hot(y, num_classes=num_classes).float()\n    if smoothing > 0:\n        oh = oh * (1 - smoothing) + smoothing / num_classes\n    return oh\n\ndef rand_bbox(W, H, lam):\n    cut_ratio = math.sqrt(1.0 - lam)\n    cut_w = int(W * cut_ratio)\n    cut_h = int(H * cut_ratio)\n\n    cx = np.random.randint(W)\n    cy = np.random.randint(H)\n\n    x1 = np.clip(cx - cut_w // 2, 0, W)\n    y1 = np.clip(cy - cut_h // 2, 0, H)\n    x2 = np.clip(cx + cut_w // 2, 0, W)\n    y2 = np.clip(cy + cut_h // 2, 0, H)\n    return x1, y1, x2, y2\n\ndef apply_mix(images, labels, num_classes: int):\n    \"\"\"Returns (mixed_images, soft_targets).\"\"\"\n    bs, _, H, W = images.shape\n    device_ = images.device\n\n    r = random.random()\n    do_mixup = (r < Config.mixup_p)\n    do_cutmix = (Config.mixup_p <= r < Config.mixup_p + Config.cutmix_p)\n\n    y1 = one_hot(labels, num_classes, smoothing=Config.label_smooth, device=device_)\n\n    if not (do_mixup or do_cutmix):\n        return images, y1\n\n    idx = torch.randperm(bs, device=device_)\n    y2 = one_hot(labels[idx], num_classes, smoothing=Config.label_smooth, device=device_)\n\n    if do_mixup:\n        lam = np.random.beta(Config.mix_alpha, Config.mix_alpha)\n        mixed = images * lam + images[idx] * (1 - lam)\n        target = y1 * lam + y2 * (1 - lam)\n        return mixed, target\n\n    # cutmix\n    lam = np.random.beta(Config.cutmix_alpha, Config.cutmix_alpha)\n    x1, y1b, x2, y2b = rand_bbox(W, H, lam)\n    mixed = images.clone()\n    mixed[:, :, y1b:y2b, x1:x2] = images[idx, :, y1b:y2b, x1:x2]\n    lam_adj = 1.0 - ((x2 - x1) * (y2b - y1b) / (W * H))\n    target = one_hot(labels, num_classes, smoothing=Config.label_smooth, device=device_) * lam_adj + \\\n             one_hot(labels[idx], num_classes, smoothing=Config.label_smooth, device=device_) * (1 - lam_adj)\n    return mixed, target\n\ndef soft_ce(logits, soft_targets):\n    return (-soft_targets * F.log_softmax(logits, dim=1)).sum(dim=1).mean()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:08:05.355508Z","iopub.execute_input":"2026-01-28T01:08:05.355772Z","iopub.status.idle":"2026-01-28T01:08:05.369534Z","shell.execute_reply.started":"2026-01-28T01:08:05.355751Z","shell.execute_reply":"2026-01-28T01:08:05.368871Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### build model","metadata":{}},{"cell_type":"code","source":"def build_model():\n    m = timm.create_model(\n        Config.model_name,\n        pretrained=Config.pretrained,\n        num_classes=Config.num_classes,\n        drop_rate=Config.drop_rate,\n    )\n    return m","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:08:05.370672Z","iopub.execute_input":"2026-01-28T01:08:05.371026Z","iopub.status.idle":"2026-01-28T01:08:05.385843Z","shell.execute_reply.started":"2026-01-28T01:08:05.370989Z","shell.execute_reply":"2026-01-28T01:08:05.385253Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ds = AnimePortraits(df_tr, TRAIN_DIR, train=True,  has_label=True)\nval_ds   = AnimePortraits(df_va, VAL_DIR,   train=False, has_label=True)\ntest_ds  = AnimePortraits(df_te, TEST_DIR,  train=False, has_label=False)\n\npin = torch.cuda.is_available()\npw = (Config.num_workers > 0)\n\ntrain_loader = DataLoader(\n    train_ds,\n    batch_size=Config.batch_size,\n    shuffle=True,\n    num_workers=Config.num_workers,\n    pin_memory=pin,\n    persistent_workers=pw,\n    drop_last=True\n)\n\nval_loader = DataLoader(\n    val_ds,\n    batch_size=Config.batch_size*2,\n    shuffle=False,\n    num_workers=Config.num_workers,\n    pin_memory=pin,\n    persistent_workers=pw\n)\n\ntest_loader = DataLoader(\n    test_ds,\n    batch_size=Config.batch_size*2,\n    shuffle=False,\n    num_workers=Config.num_workers,\n    pin_memory=pin,\n    persistent_workers=pw\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:08:05.386757Z","iopub.execute_input":"2026-01-28T01:08:05.387123Z","iopub.status.idle":"2026-01-28T01:08:05.399779Z","shell.execute_reply.started":"2026-01-28T01:08:05.387092Z","shell.execute_reply":"2026-01-28T01:08:05.399240Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = build_model().to(device)\nif Config.channels_last and device.type == \"cuda\":\n    model = model.to(memory_format=torch.channels_last)\n\noptimizer = torch.optim.AdamW(model.parameters(), lr=Config.lr, weight_decay=Config.weight_decay)\n\ntotal_steps = Config.epochs * len(train_loader)\nwarmup_steps = max(1, int(total_steps * Config.warmup_frac))\n\ndef lr_lambda(step):\n    if step < warmup_steps:\n        return (step + 1) / warmup_steps\n    t = (step - warmup_steps) / max(1, (total_steps - warmup_steps))\n    return 0.5 * (1.0 + math.cos(math.pi * t))\n\nscheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=lr_lambda)\n\nscaler = torch.cuda.amp.GradScaler(enabled=Config.use_amp)\n\nema = ModelEmaV2(model, decay=Config.ema_decay) if Config.use_ema else None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:08:05.400720Z","iopub.execute_input":"2026-01-28T01:08:05.401248Z","iopub.status.idle":"2026-01-28T01:08:09.553581Z","shell.execute_reply.started":"2026-01-28T01:08:05.401217Z","shell.execute_reply":"2026-01-28T01:08:09.552657Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.no_grad()\ndef evaluate(m):\n    m.eval()\n    correct, total = 0, 0\n    for x, y in val_loader:\n        x = x.to(device, non_blocking=True)\n        if Config.channels_last and device.type == \"cuda\":\n            x = x.to(memory_format=torch.channels_last)\n        y = torch.as_tensor(y, device=device)\n        logits = m(x)\n        pred = logits.argmax(1)\n        correct += (pred == y).sum().item()\n        total += y.numel()\n    return correct / max(1, total)\n\nbest_acc = -1.0\nbest_path = \"best.pt\"\nglobal_step = 0\n\nfor epoch in range(1, Config.epochs + 1):\n    model.train()\n    running_loss = 0.0\n\n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch:02d}/{Config.epochs}\", leave=False)\n    for x, y in pbar:\n        x = x.to(device, non_blocking=True)\n        if Config.channels_last and device.type == \"cuda\":\n            x = x.to(memory_format=torch.channels_last)\n        y = torch.as_tensor(y, device=device)\n\n        x_mix, y_soft = apply_mix(x, y, num_classes=Config.num_classes)\n\n        optimizer.zero_grad(set_to_none=True)\n        with torch.cuda.amp.autocast(enabled=Config.use_amp):\n            logits = model(x_mix)\n            loss = soft_ce(logits, y_soft)\n\n        scaler.scale(loss).backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n        scaler.step(optimizer)\n        scaler.update()\n\n        scheduler.step()\n        global_step += 1\n\n        if ema is not None:\n            ema.update(model)\n\n        running_loss += loss.item()\n        pbar.set_postfix(loss=f\"{running_loss/(pbar.n+1):.4f}\", lr=f\"{optimizer.param_groups[0]['lr']:.2e}\")\n\n    val_model = ema.module if ema is not None else model\n    val_acc = evaluate(val_model)\n\n    print(f\"Epoch {epoch:02d} | train_loss={running_loss/len(train_loader):.4f} | val_acc={val_acc:.5f}\")\n\n    if val_acc > best_acc:\n        best_acc = val_acc\n        torch.save({\"model\": val_model.state_dict(), \"acc\": best_acc, \"epoch\": epoch}, best_path)\n        print(f\"  saved {best_path} (best_acc={best_acc:.5f})\")\n\nprint(\"\\nBest val accuracy:\", best_acc)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-28T01:08:09.554659Z","iopub.execute_input":"2026-01-28T01:08:09.554968Z","execution_failed":"2026-01-28T01:13:05.967Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ckpt = torch.load(best_path, map_location=device)\ninfer_model = build_model().to(device)\ninfer_model.load_state_dict(ckpt[\"model\"], strict=True)\ninfer_model.eval()\nif Config.channels_last and device.type == \"cuda\":\n    infer_model = infer_model.to(memory_format=torch.channels_last)\n\nall_ids, all_pred = [], []\n\n@torch.no_grad()\ndef predict(loader):\n    for x, ids in tqdm(loader, desc=\"Predict\", leave=False):\n        x = x.to(device, non_blocking=True)\n        if Config.channels_last and device.type == \"cuda\":\n            x = x.to(memory_format=torch.channels_last)\n        with torch.cuda.amp.autocast(enabled=Config.use_amp):\n            logits = infer_model(x)\n        pred = logits.argmax(1).detach().cpu().numpy().tolist()\n        all_ids.extend(list(ids))\n        all_pred.extend(pred)\n\npredict(test_loader)\n\nsub = pd.DataFrame({\"id\": all_ids, \"label\": all_pred})\nsub.to_csv(\"submission.csv\", index=False)\nsub.head()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-01-28T01:13:05.968Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Final Thoughts\n\n> \"I see now that the circumstances of one's birth are irrelevant. It is what you do with the gift of life that determines who you are.\"\n>\n> — *Mewtwo (Pokémon: The First Movie)*","metadata":{}}]}