{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":99552,"databundleVersionId":13694723,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":3641.952973,"end_time":"2025-09-05T04:11:26.034188","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-09-05T03:10:44.081215","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"a14006b2-44c3-4a00-bee7-8e17da432cce","cell_type":"markdown","source":"导入所有需要的工具库，并设置了一个固定的随机种子（SEED = 42）\n","metadata":{}},{"id":"d6e46c16","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)","metadata":{"execution":{"iopub.status.busy":"2025-09-11T10:00:03.167038Z","iopub.execute_input":"2025-09-11T10:00:03.167243Z","iopub.status.idle":"2025-09-11T10:00:19.044948Z","shell.execute_reply.started":"2025-09-11T10:00:03.167225Z","shell.execute_reply":"2025-09-11T10:00:19.044144Z"},"papermill":{"duration":0.308984,"end_time":"2025-09-05T03:53:07.402263","exception":false,"start_time":"2025-09-05T03:53:07.093279","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1ee3cd2a-b5fe-4a14-a72f-f433808f06cb","cell_type":"code","source":"# 训练配置（优化参数，避免资源不足）\nCONFIG = {\n    'batch_size': 8,  # 适当增大批次（根据GPU内存调整）\n    'epochs': 20,      # 先减少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)  # 自动创建缓存文件夹","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:19.045648Z","iopub.execute_input":"2025-09-11T10:00:19.046022Z","iopub.status.idle":"2025-09-11T10:00:19.138094Z","shell.execute_reply.started":"2025-09-11T10:00:19.046005Z","shell.execute_reply":"2025-09-11T10:00:19.137275Z"}},"outputs":[],"execution_count":null},{"id":"40c8ffc7-4b24-4dd6-b5c3-27f0a039af42","cell_type":"markdown","source":"读取数据，将每个CT序列（包含很多张切片）的标签汇总成一条14标签的记录，然后划分训练集和验证集。\n模型是以一个完整的CT序列为单位进行分析的，而不是单张切片。所以必须把属于同一个序列的所有切片的标签信息汇总起来。按照“是否存在动脉瘤”这个标签来划分数据集。这是为了确保训练集和验证集中，有动脉瘤和无动脉瘤的序列比例是相同的。如果不这样做，万一验证集中全是负样本，评估结果就会毫无意义。","metadata":{}},{"id":"9fe9ecca-5949-4849-8da1-daefa204cb65","cell_type":"code","source":"# 加载并预处理训练数据（多标签版本）\ntrain_df = pd.read_csv(TRAIN_CSV_PATH)\n\n# 1. 首先检查并处理重复值\nprint(\"原始数据形状:\", train_df.shape)\nprint(\"数据列名:\", train_df.columns.tolist())\nprint(\"\\n检查重复值...\")\n\n# 检查完全重复的行\nduplicate_rows = train_df.duplicated().sum()\nprint(f\"完全重复的行数: {duplicate_rows}\")\n\n# 检查基于关键字段的重复\n# 首先确定可用的关键字段\navailable_columns = train_df.columns.tolist()\nkey_columns = ['SeriesInstanceUID']\n\n# 尝试添加其他可能的关键字段\npossible_keys = ['SOPInstanceUID', 'ImagePositionPatient', 'InstanceNumber']\nfor col in possible_keys:\n    if col in available_columns:\n        key_columns.append(col)\n        break\nelse:\n    # 如果没有找到其他关键字段，使用SeriesInstanceUID和行索引\n    print(\"警告: 未找到切片级别的唯一标识符，使用SeriesInstanceUID和行索引\")\n    train_df['temp_index'] = range(len(train_df))\n    key_columns = ['SeriesInstanceUID', 'temp_index']\n\nprint(f\"使用的关键字段: {key_columns}\")\n\n# 检查基于关键字段的重复记录\nduplicate_keys = train_df.duplicated(subset=key_columns).sum()\nprint(f\"基于关键字段的重复记录数: {duplicate_keys}\")\n\n# 处理重复值\nif duplicate_rows > 0:\n    print(\"正在删除完全重复的行...\")\n    train_df = train_df.drop_duplicates()\n    print(f\"删除后数据形状: {train_df.shape}\")\n\nif duplicate_keys > 0:\n    print(\"正在处理基于关键字段的重复记录...\")\n    # 对于重复记录，我们保留第一个出现的记录\n    train_df = train_df.drop_duplicates(subset=key_columns, keep='first')\n    print(f\"处理关键字段重复后数据形状: {train_df.shape}\")\n\n# 清理临时列\nif 'temp_index' in train_df.columns:\n    train_df = train_df.drop(columns=['temp_index'])\n\n# 2. 按序列ID分组，计算每个序列的14个标签（取最大值：存在则为1）\nprint(\"\\n按序列分组并汇总标签...\")\nseries_labels_df = train_df.groupby('SeriesInstanceUID')[TARGET_LABELS].max().reset_index()\nseries_labels_df.rename(columns={'SeriesInstanceUID': 'series_id'}, inplace=True)\n\n# 检查分组后的重复序列ID\nduplicate_series = series_labels_df.duplicated(subset=['series_id']).sum()\nif duplicate_series > 0:\n    print(f\"警告: 分组后仍有 {duplicate_series} 个重复序列ID\")\n    # 这种情况下应该进一步调查原因\n    duplicate_examples = series_labels_df[series_labels_df.duplicated(subset=['series_id'], keep=False)]\n    print(\"重复序列ID示例:\")\n    print(duplicate_examples.head())\n    # 处理序列级别的重复：保留第一个记录\n    series_labels_df = series_labels_df.drop_duplicates(subset=['series_id'], keep='first')\n    print(f\"处理序列重复后数据形状: {series_labels_df.shape}\")\nelse:\n    print(\"分组后无重复序列ID\")\n\n# 3. 验证标签分布\nprint(\"\\n14个标签的序列级正样本比例：\")\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# 4. 划分训练集/验证集（按总存在标签分层）\nprint(\"\\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# 5. 最终数据质量检查\nprint(f\"\\n最终数据质量检查:\")\nprint(f\"- 训练集阳性样本数: {train_labels['Aneurysm Present'].sum()} ({train_labels['Aneurysm Present'].mean():.2%})\")\nprint(f\"- 验证集阳性样本数: {val_labels['Aneurysm Present'].sum()} ({val_labels['Aneurysm Present'].mean():.2%})\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:19.13978Z","iopub.execute_input":"2025-09-11T10:00:19.140036Z","iopub.status.idle":"2025-09-11T10:00:19.256288Z","shell.execute_reply.started":"2025-09-11T10:00:19.140008Z","shell.execute_reply":"2025-09-11T10:00:19.255704Z"}},"outputs":[],"execution_count":null},{"id":"dc1050ac-0c60-4d80-b6c9-a0759caf4151","cell_type":"markdown","source":"写了两个函数来读取DICOM格式的医学图像，并把它们转换成普通的数组。还设计了一个缓存系统。医学影像用的是DICOM格式，和普通的JPG、PNG完全不同，需要专门的库（pydicom）来解析。读取和解析DICOM文件非常慢。而模型训练需要反复读取相同的数据。缓存机制让每个序列只在第一次被读取，之后都直接从高速的.npy格式加载，极大提升了数据读取速度，节省了宝贵的训练时间。","metadata":{}},{"id":"088c947e-be28-489b-a1e1-51c620c36a89","cell_type":"code","source":"# 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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:19.259083Z","iopub.execute_input":"2025-09-11T10:00:19.259324Z","iopub.status.idle":"2025-09-11T10:00:19.267327Z","shell.execute_reply.started":"2025-09-11T10:00:19.259301Z","shell.execute_reply":"2025-09-11T10:00:19.266567Z"}},"outputs":[],"execution_count":null},{"id":"964e848f-a4ca-4c37-bc84-4cb6ebf3f9a9","cell_type":"code","source":"# 测试缓存加载效果\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,"execution":{"iopub.status.busy":"2025-09-11T10:00:19.26802Z","iopub.execute_input":"2025-09-11T10:00:19.268452Z","iopub.status.idle":"2025-09-11T10:00:20.022664Z","shell.execute_reply.started":"2025-09-11T10:00:19.268434Z","shell.execute_reply":"2025-09-11T10:00:20.021957Z"}},"outputs":[],"execution_count":null},{"id":"dfdfcb2c-00bb-45cf-8c7e-9ceec365046e","cell_type":"markdown","source":"定义了一个Dataset类","metadata":{}},{"id":"75e44d8d-f9a5-43c9-a133-a662ab3c9078","cell_type":"code","source":"# 多标签数据集类（数据集类定义）\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.023465Z","iopub.execute_input":"2025-09-11T10:00:20.023875Z","iopub.status.idle":"2025-09-11T10:00:20.032685Z","shell.execute_reply.started":"2025-09-11T10:00:20.023841Z","shell.execute_reply":"2025-09-11T10:00:20.031986Z"}},"outputs":[],"execution_count":null},{"id":"783424cd-5ab0-4de1-95c7-77bc52af64c1","cell_type":"code","source":"# 定义图像变换\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个总存在）\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.033393Z","iopub.execute_input":"2025-09-11T10:00:20.033637Z","iopub.status.idle":"2025-09-11T10:00:20.056431Z","shell.execute_reply.started":"2025-09-11T10:00:20.033613Z","shell.execute_reply":"2025-09-11T10:00:20.055857Z"}},"outputs":[],"execution_count":null},{"id":"7e5c3f42-b025-409a-b951-4cb56130d847","cell_type":"markdown","source":"","metadata":{}},{"id":"e5a18724-1008-4c09-bfce-affe25c4c245","cell_type":"code","source":"# 模型定义\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.058906Z","iopub.execute_input":"2025-09-11T10:00:20.059114Z","iopub.status.idle":"2025-09-11T10:00:20.075935Z","shell.execute_reply.started":"2025-09-11T10:00:20.059097Z","shell.execute_reply":"2025-09-11T10:00:20.075435Z"}},"outputs":[],"execution_count":null},{"id":"9194060f-3a6d-4d24-bb41-04ace1808cfb","cell_type":"code","source":"class 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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.076606Z","iopub.execute_input":"2025-09-11T10:00:20.076862Z","iopub.status.idle":"2025-09-11T10:00:20.101064Z","shell.execute_reply.started":"2025-09-11T10:00:20.076846Z","shell.execute_reply":"2025-09-11T10:00:20.100345Z"}},"outputs":[],"execution_count":null},{"id":"9fde69a2-5afa-4e57-8804-7459309755bd","cell_type":"code","source":"class 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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.101794Z","iopub.execute_input":"2025-09-11T10:00:20.102099Z","iopub.status.idle":"2025-09-11T10:00:20.123169Z","shell.execute_reply.started":"2025-09-11T10:00:20.102071Z","shell.execute_reply":"2025-09-11T10:00:20.122671Z"}},"outputs":[],"execution_count":null},{"id":"359460f2-324c-4a8f-ba30-0d2b195bfc69","cell_type":"code","source":"# 计算模型参数量的函数\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}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.123726Z","iopub.execute_input":"2025-09-11T10:00:20.123931Z","iopub.status.idle":"2025-09-11T10:00:20.400219Z","shell.execute_reply.started":"2025-09-11T10:00:20.123918Z","shell.execute_reply":"2025-09-11T10:00:20.399368Z"}},"outputs":[],"execution_count":null},{"id":"d1b71d7e-7ac4-47aa-a762-e541977eea57","cell_type":"code","source":"# 定义损失函数、优化器和学习率调度器\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)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.40108Z","iopub.execute_input":"2025-09-11T10:00:20.401335Z","iopub.status.idle":"2025-09-11T10:00:20.406257Z","shell.execute_reply.started":"2025-09-11T10:00:20.401312Z","shell.execute_reply":"2025-09-11T10:00:20.405636Z"}},"outputs":[],"execution_count":null},{"id":"45c82ab8-fc6c-40ec-b454-22df1e65aacd","cell_type":"code","source":"# 训练和评估函数\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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.406828Z","iopub.execute_input":"2025-09-11T10:00:20.406997Z","iopub.status.idle":"2025-09-11T10:00:20.429313Z","shell.execute_reply.started":"2025-09-11T10:00:20.406984Z","shell.execute_reply":"2025-09-11T10:00:20.428829Z"}},"outputs":[],"execution_count":null},{"id":"bf281669-b300-4ec1-b990-2a9d3272f6af","cell_type":"code","source":"# 多标签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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.429949Z","iopub.execute_input":"2025-09-11T10:00:20.430173Z","iopub.status.idle":"2025-09-11T10:00:20.443932Z","shell.execute_reply.started":"2025-09-11T10:00:20.430158Z","shell.execute_reply":"2025-09-11T10:00:20.443258Z"}},"outputs":[],"execution_count":null},{"id":"601776e1-a833-426d-ac3a-666d6316411a","cell_type":"code","source":"# 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}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.44472Z","iopub.execute_input":"2025-09-11T10:00:20.444929Z","iopub.status.idle":"2025-09-11T10:00:20.466703Z","shell.execute_reply.started":"2025-09-11T10:00:20.444906Z","shell.execute_reply":"2025-09-11T10:00:20.466051Z"}},"outputs":[],"execution_count":null},{"id":"84972b36-b3b0-41cc-a986-19cbeac8c36a","cell_type":"code","source":"# 初始化训练历史\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}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.467449Z","iopub.execute_input":"2025-09-11T10:00:20.467682Z","iopub.status.idle":"2025-09-11T10:00:20.48831Z","shell.execute_reply.started":"2025-09-11T10:00:20.467641Z","shell.execute_reply":"2025-09-11T10:00:20.487698Z"}},"outputs":[],"execution_count":null},{"id":"f4aa6eb9-5926-43ec-b609-f54f727e1273","cell_type":"code","source":"for 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}）\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T10:00:20.488982Z","iopub.execute_input":"2025-09-11T10:00:20.489193Z"}},"outputs":[],"execution_count":null},{"id":"7f1ac1b6-3f05-4eb1-a7cd-62a87e2aae48","cell_type":"code","source":"# 训练完成后计算多标签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()","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}