{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"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":"nvidiaTeslaT4","dataSources":[{"sourceId":6927,"databundleVersionId":45059,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# 安装缺失依赖（仅保留必要库，移除albumentations）\n!pip install timm huggingface-hub -q --no-cache-dir\n\n# 下载UniRepLKNet核心代码\n!wget https://raw.githubusercontent.com/AILab-CVC/UniRepLKNet/main/unireplknet.py -q -O /kaggle/working/unireplknet.py\n\n# 导入库（仅用torchvision做数据增强，不依赖albumentations）\nimport os\nimport zipfile\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nimport matplotlib.pyplot as plt\nfrom huggingface_hub import hf_hub_download\n\n# Kaggle可视化配置\nplt.rcParams['figure.dpi'] = 100\nplt.rcParams['figure.figsize'] = (15, 5)\n\n# 验证GPU是否可用\nprint(\"GPU可用状态：\", torch.cuda.is_available())\nprint(\"GPU设备数：\", torch.cuda.device_count())\nprint(\"当前GPU：\", torch.cuda.get_device_name(0))\nprint(\"✅ 依赖安装与库导入完成（无albumentations）\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T04:31:08.427291Z","iopub.execute_input":"2025-12-03T04:31:08.427524Z","iopub.status.idle":"2025-12-03T04:32:35.130074Z","shell.execute_reply.started":"2025-12-03T04:31:08.427501Z","shell.execute_reply":"2025-12-03T04:32:35.129234Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def process_kaggle_carvana():\n    \"\"\"Kaggle专属：自动解压、修复嵌套、过滤有效数据\"\"\"\n    # 数据集根路径（Kaggle竞赛固定路径）\n    ROOT = \"/kaggle/input/carvana-image-masking-challenge\"\n    # 工作目录（Kaggle唯一可写目录）\n    UNZIP_DIR = \"/kaggle/working/carvana_unzipped\"\n    os.makedirs(UNZIP_DIR, exist_ok=True)\n\n    # 检查压缩包是否存在\n    zip_paths = {\n        \"img\": os.path.join(ROOT, \"train_hq.zip\"),\n        \"mask\": os.path.join(ROOT, \"train_masks.zip\")\n    }\n    for name, path in zip_paths.items():\n        if not os.path.exists(path):\n            raise FileNotFoundError(f\"请添加Carvana竞赛数据集！缺失：{path}\")\n\n    # 安全解压（支持重解压+嵌套修复）\n    def safe_unzip(zip_path, target_dir):\n        if os.path.exists(target_dir) and len(os.listdir(target_dir)) > 1000:\n            print(f\"✅ {os.path.basename(zip_path)} 已解压\")\n            return\n        # 重新解压（删除旧目录）\n        if os.path.exists(target_dir):\n            import shutil\n            shutil.rmtree(target_dir)\n        os.makedirs(target_dir, exist_ok=True)\n        \n        print(f\"📦 解压 {os.path.basename(zip_path)}...\")\n        with zipfile.ZipFile(zip_path, 'r') as zf:\n            zf.extractall(target_dir)\n        \n        # 修复嵌套文件夹（如 train_hq/train_hq）\n        nested_dir = os.path.join(target_dir, os.path.basename(zip_path).replace(\".zip\", \"\"))\n        if os.path.isdir(nested_dir) and len(os.listdir(nested_dir)) > 1000:\n            for item in os.listdir(nested_dir):\n                src = os.path.join(nested_dir, item)\n                dst = os.path.join(target_dir, item)\n                if not os.path.exists(dst):\n                    os.rename(src, dst)\n            os.rmdir(nested_dir)\n        print(f\"✅ 解压完成：{target_dir}\")\n\n    # 执行解压\n    img_dir = os.path.join(UNZIP_DIR, \"train_hq\")\n    mask_dir = os.path.join(UNZIP_DIR, \"train_masks\")\n    safe_unzip(zip_paths[\"img\"], img_dir)\n    safe_unzip(zip_paths[\"mask\"], mask_dir)\n\n    # 过滤有效文件（仅保留.jpg和对应的.gif掩码）\n    img_files = sorted([f for f in os.listdir(img_dir) if f.endswith(\".jpg\")])[:4000]\n    mask_files = [f.replace(\".jpg\", \"_mask.gif\") for f in img_files]\n    \n    # 验证掩码存在性\n    valid_pairs = []\n    for img_f, mask_f in zip(img_files, mask_files):\n        if os.path.exists(os.path.join(mask_dir, mask_f)):\n            valid_pairs.append((img_f, mask_f))\n    \n    img_files, mask_files = zip(*valid_pairs) if valid_pairs else ([], [])\n    print(f\"\\n📊 数据统计：{len(img_files)} 张图像 + {len(mask_files)} 张掩码\")\n    return img_dir, mask_dir, img_files, mask_files\n\n# 执行数据集处理（核心步骤）\nIMG_DIR, MASK_DIR, IMG_FILES, MASK_FILES = process_kaggle_carvana()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T04:32:35.131643Z","iopub.execute_input":"2025-12-03T04:32:35.13194Z","iopub.status.idle":"2025-12-03T04:32:45.077126Z","shell.execute_reply.started":"2025-12-03T04:32:35.131922Z","shell.execute_reply":"2025-12-03T04:32:45.076337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\n# 自定义ToTensor类（处理类型+负步长）\nclass ToTensorFromNumpy:\n    def __call__(self, arr):\n        arr = arr.copy()  # 消除负步长\n        if arr.dtype == np.object_:\n            arr = np.array([[np.array(pixel, dtype=np.uint8) for pixel in row] for row in arr], dtype=np.uint8)\n        if len(arr.shape) != 3 or arr.shape[2] != 3:\n            arr = cv2.cvtColor(arr, cv2.COLOR_GRAY2RGB) if len(arr.shape) == 2 else arr[..., :3]\n        arr = arr.astype(np.uint8)\n        arr = arr.transpose((2, 0, 1))  # HWC→CHW\n        return torch.from_numpy(arr).float() / 255.0\n\n# 水平翻转增强（安全版）\nclass RandomHorizontalFlip:\n    def __init__(self, p=0.5):\n        self.p = p\n    def __call__(self, img):\n        if np.random.rand() < self.p:\n            img = img[:, ::-1, :].copy()  # 翻转后复制\n        return img\n\n# 自定义数据集（容错版）\nclass CarvanaKaggleDataset(Dataset):\n    def __init__(self, img_dir, mask_dir, img_files, mask_files, transform=None):\n        self.img_dir = img_dir\n        self.mask_dir = mask_dir\n        self.img_files = img_files\n        self.mask_files = mask_files\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.img_files)\n\n    def __getitem__(self, idx):\n        # 读取图像\n        img_path = os.path.join(self.img_dir, self.img_files[idx])\n        try:\n            img = cv2.imread(img_path)\n            if img is None or img.dtype == np.object_:\n                img = np.full((384, 384, 3), 127, dtype=np.uint8)\n            else:\n                img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n                img = cv2.resize(img, (384, 384))\n                img = np.ascontiguousarray(img, dtype=np.uint8)\n        except:\n            img = np.full((384, 384, 3), 127, dtype=np.uint8)\n        \n        # 读取掩码\n        mask_path = os.path.join(self.mask_dir, self.mask_files[idx])\n        try:\n            mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)\n            if mask is None or mask.dtype == np.object_:\n                mask = np.zeros((384, 384), dtype=np.uint8)\n            else:\n                mask = cv2.resize(mask, (384, 384))\n                mask = np.ascontiguousarray(mask, dtype=np.uint8)\n        except:\n            mask = np.zeros((384, 384), dtype=np.uint8)\n        mask = mask / 255.0  # 归一化\n\n        # 应用transform\n        if self.transform:\n            img = self.transform(img)\n        \n        # 掩码转tensor\n        mask = torch.from_numpy(mask.copy()).unsqueeze(0).float()\n        return img, mask\n\n# 数据预处理\ntrain_transform = transforms.Compose([\n    RandomHorizontalFlip(p=0.5),\n    ToTensorFromNumpy(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\nval_transform = transforms.Compose([\n    ToTensorFromNumpy(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# 划分训练/验证集（8:2）\ntrain_size = int(0.8 * len(IMG_FILES))\ntrain_dataset = CarvanaKaggleDataset(\n    IMG_DIR, MASK_DIR, IMG_FILES[:train_size], MASK_FILES[:train_size], train_transform\n)\nval_dataset = CarvanaKaggleDataset(\n    IMG_DIR, MASK_DIR, IMG_FILES[train_size:], MASK_FILES[train_size:], val_transform\n)\n\n# 构建DataLoader\ntrain_loader = DataLoader(\n    train_dataset, batch_size=2, shuffle=True, num_workers=0, pin_memory=True\n)\nval_loader = DataLoader(\n    val_dataset, batch_size=2, shuffle=False, num_workers=0, pin_memory=True\n)\n\n# 验证数据加载\nprint(f\"\\n🚀 数据集加载完成：\")\nprint(f\"训练集样本数：{len(train_dataset)} | 批次：{len(train_loader)}\")\nprint(f\"验证集样本数：{len(val_dataset)} | 批次：{len(val_loader)}\")\ntry:\n    img_sample, mask_sample = train_dataset[0]\n    print(f\"图像形状：{img_sample.shape} | 掩码形状：{mask_sample.shape}\")\n    print(f\"图像值范围：{img_sample.min():.2f} ~ {img_sample.max():.2f}\")\n    print(f\"掩码值范围：{mask_sample.min():.2f} ~ {mask_sample.max():.2f}\")\n    print(\"✅ 数据验证成功！\")\nexcept Exception as e:\n    print(f\"❌ 数据验证失败：{str(e)}\")\n    # 打印详细错误信息，定位问题\n    import traceback\n    traceback.print_exc()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T04:32:45.078027Z","iopub.execute_input":"2025-12-03T04:32:45.078687Z","iopub.status.idle":"2025-12-03T04:32:45.717693Z","shell.execute_reply.started":"2025-12-03T04:32:45.078656Z","shell.execute_reply":"2025-12-03T04:32:45.716663Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn as nn\n\n# 自定义轻量级特征提取器（替代UniRepLKNet，无外部依赖）\nclass LightweightBackbone(nn.Module):\n    def __init__(self):\n        super().__init__()\n        # 4个stage，总下采样率16，输出768维特征图\n        self.stage1 = nn.Sequential(\n            nn.Conv2d(3, 96, 3, stride=2, padding=1),\n            nn.BatchNorm2d(96),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(96, 96, 3, padding=1),\n            nn.BatchNorm2d(96),\n            nn.ReLU(inplace=True)\n        )\n        self.stage2 = nn.Sequential(\n            nn.Conv2d(96, 192, 3, stride=2, padding=1),\n            nn.BatchNorm2d(192),\n            nn.ReLU(inplace=True),\n            *[nn.Sequential(\n                nn.Conv2d(192, 192, 3, padding=1),\n                nn.BatchNorm2d(192),\n                nn.ReLU(inplace=True)\n            ) for _ in range(5)]  # 共6层\n        )\n        self.stage3 = nn.Sequential(\n            nn.Conv2d(192, 384, 3, stride=2, padding=1),\n            nn.BatchNorm2d(384),\n            nn.ReLU(inplace=True),\n            *[nn.Sequential(\n                nn.Conv2d(384, 384, 3, padding=1),\n                nn.BatchNorm2d(384),\n                nn.ReLU(inplace=True)\n            ) for _ in range(13)]  # 共14层\n        )\n        self.stage4 = nn.Sequential(\n            nn.Conv2d(384, 768, 3, stride=2, padding=1),\n            nn.BatchNorm2d(768),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(768, 768, 3, padding=1),\n            nn.BatchNorm2d(768),\n            nn.ReLU(inplace=True)\n        )\n    \n    def forward(self, x):\n        x = self.stage1(x)  # 384→192\n        x = self.stage2(x)  # 192→96\n        x = self.stage3(x)  # 96→48\n        x = self.stage4(x)  # 48→24\n        return x  # 输出：[B, 768, 24, 24]\n\n# 二值分割头\nclass SegHead(nn.Module):\n    def __init__(self, in_dim=768, upscale=16):\n        super().__init__()\n        self.decoder = nn.Sequential(\n            nn.Conv2d(768, 192, 3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(192, 96, 3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.Conv2d(96, 1, 1)\n        )\n        self.upsample = nn.Upsample(scale_factor=16, mode=\"bilinear\", align_corners=True)\n    \n    def forward(self, x):\n        return self.upsample(self.decoder(x))\n\n# 完整分割模型\ndef build_seg_model():\n    backbone = LightweightBackbone()\n    seg_head = SegHead()\n    model = nn.Sequential(backbone, seg_head).cuda()\n    return model\n\n# 构建模型并验证维度\nmodel = build_seg_model()\nprint(f\"✅ 自定义模型构建完成，总参数量：{sum(p.numel() for p in model.parameters()):,}\")\n\n# 维度验证\ntest_input = torch.randn(2, 3, 384, 384).cuda()\nwith torch.no_grad():\n    test_output = model(test_input)\nprint(f\"输入形状：{test_input.shape}\")\nprint(f\"输出形状：{test_output.shape}\")\nassert test_output.shape == (2, 1, 384, 384), f\"维度错误！当前：{test_output.shape}\"\nprint(\"✅ 维度验证通过！可以直接训练！\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T04:32:45.718657Z","iopub.execute_input":"2025-12-03T04:32:45.718863Z","iopub.status.idle":"2025-12-03T04:32:46.567354Z","shell.execute_reply.started":"2025-12-03T04:32:45.718847Z","shell.execute_reply":"2025-12-03T04:32:46.566649Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.optim as optim\n\n# Dice系数（保留，双指标对比）\ndef dice_score(pred, target, threshold=0.5):\n    pred = (torch.sigmoid(pred) > threshold).float()\n    intersection = (pred * target).sum()\n    union = pred.sum() + target.sum()\n    return (2 * intersection) / (union + 1e-6) if union != 0 else torch.tensor(1.0).cuda()\n\n# IoU（交并比，新增核心指标）\ndef iou_score(pred, target, threshold=0.5):\n    pred = (torch.sigmoid(pred) > threshold).float()\n    intersection = (pred * target).sum()  # 交集\n    union = pred.sum() + target.sum() - intersection  # 并集 = A + B - 交集\n    return intersection / (union + 1e-6) if union != 0 else torch.tensor(1.0).cuda()\n\n# 损失函数+优化器（不变）\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=8, eta_min=1e-6)\n\nprint(\"✅ 损失函数、优化器、双指标（Dice+IoU）配置完成\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T04:32:46.56815Z","iopub.execute_input":"2025-12-03T04:32:46.568853Z","iopub.status.idle":"2025-12-03T04:32:46.575385Z","shell.execute_reply.started":"2025-12-03T04:32:46.568832Z","shell.execute_reply":"2025-12-03T04:32:46.574842Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 重新定义device（确保作用域可见）\nimport torch\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# 训练配置\nepochs = 8\nbest_dice = 0.0\nbest_iou = 0.0\nmodel_save_path = \"/kaggle/working/carvana_seg_best.pth\"\n\n# 记录训练过程中的指标（用于绘图）\ntrain_metrics = {\n    \"loss\": [],\n    \"dice\": [],\n    \"iou\": []\n}\nval_metrics = {\n    \"loss\": [],\n    \"dice\": [],\n    \"iou\": []\n}\n\n# 确保关键函数和变量可见（重复定义避免作用域问题）\ndef dice_score(pred, target, threshold=0.5):\n    pred = (torch.sigmoid(pred) > threshold).float()\n    intersection = (pred * target).sum()\n    union = pred.sum() + target.sum()\n    return (2 * intersection) / (union + 1e-6) if union != 0 else torch.tensor(1.0).to(device)\n\ndef iou_score(pred, target, threshold=0.5):\n    pred = (torch.sigmoid(pred) > threshold).float()\n    intersection = (pred * target).sum()\n    union = pred.sum() + target.sum() - intersection\n    return intersection / (union + 1e-6) if union != 0 else torch.tensor(1.0).to(device)\n\nprint(f\"🔥 开始训练（设备：{device}）...\")\nprint(f\"训练集：{len(train_dataset)} 样本 | {len(train_loader)} 批次\")\nprint(f\"验证集：{len(val_dataset)} 样本 | {len(val_loader)} 批次\\n\")\n\nfor epoch in range(epochs):\n    # ---------------------- 训练阶段 ----------------------\n    model.train()\n    train_loss, train_dice, train_iou = 0.0, 0.0, 0.0\n    \n    for batch_idx, (imgs, masks) in enumerate(train_loader):\n        imgs, masks = imgs.to(device), masks.to(device)\n        \n        # 前向传播\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, masks)\n        \n        # 反向传播+优化\n        loss.backward()\n        optimizer.step()\n        \n        # 累计指标\n        train_loss += loss.item()\n        batch_dice = dice_score(outputs, masks).item()\n        batch_iou = iou_score(outputs, masks).item()\n        train_dice += batch_dice\n        train_iou += batch_iou\n        \n        # 每100批打印进度\n        if (batch_idx + 1) % 100 == 0:\n            avg_loss = train_loss / (batch_idx + 1)\n            avg_dice = train_dice / (batch_idx + 1)\n            avg_iou = train_iou / (batch_idx + 1)\n            print(f\"Epoch {epoch+1}/{epochs} | Batch {batch_idx+1:4d} | Loss: {avg_loss:.4f} | Dice: {avg_dice:.4f} | IoU: {avg_iou:.4f}\")\n    \n    # 学习率调度\n    scheduler.step()\n    \n    # ---------------------- 验证阶段 ----------------------\n    model.eval()\n    val_loss, val_dice, val_iou = 0.0, 0.0, 0.0\n    \n    with torch.no_grad():\n        for imgs, masks in val_loader:\n            imgs, masks = imgs.to(device), masks.to(device)\n            outputs = model(imgs)\n            val_loss += criterion(outputs, masks).item()\n            val_dice += dice_score(outputs, masks).item()\n            val_iou += iou_score(outputs, masks).item()\n    \n    # 计算平均指标\n    avg_train_loss = train_loss / len(train_loader)\n    avg_train_dice = train_dice / len(train_loader)\n    avg_train_iou = train_iou / len(train_loader)\n    avg_val_loss = val_loss / len(val_loader)\n    avg_val_dice = val_dice / len(val_loader)\n    avg_val_iou = val_iou / len(val_loader)\n    \n    # 保存当前epoch的指标（用于绘图）\n    train_metrics[\"loss\"].append(avg_train_loss)\n    train_metrics[\"dice\"].append(avg_train_dice)\n    train_metrics[\"iou\"].append(avg_train_iou)\n    val_metrics[\"loss\"].append(avg_val_loss)\n    val_metrics[\"dice\"].append(avg_val_dice)\n    val_metrics[\"iou\"].append(avg_val_iou)\n    \n    # 打印epoch总结\n    print(f\"\\n{'='*60}\")\n    print(f\"Epoch {epoch+1}/{epochs} 总结\")\n    print(f\"训练集：Loss={avg_train_loss:.4f} | Dice={avg_train_dice:.4f} | IoU={avg_train_iou:.4f}\")\n    print(f\"验证集：Loss={avg_val_loss:.4f} | Dice={avg_val_dice:.4f} | IoU={avg_val_iou:.4f}\")\n    print(f\"{'='*60}\\n\")\n    \n    # 保存最优模型（按Dice系数）\n    if avg_val_dice > best_dice:\n        best_dice = avg_val_dice\n        best_iou = avg_val_iou\n        torch.save(model.state_dict(), model_save_path)\n        print(f\"✅ 保存最优模型！Best Dice: {best_dice:.4f} | Best IoU: {best_iou:.4f}\\n\")\n\n# 保存最终部署模型\ntorch.save(model.state_dict(), \"/kaggle/working/carvana_seg_deploy.pth\")\nprint(\"🎉 训练完全结束！\")\nprint(f\"📁 最优模型路径：{model_save_path}\")\nprint(f\"📁 部署模型路径：/kaggle/working/carvana_seg_deploy.pth\")\nprint(f\"🏆 最佳验证集指标：Dice={best_dice:.4f} | IoU={best_iou:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T04:32:46.576218Z","iopub.execute_input":"2025-12-03T04:32:46.576416Z","iopub.status.idle":"2025-12-03T05:32:04.691891Z","shell.execute_reply.started":"2025-12-03T04:32:46.576402Z","shell.execute_reply":"2025-12-03T05:32:04.691214Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nplt.rcParams['figure.dpi'] = 100\nplt.rcParams['figure.figsize'] = (12, 8)\n\ndef plot_training_curves(train_metrics, val_metrics, epochs):\n    \"\"\"绘制训练/验证的Loss、Dice、IoU曲线\"\"\"\n    epochs_range = range(1, epochs + 1)\n    \n    # 创建2x2子图\n    fig, axes = plt.subplots(2, 2, tight_layout=True)\n    \n    # 1. 损失曲线（左上）\n    axes[0, 0].plot(epochs_range, train_metrics[\"loss\"], label=\"Train Loss\", marker='o', linewidth=2)\n    axes[0, 0].plot(epochs_range, val_metrics[\"loss\"], label=\"Val Loss\", marker='s', linewidth=2)\n    axes[0, 0].set_title(\"Loss Curve\", fontsize=12, fontweight='bold')\n    axes[0, 0].set_xlabel(\"Epoch\")\n    axes[0, 0].set_ylabel(\"Loss\")\n    axes[0, 0].legend()\n    axes[0, 0].grid(True, alpha=0.3)\n    \n    # 2. Dice系数曲线（右上）\n    axes[0, 1].plot(epochs_range, train_metrics[\"dice\"], label=\"Train Dice\", marker='o', linewidth=2, color='green')\n    axes[0, 1].plot(epochs_range, val_metrics[\"dice\"], label=\"Val Dice\", marker='s', linewidth=2, color='darkgreen')\n    axes[0, 1].set_title(\"Dice Coefficient Curve\", fontsize=12, fontweight='bold')\n    axes[0, 1].set_xlabel(\"Epoch\")\n    axes[0, 1].set_ylabel(\"Dice Score\")\n    axes[0, 1].legend()\n    axes[0, 1].grid(True, alpha=0.3)\n    axes[0, 1].set_ylim(0, 1.0)  # Dice范围固定0-1\n    \n    # 3. IoU曲线（左下）\n    axes[1, 0].plot(epochs_range, train_metrics[\"iou\"], label=\"Train IoU\", marker='o', linewidth=2, color='orange')\n    axes[1, 0].plot(epochs_range, val_metrics[\"iou\"], label=\"Val IoU\", marker='s', linewidth=2, color='darkorange')\n    axes[1, 0].set_title(\"IoU Curve\", fontsize=12, fontweight='bold')\n    axes[1, 0].set_xlabel(\"Epoch\")\n    axes[1, 0].set_ylabel(\"IoU Score\")\n    axes[1, 0].legend()\n    axes[1, 0].grid(True, alpha=0.3)\n    axes[1, 0].set_ylim(0, 1.0)  # IoU范围固定0-1\n    \n    # 4. 综合对比（右下：Loss+Dice）\n    axes[1, 1].plot(epochs_range, train_metrics[\"loss\"], label=\"Train Loss\", marker='o', linewidth=2, color='red')\n    axes[1, 1].plot(epochs_range, val_metrics[\"loss\"], label=\"Val Loss\", marker='s', linewidth=2, color='darkred')\n    # 双y轴（右侧y轴显示Dice）\n    ax2 = axes[1, 1].twinx()\n    ax2.plot(epochs_range, train_metrics[\"dice\"], label=\"Train Dice\", marker='^', linewidth=2, color='green')\n    ax2.plot(epochs_range, val_metrics[\"dice\"], label=\"Val Dice\", marker='v', linewidth=2, color='darkgreen')\n    # 设置标签\n    axes[1, 1].set_title(\"Loss + Dice Combined\", fontsize=12, fontweight='bold')\n    axes[1, 1].set_xlabel(\"Epoch\")\n    axes[1, 1].set_ylabel(\"Loss\", color='red')\n    ax2.set_ylabel(\"Dice Score\", color='green')\n    axes[1, 1].tick_params(axis='y', labelcolor='red')\n    ax2.tick_params(axis='y', labelcolor='green')\n    # 合并图例\n    lines1, labels1 = axes[1, 1].get_legend_handles_labels()\n    lines2, labels2 = ax2.get_legend_handles_labels()\n    axes[1, 1].legend(lines1 + lines2, labels1 + labels2, loc='center right')\n    axes[1, 1].grid(True, alpha=0.3)\n    \n    # 整体标题\n    fig.suptitle(\"Training & Validation Metrics Curves\", fontsize=14, fontweight='bold')\n    plt.show()\n\n# 执行绘图（训练完成后运行）\nprint(\"📈 绘制训练指标曲线...\")\n# 确保epochs变量可见（若之前未定义，手动指定）\ntry:\n    plot_training_curves(train_metrics, val_metrics, epochs=epochs)\nexcept NameError:\n    plot_training_curves(train_metrics, val_metrics, epochs=8)  # 手动指定epochs=8","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T05:32:04.693626Z","iopub.execute_input":"2025-12-03T05:32:04.693838Z","iopub.status.idle":"2025-12-03T05:32:05.785048Z","shell.execute_reply.started":"2025-12-03T05:32:04.69382Z","shell.execute_reply":"2025-12-03T05:32:05.78445Z"}},"outputs":[],"execution_count":null}]}