{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport re\nimport zipfile\nimport random\nfrom pathlib import Path\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\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torchvision.models import efficientnet_b3, EfficientNet_B3_Weights\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import log_loss\n\n\n# =====================\n# Config\n# =====================\nSEED = 42\nDATA_DIR = Path(\"/kaggle/input/competitions/dogs-vs-cats-redux-kernels-edition\")\nWORK_DIR = Path(\"/kaggle/working/\")\nTRAIN_ZIP = DATA_DIR / \"train.zip\"\nTEST_ZIP = DATA_DIR / \"test.zip\"\n\nIMG_SIZE = 300\nBATCH_SIZE = 32\nEPOCHS = 5\nLR = 2e-4\nWEIGHT_DECAY = 1e-4\nVALID_SIZE = 0.08\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", device)\n\n\n# =====================\n# Reproducibility\n# =====================\ndef seed_everything(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nseed_everything(SEED)\n\n\n# =====================\n# Unzip data\n# =====================\ndef unzip_once(zip_path, out_dir):\n    out_dir.mkdir(parents=True, exist_ok=True)\n    marker = out_dir / \".unzipped\"\n    if marker.exists():\n        return\n\n    print(f\"Unzipping {zip_path.name} ...\")\n    with zipfile.ZipFile(zip_path, \"r\") as z:\n        z.extractall(out_dir)\n\n    marker.write_text(\"done\")\n\nunzip_once(TRAIN_ZIP, WORK_DIR)\nunzip_once(TEST_ZIP, WORK_DIR)\n\ntrain_dir = WORK_DIR / \"train\"\ntest_dir = WORK_DIR / \"test\"\n\nprint(\"Train images:\", len(list(train_dir.glob(\"*.jpg\"))))\nprint(\"Test images:\", len(list(test_dir.glob(\"*.jpg\"))))\n\n\n# =====================\n# Build dataframe\n# cat = 0, dog = 1\n# =====================\ntrain_files = sorted(train_dir.glob(\"*.jpg\"))\n\ndf = pd.DataFrame({\n    \"path\": [str(p) for p in train_files],\n    \"label\": [1 if p.name.startswith(\"dog\") else 0 for p in train_files],\n})\n\ntrain_df, valid_df = train_test_split(\n    df,\n    test_size=VALID_SIZE,\n    random_state=SEED,\n    stratify=df[\"label\"]\n)\n\ntrain_df = train_df.reset_index(drop=True)\nvalid_df = valid_df.reset_index(drop=True)\n\nprint(train_df[\"label\"].value_counts())\nprint(valid_df[\"label\"].value_counts())\n\n\n# =====================\n# Dataset\n# =====================\nclass CatDogDataset(Dataset):\n    def __init__(self, dataframe, transform=None, is_test=False):\n        self.df = dataframe.reset_index(drop=True)\n        self.transform = transform\n        self.is_test = is_test\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        path = self.df.loc[idx, \"path\"]\n        image = Image.open(path).convert(\"RGB\")\n\n        if self.transform:\n            image = self.transform(image)\n\n        if self.is_test:\n            return image, path\n\n        label = float(self.df.loc[idx, \"label\"])\n        return image, torch.tensor(label, dtype=torch.float32)\n\n\nweights = EfficientNet_B3_Weights.IMAGENET1K_V1\nimagenet_mean = weights.transforms().mean\nimagenet_std = weights.transforms().std\n\ntrain_tfms = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomRotation(8),\n    transforms.ColorJitter(\n        brightness=0.12,\n        contrast=0.12,\n        saturation=0.08,\n        hue=0.02\n    ),\n    transforms.ToTensor(),\n    transforms.Normalize(imagenet_mean, imagenet_std),\n])\n\nvalid_tfms = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(imagenet_mean, imagenet_std),\n])\n\ntrain_loader = DataLoader(\n    CatDogDataset(train_df, train_tfms),\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\nvalid_loader = DataLoader(\n    CatDogDataset(valid_df, valid_tfms),\n    batch_size=BATCH_SIZE * 2,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\n\n# =====================\n# Model\n# =====================\nmodel = efficientnet_b3(weights=weights)\n\n# EfficientNet-B3 原分类头是多分类，这里改成单 logit 二分类\nin_features = model.classifier[1].in_features\nmodel.classifier[1] = nn.Linear(in_features, 1)\n\nmodel = model.to(device)\n\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\n\nscaler = torch.cuda.amp.GradScaler(enabled=(device.type == \"cuda\"))\n\n\n# =====================\n# Train / Validate\n# =====================\ndef train_one_epoch(epoch):\n    model.train()\n    total_loss = 0.0\n\n    pbar = tqdm(train_loader, desc=f\"Epoch {epoch} train\")\n    for images, labels in pbar:\n        images = images.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True).view(-1, 1)\n\n        optimizer.zero_grad(set_to_none=True)\n\n        with torch.cuda.amp.autocast(enabled=(device.type == \"cuda\")):\n            logits = model(images)\n            loss = criterion(logits, labels)\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        total_loss += loss.item() * images.size(0)\n        pbar.set_postfix(loss=loss.item())\n\n    return total_loss / len(train_loader.dataset)\n\n\n@torch.no_grad()\ndef validate():\n    model.eval()\n\n    losses = []\n    preds = []\n    targets = []\n\n    for images, labels in tqdm(valid_loader, desc=\"Valid\"):\n        images = images.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True).view(-1, 1)\n\n        with torch.cuda.amp.autocast(enabled=(device.type == \"cuda\")):\n            logits = model(images)\n            loss = criterion(logits, labels)\n\n        prob = torch.sigmoid(logits).detach().cpu().numpy().reshape(-1)\n\n        losses.append(loss.item() * images.size(0))\n        preds.extend(prob.tolist())\n        targets.extend(labels.detach().cpu().numpy().reshape(-1).tolist())\n\n    preds_clip = np.clip(np.array(preds), 1e-5, 1 - 1e-5)\n    score = log_loss(targets, preds_clip)\n\n    return sum(losses) / len(valid_loader.dataset), score\n\n\nbest_score = float(\"inf\")\nbest_path = \"/kaggle/working/best_effnet_b3.pth\"\n\nfor epoch in range(1, EPOCHS + 1):\n    train_loss = train_one_epoch(epoch)\n    valid_loss, valid_logloss = validate()\n    scheduler.step()\n\n    print(\n        f\"Epoch {epoch}: \"\n        f\"train_loss={train_loss:.5f}, \"\n        f\"valid_loss={valid_loss:.5f}, \"\n        f\"valid_logloss={valid_logloss:.5f}\"\n    )\n\n    if valid_logloss < best_score:\n        best_score = valid_logloss\n        torch.save(model.state_dict(), best_path)\n        print(f\"Saved best model: {best_score:.5f}\")\n\n\n# =====================\n# Test prediction with simple TTA\n# =====================\nmodel.load_state_dict(torch.load(best_path, map_location=device))\nmodel.eval()\n\ntest_files = sorted(\n    test_dir.glob(\"*.jpg\"),\n    key=lambda p: int(re.search(r\"\\d+\", p.stem).group())\n)\n\ntest_df = pd.DataFrame({\n    \"path\": [str(p) for p in test_files]\n})\n\ntest_tfms = valid_tfms\n\ntest_loader = DataLoader(\n    CatDogDataset(test_df, test_tfms, is_test=True),\n    batch_size=BATCH_SIZE * 2,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n\n\n@torch.no_grad()\ndef predict_with_tta(loader):\n    all_ids = []\n    all_probs = []\n\n    for images, paths in tqdm(loader, desc=\"Test\"):\n        images = images.to(device, non_blocking=True)\n\n        with torch.cuda.amp.autocast(enabled=(device.type == \"cuda\")):\n            logits1 = model(images)\n            logits2 = model(torch.flip(images, dims=[3]))\n\n        probs = (\n            torch.sigmoid(logits1) +\n            torch.sigmoid(logits2)\n        ) / 2.0\n\n        probs = probs.detach().cpu().numpy().reshape(-1)\n\n        for path, prob in zip(paths, probs):\n            img_id = int(Path(path).stem)\n            all_ids.append(img_id)\n            all_probs.append(float(prob))\n\n    return all_ids, all_probs\n\n\nids, probs = predict_with_tta(test_loader)\n\nsubmission = pd.DataFrame({\n    \"id\": ids,\n    \"label\": probs\n}).sort_values(\"id\")\n\n# Kaggle 这个比赛看 logloss，极端 0/1 会被罚很重，轻微 clip 通常更稳\nsubmission[\"label\"] = submission[\"label\"].clip(0.003, 0.997)\n\nsubmission.to_csv(\"/kaggle/working/submission.csv\", index=False)\n\nprint(\"Best valid logloss:\", best_score)\nprint(submission.head())\nprint(submission.tail())\nprint(\"Saved to /kaggle/working/submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-24T05:04:15.537245Z","iopub.execute_input":"2026-06-24T05:04:15.538066Z","iopub.status.idle":"2026-06-24T05:25:54.321930Z","shell.execute_reply.started":"2026-06-24T05:04:15.538030Z","shell.execute_reply":"2026-06-24T05:25:54.321075Z"}},"outputs":[],"execution_count":null}]}