{"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":[{"sourceType":"competition","sourceId":20270,"databundleVersionId":1222630}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =========================================================\n# SIIM-ISIC Melanoma Classification\n# Image-only EfficientNet-B7 end-to-end training / inference\n# =========================================================\n\nimport os\nimport gc\nimport math\nimport random\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom tqdm.auto import tqdm\n\nfrom sklearn.model_selection import GroupShuffleSplit\nfrom sklearn.metrics import roc_auc_score\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\nfrom torch.amp import autocast, GradScaler\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nimport timm\n\n\n# =========================\n# 1. Reproducibility\n# =========================\ndef seed_everything(seed: int = 42) -> None:\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\n    # cudnn 관련 재현성 옵션\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\n\n# =========================\n# 2. Config\n# =========================\n@dataclass\nclass Config:\n    # ---- paths ----\n    # data_root: str = \"/content/drive/MyDrive/Colab Notebooks/melanoma_classification/siim-isic-melanoma-classification\"\n    # data_root: str = \"/mnt/c/Users/JH/Desktop/data_root/siim-isic-melanoma-classification\"\n    data_root: str = \"/kaggle/input/competitions/siim-isic-melanoma-classification\"\n    train_csv: str = os.path.join(data_root, \"train.csv\")\n    test_csv: str = os.path.join(data_root, \"test.csv\")\n    train_img_dir: str = os.path.join(data_root, \"jpeg/train\")\n    test_img_dir: str = os.path.join(data_root, \"jpeg/test\")\n\n    # ---- model ----\n    model_name: str = \"tf_efficientnet_b4_ns\"\n    pretrained: bool = True\n    image_size: int = 384\n\n    # ---- training ----\n    seed: int = 42\n    num_epochs: int = 10\n    batch_size: int = 32\n    num_workers: int = 2\n    lr: float = 1e-4\n    weight_decay: float = 1e-5\n    min_lr: float = 1e-6\n\n    # ---- imbalance ----\n    use_weighted_sampler: bool = True\n\n    # ---- system ----\n    device: str = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    amp: bool = True\n\n    # ---- save ----\n    # output_dir: str = os.path.join(data_root, \"outputs\")\n    # best_model_path: str = os.path.join(data_root, \"outputs/best_effb7_siim_isic.pth\")\n    output_dir: str = \"./outputs\"\n    best_model_path: str = \"./outputs/best_effb4_siim_isic.pth\"\n\n\nCFG = Config()\n\n\n# =========================\n# 3. Utils\n# =========================\ndef make_dirs():\n    os.makedirs(CFG.output_dir, exist_ok=True)\n\n\ndef read_image(image_path: str) -> np.ndarray:\n    \"\"\"\n    OpenCV는 BGR로 읽기 때문에 RGB로 변환.\n    출력 shape: [H, W, 3], dtype=uint8\n    \"\"\"\n    img = cv2.imread(image_path)\n    if img is None:\n        raise FileNotFoundError(f\"Image not found: {image_path}\")\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    return img\n\n\n# =========================\n# 4. Dataset\n# =========================\nclass MelanomaDataset(Dataset):\n    def __init__(self, df: pd.DataFrame, image_dir: str, transforms=None, is_test: bool = False):\n        self.df = df.reset_index(drop=True).copy()\n        self.image_dir = image_dir\n        self.transforms = transforms\n        self.is_test = is_test\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx: int):\n        row = self.df.iloc[idx]\n\n        image_name = row[\"image_name\"]\n        image_path = os.path.join(self.image_dir, f\"{image_name}.jpg\")\n        image = read_image(image_path)\n\n        if self.transforms is not None:\n            image = self.transforms(image=image)[\"image\"]  # [3, H, W], float32\n\n        if self.is_test:\n            return {\n                \"image\": image,\n                \"image_name\": image_name,\n            }\n\n        target = torch.tensor(row[\"target\"], dtype=torch.float32)\n        return {\n            \"image\": image,\n            \"target\": target,\n            \"image_name\": image_name,\n        }\n\n\n# =========================\n# 5. Augmentations\n# =========================\ndef get_train_transforms(img_size: int):\n    return A.Compose([\n        A.Resize(img_size, img_size),\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.ShiftScaleRotate(\n            shift_limit=0.05,\n            scale_limit=0.10,\n            rotate_limit=20,\n            border_mode=cv2.BORDER_CONSTANT,\n            p=0.5\n        ),\n        A.RandomBrightnessContrast(p=0.5),\n        A.HueSaturationValue(\n            hue_shift_limit=10,\n            sat_shift_limit=10,\n            val_shift_limit=10,\n            p=0.5\n        ),\n        A.Normalize(\n            mean=(0.485, 0.456, 0.406),\n            std=(0.229, 0.224, 0.225),\n            max_pixel_value=255.0\n        ),\n        ToTensorV2(),\n    ])\n\n\ndef get_valid_transforms(img_size: int):\n    return A.Compose([\n        A.Resize(img_size, img_size),\n        A.Normalize(\n            mean=(0.485, 0.456, 0.406),\n            std=(0.229, 0.224, 0.225),\n            max_pixel_value=255.0\n        ),\n        ToTensorV2(),\n    ])\n\n\n# =========================\n# 6. Model\n# =========================\nclass EfficientNetBinaryClassifier(nn.Module):\n    def __init__(self, model_name: str, pretrained: bool = True):\n        super().__init__()\n        # num_classes=1 로 지정하면 마지막 classifier 출력이 1차원 로짓이 됨\n        self.model = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            num_classes=1,\n            drop_rate=0.2,\n            drop_path_rate=0.1,\n        )\n\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        \"\"\"\n        x: [B, 3, H, W]\n        out: [B]\n        \"\"\"\n        logits = self.model(x)  # [B, 1]\n        logits = logits.squeeze(1)  # [B]\n        return logits\n\n\n# =========================\n# 7. Metrics\n# =========================\ndef safe_auc(y_true: np.ndarray, y_prob: np.ndarray) -> float:\n    \"\"\"\n    valid fold에 한 클래스만 존재하면 roc_auc_score가 깨질 수 있어서 보호.\n    \"\"\"\n    if len(np.unique(y_true)) < 2:\n        return 0.5\n    return roc_auc_score(y_true, y_prob)\n\n\n# =========================\n# 8. Train / Valid loops\n# =========================\ndef train_one_epoch(model, loader, optimizer, criterion, scaler, device):\n    model.train()\n\n    running_loss = 0.0\n    preds_list = []\n    targets_list = []\n\n    pbar = tqdm(loader, total=len(loader), desc=\"Train\", leave=False)\n\n    for batch in pbar:\n        images = batch[\"image\"].to(device, non_blocking=True)   # [B, 3, H, W]\n        targets = batch[\"target\"].to(device, non_blocking=True) # [B]\n\n        optimizer.zero_grad(set_to_none=True)\n\n        with autocast(device_type=\"cuda\", enabled=(CFG.amp and device.startswith(\"cuda\"))):\n            logits = model(images)                  # [B]\n            loss = criterion(logits, targets)      # scalar\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n\n        running_loss += loss.item() * images.size(0)\n\n        probs = torch.sigmoid(logits).detach().cpu().numpy()\n        y_true = targets.detach().cpu().numpy()\n\n        preds_list.append(probs)\n        targets_list.append(y_true)\n\n        pbar.set_postfix(loss=f\"{loss.item():.4f}\")\n\n    epoch_loss = running_loss / len(loader.dataset)\n    preds = np.concatenate(preds_list)\n    targets = np.concatenate(targets_list)\n    epoch_auc = safe_auc(targets, preds)\n\n    return epoch_loss, epoch_auc\n\n\n@torch.no_grad()\ndef valid_one_epoch(model, loader, criterion, device):\n    model.eval()\n\n    running_loss = 0.0\n    preds_list = []\n    targets_list = []\n\n    pbar = tqdm(loader, total=len(loader), desc=\"Valid\", leave=False)\n\n    for batch in pbar:\n        images = batch[\"image\"].to(device, non_blocking=True)\n        targets = batch[\"target\"].to(device, non_blocking=True)\n\n        with autocast(device_type=\"cuda\", enabled=(CFG.amp and device.startswith(\"cuda\"))):\n            logits = model(images)\n            loss = criterion(logits, targets)\n\n        running_loss += loss.item() * images.size(0)\n\n        probs = torch.sigmoid(logits).cpu().numpy()\n        y_true = targets.cpu().numpy()\n\n        preds_list.append(probs)\n        targets_list.append(y_true)\n\n    epoch_loss = running_loss / len(loader.dataset)\n    preds = np.concatenate(preds_list)\n    targets = np.concatenate(targets_list)\n    epoch_auc = safe_auc(targets, preds)\n\n    return epoch_loss, epoch_auc, preds, targets\n\n\n# =========================\n# 9. Inference\n# =========================\n@torch.no_grad()\ndef inference(model, loader, device):\n    model.eval()\n\n    image_names = []\n    probs_all = []\n\n    pbar = tqdm(loader, total=len(loader), desc=\"Inference\", leave=False)\n\n    for batch in pbar:\n        images = batch[\"image\"].to(device, non_blocking=True)\n        names = batch[\"image_name\"]\n\n        with autocast(device_type=\"cuda\", enabled=(CFG.amp and device.startswith(\"cuda\"))):\n            logits = model(images)\n            probs = torch.sigmoid(logits).cpu().numpy()\n\n        image_names.extend(list(names))\n        probs_all.extend(list(probs))\n\n    return pd.DataFrame({\n        \"image_name\": image_names,\n        \"target\": probs_all\n    })\n\n\n# =========================\n# 10. Data split\n# =========================\ndef make_train_valid_split(df: pd.DataFrame, seed: int = 42):\n    \"\"\"\n    patient_id 기준 group split.\n    같은 patient가 train/valid에 동시에 들어가는 걸 막는다.\n    \"\"\"\n    splitter = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=seed)\n    groups = df[\"patient_id\"].fillna(\"unknown_patient\")\n\n    train_idx, valid_idx = next(splitter.split(df, df[\"target\"], groups=groups))\n    train_df = df.iloc[train_idx].reset_index(drop=True)\n    valid_df = df.iloc[valid_idx].reset_index(drop=True)\n\n    return train_df, valid_df\n\n\n# =========================\n# 11. Sampler\n# =========================\ndef build_weighted_sampler(train_df: pd.DataFrame):\n    \"\"\"\n    클래스 불균형 대응.\n    malignant(1)이 적기 때문에 inverse frequency 방식으로 샘플 가중치를 준다.\n    \"\"\"\n    class_counts = train_df[\"target\"].value_counts().to_dict()\n    weights = train_df[\"target\"].map(lambda x: 1.0 / class_counts[x]).values\n    weights = torch.DoubleTensor(weights)\n\n    sampler = WeightedRandomSampler(\n        weights=weights,\n        num_samples=len(weights),\n        replacement=True\n    )\n    return sampler\n\n\n# =========================\n# 12. Main train pipeline\n# =========================\ndef train_pipeline():\n    seed_everything(CFG.seed)\n    make_dirs()\n\n    print(f\"Device: {CFG.device}\")\n    print(f\"Model: {CFG.model_name}\")\n\n    # ---- CSV 읽기 ----\n    train_df = pd.read_csv(CFG.train_csv)\n    test_df = pd.read_csv(CFG.test_csv)\n\n    # ---- split ----\n    train_fold, valid_fold = make_train_valid_split(train_df, CFG.seed)\n\n    print(f\"Train size: {len(train_fold)}\")\n    print(f\"Valid size: {len(valid_fold)}\")\n    print(f\"Train positive ratio: {train_fold['target'].mean():.5f}\")\n    print(f\"Valid positive ratio: {valid_fold['target'].mean():.5f}\")\n\n    # ---- dataset ----\n    train_dataset = MelanomaDataset(\n        df=train_fold,\n        image_dir=CFG.train_img_dir,\n        transforms=get_train_transforms(CFG.image_size),\n        is_test=False\n    )\n\n    valid_dataset = MelanomaDataset(\n        df=valid_fold,\n        image_dir=CFG.train_img_dir,\n        transforms=get_valid_transforms(CFG.image_size),\n        is_test=False\n    )\n\n    test_dataset = MelanomaDataset(\n        df=test_df,\n        image_dir=CFG.test_img_dir,\n        transforms=get_valid_transforms(CFG.image_size),\n        is_test=True\n    )\n\n    # ---- loader ----\n    if CFG.use_weighted_sampler:\n        train_sampler = build_weighted_sampler(train_fold)\n        train_loader = DataLoader(\n            train_dataset,\n            batch_size=CFG.batch_size,\n            sampler=train_sampler,\n            num_workers=CFG.num_workers,\n            pin_memory=True,\n            drop_last=True\n        )\n    else:\n        train_loader = DataLoader(\n            train_dataset,\n            batch_size=CFG.batch_size,\n            shuffle=True,\n            num_workers=CFG.num_workers,\n            pin_memory=True,\n            drop_last=True\n        )\n\n    valid_loader = DataLoader(\n        valid_dataset,\n        batch_size=CFG.batch_size,\n        shuffle=False,\n        num_workers=CFG.num_workers,\n        pin_memory=True\n    )\n\n    test_loader = DataLoader(\n        test_dataset,\n        batch_size=CFG.batch_size,\n        shuffle=False,\n        num_workers=CFG.num_workers,\n        pin_memory=True\n    )\n\n    # ---- model ----\n    model = EfficientNetBinaryClassifier(\n        model_name=CFG.model_name,\n        pretrained=CFG.pretrained\n    ).to(CFG.device)\n\n    # ---- loss ----\n    criterion = nn.BCEWithLogitsLoss()\n\n    # ---- optimizer / scheduler ----\n    head_params_ids = [id(p) for p in model.model.classifier.parameters()]\n    \n    backbone_params = [p for p in model.model.parameters() if id(p) not in head_params_ids]\n    head_params = model.model.classifier.parameters()\n    \n    optimizer = torch.optim.AdamW([\n        {\"params\": backbone_params, \"lr\": 2e-5},\n        {\"params\": head_params, \"lr\": 1e-4},\n    ], weight_decay=CFG.weight_decay)\n\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n        optimizer,\n        T_max=CFG.num_epochs,\n        eta_min=CFG.min_lr\n    )\n\n    scaler = GradScaler(enabled=(CFG.amp and CFG.device.startswith(\"cuda\")))\n\n    # ---- train loop ----\n    best_auc = -1.0\n\n    for epoch in range(1, CFG.num_epochs + 1):\n        print(f\"\\n===== Epoch {epoch}/{CFG.num_epochs} =====\")\n\n        train_loss, train_auc = train_one_epoch(\n            model, train_loader, optimizer, criterion, scaler, CFG.device\n        )\n\n        valid_loss, valid_auc, valid_probs, valid_targets = valid_one_epoch(\n            model, valid_loader, criterion, CFG.device\n        )\n\n        scheduler.step()\n\n        print(\n            f\"Epoch {epoch} | \"\n            f\"train_loss={train_loss:.5f}, train_auc={train_auc:.5f} | \"\n            f\"valid_loss={valid_loss:.5f}, valid_auc={valid_auc:.5f}\"\n        )\n\n        if valid_auc > best_auc:\n            best_auc = valid_auc\n            torch.save(\n                {\n                    \"model_state_dict\": model.state_dict(),\n                    \"best_auc\": best_auc,\n                    \"config\": CFG.__dict__,\n                },\n                CFG.best_model_path\n            )\n            print(f\"Best model saved! valid_auc={best_auc:.5f}\")\n\n    print(f\"\\nTraining finished. Best valid AUC = {best_auc:.5f}\")\n\n    # ---- load best model ----\n    checkpoint = torch.load(CFG.best_model_path, map_location=CFG.device)\n    model.load_state_dict(checkpoint[\"model_state_dict\"])\n\n    # ---- inference ----\n    submission = inference(model, test_loader, CFG.device)\n    submission.to_csv(os.path.join(CFG.output_dir, \"submission.csv\"), index=False)\n\n    print(f\"submission saved to: {os.path.join(CFG.output_dir, 'submission.csv')}\")\n    print(submission.head())\n\n    return model\n\n\n\nfrom PIL import Image\nimport matplotlib.pyplot as plt\n\n\n\n\nif __name__ == \"__main__\":\n    train_pipeline()\n    # print(CFG.train_img_dir)\n    # temp = os.listdir(CFG.train_img_dir)\n\n    # # print(len(temp))\n\n    # # print(temp[:10])\n\n    # for filename in temp[:10]:\n    #     full_path = os.path.join(CFG.train_img_dir, filename)\n\n    #     img = Image.open(full_path)\n    #     img = np.array(img)\n\n    #     plt.imshow(img)\n    #     plt.axis(\"off\")\n    #     plt.savefig(f\"test_{filename}.png\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-03-15T21:41:46.960057Z","iopub.execute_input":"2026-03-15T21:41:46.960355Z","iopub.status.idle":"2026-03-16T02:08:56.534104Z","shell.execute_reply.started":"2026-03-15T21:41:46.960329Z","shell.execute_reply":"2026-03-16T02:08:56.532374Z"}},"outputs":[],"execution_count":null}]}