{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":281674115,"sourceType":"kernelVersion"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"pip install segmentation_models_pytorch -q","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:43:23.684913Z","iopub.execute_input":"2025-11-25T13:43:23.685506Z","iopub.status.idle":"2025-11-25T13:43:27.054019Z","shell.execute_reply.started":"2025-11-25T13:43:23.685478Z","shell.execute_reply":"2025-11-25T13:43:27.052877Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport random\nimport warnings\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nimport albumentations as A\nimport numpy as np\nimport pandas as pd\nimport segmentation_models_pytorch as smp\nimport torch\nimport torch.nn.functional as F\nimport torch.optim as optim\nimport wandb\nimport matplotlib.pyplot as plt\nfrom albumentations.pytorch import ToTensorV2\nfrom PIL import Image\nfrom sklearn.model_selection import KFold\nfrom timm.utils import ModelEmaV2\nfrom torch import nn\nfrom torch.utils.data import DataLoader, Dataset\nfrom tqdm import tqdm\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:43:27.055835Z","iopub.execute_input":"2025-11-25T13:43:27.056096Z","iopub.status.idle":"2025-11-25T13:43:27.063188Z","shell.execute_reply.started":"2025-11-25T13:43:27.056071Z","shell.execute_reply":"2025-11-25T13:43:27.062461Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def save_model(state_dict, save_path, **kwargs):\n    dict_ = {\"model_state_dict\": state_dict}\n    dict_.update(kwargs)\n    torch.save(dict_, save_path)\n\n\ndef set_seed(seed=42, cudnn_deterministic=True):\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    np.random.seed(seed)\n    random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = cudnn_deterministic\n    torch.backends.cudnn.benchmark = False\n    torch.use_deterministic_algorithms = True\n\n\n# プログレスバーのヘルパー関数\ndef get_pbar(dataloader: DataLoader) -> tqdm:\n    pbar = tqdm(\n        enumerate(dataloader),\n        total=len(dataloader),\n        bar_format=\"{l_bar}{bar:10}{r_bar}\",\n    )\n    return pbar\n\n\ndef pbar_train_desc(\n    pbar_train: tqdm,\n    lr: float,\n    epoch: int,\n    total_epoch: int,\n    average_loss: float,\n) -> None:\n    learning_rate = f\"LR : {lr:.2E}\"\n    gpu_memory = f\"Mem : {torch.cuda.memory_reserved() / 1e9:.3g}GB\"\n    epoch_info = f\"Epoch {epoch}/{total_epoch}\"\n    loss = f\"Loss: {average_loss:.4f}\"\n    description = f\"{epoch_info:12} {gpu_memory:15} {learning_rate:15} {loss:15}\"\n    pbar_train.set_description(description)\n\n\ndef pbar_valid_desc(pbar_val: tqdm, average_loss: float) -> None:\n    average_val_loss = f\"Val Loss: {average_loss:.4f}\"\n    description = f\"{average_val_loss:18}\"\n    pbar_val.set_description(description)\n\n\ndef find_best_threshold(model, dataloader, device, thresholds=None):\n    \"\"\"検証データセットでIoUを最大化する最適な閾値を見つける関数。\n\n    この関数は異なる閾値を評価して、検証データセットでIntersection over Union (IoU)\n    メトリクスを最大化する最適な閾値を見つけます。\n\n    Args:\n        model: 評価対象の訓練済みセグメンテーションモデル\n        dataloader: 検証データを含むDataLoader (images, masks, image_ids)\n        device: 推論を実行するデバイス (cuda または cpu)\n        thresholds: テストする閾値の配列。Noneの場合、np.arange(0.1, 0.9, 0.05)を使用\n\n    Returns:\n        tuple: 以下を含むタプル:\n            - best_threshold (float): 最高IoUを達成した閾値\n            - best_iou (float): 達成された最高IoUスコア\n            - threshold_ious (list): テストされた全閾値の(threshold, iou)タプルのリスト\n    \"\"\"\n    if thresholds is None:\n        thresholds = np.arange(0.1, 0.9, 0.05)\n\n    model.eval()\n    all_preds = []\n    all_targets = []\n\n    with torch.no_grad():\n        for images, masks, _ in tqdm(dataloader, desc=\"Collecting predictions for threshold search\"):\n            images = images.to(device)\n            masks = masks.to(device)\n            outputs = model(images)\n            all_preds.append(torch.sigmoid(outputs).cpu())\n            all_targets.append(masks.cpu())\n\n    all_preds = torch.cat(all_preds, dim=0)\n    all_targets = torch.cat(all_targets, dim=0)\n\n    best_threshold = 0.5\n    best_iou = 0.0\n    threshold_ious = []\n\n    for threshold in thresholds:\n        pred_binary = (all_preds > threshold).float()\n        intersection = (pred_binary * all_targets).sum()\n        union = pred_binary.sum() + all_targets.sum() - intersection\n        iou = (intersection + 1e-7) / (union + 1e-7)\n        iou = iou.item()\n        threshold_ious.append((threshold, iou))\n\n        if iou > best_iou:\n            best_iou = iou\n            best_threshold = threshold\n\n    return best_threshold, best_iou, threshold_ious\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:43:27.064164Z","iopub.execute_input":"2025-11-25T13:43:27.064440Z","iopub.status.idle":"2025-11-25T13:43:27.089403Z","shell.execute_reply.started":"2025-11-25T13:43:27.064417Z","shell.execute_reply":"2025-11-25T13:43:27.088578Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_iou(pred, target, num_channels=2, threshold=0.5):\n    \"\"\"\n    マルチラベルセグメンテーションのIoUを計算\n\n    Args:\n        pred: モデルの出力 (B, num_channels, H, W) - logits\n        target: ターゲットマスク (B, num_channels, H, W) - binary masks (0 or 1)\n        num_channels: チャンネル数 (0=clover, 1=weed)\n        threshold: 予測の閾値\n\n    Returns:\n        mean_iou: 全チャンネルの平均IoU\n    \"\"\"\n    pred = torch.sigmoid(pred)  # Apply sigmoid\n    pred = (pred > threshold).float()  # Binarize\n\n    ious = []\n    for ch in range(num_channels):\n        pred_ch = pred[:, ch]\n        target_ch = target[:, ch]\n\n        intersection = (pred_ch * target_ch).sum().float()\n        union = (pred_ch + target_ch).clamp(0, 1).sum().float()\n\n        if union == 0:\n            # このチャンネルがターゲットに存在しない場合はスキップ\n            continue\n\n        iou = (intersection + 1e-7) / (union + 1e-7)\n        ious.append(iou.item())\n\n    return sum(ious) / len(ious) if ious else 0.0\n\n\ndef calculate_class_iou(pred, target, num_channels=2, threshold=0.5):\n    \"\"\"\n    チャンネルごとのIoUを計算\n\n    Args:\n        pred: モデルの出力 (B, num_channels, H, W) - logits\n        target: ターゲットマスク (B, num_channels, H, W) - binary masks (0 or 1)\n        num_channels: チャンネル数\n        threshold: 予測の閾値\n\n    Returns:\n        dict: {channel_id: iou_value}\n    \"\"\"\n    pred = torch.sigmoid(pred)  # Apply sigmoid\n    pred = (pred > threshold).float()  # Binarize\n\n    channel_ious = {}\n    for ch in range(num_channels):\n        pred_ch = pred[:, ch]\n        target_ch = target[:, ch]\n\n        intersection = (pred_ch * target_ch).sum().float()\n        union = (pred_ch + target_ch).clamp(0, 1).sum().float()\n\n        if union > 0:\n            iou = (intersection + 1e-7) / (union + 1e-7)\n            channel_ious[ch] = iou.item()\n        else:\n            channel_ious[ch] = 0.0\n\n    return channel_ious\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:43:27.091174Z","iopub.execute_input":"2025-11-25T13:43:27.091463Z","iopub.status.idle":"2025-11-25T13:43:27.111616Z","shell.execute_reply.started":"2025-11-25T13:43:27.091437Z","shell.execute_reply":"2025-11-25T13:43:27.110687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class DiceLoss(nn.Module):\n    def __init__(self, num_channels=2, smooth=1.0):\n        super().__init__()\n        self.num_channels = num_channels\n        self.smooth = smooth\n\n    def forward(self, pred, target):\n        \"\"\"\n        マルチラベルDice Loss\n\n        Args:\n            pred: (B, num_channels, H, W) - logits\n            target: (B, num_channels, H, W) - binary masks (0 or 1)\n        \"\"\"\n        pred = torch.sigmoid(pred)  # Apply sigmoid for multilabel\n\n        dice_loss = 0\n        for ch in range(self.num_channels):\n            pred_ch = pred[:, ch]\n            target_ch = target[:, ch]\n\n            intersection = (pred_ch * target_ch).sum()\n            dice = (2.0 * intersection + self.smooth) / (pred_ch.sum() + target_ch.sum() + self.smooth)\n            dice_loss += 1 - dice\n\n        return dice_loss / self.num_channels\n\n\nclass CombinedLoss(nn.Module):\n    def __init__(self, num_channels=2):\n        super().__init__()\n        self.bce = nn.BCEWithLogitsLoss()\n        self.dice = DiceLoss(num_channels=num_channels)\n\n    def forward(self, pred, target):\n        \"\"\"\n        BCEWithLogitsLoss + Dice Loss for multilabel segmentation\n\n        Args:\n            pred: (B, num_channels, H, W) - logits\n            target: (B, num_channels, H, W) - binary masks (0 or 1)\n        \"\"\"\n        bce_loss = self.bce(pred, target)\n        dice_loss = self.dice(pred, target)\n\n        return bce_loss + dice_loss\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:43:27.112577Z","iopub.execute_input":"2025-11-25T13:43:27.112919Z","iopub.status.idle":"2025-11-25T13:43:27.130824Z","shell.execute_reply.started":"2025-11-25T13:43:27.112894Z","shell.execute_reply":"2025-11-25T13:43:27.129851Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_fold_csv(image_dir, mask_dir, fold_csv_path, num_folds):\n    # データセットから全ての画像IDを取得\n    image_dir = Path(image_dir)\n    mask_dir = Path(mask_dir)\n    image_files = sorted([f.replace(\".jpg\", \"\") for f in os.listdir(image_dir) if f.endswith(\".jpg\")])\n    mask_files = sorted([f.replace(\".png\", \"\") for f in os.listdir(mask_dir) if f.endswith(\".png\")])\n    print(f\"Found {len(mask_files)} images in {mask_dir}\")\n\n    # フォールド分割を作成\n    kfold = KFold(n_splits=num_folds, shuffle=True, random_state=42)\n\n    # フォールドデータフレームを作成\n    fold_data = []\n    for fold, (train_idx, val_idx) in enumerate(kfold.split(mask_files)):\n        for idx in val_idx:\n            fold_data.append({\"image_id\": mask_files[idx], \"fold\": fold})\n\n    fold_df = pd.DataFrame(fold_data)\n    fold_df.to_csv(fold_csv_path, index=False)\n    print(f\"Fold CSV saved to {fold_csv_path}\")\n    print(f\"Total samples: {len(fold_df)}\")\n    print(f\"Fold distribution:\\n{fold_df['fold'].value_counts().sort_index()}\")\n\n\nclass TrainSegmentationDataset(Dataset):\n    \"\"\"Dataset for training/validation with masks\"\"\"\n\n    def __init__(self, image_ids, data_dir, mask_dir, transform=None):\n        self.image_ids = image_ids\n        self.data_dir = data_dir\n        self.mask_dir = mask_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.image_ids)\n\n    def __getitem__(self, idx):\n        image_id = self.image_ids[idx]\n\n        # Load image\n        image_path = f\"{self.data_dir}/{image_id}.jpg\"\n        image = np.array(Image.open(image_path).convert(\"RGB\"))\n\n        # Train/valid mode: load existing mask\n        # Mask values: 0=background, 1=clover, 2=weed\n        mask_path = f\"{self.mask_dir}/{image_id}.png\"\n        mask = np.array(Image.open(mask_path))\n\n        # Convert from multiclass (0, 1, 2) to multilabel (2 channels)\n        # Channel 0: clover (1 where mask==1, 0 elsewhere)\n        # Channel 1: weed (1 where mask==2, 0 elsewhere)\n        clover_mask = (mask == 1).astype(np.float32)\n        weed_mask = (mask == 2).astype(np.float32)\n\n        if self.transform:\n            # For multilabel, we need to transform each mask separately\n            augmented = self.transform(image=image, masks=[clover_mask, weed_mask])\n            image = augmented[\"image\"]\n            masks = augmented[\"masks\"]\n\n            # Stack masks: (2, H, W)\n            # Handle both tensor and numpy array cases (ToTensorV2 converts masks to tensors)\n            import torch\n\n            mask = torch.stack([m if isinstance(m, torch.Tensor) else torch.from_numpy(m) for m in masks], dim=0)\n        else:\n            # Stack masks without transform: (2, H, W)\n            import torch\n\n            mask = torch.stack([torch.from_numpy(clover_mask), torch.from_numpy(weed_mask)], dim=0)\n\n        return image, mask, image_id\n\n\nclass TestSegmentationDataset(Dataset):\n    \"\"\"Dataset for test/inference with sliding window for large images\"\"\"\n\n    def __init__(self, image_ids, data_dir, transform=None, tile_size=448, overlap=224):\n        self.image_ids = image_ids\n        self.data_dir = data_dir\n        self.transform = transform\n        self.tile_size = tile_size\n        self.overlap = overlap\n\n        # Pre-calculate all tiles for all images\n        self.tiles = []\n        for image_id in image_ids:\n            image_path = f\"{self.data_dir}/{image_id}.jpg\"\n            image = Image.open(image_path).convert(\"RGB\")\n            h, w = image.size[1], image.size[0]  # PIL uses (width, height)\n\n            # Calculate stride (step size between windows)\n            stride = tile_size - overlap\n\n            # Generate all tile positions for this image\n            for y in range(0, h, stride):\n                for x in range(0, w, stride):\n                    # Ensure we don't go beyond image boundaries\n                    y_end = min(y + tile_size, h)\n                    x_end = min(x + tile_size, w)\n\n                    # Adjust starting position if we're at the edge\n                    y_start = max(0, y_end - tile_size)\n                    x_start = max(0, x_end - tile_size)\n\n                    self.tiles.append(\n                        {\n                            \"image_id\": image_id,\n                            \"x\": x_start,\n                            \"y\": y_start,\n                            \"width\": tile_size,\n                            \"height\": tile_size,\n                            \"original_h\": h,\n                            \"original_w\": w,\n                        }\n                    )\n\n    def __len__(self):\n        return len(self.tiles)\n\n    def __getitem__(self, idx):\n        tile_info = self.tiles[idx]\n        image_id = tile_info[\"image_id\"]\n\n        # Load full image\n        image_path = f\"{self.data_dir}/{image_id}.jpg\"\n        image = np.array(Image.open(image_path).convert(\"RGB\"))\n\n        # Extract tile\n        y, x = tile_info[\"y\"], tile_info[\"x\"]\n        h, w = tile_info[\"height\"], tile_info[\"width\"]\n        tile = image[y : y + h, x : x + w]\n\n        if self.transform:\n            augmented = self.transform(image=tile)\n            tile = augmented[\"image\"]\n\n        # Return tile, image_id, and position information for reconstruction\n        return tile, image_id, tile_info[\"x\"], tile_info[\"y\"], tile_info[\"original_w\"], tile_info[\"original_h\"]\n\n\n# Backward compatibility alias\nSegmentationDataset = TrainSegmentationDataset\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:43:27.131836Z","iopub.execute_input":"2025-11-25T13:43:27.132139Z","iopub.status.idle":"2025-11-25T13:43:27.151682Z","shell.execute_reply.started":"2025-11-25T13:43:27.132113Z","shell.execute_reply":"2025-11-25T13:43:27.150784Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def rand_bbox(size, lam):\n    \"\"\"Generate random bounding box for CutMix\"\"\"\n    W = size[2]\n    H = size[3]\n    cut_rat = np.sqrt(1.0 - lam)\n    cut_w = int(W * cut_rat)\n    cut_h = int(H * cut_rat)\n\n    # uniform\n    cx = np.random.randint(W)\n    cy = np.random.randint(H)\n\n    bbx1 = np.clip(cx - cut_w // 2, 0, W)\n    bby1 = np.clip(cy - cut_h // 2, 0, H)\n    bbx2 = np.clip(cx + cut_w // 2, 0, W)\n    bby2 = np.clip(cy + cut_h // 2, 0, H)\n\n    return bbx1, bby1, bbx2, bby2\n\n\ndef mixup(images, masks, alpha=1.0, num_channels=2):\n    \"\"\"Apply MixUp augmentation for multilabel segmentation\n\n    Args:\n        images: (B, C, H, W)\n        masks: (B, num_channels, H, W) - binary masks for each label\n        alpha: mixup parameter\n        num_channels: number of label channels\n\n    Returns:\n        mixed_images: (B, C, H, W)\n        mixed_masks: (B, num_channels, H, W)\n    \"\"\"\n    if alpha > 0:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1\n\n    batch_size = images.size(0)\n    index = torch.randperm(batch_size).to(images.device)\n\n    # For multilabel, masks are already (B, num_channels, H, W)\n    mixed_images = lam * images + (1 - lam) * images[index]\n    mixed_masks = lam * masks + (1 - lam) * masks[index]\n\n    return mixed_images, mixed_masks\n\n\ndef cutmix(images, masks, alpha=2, beta=5, num_channels=2):\n    \"\"\"Apply CutMix augmentation for multilabel segmentation\n\n    Args:\n        images: (B, C, H, W)\n        masks: (B, num_channels, H, W) - binary masks for each label\n        alpha: cutmix parameter\n        num_channels: number of label channels\n\n    Returns:\n        mixed_images: (B, C, H, W)\n        mixed_masks: (B, num_channels, H, W)\n    \"\"\"\n    if alpha > 0:\n        lam = np.random.beta(alpha, beta)\n    else:\n        lam = 1\n\n    batch_size = images.size(0)\n    index = torch.randperm(batch_size).to(images.device)\n\n    # For multilabel, masks are already (B, num_channels, H, W)\n    bbx1, bby1, bbx2, bby2 = rand_bbox(images.size(), lam)\n    images[:, :, bbx1:bbx2, bby1:bby2] = images[index, :, bbx1:bbx2, bby1:bby2]\n    masks[:, :, bbx1:bbx2, bby1:bby2] = masks[index, :, bbx1:bbx2, bby1:bby2]\n\n    return images, masks\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:43:27.152565Z","iopub.execute_input":"2025-11-25T13:43:27.152923Z","iopub.status.idle":"2025-11-25T13:43:27.172057Z","shell.execute_reply.started":"2025-11-25T13:43:27.152856Z","shell.execute_reply":"2025-11-25T13:43:27.171314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_tta_transforms(image):\n    \"\"\"\n    Apply Test Time Augmentation transforms\n\n    Returns:\n        List of (transformed_image, transform_id) tuples\n        transform_id is used to reverse the transformation\n    \"\"\"\n    import torch\n\n    transforms = []\n\n    # Original\n    transforms.append((image, \"original\"))\n\n    # Horizontal flip\n    transforms.append((torch.flip(image, dims=[3]), \"hflip\"))\n\n    # Vertical flip\n    transforms.append((torch.flip(image, dims=[2]), \"vflip\"))\n\n    # Both flips (180 degree rotation)\n    transforms.append((torch.flip(image, dims=[2, 3]), \"hvflip\"))\n\n    # 90 degree rotation (clockwise)\n    transforms.append((torch.rot90(image, k=1, dims=[2, 3]), \"rot90\"))\n\n    # 180 degree rotation\n    transforms.append((torch.rot90(image, k=2, dims=[2, 3]), \"rot180\"))\n\n    # 270 degree rotation (clockwise)\n    transforms.append((torch.rot90(image, k=3, dims=[2, 3]), \"rot270\"))\n\n    return transforms\n\n\ndef reverse_tta_transform(prediction, transform_id):\n    \"\"\"\n    Reverse the TTA transformation applied to get back to original orientation\n\n    Args:\n        prediction: numpy array (C, H, W)\n        transform_id: string identifying the transformation\n\n    Returns:\n        Reversed prediction (C, H, W)\n    \"\"\"\n    import torch\n\n    # Convert to tensor for easy manipulation\n    pred_tensor = torch.from_numpy(prediction)\n\n    if transform_id == \"original\":\n        return prediction\n    elif transform_id == \"hflip\":\n        return torch.flip(pred_tensor, dims=[2]).numpy()\n    elif transform_id == \"vflip\":\n        return torch.flip(pred_tensor, dims=[1]).numpy()\n    elif transform_id == \"hvflip\":\n        return torch.flip(pred_tensor, dims=[1, 2]).numpy()\n    elif transform_id == \"rot90\":\n        # Reverse 90 CW = 270 CW = 90 CCW\n        return torch.rot90(pred_tensor, k=3, dims=[1, 2]).numpy()\n    elif transform_id == \"rot180\":\n        return torch.rot90(pred_tensor, k=2, dims=[1, 2]).numpy()\n    elif transform_id == \"rot270\":\n        # Reverse 270 CW = 90 CW\n        return torch.rot90(pred_tensor, k=1, dims=[1, 2]).numpy()\n    else:\n        raise ValueError(f\"Unknown transform_id: {transform_id}\")\n\n\ndef create_gaussian_weight(tile_h, tile_w, sigma_scale=0.25):\n    \"\"\"\n    Create 2D Gaussian weight matrix with higher weights in center,\n    lower at edges to reduce boundary artifacts\n\n    Args:\n        tile_h: Tile height\n        tile_w: Tile width\n        sigma_scale: Controls how quickly weights decay from center (lower = faster decay)\n    \"\"\"\n    # Create 1D Gaussian curves for each dimension\n    y = np.linspace(-1, 1, tile_h)\n    x = np.linspace(-1, 1, tile_w)\n\n    # Gaussian function: exp(-(x^2) / (2*sigma^2))\n    sigma = sigma_scale\n    gauss_y = np.exp(-(y**2) / (2 * sigma**2))\n    gauss_x = np.exp(-(x**2) / (2 * sigma**2))\n\n    # Create 2D Gaussian by outer product\n    weight = np.outer(gauss_y, gauss_x).astype(np.float32)\n\n    return weight\n\n\ndef predict(model, dataloader, device):\n    \"\"\"\n    Perform prediction on the given dataloader with sliding window and TTA\n    Always assumes test mode with sliding window dataloader\n\n    Args:\n        model: The model to use for prediction\n        dataloader: DataLoader with sliding window tiles\n        device: Device to run inference on\n\n    Returns:\n        predictions, image_ids\n    \"\"\"\n    model.eval()\n    predictions = []\n    image_ids_output = []\n    original_images = []\n\n    # Dictionary to accumulate tiles for each image\n    image_tiles = {}  # {image_id: {'tiles': [], 'positions': [], 'size': (w, h)}}\n\n    with torch.no_grad():\n        for batch in tqdm(dataloader, desc=\"Predicting (sliding window with TTA)\"):\n            # Test mode with sliding window: (tiles, ids, x, y, w, h)\n            tiles, ids, xs, ys, ws, hs = batch\n\n            tiles = tiles.to(device)\n            batch_size = tiles.shape[0]\n\n            # Apply TTA: predict on multiple augmented versions and average\n            # Get all TTA transforms for the first tile to know what transforms to apply\n            tta_transform_list = apply_tta_transforms(tiles[0:1])\n            num_tta = len(tta_transform_list)\n\n            # Prepare all TTA variants for all tiles in the batch\n            all_tta_tiles = []\n            transform_ids = []\n\n            for i in range(batch_size):\n                single_tile = tiles[i : i + 1]  # (1, C, H, W)\n                tta_transforms = apply_tta_transforms(single_tile)\n\n                for transformed_tile, transform_id in tta_transforms:\n                    all_tta_tiles.append(transformed_tile)\n                    transform_ids.append(transform_id)\n\n            # Stack all TTA tiles into a single batch: (batch_size * num_tta, C, H, W)\n            all_tta_tiles = torch.cat(all_tta_tiles, dim=0)\n\n            # Process all TTA variants in larger batches\n            tta_batch_size = 32  # Process multiple TTA variants at once\n            all_outputs = []\n\n            for start_idx in range(0, len(all_tta_tiles), tta_batch_size):\n                end_idx = min(start_idx + tta_batch_size, len(all_tta_tiles))\n                batch_tta_tiles = all_tta_tiles[start_idx:end_idx]\n\n                # bfloat16での推論\n                with torch.amp.autocast(\"cuda\", dtype=torch.bfloat16):\n                    outputs = model(batch_tta_tiles)\n\n                # bfloat16をfloat32に変換してからnumpyに変換\n                preds_batch = torch.sigmoid(outputs).float().cpu().numpy()\n                all_outputs.append(preds_batch)\n\n            # Concatenate all outputs\n            all_preds = np.concatenate(all_outputs, axis=0)  # (batch_size * num_tta, 2, H, W)\n\n            # Reverse TTA transforms and average predictions for each original tile\n            tta_preds = []\n            for i in range(batch_size):\n                tile_tta_preds = []\n\n                for j in range(num_tta):\n                    idx = i * num_tta + j\n                    pred = all_preds[idx]  # (2, H, W)\n                    transform_id = transform_ids[idx]\n\n                    # Reverse the transformation\n                    pred_reversed = reverse_tta_transform(pred, transform_id)\n                    tile_tta_preds.append(pred_reversed)\n\n                # Average all TTA predictions for this tile\n                avg_pred = np.mean(tile_tta_preds, axis=0)  # (2, H, W)\n                tta_preds.append(avg_pred)\n\n            preds = np.array(tta_preds)  # (B, 2, H, W)\n\n            # Group tiles by image_id\n            for i in range(len(ids)):\n                img_id = ids[i]\n                tile_pred = preds[i]  # (2, H, W)\n                x = xs[i].item() if hasattr(xs[i], \"item\") else xs[i]\n                y = ys[i].item() if hasattr(ys[i], \"item\") else ys[i]\n                w = ws[i].item() if hasattr(ws[i], \"item\") else ws[i]\n                h = hs[i].item() if hasattr(hs[i], \"item\") else hs[i]\n\n                if img_id not in image_tiles:\n                    image_tiles[img_id] = {\"tiles\": [], \"positions\": [], \"size\": (w, h)}\n\n                image_tiles[img_id][\"tiles\"].append(tile_pred)\n                image_tiles[img_id][\"positions\"].append((x, y))\n\n    # Create Gaussian weight matrix for blending tiles\n\n    # Merge tiles for each image\n    for img_id in sorted(image_tiles.keys()):\n        data = image_tiles[img_id]\n        original_w, original_h = data[\"size\"]\n\n        # Create accumulation arrays for weighted averaging\n        # Shape: (num_classes, H, W)\n        prediction_sum = np.zeros((2, original_h, original_w), dtype=np.float32)\n        weight_sum = np.zeros((original_h, original_w), dtype=np.float32)\n\n        # Add each tile with Gaussian weights to reduce edge artifacts\n        for tile_pred, (x, y) in zip(data[\"tiles\"], data[\"positions\"]):\n            tile_h, tile_w = tile_pred.shape[1], tile_pred.shape[2]\n\n            # Create Gaussian weight matrix (higher in center, decays toward edges)\n            weight = create_gaussian_weight(tile_h, tile_w, sigma_scale=0.25)\n\n            # Accumulate predictions with Gaussian weights\n            prediction_sum[:, y : y + tile_h, x : x + tile_w] += tile_pred * weight\n            weight_sum[y : y + tile_h, x : x + tile_w] += weight\n\n        # Average predictions\n        # Avoid division by zero (should not happen but just in case)\n        weight_sum = np.maximum(weight_sum, 1e-8)\n        merged_pred = prediction_sum / weight_sum  # (2, H, W)\n\n        predictions.append(merged_pred)\n        image_ids_output.append(img_id)\n        original_images.append(None)\n\n    return predictions, None, original_images, image_ids_output\n\n\ndef visualize_predictions(\n    predictions,\n    ground_truths,\n    images,\n    image_ids,\n    output_dir,\n    num_samples=10,\n    fold=0,\n    species_dict=None,\n    threshold=0.5,\n    data_dir=None,\n):\n    num_samples = min(num_samples, len(predictions))\n\n    if num_samples == 0:\n        print(\"No samples to visualize\")\n        return\n\n    # Use all samples if num_samples >= total, otherwise randomly select\n    if num_samples >= len(predictions):\n        indices = np.arange(len(predictions))\n    else:\n        indices = np.random.choice(len(predictions), num_samples, replace=False)\n\n    # Denormalize images\n    mean = np.array([0.485, 0.456, 0.406])\n    std = np.array([0.229, 0.224, 0.225])\n\n    # Check if we're in sliding window mode (images are None)\n    sliding_window_mode = images[0] is None if len(images) > 0 else False\n\n    # Determine number of columns based on whether we have ground truth\n    num_cols = 6 if ground_truths is not None else 5\n    fig, axes = plt.subplots(len(indices), num_cols, figsize=(4 * num_cols, 4 * len(indices)))\n\n    # Handle single sample case (axes will be 1D)\n    if len(indices) == 1:\n        axes = axes.reshape(1, -1)\n\n    for i, idx in enumerate(indices):\n        # Load and prepare image\n        if sliding_window_mode:\n            # Load original image from disk\n            image_id = image_ids[idx]\n            if data_dir is None:\n                raise ValueError(\"data_dir must be provided when using sliding window mode\")\n            image_path = f\"{data_dir}/{image_id}.jpg\"\n            img = np.array(Image.open(image_path).convert(\"RGB\"))\n            # Resize to match prediction size for visualization\n            pred_h, pred_w = predictions[idx].shape[1], predictions[idx].shape[2]\n            img = np.array(Image.fromarray(img).resize((pred_w, pred_h), Image.BILINEAR))\n            img = img.astype(np.float32) / 255.0  # Normalize to [0, 1]\n        else:\n            # Denormalize preprocessed image\n            img = images[idx].transpose(1, 2, 0)\n            img = img * std + mean\n            img = np.clip(img, 0, 1)\n\n        # Multilabel predictions: (2, H, W) - [clover, weed]\n        clover_prob = predictions[idx][0]  # (H, W)\n        weed_prob = predictions[idx][1]  # (H, W)\n\n        # Create visualization mask\n        # Green for clover, red for weed, yellow for overlap\n        pred_mask = np.zeros((clover_prob.shape[0], clover_prob.shape[1], 3), dtype=np.uint8)\n        clover_mask = clover_prob > threshold\n        weed_mask = weed_prob > threshold\n\n        # Set colors: green for clover, red for weed\n        pred_mask[clover_mask & ~weed_mask] = [0, 255, 0]  # Green for clover only\n        pred_mask[~clover_mask & weed_mask] = [255, 0, 0]  # Red for weed only\n        pred_mask[clover_mask & weed_mask] = [255, 255, 0]  # Yellow for overlap\n\n        # Get species info if available\n        image_id = image_ids[idx]\n        title = f\"Image: {image_id}\"\n        if species_dict and image_id in species_dict:\n            title = f\"{image_id}\\nSpecies: {species_dict[image_id]}\"\n\n        col = 0\n        # Original image\n        axes[i, col].imshow(img)\n        axes[i, col].set_title(title, fontsize=10)\n        axes[i, col].axis(\"off\")\n        col += 1\n\n        # Ground truth (only if available)\n        if ground_truths is not None:\n            gt = ground_truths[idx]  # (2, H, W)\n            gt_mask = np.zeros((gt.shape[1], gt.shape[2], 3), dtype=np.uint8)\n            gt_clover_mask = gt[0] > 0.5\n            gt_weed_mask = gt[1] > 0.5\n\n            gt_mask[gt_clover_mask & ~gt_weed_mask] = [0, 255, 0]  # Green for clover only\n            gt_mask[~gt_clover_mask & gt_weed_mask] = [255, 0, 0]  # Red for weed only\n            gt_mask[gt_clover_mask & gt_weed_mask] = [255, 255, 0]  # Yellow for overlap\n\n            axes[i, col].imshow(gt_mask)\n            axes[i, col].set_title(\"GT (G=Clover, R=Weed, Y=Both)\")\n            axes[i, col].axis(\"off\")\n            col += 1\n\n        # Prediction\n        axes[i, col].imshow(pred_mask)\n        axes[i, col].set_title(\"Pred (G=Clover, R=Weed, Y=Both)\")\n        axes[i, col].axis(\"off\")\n        col += 1\n\n        # Clover probability\n        axes[i, col].imshow(clover_prob, cmap=\"hot\", vmin=0, vmax=1)\n        axes[i, col].set_title(\"Clover Probability\")\n        axes[i, col].axis(\"off\")\n        col += 1\n\n        # Weed probability\n        axes[i, col].imshow(weed_prob, cmap=\"hot\", vmin=0, vmax=1)\n        axes[i, col].set_title(\"Weed Probability\")\n        axes[i, col].axis(\"off\")\n        col += 1\n\n        # Overlap probability\n        overlap_prob = np.minimum(clover_prob, weed_prob)\n        axes[i, col].imshow(overlap_prob, cmap=\"hot\", vmin=0, vmax=1)\n        axes[i, col].set_title(\"Overlap Probability\")\n        axes[i, col].axis(\"off\")\n\n    plt.tight_layout()\n    plt.savefig(os.path.join(output_dir, f\"fold{fold}_predictions.png\"), dpi=150, bbox_inches=\"tight\")\n    plt.show()\n    print(f\"Visualization saved to {output_dir}/fold{fold}_predictions.png with {len(indices)} samples\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:43:27.173012Z","iopub.execute_input":"2025-11-25T13:43:27.173355Z","iopub.status.idle":"2025-11-25T13:43:27.206989Z","shell.execute_reply.started":"2025-11-25T13:43:27.173331Z","shell.execute_reply":"2025-11-25T13:43:27.206190Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"warnings.simplefilter(action=\"ignore\")\nset_seed(42)\n\n\n# ============================================================================\n# Settings\n# ============================================================================\n@dataclass\nclass CFG:\n    WANDB_MODE: str = \"offline\"\n\n    THIS_FILE_PATH: Path = Path(\"/kaggle/working/tmp.ipynb\")\n\n    # Data\n    IMAGE_DIR: str = \"/kaggle/input/csiro-biomass/train\"\n    MASK_DIR: str = \"/kaggle/input/create-dataset-for-clover-weed-segmentation/seg_green/masks/\"\n    FOLD_CSV: str = f\"/kaggle/working/fold.csv\"\n    OUTPUT_DIR: str = f\"/kaggle/working/outputs\"\n\n    # Model\n    MODEL_NAME: str = \"unet\"\n    ENCODER_NAME: str = \"tu-maxvit_tiny_tf_224\"\n    ENCODER_WEIGHTS: str = \"imagenet\"\n\n    # Training\n    NUM_FOLDS: int = 3\n    EPOCHS: int = 25\n    BATCH_SIZE: int = 16\n    LEARNING_RATE: float = 1e-4\n    IMAGE_SIZE: int = 448\n    NUM_WORKERS: int = 4\n\n    # Augmentation\n    USE_MIXUP: bool = True\n    USE_CUTMIX: bool = True\n    MIXUP_ALPHA: float = 1.0\n    CUTMIX_ALPHA: float = 2\n    CUTMIX_BETA: float = 5\n    MIX_PROB: float = 0.9\n\n    # EMA\n    USE_EMA: bool = True\n    EMA_DECAY: float = 0.95\n\n    # Visualization\n    NUM_VIS_SAMPLES: int = 20\n\n    DEVICE: torch.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:44:04.584215Z","iopub.execute_input":"2025-11-25T13:44:04.584964Z","iopub.status.idle":"2025-11-25T13:44:04.594797Z","shell.execute_reply.started":"2025-11-25T13:44:04.584937Z","shell.execute_reply":"2025-11-25T13:44:04.594260Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CFG = tyro.cli(CFG)\nEXP_NAME: str = f\"{CFG.THIS_FILE_PATH.parent.parent.stem}_{CFG.THIS_FILE_PATH.parent.stem}\"\n\n\nTRAIN_TRANSFORM = A.Compose(\n    [\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=0.5),\n        A.RandomCrop(1000, 1000),\n        A.Resize(CFG.IMAGE_SIZE, CFG.IMAGE_SIZE),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2(),\n    ]\n)\n\n\nVALID_TRANSFORM = A.Compose(\n    [\n        A.Resize(CFG.IMAGE_SIZE, CFG.IMAGE_SIZE),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2(),\n    ]\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:44:04.726321Z","iopub.execute_input":"2025-11-25T13:44:04.726874Z","iopub.status.idle":"2025-11-25T13:44:04.737537Z","shell.execute_reply.started":"2025-11-25T13:44:04.726850Z","shell.execute_reply":"2025-11-25T13:44:04.735440Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# Dataset\n# ============================================================================\nos.makedirs(CFG.OUTPUT_DIR, exist_ok=True)\n\ncreate_fold_csv(CFG.IMAGE_DIR, CFG.MASK_DIR, CFG.FOLD_CSV, CFG.NUM_FOLDS)\n\n# Train all folds\nall_results = []\nfor fold in range(CFG.NUM_FOLDS):\n    print(f\"\\n{'=' * 80}\")\n    print(f\"Training Fold {fold}\")\n    print(f\"{'=' * 80}\\n\")\n\n    # Load fold data\n    df = pd.read_csv(CFG.FOLD_CSV)\n\n    # Split data\n    train_ids = df[df[\"fold\"] != fold][\"image_id\"].unique().tolist()\n    valid_ids = df[df[\"fold\"] == fold][\"image_id\"].unique().tolist()\n\n    print(f\"Train samples: {len(train_ids)}, Valid samples: {len(valid_ids)}\")\n\n    # Create datasets\n    train_dataset = TrainSegmentationDataset(train_ids, CFG.IMAGE_DIR, CFG.MASK_DIR, TRAIN_TRANSFORM)\n    valid_dataset = TrainSegmentationDataset(valid_ids, CFG.IMAGE_DIR, CFG.MASK_DIR, VALID_TRANSFORM)\n\n    # Create dataloaders\n    train_loader = DataLoader(\n        train_dataset, batch_size=CFG.BATCH_SIZE, shuffle=True, num_workers=CFG.NUM_WORKERS, pin_memory=True\n    )\n    valid_loader = DataLoader(\n        valid_dataset, batch_size=CFG.BATCH_SIZE, shuffle=False, num_workers=CFG.NUM_WORKERS, pin_memory=True\n    )\n\n    print(f\"Number of training batches: {len(train_loader)}\")\n    print(f\"Number of validation batches: {len(valid_loader)}\")\n\n    # ============================================================================\n    # Model\n    # ============================================================================\n\n    model = smp.Unet(\n        encoder_name=CFG.ENCODER_NAME,\n        encoder_weights=CFG.ENCODER_WEIGHTS,\n        in_channels=3,\n        classes=2,  # Multilabel: 0=clover, 1=weed\n        activation=None,  # No activation, will use BCEWithLogitsLoss\n        decoder_attention_type=\"scse\",\n    ).to(CFG.DEVICE)\n\n    # EMAモデルの初期化\n    model_ema = None\n    if CFG.USE_EMA:\n        model_ema = ModelEmaV2(model, decay=CFG.EMA_DECAY)\n\n    criterion = CombinedLoss(num_channels=2)\n    optimizer = optim.Adam(model.parameters(), lr=CFG.LEARNING_RATE)\n    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CFG.EPOCHS)\n\n    model_save_path = f\"{CFG.THIS_FILE_PATH.parent}/models/best_{fold}.pth\"\n    Path(model_save_path).parent.mkdir(parents=True, exist_ok=True)\n\n    # ============================================================================\n    # Training\n    # ============================================================================\n    best_valid_iou = 0\n    epoch_history = []\n\n    # bfloat16用のGradScalerを初期化\n    scaler = torch.amp.GradScaler(\"cuda\", enabled=True)\n\n    wandb.init(\n        project=\"csiro_seg_green\",\n        name=f\"{EXP_NAME}_fold{fold}\",\n        dir=\"/tmp/\",\n        mode=CFG.WANDB_MODE,\n        config={\n            \"fold\": fold,\n            \"use_mixup\": CFG.USE_MIXUP,\n            \"use_cutmix\": CFG.USE_CUTMIX,\n            \"mixup_alpha\": CFG.MIXUP_ALPHA,\n            \"cutmix_alpha\": CFG.CUTMIX_ALPHA,\n            \"mix_prob\": CFG.MIX_PROB,\n            \"dtype\": \"bfloat16\",\n        },\n    )\n\n    for epoch in range(CFG.EPOCHS):\n        current_lr = optimizer.param_groups[0][\"lr\"]\n\n        # Training\n        model.train()\n        total_train_loss = 0\n        total_train_iou = 0\n\n        pbar = get_pbar(train_loader)\n        for i_batch, (images, masks, _) in pbar:\n            images = images.to(CFG.DEVICE)\n            masks = masks.to(CFG.DEVICE)\n\n            if CFG.USE_CUTMIX and np.random.rand() < CFG.MIX_PROB:\n                images, masks = cutmix(images, masks, CFG.CUTMIX_ALPHA, CFG.CUTMIX_BETA)\n            if CFG.USE_MIXUP and np.random.rand() < CFG.MIX_PROB:\n                images, masks = mixup(images, masks, CFG.MIXUP_ALPHA)\n\n            optimizer.zero_grad()\n\n            # bfloat16での順伝播\n            with torch.amp.autocast(\"cuda\", dtype=torch.bfloat16):\n                outputs = model(images)\n                loss = criterion(outputs, masks)\n\n            # スケーリングされた逆伝播\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n\n            # EMAモデルの更新\n            if model_ema is not None:\n                model_ema.update(model)\n\n            total_train_loss += loss.item()\n            total_train_iou += calculate_iou(outputs, masks)\n\n            train_loss_epoch = total_train_loss / (i_batch + 1)\n            pbar_train_desc(pbar, current_lr, epoch + 1, CFG.EPOCHS, train_loss_epoch)\n\n        train_loss = total_train_loss / len(train_loader)\n        train_iou = total_train_iou / len(train_loader)\n\n        # Validation\n        # EMAモデルを使用して検証\n        eval_model = model_ema.module if model_ema is not None else model\n        eval_model.eval()\n        total_valid_loss = 0\n        total_valid_iou = 0\n\n        pbar = get_pbar(valid_loader)\n        with torch.no_grad():\n            for i_batch, (images, masks, _) in pbar:\n                images = images.to(CFG.DEVICE)\n                masks = masks.to(CFG.DEVICE)\n\n                # bfloat16での推論\n                with torch.amp.autocast(\"cuda\", dtype=torch.bfloat16):\n                    outputs = eval_model(images)\n                    loss = criterion(outputs, masks)\n\n                total_valid_loss += loss.item()\n                total_valid_iou += calculate_iou(outputs, masks)\n\n                valid_loss_epoch = total_valid_loss / (i_batch + 1)\n                pbar_valid_desc(pbar, valid_loss_epoch)\n\n        valid_loss = total_valid_loss / len(valid_loader)\n        valid_iou = total_valid_iou / len(valid_loader)\n\n        scheduler.step()\n\n        # Log metrics\n        epoch_metrics = {\n            \"epoch\": epoch + 1,\n            \"train_loss\": train_loss,\n            \"train_iou\": train_iou,\n            \"valid_loss\": valid_loss,\n            \"valid_iou\": valid_iou,\n            \"learning_rate\": current_lr,\n        }\n        wandb.log(epoch_metrics)\n\n        epoch_history.append(epoch_metrics)\n\n        if valid_iou > best_valid_iou:\n            best_valid_iou = valid_iou\n            # EMAモデルの状態を保存\n            save_model = model_ema.module if model_ema is not None else model\n            torch.save(save_model.state_dict(), model_save_path)\n            print(\n                f\"Epoch {epoch + 1}/{CFG.EPOCHS}  \"\n                f\"Train Loss: {train_loss:.4f}  Train IoU: {train_iou:.4f}  \"\n                f\"Valid Loss: {valid_loss:.4f}  Valid IoU: {valid_iou:.4f}  \"\n                f\"SAVED MODEL\\n\"\n            )\n        else:\n            print(\n                f\"Epoch {epoch + 1}/{CFG.EPOCHS}  \"\n                f\"Train Loss: {train_loss:.4f}  Train IoU: {train_iou:.4f}  \"\n                f\"Valid Loss: {valid_loss:.4f}  Valid IoU: {valid_iou:.4f}\\n\"\n            )\n\n    # ============================================================================\n    # Prediction\n    # ============================================================================\n\n    # Load best model for inference\n    model.load_state_dict(torch.load(model_save_path))\n\n    # train.csvを読み込み、アンダースコアを含む種でフィルタリング\n    train_csv_path = \"/kaggle/input/csiro-biomass/train.csv\"\n    train_df = pd.read_csv(train_csv_path)\n    train_df[\"image_id\"] = train_df[\"image_path\"].apply(lambda x: x.split(\"/\")[-1].replace(\".jpg\", \"\"))\n\n    # アンダースコアを含む種でフィルタリング\n    species_with_underscore_df = train_df[train_df[\"Species\"].str.contains(\"_\", na=False)]\n    filtered_image_ids = species_with_underscore_df[\"image_id\"].unique().tolist()\n\n    # 可視化用の種辞書を作成\n    species_dict = species_with_underscore_df.groupby(\"image_id\")[\"Species\"].first().to_dict()\n\n    # valid_idsをアンダースコアを含む種の画像のみに絞り込み\n    filtered_valid_ids = [img_id for img_id in train_df[\"image_id\"].unique().tolist() if img_id in filtered_image_ids][\n        :10\n    ]\n\n    # フィルタリングされたIDでテストデータセットとローダーを作成（スライディングウィンドウモード）\n    test_dataset = TestSegmentationDataset(\n        filtered_valid_ids,\n        \"/kaggle/input/csiro-biomass/train\",\n        VALID_TRANSFORM,\n        tile_size=CFG.IMAGE_SIZE,\n        overlap=CFG.IMAGE_SIZE // 3,\n    )\n    test_loader = DataLoader(\n        test_dataset, batch_size=CFG.BATCH_SIZE, shuffle=False, num_workers=CFG.NUM_WORKERS, pin_memory=True\n    )\n\n    # フィルタリングされたテストセットで推論（スライディングウィンドウモード + TTA）\n    predictions, ground_truths, images, image_ids = predict(model, test_loader, CFG.DEVICE)\n\n    print(f\"推論結果: {len(predictions)}枚の画像\")\n\n    # 種情報付きで予測結果を可視化（すべてのサンプルを可視化）\n    visualize_predictions(\n        predictions,\n        ground_truths,\n        images,\n        image_ids,\n        CFG.OUTPUT_DIR,\n        num_samples=CFG.NUM_VIS_SAMPLES,  # すべてのサンプルを可視化\n        fold=fold,\n        species_dict=species_dict,\n        data_dir=\"/kaggle/input/csiro-biomass/train\",  # スライディングウィンドウモード用\n    )\n\n    wandb.finish()\n\n    fold_result = {\n        \"fold\": fold,\n        \"best_iou\": best_valid_iou,\n        \"epoch_history\": epoch_history,\n    }\n    all_results.append(fold_result)\n    break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-25T13:44:04.872503Z","iopub.execute_input":"2025-11-25T13:44:04.873223Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}