{"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":"none","dataSources":[{"sourceType":"competition","sourceId":99552,"databundleVersionId":13441085}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport pydicom\nfrom pydicom.pixel_data_handlers.util import apply_voi_lut\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score\nfrom tqdm import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# 固定随机种子（保证结果可复现）\nSEED = 42\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\n\n# 训练配置（优化参数，避免资源不足）\nCONFIG = {\n    'batch_size': 8,  # 适当增大批次（根据GPU内存调整）\n    'epochs': 5,      # 先减少epoch数快速验证\n    'learning_rate': 1e-4,\n    'image_size': (128, 128),  \n    'num_classes': 14,  # 更新为14个标签\n    'train_split': 0.8,\n    'max_slices': 8,   # 从16→8（减少每个序列的切片数）\n    'device': 'cuda' if torch.cuda.is_available() else 'cpu'\n}\n\n# 14个目标标签列名\nTARGET_LABELS = [\n    'Left Infraclinoid Internal Carotid Artery',\n    'Right Infraclinoid Internal Carotid Artery',\n    'Left Supraclinoid Internal Carotid Artery',\n    'Right Supraclinoid Internal Carotid Artery',\n    'Left Middle Cerebral Artery',\n    'Right Middle Cerebral Artery',\n    'Anterior Communicating Artery',\n    'Left Anterior Cerebral Artery',\n    'Right Anterior Cerebral Artery',\n    'Left Posterior Communicating Artery',\n    'Right Posterior Communicating Artery',\n    'Basilar Tip',\n    'Other Posterior Circulation',\n    'Aneurysm Present'  # 总存在标签（权重13）\n]\n\n# 核心数据路径（根据Kaggle目录结构）\nBASE_PATH = '/kaggle/input/rsna-intracranial-aneurysm-detection'\nTRAIN_CSV_PATH = os.path.join(BASE_PATH, 'train.csv')\nSERIES_PATH = os.path.join(BASE_PATH, 'series')\n\n# 缓存路径（用于存储预处理后的图像，避免重复解析DICOM）\nCACHE_PATH = '/kaggle/working/aneurysm_cache'\nos.makedirs(CACHE_PATH, exist_ok=True)  # 自动创建缓存文件夹\n\n# 加载并预处理训练数据（多标签版本）\ntrain_df = pd.read_csv(TRAIN_CSV_PATH)\n\n# 按序列ID分组，计算每个序列的14个标签（取最大值：存在则为1）\nseries_labels_df = train_df.groupby('SeriesInstanceUID')[TARGET_LABELS].max().reset_index()\nseries_labels_df.rename(columns={'SeriesInstanceUID': 'series_id'}, inplace=True)\n\n# 验证标签分布\nprint(\"14个标签的序列级正样本比例：\")\nlabel_pos_ratio = (series_labels_df[TARGET_LABELS].sum() / len(series_labels_df)).round(4)\nfor label, ratio in label_pos_ratio.items():\n    print(f\"{label}: {ratio:.2%}\")\n\n# 划分训练集/验证集（按总存在标签分层）\ntrain_series, val_series = train_test_split(\n    series_labels_df['series_id'].values,\n    test_size=1 - CONFIG['train_split'],\n    random_state=SEED,\n    stratify=series_labels_df['Aneurysm Present']\n)\n\n# 分离训练/验证集的标签（14维）\ntrain_labels = series_labels_df[series_labels_df['series_id'].isin(train_series)].reset_index(drop=True)\nval_labels = series_labels_df[series_labels_df['series_id'].isin(val_series)].reset_index(drop=True)\n\nprint(f\"\\n训练集序列数：{len(train_labels)}，标签维度：{train_labels[TARGET_LABELS].shape}\")\nprint(f\"验证集序列数：{len(val_labels)}，标签维度：{val_labels[TARGET_LABELS].shape}\")\n\n# DICOM加载和缓存函数\ndef load_dicom(path):\n    \"\"\"修复DICOM加载：用dcmread替代read_file，添加异常处理\"\"\"\n    try:\n        dicom = pydicom.dcmread(path)\n        img = apply_voi_lut(dicom.pixel_array, dicom)\n\n        # 确保单通道（部分DICOM可能多通道，取第一通道）\n        if len(img.shape) != 2:\n            img = img[:, :, 0]\n\n        # 归一化到[0,255]（避免数值范围异常）\n        img = img - img.min()\n        if img.max() != 0:  # 避免除以零\n            img = img / img.max()\n        img = (img * 255).astype(np.uint8)\n        return img\n    except Exception as e:\n        print(f\"加载DICOM失败 {path}: {str(e)}\")\n        return None\n\n\ndef get_cached_series(series_id):\n    \"\"\"缓存机制：第一次加载DICOM并保存为.npy，后续直接加载缓存\"\"\"\n    # 缓存文件路径（每个序列对应一个缓存文件）\n    cache_file = os.path.join(CACHE_PATH, f\"{series_id}.npy\")\n\n    # 1. 有缓存：直接加载\n    if os.path.exists(cache_file):\n        return np.load(cache_file)\n\n    # 2. 无缓存：加载DICOM并生成缓存\n    series_folder = os.path.join(SERIES_PATH, str(series_id))\n    if not os.path.exists(series_folder):\n        print(f\"序列文件夹不存在: {series_folder}\")\n        return None\n\n    # 获取DICOM文件（按名称排序，确保切片顺序正确）\n    dicom_files = [os.path.join(series_folder, f) for f in os.listdir(series_folder) if f.endswith('.dcm')]\n    dicom_files.sort()  # 关键：医学图像切片必须按顺序加载\n\n    # 加载DICOM（只取前CONFIG['max_slices']个）\n    images = []\n    for i, file in enumerate(dicom_files):\n        if i >= CONFIG['max_slices']:  # 超过max_slices直接停止\n            break\n        img = load_dicom(file)\n        if img is not None:\n            images.append(img)\n\n    # 保存缓存\n    if len(images) > 0:\n        np.save(cache_file, np.array(images))\n    return np.array(images) if len(images) > 0 else None\n\n# 测试缓存加载效果\nsample_series_id = train_series[0]\nprint(f\"\\n测试序列 {sample_series_id} 加载...\")\nsample_images = get_cached_series(sample_series_id)\nif sample_images is not None:\n    print(f\"序列 {sample_series_id} 加载成功，共 {len(sample_images)} 张切片\")\n    print(f\"切片形状: {sample_images[0].shape}\")\n\n    # 可视化前3张切片\n    plt.figure(figsize=(12, 4))\n    for i in range(min(3, len(sample_images))):\n        plt.subplot(1, 3, i + 1)\n        plt.imshow(sample_images[i], cmap='gray')\n        plt.title(f\"切片 {i + 1}\")\n        plt.axis('off')\n    plt.show()\nelse:\n    print(f\"序列 {sample_series_id} 加载失败\")\n\n# 多标签数据集类\nclass AneurysmMultiLabelDataset(Dataset):\n    def __init__(self, series_labels_df, transform=None):\n        self.series_labels_df = series_labels_df  # 包含series_id和14个标签\n        self.series_ids = series_labels_df['series_id'].values\n        self.transform = transform\n        self.max_slices = CONFIG['max_slices']\n        self.target_labels = TARGET_LABELS  # 14个标签\n\n    def __len__(self):\n        return len(self.series_ids)\n\n    def __getitem__(self, idx):\n        # 1. 加载序列图像（复用缓存机制）\n        series_id = self.series_ids[idx]\n        images = get_cached_series(series_id)\n\n        # 处理加载失败的情况（返回随机图像+全0标签）\n        if images is None or len(images) == 0:\n            img_tensor = torch.randn(1, self.max_slices, *CONFIG['image_size'])\n            label_tensor = torch.zeros(len(self.target_labels), dtype=torch.float32)\n            return img_tensor.float(), label_tensor\n\n        # 2. 调整切片数量（补零/截断）\n        if len(images) < self.max_slices:\n            pad_width = self.max_slices - len(images)\n            images = np.pad(images, ((0, pad_width), (0, 0), (0, 0)), mode='constant')\n        else:\n            images = images[:self.max_slices]\n\n        # 3. 图像变换\n        processed_images = []\n        for img in images:\n            if self.transform:\n                img = self.transform(img)  # [1, H, W]\n            processed_images.append(img)\n\n        # 4. 调整维度：[max_slices, 1, H, W] → [1, max_slices, H, W]\n        img_tensor = torch.stack(processed_images).permute(1, 0, 2, 3)\n\n        # 5. 获取14维标签\n        label_row = self.series_labels_df[self.series_labels_df['series_id'] == series_id]\n        label_tensor = torch.tensor(\n            label_row[self.target_labels].values[0], \n            dtype=torch.float32\n        )\n\n        return img_tensor.float(), label_tensor\n\n# 定义图像变换\ntransform = transforms.Compose([\n    transforms.ToPILImage(),  # 转为PIL图像（便于Resize）\n    transforms.Resize(CONFIG['image_size']),  # 统一尺寸\n    transforms.ToTensor(),  # 转为Tensor（范围[0,1]）\n    transforms.Normalize(mean=[0.5], std=[0.5])  # 归一化到[-1,1]\n])\n\n# 创建多标签数据集和DataLoader\ntrain_dataset = AneurysmMultiLabelDataset(train_labels, transform=transform)\nval_dataset = AneurysmMultiLabelDataset(val_labels, transform=transform)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=CONFIG['batch_size'],\n    shuffle=True,\n    num_workers=0,\n    pin_memory=CONFIG['device'] == 'cuda'\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=CONFIG['batch_size'],\n    shuffle=False,\n    num_workers=0,\n    pin_memory=CONFIG['device'] == 'cuda'\n)\n\nprint(f\"\\n多标签数据集初始化完成：\")\nprint(f\"训练集：{len(train_dataset)}个样本，{len(train_loader)}个批次\")\nprint(f\"验证集：{len(val_dataset)}个样本，{len(val_loader)}个批次\")\nprint(f\"标签维度：{len(TARGET_LABELS)}（13个部位 + 1个总存在）\")\n\n# 模型定义\nclass PatchEmbedding(nn.Module):\n    \"\"\"将单张医学图像切片转为Transformer输入的Patch嵌入\"\"\"\n    def __init__(self, image_size, patch_size, in_channels=1, embed_dim=256):\n        super().__init__()\n        self.patch_size = patch_size\n        # 计算每个切片的Patch数量\n        self.num_patches = (image_size[0] // patch_size) * (image_size[1] // patch_size)\n\n        # 用卷积层实现Patch分割+嵌入\n        self.proj = nn.Conv2d(\n            in_channels,\n            embed_dim,\n            kernel_size=patch_size,\n            stride=patch_size\n        )\n\n    def forward(self, x):\n        # x: [batch_size, 1, H, W] → 单通道图像\n        x = self.proj(x)  # [batch_size, embed_dim, num_patches_H, num_patches_W]\n        x = x.flatten(2)  # [batch_size, embed_dim, num_patches]\n        x = x.transpose(1, 2)  # [batch_size, num_patches, embed_dim]\n        return x\n\n\nclass SliceTransformer(nn.Module):\n    \"\"\"处理单张切片的Transformer（提取切片级特征）\"\"\"\n    def __init__(self, image_size=CONFIG['image_size'], patch_size=32, embed_dim=256, num_heads=8, num_layers=4):\n        super().__init__()\n        self.patch_embed = PatchEmbedding(image_size, patch_size, embed_dim=embed_dim)\n        num_patches = self.patch_embed.num_patches\n\n        # 位置嵌入\n        self.pos_embed = nn.Parameter(torch.randn(1, num_patches, embed_dim))\n\n        # Transformer编码器\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=embed_dim,\n            nhead=num_heads,\n            dim_feedforward=embed_dim * 4,\n            dropout=0.1,\n            batch_first=True\n        )\n        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)\n\n        # 切片特征聚合\n        self.feature_pool = nn.AdaptiveAvgPool1d(1)\n\n    def forward(self, x):\n        # x: [batch_size, 1, H, W] → 单张切片\n        x = self.patch_embed(x)  # [batch_size, num_patches, embed_dim]\n        x = x + self.pos_embed  # 添加位置嵌入\n\n        x = self.transformer(x)  # [batch_size, num_patches, embed_dim]\n        x = x.transpose(1, 2)  # [batch_size, embed_dim, num_patches]\n        x = self.feature_pool(x).squeeze(-1)  # [batch_size, embed_dim] → 切片级特征\n        return x\n\n\nclass SeriesTransformerMultiLabel(nn.Module):\n    \"\"\"适配多标签分类的序列Transformer模型\"\"\"\n    def __init__(self, max_slices=CONFIG['max_slices'], slice_embed_dim=256, seq_embed_dim=128, num_heads=4, num_layers=2):\n        super().__init__()\n        # 1. 切片级特征提取\n        self.slice_transformer = SliceTransformer(embed_dim=slice_embed_dim)\n\n        # 2. 序列级特征投影\n        self.seq_proj = nn.Linear(slice_embed_dim, seq_embed_dim)\n        self.seq_pos_embed = nn.Parameter(torch.randn(1, max_slices, seq_embed_dim))\n\n        # 3. 序列级Transformer\n        seq_encoder_layer = nn.TransformerEncoderLayer(\n            d_model=seq_embed_dim,\n            nhead=num_heads,\n            dim_feedforward=seq_embed_dim * 4,\n            dropout=0.1,\n            batch_first=True\n        )\n        self.seq_transformer = nn.TransformerEncoder(seq_encoder_layer, num_layers=num_layers)\n\n        # 4. 多标签分类头\n        self.classifier = nn.Sequential(\n            nn.Linear(seq_embed_dim, 64),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(64, len(TARGET_LABELS)),  # 输出14个概率\n            nn.Sigmoid()  # 每个标签独立二分类，用Sigmoid激活\n        )\n\n    def forward(self, x):\n        # x: [batch_size, 1, max_slices, H, W]\n        batch_size, _, max_slices, H, W = x.shape\n\n        # 1. 切片级特征提取\n        slice_features = []\n        for i in range(max_slices):\n            slice_img = x[:, :, i, :, :]  # [batch_size, 1, H, W]\n            feat = self.slice_transformer(slice_img)  # [batch_size, slice_embed_dim]\n            slice_features.append(feat)\n\n        # 2. 序列级特征聚合\n        seq_feat = torch.stack(slice_features, dim=1)  # [batch_size, max_slices, slice_embed_dim]\n        seq_feat = self.seq_proj(seq_feat)  # [batch_size, max_slices, seq_embed_dim]\n        seq_feat = seq_feat + self.seq_pos_embed\n        seq_feat = self.seq_transformer(seq_feat)  # [batch_size, max_slices, seq_embed_dim]\n        seq_feat = seq_feat.mean(dim=1)  # [batch_size, seq_embed_dim]\n\n        # 3. 多标签预测\n        output = self.classifier(seq_feat)  # [batch_size, 14]\n        return output\n\n# 计算模型参数量的函数\ndef count_params(model):\n    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n\n# 创建多标签模型\nmodel = SeriesTransformerMultiLabel()\nmodel = model.to(CONFIG['device'])\n\nprint(f\"\\n模型可训练参数量: {count_params(model):,}\")\nprint(f\"模型设备: {next(model.parameters()).device}\")\n\n# 定义损失函数、优化器和学习率调度器\ncriterion = nn.BCELoss()  # 多标签二分类交叉熵损失\noptimizer = optim.Adam(\n    model.parameters(),\n    lr=CONFIG['learning_rate'],\n    weight_decay=1e-5  # L2正则化\n)\n\n# 学习率调度器\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer,\n    mode='min',  # 目标：最小化验证损失\n    patience=2,  # 2个epoch损失不下降则降学习率\n    factor=0.5,  # 学习率 *= 0.5\n    verbose=True\n)\n\n# 训练和评估函数\ndef train_epoch(model, dataloader, criterion, optimizer, device):\n    \"\"\"训练一个epoch，返回平均损失\"\"\"\n    model.train()\n    total_loss = 0.0\n    total_samples = 0\n\n    loop = tqdm(dataloader, desc=f\"训练中\", leave=True)\n    for images, labels in loop:\n        images = images.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True)\n\n        # 前向传播\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n\n        # 反向传播与参数更新\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n\n        # 累计损失\n        total_loss += loss.item() * images.size(0)\n        total_samples += images.size(0)\n        \n        # 更新进度条\n        loop.set_postfix({\n            'batch_loss': f\"{loss.item():.4f}\",\n            'avg_loss': f\"{total_loss/total_samples:.4f}\"\n        })\n\n    epoch_loss = total_loss / total_samples\n    return epoch_loss\n\n\ndef evaluate(model, dataloader, criterion, device):\n    \"\"\"评估模型，返回平均损失\"\"\"\n    model.eval()\n    total_loss = 0.0\n    total_samples = 0\n\n    with torch.no_grad():\n        loop = tqdm(dataloader, desc=f\"评估中\", leave=True)\n        for images, labels in loop:\n            images = images.to(device, non_blocking=True)\n            labels = labels.to(device, non_blocking=True)\n\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            total_loss += loss.item() * images.size(0)\n            total_samples += images.size(0)\n\n            loop.set_postfix({\n                'batch_loss': f\"{loss.item():.4f}\",\n                'avg_loss': f\"{total_loss/total_samples:.4f}\"\n            })\n\n    epoch_loss = total_loss / total_samples\n    return epoch_loss\n\n# 多标签AUC计算与加权得分函数\ndef calculate_multilabel_auc(model, dataloader, device, target_labels):\n    \"\"\"计算多标签分类的AUC ROC和加权最终得分\"\"\"\n    model.eval()\n    all_y_true = []  # 存储所有样本的真实标签（shape: [n_samples, 14]）\n    all_y_pred = []  # 存储所有样本的预测概率（shape: [n_samples, 14]）\n\n    with torch.no_grad():\n        loop = tqdm(dataloader, desc=\"计算多标签AUC\", leave=True)\n        for images, labels in loop:\n            images = images.to(device, non_blocking=True)\n            labels = labels.to(device, non_blocking=True)\n\n            # 前向传播获取14维预测概率\n            outputs = model(images)  # [batch_size, 14]\n\n            # 收集结果\n            all_y_true.extend(labels.cpu().numpy())\n            all_y_pred.extend(outputs.cpu().numpy())\n\n    # 转为numpy数组\n    all_y_true = np.array(all_y_true)\n    all_y_pred = np.array(all_y_pred)\n\n    # 1. 计算每个标签的AUC\n    label_aucs = []\n    for i, label in enumerate(target_labels):\n        y_true = all_y_true[:, i]\n        y_pred = all_y_pred[:, i]\n\n        # 处理单类别情况\n        if len(np.unique(y_true)) < 2:\n            auc = 0.5  # 随机猜测，AUC设为0.5\n        else:\n            auc = roc_auc_score(y_true, y_pred)\n        \n        label_aucs.append(auc)\n        print(f\"{label:<40} AUC: {auc:.4f}\")\n\n    # 2. 计算加权最终得分\n    present_idx = target_labels.index('Aneurysm Present')\n    present_auc = label_aucs[present_idx]\n    other_aucs = [auc for i, auc in enumerate(label_aucs) if i != present_idx]\n\n    # 最终得分 = (13*present_auc + sum(other_aucs)) / 26\n    weighted_final_score = (13 * present_auc + sum(other_aucs)) / (13 + 13)\n    print(f\"\\n{'='*50}\")\n    print(f\"Aneurysm Present AUC（权重13）: {present_auc:.4f}\")\n    print(f\"其他13个标签平均AUC（权重1）: {np.mean(other_aucs):.4f}\")\n    print(f\"加权最终得分: {weighted_final_score:.4f}\")\n    print(f\"{'='*50}\")\n\n    return label_aucs, weighted_final_score, all_y_true, all_y_pred\n\n# AUC可视化函数\ndef plot_multilabel_auc(label_aucs, target_labels, save_path):\n    \"\"\"绘制多标签AUC条形图\"\"\"\n    plt.figure(figsize=(16, 8))\n    \n    # 定义颜色：Aneurysm Present用红色，其他用蓝色\n    colors = ['#1f77b4'] * len(target_labels)\n    present_idx = target_labels.index('Aneurysm Present')\n    colors[present_idx] = '#d62728'  # 红色标记总存在标签\n\n    # 绘制条形图\n    bars = plt.barh(\n        y=range(len(target_labels)), \n        width=label_aucs, \n        color=colors, \n        alpha=0.8,\n        edgecolor='black',\n        linewidth=0.5\n    )\n\n    # 添加数值标签\n    for i, (bar, auc) in enumerate(zip(bars, label_aucs)):\n        plt.text(\n            auc + 0.01,\n            bar.get_y() + bar.get_height()/2,\n            f'{auc:.4f}', \n            va='center', \n            fontsize=9\n        )\n\n    # 设置坐标轴\n    plt.yticks(range(len(target_labels)), target_labels, fontsize=10)\n    plt.xlabel('ROC AUC Score', fontsize=12, fontweight='bold')\n    plt.title('14 Target Labels ROC AUC Performance\\n(Red = Aneurysm Present, Weight=13)', fontsize=14, fontweight='bold')\n    plt.xlim(0, 1.0)\n    plt.grid(axis='x', alpha=0.3, linestyle='--')\n\n    # 添加水平参考线（AUC=0.5）\n    plt.axvline(x=0.5, color='gray', linestyle='--', alpha=0.7, label='Random Guess (AUC=0.5)')\n    plt.legend(loc='lower right')\n\n    # 保存图像\n    plt.tight_layout()\n    plt.savefig(save_path, dpi=300, bbox_inches='tight')\n    plt.show()\n    print(f\"AUC可视化图已保存至：{save_path}\")\n\n# 初始化训练历史\nhistory = {\n    'train_loss': [],\n    'val_loss': []\n}\n\n# 最佳模型保存配置\nbest_model_path = '/kaggle/working/best_aneurysm_model.pth'\nbest_val_loss = float('inf')\n\n# 开始训练循环\nprint(f\"\\n{'='*50}\")\nprint(f\"开始训练（共 {CONFIG['epochs']} 个Epoch）\")\nprint(f\"{'='*50}\")\n\nfor epoch in range(CONFIG['epochs']):\n    print(f\"\\nEpoch {epoch + 1}/{CONFIG['epochs']}\")\n    print(f\"-\"*30)\n\n    # 1. 训练一个Epoch\n    train_loss = train_epoch(model, train_loader, criterion, optimizer, CONFIG['device'])\n    print(f\"训练结果 → 损失: {train_loss:.4f}\")\n\n    # 2. 验证一个Epoch\n    val_loss = evaluate(model, val_loader, criterion, CONFIG['device'])\n    print(f\"验证结果 → 损失: {val_loss:.4f}\")\n\n    # 3. 更新学习率调度器\n    scheduler.step(val_loss)\n\n    history['train_loss'].append(train_loss)\n    history['val_loss'].append(val_loss)\n\n    # 4. 保存最佳模型\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        torch.save(model.state_dict(), best_model_path)\n        print(f\"✅ 保存最佳模型（验证损失: {best_val_loss:.4f}）\")\n\n# 训练完成后计算多标签AUC和最终得分\nprint(f\"\\n{'='*50}\")\nprint(f\"训练完成！加载最佳模型计算AUC...\")\nprint(f\"{'='*50}\")\n\n# 加载最佳模型\nmodel.load_state_dict(torch.load(best_model_path))\n\n# 计算验证集的多标签AUC与最终得分\nval_label_aucs, val_final_score, val_y_true, val_y_pred = calculate_multilabel_auc(\n    model=model,\n    dataloader=val_loader,\n    device=CONFIG['device'],\n    target_labels=TARGET_LABELS\n)\n\n# 绘制并保存AUC结果\nauc_plot_path = '/kaggle/working/multilabel_auc_plot.png'\nplot_multilabel_auc(\n    label_aucs=val_label_aucs,\n    target_labels=TARGET_LABELS,\n    save_path=auc_plot_path\n)\n\n# 绘制训练损失曲线\nplt.figure(figsize=(10, 5))\nplt.plot(history['train_loss'], label='训练损失', linewidth=2, marker='o', markersize=4)\nplt.plot(history['val_loss'], label='验证损失', linewidth=2, marker='s', markersize=4)\nplt.title('训练与验证损失曲线', fontsize=12)\nplt.xlabel('Epoch', fontsize=10)\nplt.ylabel('损失值', fontsize=10)\nplt.legend(fontsize=10)\nplt.grid(alpha=0.3)\nplt.tight_layout()\nplt.savefig('/kaggle/working/training_curves.png', dpi=300, bbox_inches='tight')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-05T02:25:05.093423Z","iopub.execute_input":"2025-09-05T02:25:05.093701Z","iopub.status.idle":"2025-09-05T03:09:28.182067Z","shell.execute_reply.started":"2025-09-05T02:25:05.093678Z","shell.execute_reply":"2025-09-05T03:09:28.181057Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport pydicom\nfrom pydicom.pixel_data_handlers.util import apply_voi_lut\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom sklearn.model_selection import train_test_split\nfrom tqdm import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# 固定随机种子（保证结果可复现）\nSEED = 42\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 训练配置（优化参数，避免资源不足）\nCONFIG = {\n    'batch_size': 8,  # 适当增大批次（根据GPU内存调整）\n    'epochs': 5,      # 先减少epoch数快速验证\n    'learning_rate': 1e-4,\n    'image_size': (128, 128),  \n    'num_classes': 2,\n    'train_split': 0.8,\n    'max_slices': 8,   # 从16→8（减少每个序列的切片数）\n    'device': 'cuda' if torch.cuda.is_available() else 'cpu'\n}\n\n# 核心数据路径（根据Kaggle目录结构）\nBASE_PATH = '/kaggle/input/rsna-intracranial-aneurysm-detection'\nTRAIN_CSV_PATH = os.path.join(BASE_PATH, 'train.csv')\nSERIES_PATH = os.path.join(BASE_PATH, 'series')\n\n# 新增：缓存路径（用于存储预处理后的图像，避免重复解析DICOM）\nCACHE_PATH = '/kaggle/working/aneurysm_cache'\nos.makedirs(CACHE_PATH, exist_ok=True)  # 自动创建缓存文件夹","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 加载并预处理训练数据（修改列名相关逻辑）\ntrain_df = pd.read_csv(TRAIN_CSV_PATH)\n\n# 定义真实列名\nSERIES_ID_COL = 'SeriesInstanceUID'\nANEURYSM_COL = 'Aneurysm Present'\n\n# 将 Aneurysm Present 列的概率转换为 0/1 标签\ntrain_df['aneurysm_label'] = (train_df[ANEURYSM_COL] > 0.5).astype(int)\n\n# 按序列ID分组，获取每个序列的动脉瘤标签\nseries_aneurysm = train_df.groupby(SERIES_ID_COL)['aneurysm_label'].max().reset_index()\nseries_aneurysm.rename(columns={SERIES_ID_COL:'series_id', 'aneurysm_label': 'aneurysm'}, inplace=True)\n\n# 划分训练集/验证集\ntrain_series, val_series = train_test_split(\n    series_aneurysm['series_id'].values,\n    test_size=1 - CONFIG['train_split'],\n    random_state=SEED,\n    stratify=series_aneurysm['aneurysm']\n)\n\nprint(f\"训练集序列数: {len(train_series)}\")\nprint(f\"验证集序列数: {len(val_series)}\")\nprint(f\"训练集正负样本比例: \\n{series_aneurysm[series_aneurysm['series_id'].isin(train_series)]['aneurysm'].value_counts(normalize=True)}\")\nprint(f\"\\n修改后的series_aneurysm前5行：\")\nprint(series_aneurysm[['series_id', 'aneurysm']].head()) ","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_dicom(path):\n    \"\"\"修复DICOM加载：用dcmread替代read_file，添加异常处理\"\"\"\n    try:\n        dicom = pydicom.dcmread(path)\n        img = apply_voi_lut(dicom.pixel_array, dicom)\n\n        # 确保单通道（部分DICOM可能多通道，取第一通道）\n        if len(img.shape) != 2:\n            img = img[:, :, 0]\n\n        # 归一化到[0,255]（避免数值范围异常）\n        img = img - img.min()\n        if img.max() != 0:  # 避免除以零\n            img = img / img.max()\n        img = (img * 255).astype(np.uint8)\n        return img\n    except Exception as e:\n        print(f\"加载DICOM失败 {path}: {str(e)}\")\n        return None\n\n\ndef get_cached_series(series_id):\n    \"\"\"新增缓存机制：第一次加载DICOM并保存为.npy，后续直接加载缓存\"\"\"\n    # 缓存文件路径（每个序列对应一个缓存文件）\n    cache_file = os.path.join(CACHE_PATH, f\"{series_id}.npy\")\n\n    # 1. 有缓存：直接加载（速度提升10倍以上）\n    if os.path.exists(cache_file):\n        return np.load(cache_file)\n\n    # 2. 无缓存：加载DICOM并生成缓存\n    series_folder = os.path.join(SERIES_PATH, str(series_id))\n    if not os.path.exists(series_folder):\n        print(f\"序列文件夹不存在: {series_folder}\")\n        return None\n\n    # 获取DICOM文件（按名称排序，确保切片顺序正确）\n    dicom_files = [os.path.join(series_folder, f) for f in os.listdir(series_folder) if f.endswith('.dcm')]\n    dicom_files.sort()  # 关键：医学图像切片必须按顺序加载\n\n    # 加载DICOM（只取前CONFIG['max_slices']个，减少加载量）\n    images = []\n    for i, file in enumerate(dicom_files):\n        if i >= CONFIG['max_slices']:  # 优化：超过max_slices直接停止，避免冗余加载\n            break\n        img = load_dicom(file)\n        if img is not None:\n            images.append(img)\n\n    # 保存缓存（后续训练直接用）\n    if len(images) > 0:\n        np.save(cache_file, np.array(images))\n    return np.array(images) if len(images) > 0 else None\n\n# 测试缓存加载效果\nsample_series_id = train_series[0]\nprint(f\"\\n测试序列 {sample_series_id} 加载...\")\nsample_images = get_cached_series(sample_series_id)\nif sample_images is not None:\n    print(f\"序列 {sample_series_id} 加载成功，共 {len(sample_images)} 张切片\")\n    print(f\"切片形状: {sample_images[0].shape}\")\n\n    # 可视化前3张切片\n    plt.figure(figsize=(12, 4))\n    for i in range(min(3, len(sample_images))):\n        plt.subplot(1, 3, i + 1)\n        plt.imshow(sample_images[i], cmap='gray')\n        plt.title(f\"切片 {i + 1}\")\n        plt.axis('off')\n    plt.show()\nelse:\n    print(f\"序列 {sample_series_id} 加载失败\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class AneurysmDataset(Dataset):\n    def __init__(self, series_ids, series_aneurysm, transform=None):\n        self.series_ids = series_ids\n        self.series_aneurysm = series_aneurysm\n        self.transform = transform\n        self.max_slices = CONFIG['max_slices']\n\n    def __len__(self):\n        return len(self.series_ids)\n\n    def __getitem__(self, idx):\n        series_id = self.series_ids[idx]\n        images = get_cached_series(series_id)  # 从缓存加载序列\n\n        # 处理加载失败的情况\n        if images is None or len(images) == 0:\n            # 返回符合预期维度的随机张量：[1, max_slices, H, W]\n            return torch.randn(1, self.max_slices, *CONFIG['image_size']), torch.tensor(0, dtype=torch.float32)\n\n        # 步骤1：调整切片数量（补零或截断）\n        if len(images) < self.max_slices:\n            pad_width = self.max_slices - len(images)\n            images = np.pad(images, ((0, pad_width), (0, 0), (0, 0)), mode='constant')\n        else:\n            images = images[:self.max_slices]\n\n        # 步骤2：应用图像变换（ToTensor()会自动添加通道维度[1, H, W]）\n        processed_images = []\n        for img in images:\n            if self.transform:\n                img = self.transform(img)  # 输出：[1, H, W]（单通道）\n            processed_images.append(img)\n\n        # 步骤3：堆叠并调整维度顺序（关键修改！解决维度不匹配）\n        # 堆叠后维度：[max_slices, 1, H, W] → 调整为 [1, max_slices, H, W]（通道在前，切片数在后）\n        images_tensor = torch.stack(processed_images)  # [max_slices, 1, H, W]\n        images_tensor = images_tensor.permute(1, 0, 2, 3)  # 交换维度1和0 → [1, max_slices, H, W]\n\n        # 步骤4：获取标签（依赖数据预处理中统一的'series_id'和'aneurysm'列）\n        label = self.series_aneurysm.loc[\n            self.series_aneurysm['series_id'] == series_id,\n            'aneurysm'\n        ].values[0]\n\n        # 最终输出：图像[1, max_slices, H, W] + 标签[0/1]（匹配模型输入维度）\n        return images_tensor.float(), torch.tensor(label, dtype=torch.float32)\n\n# 定义图像变换\ntransform = transforms.Compose([\n    transforms.ToPILImage(),  # 转为PIL图像（便于Resize）\n    transforms.Resize(CONFIG['image_size']),  # 统一尺寸\n    transforms.ToTensor(),  # 转为Tensor（范围[0,1]）\n    transforms.Normalize(mean=[0.5], std=[0.5])  # 归一化到[-1,1]（适配Transformer）\n])\n\n# 创建数据集和DataLoader\ntrain_dataset = AneurysmDataset(train_series, series_aneurysm, transform=transform)\nval_dataset = AneurysmDataset(val_series, series_aneurysm, transform=transform)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=CONFIG['batch_size'],\n    shuffle=True,\n    num_workers=0,\n    pin_memory=CONFIG['device'] == 'cuda'\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=CONFIG['batch_size'],\n    shuffle=False,\n    num_workers=0,\n    pin_memory=CONFIG['device'] == 'cuda'\n)\n\nprint(f\"\\n训练集样本数: {len(train_dataset)}\")\nprint(f\"验证集样本数: {len(val_dataset)}\")\nprint(f\"训练集批次数量: {len(train_loader)}\")\nprint(f\"验证集批次数量: {len(val_loader)}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class PatchEmbedding(nn.Module):\n    \"\"\"将单张医学图像切片转为Transformer输入的Patch嵌入\"\"\"\n    def __init__(self, image_size, patch_size, in_channels=1, embed_dim=256):\n        super().__init__()\n        self.patch_size = patch_size\n        # 计算每个切片的Patch数量（如256x256图像，32x32 Patch → 8x8=64个Patch）\n        self.num_patches = (image_size[0] // patch_size) * (image_size[1] // patch_size)\n\n        # 用卷积层实现Patch分割+嵌入（替代手动分割）\n        self.proj = nn.Conv2d(\n            in_channels,\n            embed_dim,\n            kernel_size=patch_size,\n            stride=patch_size\n        )\n\n    def forward(self, x):\n        # x: [batch_size, 1, H, W] → 单通道图像\n        x = self.proj(x)  # [batch_size, embed_dim, num_patches_H, num_patches_W]\n        x = x.flatten(2)  # [batch_size, embed_dim, num_patches]\n        x = x.transpose(1, 2)  # [batch_size, num_patches, embed_dim] → 适配Transformer输入\n        return x\n\n\nclass SliceTransformer(nn.Module):\n    \"\"\"处理单张切片的Transformer（提取切片级特征）\"\"\"\n    def __init__(self, image_size=CONFIG['image_size'], patch_size=32, embed_dim=256, num_heads=8, num_layers=4):\n        super().__init__()\n        self.patch_embed = PatchEmbedding(image_size, patch_size, embed_dim=embed_dim)\n        num_patches = self.patch_embed.num_patches\n\n        # 位置嵌入（Transformer需要位置信息）\n        self.pos_embed = nn.Parameter(torch.randn(1, num_patches, embed_dim))\n\n        # Transformer编码器（简化结构，避免过拟合）\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=embed_dim,\n            nhead=num_heads,\n            dim_feedforward=embed_dim * 4,\n            dropout=0.1,\n            batch_first=True\n        )\n        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)\n\n        # 切片特征聚合（平均池化，简化计算）\n        self.feature_pool = nn.AdaptiveAvgPool1d(1)\n\n    def forward(self, x):\n        # x: [batch_size, 1, H, W] → 单张切片\n        x = self.patch_embed(x)  # [batch_size, num_patches, embed_dim]\n        x = x + self.pos_embed  # 添加位置嵌入\n\n        x = self.transformer(x)  # [batch_size, num_patches, embed_dim]\n        x = x.transpose(1, 2)  # [batch_size, embed_dim, num_patches]\n        x = self.feature_pool(x).squeeze(-1)  # [batch_size, embed_dim] → 切片级特征\n        return x\n\n\nclass SeriesTransformer(nn.Module):\n    \"\"\"处理整个序列的Transformer（聚合切片特征，判断是否有动脉瘤）\"\"\"\n    def __init__(self, max_slices=CONFIG['max_slices'], slice_embed_dim=256, seq_embed_dim=128, num_heads=4, num_layers=2):\n        super().__init__()\n        # 1. 切片级特征提取\n        self.slice_transformer = SliceTransformer(embed_dim=slice_embed_dim)\n\n        # 2. 序列级特征投影（将切片特征转为序列嵌入维度）\n        self.seq_proj = nn.Linear(slice_embed_dim, seq_embed_dim)\n\n        # 3. 序列位置嵌入\n        self.seq_pos_embed = nn.Parameter(torch.randn(1, max_slices, seq_embed_dim))\n\n        # 4. 序列级Transformer（聚合切片间信息）\n        seq_encoder_layer = nn.TransformerEncoderLayer(\n            d_model=seq_embed_dim,\n            nhead=num_heads,\n            dim_feedforward=seq_embed_dim * 4,\n            dropout=0.1,\n            batch_first=True\n        )\n        self.seq_transformer = nn.TransformerEncoder(seq_encoder_layer, num_layers=num_layers)\n\n        # 5. 最终分类头（二分类：有/无动脉瘤）\n        self.classifier = nn.Sequential(\n            nn.Linear(seq_embed_dim, 64),  # 降维\n            nn.ReLU(),  # 激活函数\n            nn.Dropout(0.5),  # 正则化\n            nn.Linear(64, 1),  # 输出1个值\n            nn.Sigmoid()  # 转为概率（0~1）\n        )\n\n    def forward(self, x):\n        # x: [batch_size, 1, max_slices, H, W] → 序列数据\n        batch_size, _, max_slices, H, W = x.shape\n\n        # 1. 提取每个切片的特征\n        slice_features = []\n        for i in range(max_slices):\n            slice_img = x[:, :, i, :, :]  # [batch_size, 1, H, W] → 单张切片\n            feat = self.slice_transformer(slice_img)  # [batch_size, slice_embed_dim]\n            slice_features.append(feat)\n\n        # 2. 拼接切片特征，形成序列\n        seq_feat = torch.stack(slice_features, dim=1)  # [batch_size, max_slices, slice_embed_dim]\n        seq_feat = self.seq_proj(seq_feat)  # [batch_size, max_slices, seq_embed_dim]\n        seq_feat = seq_feat + self.seq_pos_embed  # 添加序列位置嵌入\n\n        # 3. 序列级特征聚合\n        seq_feat = self.seq_transformer(seq_feat)  # [batch_size, max_slices, seq_embed_dim]\n        seq_feat = seq_feat.mean(dim=1)  # [batch_size, seq_embed_dim] → 平均池化聚合序列信息\n\n        # 4. 分类预测\n        output = self.classifier(seq_feat).squeeze(-1)  # [batch_size] → 每个样本的概率\n        return output\n\n\n# 创建模型并移至设备\nmodel = SeriesTransformer()\nmodel = model.to(CONFIG['device'])\n\n# 打印模型参数量（避免模型过大导致GPU内存不足）\ndef count_params(model):\n    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n\n\nprint(f\"\\n模型可训练参数量: {count_params(model):,}\")\nprint(f\"模型设备: {next(model.parameters()).device}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 定义损失函数、优化器和学习率调度器\ncriterion = nn.BCELoss()  # 二分类交叉熵损失（适配Sigmoid输出）\noptimizer = optim.Adam(\n    model.parameters(),\n    lr=CONFIG['learning_rate'],\n    weight_decay=1e-5  # L2正则化，减少过拟合\n)\n# 学习率调度器：验证损失不下降时降低学习率\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer,\n    mode='min',  # 目标：最小化验证损失\n    patience=2,  # 2个epoch损失不下降则降学习率\n    factor=0.5,  # 学习率 *= 0.5\n    verbose=True  # 打印学习率变化\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_epoch(model, dataloader, criterion, optimizer, device):\n    \"\"\"训练一个epoch，返回平均损失和准确率\"\"\"\n    model.train()  # 开启训练模式（启用Dropout等）\n    total_loss = 0.0\n    total_correct = 0\n    total_samples = 0\n\n    # tqdm进度条（显示实时训练状态）\n    loop = tqdm(dataloader, desc=f\"训练中\", leave=True)\n    for batch_idx, (images, labels) in enumerate(loop):\n        # 数据移至设备（GPU/CPU）\n        images = images.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True)\n\n        # 1. 前向传播\n        outputs = model(images)  # [batch_size] → 预测概率\n        loss = criterion(outputs, labels)  # 计算损失\n\n        # 2. 反向传播与参数更新\n        optimizer.zero_grad()  # 清空梯度\n        loss.backward()  # 计算梯度\n        optimizer.step()  # 更新参数\n\n        # 3. 计算指标\n        total_loss += loss.item() * images.size(0)  # 累计损失（乘以批次大小，避免批次不均衡）\n        predictions = (outputs > 0.5).float()  # 概率>0.5视为正样本\n        total_correct += (predictions == labels).sum().item()  # 累计正确数\n        total_samples += images.size(0)  # 累计样本数\n\n        # 更新进度条显示\n        loop.set_postfix({\n            'batch_loss': f\"{loss.item():.4f}\",\n            'avg_loss': f\"{total_loss/total_samples:.4f}\",\n            'acc': f\"{total_correct/total_samples:.4f}\"\n        })\n\n    # 计算epoch级指标\n    epoch_loss = total_loss / total_samples\n    epoch_acc = total_correct / total_samples\n    return epoch_loss, epoch_acc\n\n\ndef evaluate(model, dataloader, criterion, device):\n    \"\"\"评估模型，返回平均损失和准确率（无梯度计算）\"\"\"\n    model.eval()  # 开启评估模式（关闭Dropout等）\n    total_loss = 0.0\n    total_correct = 0\n    total_samples = 0\n\n    # 禁用梯度计算（加速评估，避免内存占用）\n    with torch.no_grad():\n        loop = tqdm(dataloader, desc=f\"评估中\", leave=True)\n        for images, labels in loop:\n            images = images.to(device, non_blocking=True)\n            labels = labels.to(device, non_blocking=True)\n\n            # 前向传播\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n            # 计算指标\n            total_loss += loss.item() * images.size(0)\n            predictions = (outputs > 0.5).float()\n            total_correct += (predictions == labels).sum().item()\n            total_samples += images.size(0)\n\n            # 更新进度条\n            loop.set_postfix({\n                'batch_loss': f\"{loss.item():.4f}\",\n                'avg_loss': f\"{total_loss/total_samples:.4f}\",\n                'acc': f\"{total_correct/total_samples:.4f}\"\n            })\n\n    epoch_loss = total_loss / total_samples\n    epoch_acc = total_correct / total_samples\n    return epoch_loss, epoch_acc","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 初始化训练历史（记录损失和准确率）\nhistory = {\n    'train_loss': [],\n    'train_acc': [],\n    'val_loss': [],\n    'val_acc': []\n}\n\n# 最佳模型保存配置（保存验证损失最小的模型）\nbest_model_path = '/kaggle/working/best_aneurysm_model.pth'\nbest_val_loss = float('inf')  # 初始值设为无穷大\n\n# 开始训练循环\nprint(f\"\\n{'='*50}\")\nprint(f\"开始训练（共 {CONFIG['epochs']} 个Epoch）\")\nprint(f\"{'='*50}\")\n\nfor epoch in range(CONFIG['epochs']):\n    print(f\"\\nEpoch {epoch + 1}/{CONFIG['epochs']}\")\n    print(f\"-\"*30)\n\n    # 1. 训练一个Epoch\n    train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, CONFIG['device'])\n    print(f\"训练结果 → 损失: {train_loss:.4f}, 准确率: {train_acc:.4f}\")\n\n    # 2. 验证一个Epoch\n    val_loss, val_acc = evaluate(model, val_loader, criterion, CONFIG['device'])\n    print(f\"验证结果 → 损失: {val_loss:.4f}, 准确率: {val_acc:.4f}\")\n\n    # 3. 更新学习率调度器\n    scheduler.step(val_loss)\n\n    history['train_loss'].append(train_loss)\n    history['train_acc'].append(train_acc)\n    history['val_loss'].append(val_loss)\n    history['val_acc'].append(val_acc)\n\n    # 5. 保存最佳模型（仅当验证损失下降时）\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        torch.save(model.state_dict(), best_model_path)\n        print(f\"✅ 保存最佳模型（验证损失: {best_val_loss:.4f}）\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 训练完成后可视化训练曲线\nprint(f\"\\n{'='*50}\")\nprint(f\"训练完成！最佳验证损失: {best_val_loss:.4f}\")\nprint(f\"最佳模型路径: {best_model_path}\")\nprint(f\"{'='*50}\")\n\n# 绘制损失曲线和准确率曲线\nplt.figure(figsize=(14, 5))\n\n# 1. 损失曲线\nplt.subplot(1, 2, 1)\nplt.plot(history['train_loss'], label='训练损失', linewidth=2, marker='o', markersize=4)\nplt.plot(history['val_loss'], label='验证损失', linewidth=2, marker='s', markersize=4)\nplt.title('训练与验证损失曲线', fontsize=12)\nplt.xlabel('Epoch', fontsize=10)\nplt.ylabel('损失值', fontsize=10)\nplt.legend(fontsize=10)\nplt.grid(alpha=0.3)\n\n# 2. 准确率曲线\nplt.subplot(1, 2, 2)\nplt.plot(history['train_acc'], label='训练准确率', linewidth=2, marker='o', markersize=4)\nplt.plot(history['val_acc'], label='验证准确率', linewidth=2, marker='s', markersize=4)\nplt.title('训练与验证准确率曲线', fontsize=12)\nplt.xlabel('Epoch', fontsize=10)\nplt.ylabel('准确率', fontsize=10)\nplt.legend(fontsize=10)\nplt.grid(alpha=0.3)\n\n# 保存图像（Kaggle中可在\"Output\"查看）\nplt.tight_layout()\nplt.savefig('/kaggle/working/training_curves.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}