{"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":"gpu","dataSources":[{"sourceId":99552,"databundleVersionId":13851420,"sourceType":"competition"},{"sourceId":12780021,"sourceType":"datasetVersion","datasetId":8079690}],"dockerImageVersionId":31154,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport sys\nimport gc\nimport random\nimport time\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport cv2\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom collections import OrderedDict\nimport multiprocessing as mp\nfrom functools import partial\n\n# 深度学习框架\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import autocast\nfrom torch.optim import AdamW\nfrom torch.optim.lr_scheduler import CosineAnnealingWarmRestarts\nimport timm\n\n# 数据增强\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# 交叉验证和评估\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score\n\n# 设置设备\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"使用设备: {device}\")\n\n# ====================================================\n# 配置参数 - 优化数据加载\n# ====================================================\nclass CFG:\n    # 数据路径\n    train_dir = Path('/kaggle/input/rsna-intracranial-aneurysm-detection/series')\n    train_csv = Path('/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv')\n    \n    # 输出路径\n    output_dir = Path('/kaggle/working')\n    model_dir = output_dir / 'models'\n    cache_dir = output_dir / 'cache'\n    \n    # 图像参数\n    img_size = 256\n    num_slices = 32\n    slice_size = (img_size, img_size)\n    in_channels = 32\n    \n    # 训练参数\n    batch_size = 8\n    val_batch_size = 16\n    chunk_size = 300  # 分批加载大小\n    num_workers = min(8, os.cpu_count() - 1)  # 增加工作进程\n    epochs = 30\n    lr = 1e-4\n    weight_decay = 1e-5\n    fold = 5\n    selected_folds = [0, 1, 2, 3, 4]\n    patience = 8\n    min_lr = 1e-6\n    T_0 = 10\n    \n    # 模型设置\n    model_name = 'tf_efficientnetv2_m.in21k_ft_in1k'\n    pretrained = True\n    dropout = 0.5\n    label_smoothing = 0.05\n    \n    # 优化设置\n    use_amp = True\n    gradient_checkpointing = False\n    max_cache_size = 12 * 1024**3  # 增加缓存大小到12GB\n\n# 创建目录\nfor dir_path in [CFG.model_dir, CFG.cache_dir]:\n    dir_path.mkdir(parents=True, exist_ok=True)\n\n# ====================================================\n# 类别信息\n# ====================================================\nID_COL = 'SeriesInstanceUID'\nLABEL_COLS = [\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',\n]\nNUM_CLASSES = len(LABEL_COLS)\n\n# ====================================================\n# 数据加载与预处理 - 增加CTA窗口处理\n# ====================================================\n\ndef apply_ct_window(img, window_center, window_width):\n    \"\"\"Apply CT windowing to a single image.\"\"\"\n    min_value = window_center - window_width // 2\n    max_value = window_center + window_width // 2\n    img = np.clip(img, min_value, max_value)\n    img = (img - min_value) / (max_value - min_value + 1e-5)\n    return img\n\ndef preprocess_mri(img):\n    \"\"\"Apply MRI-specific preprocessing if needed.\"\"\"\n    # MRI 标准化\n    img = (img - np.min(img)) / (np.max(img) - np.min(img) + 1e-5)\n    return img\n\ndef sort_slices_by_position(dicom_files):\n    \"\"\"Sort DICOM files by z-position (ImagePositionPatient[2]) or InstanceNumber.\"\"\"\n    slice_positions = []\n    for dcm_file in dicom_files:\n        dcm = pydicom.dcmread(dcm_file)\n        position = getattr(dcm, 'ImagePositionPatient', None)\n        if position is not None and len(position) >= 3:\n            z = float(position[2])\n        else:\n            z = float(getattr(dcm, 'InstanceNumber', 0))\n        slice_positions.append((dcm_file, z))\n    # Sort by z-position\n    sorted_files = [f for f, _ in sorted(slice_positions, key=lambda x: x[1])]\n    return sorted_files\n\ndef load_dicom_series(series_path, img_size=(128, 128), num_slices=64):\n    dicom_files = sorted(list(series_path.glob(\"*.dcm\")))\n    slices = []\n    # If only one file and it's multi-frame (3D)\n    if len(dicom_files) == 1:\n        dcm = pydicom.dcmread(dicom_files[0])\n        if hasattr(dcm, \"NumberOfFrames\") and dcm.NumberOfFrames > 1:\n            # Multi-frame DICOM (3D)\n            frames = dcm.pixel_array.astype(np.float32)  # shape: (num_frames, H, W)\n            for img in frames:\n                img = cv2.resize(img, img_size)\n                modality = getattr(dcm, 'Modality', 'CT')\n                if modality == 'CT':\n                    img = apply_ct_window(img, window_center=40, window_width=80)\n                elif modality == 'MRI':\n                    img = preprocess_mri(img)\n                else:\n                    img = (img - np.min(img)) / (np.max(img) - np.min(img) + 1e-5)\n                slices.append(img)\n        else:\n            # Single 2D image\n            img = dcm.pixel_array.astype(np.float32)\n            img = cv2.resize(img, img_size)\n            modality = getattr(dcm, 'Modality', 'CT')\n            if modality == 'CT':\n                img = apply_ct_window(img, window_center=40, window_width=80)\n            elif modality == 'MRI':\n                img = preprocess_mri(img)\n            else:\n                img = (img - np.min(img)) / (np.max(img) - np.min(img) + 1e-5)\n            slices.append(img)\n    else:\n        # Multiple 2D slices\n        dicom_files = sort_slices_by_position(dicom_files)\n        for dcm_file in dicom_files:\n            dcm = pydicom.dcmread(dcm_file)\n            img = dcm.pixel_array.astype(np.float32)\n            img = cv2.resize(img, img_size)\n            modality = getattr(dcm, 'Modality', 'CT')\n            if modality == 'CT':\n                img = apply_ct_window(img, window_center=40, window_width=80)\n            elif modality == 'MRI':\n                img = preprocess_mri(img)\n            else:\n                img = (img - np.min(img)) / (np.max(img) - np.min(img) + 1e-5)\n            slices.append(img)\n    # Pad or crop to num_slices\n    if len(slices) < num_slices:\n        pad = [np.zeros(img_size, dtype=np.float32)] * (num_slices - len(slices))\n        slices = pad + slices\n    elif len(slices) > num_slices:\n        center = len(slices) // 2\n        slices = slices[center - num_slices//2 : center + num_slices//2]\n    volume = np.stack(slices, axis=0)  # shape: (num_slices, H, W)\n    # Add channel dimension for 3D CNN: (C, D, H, W)\n    volume = np.expand_dims(volume, axis=0)  # (1, D, H, W)\n    return volume\n\n# 预加载所有 DICOM 卷到内存缓存\ndef cache_volume_to_disk(series_id, volume, cache_dir):\n    cache_path = cache_dir / f\"{series_id}.npy\"\n    np.save(cache_path, volume)\n\ndef load_volume_from_disk(series_id, cache_dir):\n    cache_path = cache_dir / f\"{series_id}.npy\"\n    if cache_path.exists():\n        return np.load(cache_path)\n    return None\n\ndef preload_dicom_volumes(df, series_dir, img_size=(128,128), num_slices=64, cache_dir=None):\n    cache = {}\n    for idx, row in df.iterrows():\n        series_id = row[ID_COL]\n        if cache_dir:\n            volume = load_volume_from_disk(series_id, cache_dir)\n            if volume is not None:\n                cache[series_id] = volume\n                continue\n        series_path = series_dir / str(series_id)\n        volume = load_dicom_series(series_path, img_size, num_slices)\n        cache[series_id] = volume\n        if cache_dir:\n            cache_volume_to_disk(series_id, volume, cache_dir)\n    return cache\n\n#多线程加载\ndef preprocess_worker(args):\n    series_id, series_path, img_size, num_slices = args\n    volume = load_dicom_series(series_path, img_size, num_slices)\n    return series_id, volume\n\ndef preload_dicom_volumes_parallel(df, series_dir, img_size=(128,128), num_slices=64, num_workers=4):\n    args_list = [\n        (row[ID_COL], series_dir / str(row[ID_COL]), img_size, num_slices)\n        for _, row in df.iterrows()\n    ]\n    cache = {}\n    with mp.Pool(num_workers) as pool:\n        for series_id, volume in pool.imap(preprocess_worker, args_list):\n            cache[series_id] = volume\n    return cache\n\n# =========================\n# 数据增强 Transform 示例\n# =========================\nclass Simple3DTransform:\n    def __init__(self, flip_prob=0.5, rotate_prob=0.5, normalize=True):\n        self.flip_prob = flip_prob\n        self.rotate_prob = rotate_prob\n        self.normalize = normalize\n\n    def __call__(self, volume):\n        # volume shape: (1, D, H, W)\n        vol = volume.copy()\n\n        # Random flip along depth axis\n        if np.random.rand() < self.flip_prob:\n            vol = np.flip(vol, axis=1)  # flip D\n\n        # Random flip along height axis\n        if np.random.rand() < self.flip_prob:\n            vol = np.flip(vol, axis=2)  # flip H\n\n        # Random flip along width axis\n        if np.random.rand() < self.flip_prob:\n            vol = np.flip(vol, axis=3)  # flip W\n\n        # Random 90-degree rotation (depth axis stays, rotate H/W)\n        if np.random.rand() < self.rotate_prob:\n            k = np.random.choice([1, 2, 3])\n            vol = np.rot90(vol, k=k, axes=(2, 3))  # rotate H/W\n\n        # Normalize to [0, 1]\n        if self.normalize:\n            vol = (vol - vol.min()) / (vol.max() - vol.min() + 1e-5)\n\n        return vol.astype(np.float32)\n\n# Usage in RSNADataset\ntrain_transform_3d = Simple3DTransform(flip_prob=0.5, rotate_prob=0.5, normalize=True)\n\n# ====================================================\n# 自定义 Dataset\n# ====================================================\nclass RSNADataset(Dataset):\n    def __init__(self, df, series_dir, img_size=(128,128), num_slices=64, transforms=None, cache=None):\n        self.df = df\n        self.series_dir = series_dir\n        self.img_size = img_size\n        self.num_slices = num_slices\n        self.transforms = transforms\n        self.cache = cache\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        series_id = row[ID_COL]\n        label = row[LABEL_COLS].values.astype(np.float32)\n        if self.cache and series_id in self.cache:\n            volume = self.cache[series_id]\n        else:\n            series_path = self.series_dir / str(series_id)\n            volume = load_dicom_series(series_path, self.img_size, self.num_slices)\n        # volume shape: (1, D, H, W)\n        if self.transforms:\n            volume = self.transforms(volume)  # Apply 3D transform to whole volume\n        return torch.tensor(volume, dtype=torch.float32), torch.tensor(label, dtype=torch.float32)\n\n# ====================================================\n# 创建 DataLoaders\n# ====================================================\n# 加载 CSV\ntrain_df = pd.read_csv(CFG.train_csv)\n# Example: train_df = train_df[LABEL_COLS + [ID_COL]]\n\n# 创建 dataset 和 dataloader\ntrain_cache = preload_dicom_volumes(train_df, CFG.train_dir, img_size=(128,128), num_slices=64)\ntrain_dataset = RSNADataset(train_df, CFG.train_dir, img_size=(128,128), num_slices=64, transforms=train_transform_3d, cache=train_cache)\ntrain_loader = DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=CFG.num_workers, pin_memory=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-07T12:24:50.397019Z","iopub.execute_input":"2025-10-07T12:24:50.397525Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# 模型定义（保持不变）\n# ====================================================\nclass AttentionGate(nn.Module):\n    \"\"\"注意力门控模块\"\"\"\n    def __init__(self, in_channels, gating_channels, inter_channels):\n        super().__init__()\n        self.W_g = nn.Conv2d(gating_channels, inter_channels, kernel_size=1, stride=1, padding=0)\n        self.W_x = nn.Conv2d(in_channels, inter_channels, kernel_size=1, stride=1, padding=0)\n        self.psi = nn.Conv2d(inter_channels, 1, kernel_size=1, stride=1, padding=0)\n        self.relu = nn.ReLU(inplace=True)\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x, g):\n        g_conv = self.W_g(g)\n        x_conv = self.W_x(x)\n        psi = self.relu(g_conv + x_conv)\n        psi = self.sigmoid(self.psi(psi))\n        return x * psi\n\nclass Dense3DBlock(nn.Module):\n    \"\"\"3D卷积块\"\"\"\n    def __init__(self, in_channels, out_channels):\n        super().__init__()\n        self.conv1 = nn.Conv3d(in_channels, out_channels, kernel_size=3, padding=1)\n        self.conv2 = nn.Conv3d(out_channels, out_channels, kernel_size=3, padding=1)\n        self.bn1 = nn.BatchNorm3d(out_channels)\n        self.bn2 = nn.BatchNorm3d(out_channels)\n        self.relu = nn.ReLU(inplace=True)\n        \n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.bn1(x)\n        x = self.relu(x)\n        x = self.conv2(x)\n        x = self.bn2(x)\n        x = self.relu(x)\n        return x\n\nclass RSNA_Model(nn.Module):\n    \"\"\"融合模型架构\"\"\"\n    def __init__(self, model_name, num_classes, in_chans, pretrained=True):\n        super().__init__()\n        # 3D特征提取\n        self.dense3d = Dense3DBlock(1, 8)\n        \n        # 主干网络\n        self.backbone = timm.create_model(\n            model_name,\n            pretrained=pretrained,\n            num_classes=num_classes,\n            in_chans=in_chans + 8  # 结合原始通道和3D特征\n        )\n        \n        # 注意力机制\n        self.attention = AttentionGate(\n            in_channels=in_chans,\n            gating_channels=in_chans,\n            inter_channels=in_chans // 2\n        )\n        \n        # 分类头\n        self.classifier = nn.Sequential(\n            nn.AdaptiveAvgPool2d(1),\n            nn.Flatten(),\n            nn.Dropout(CFG.dropout),\n            nn.Linear(self.backbone.num_features, num_classes)\n        )\n        \n        self._init_weights()\n    \n    def _init_weights(self):\n        \"\"\"初始化权重\"\"\"\n        for m in self.modules():\n            if isinstance(m, nn.Conv3d):\n                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')\n                if m.bias is not None:\n                    nn.init.constant_(m.bias, 0)\n            elif isinstance(m, nn.BatchNorm3d):\n                nn.init.constant_(m.weight, 1)\n                nn.init.constant_(m.bias, 0)\n    \n    def forward(self, x):\n        # 输入形状: (B, C, H, W)\n        batch_size, channels, height, width = x.shape\n        \n        # 3D特征提取\n        x_3d = x.unsqueeze(1)  # 增加通道维度 (B, 1, C, H, W)\n        x_3d = self.dense3d(x_3d)  # (B, 8, C, H, W)\n        x_3d_agg = torch.mean(x_3d, dim=2)  # 聚合通道维度 (B, 8, H, W)\n        \n        # 注意力机制\n        x_att = self.attention(x, x)  # (B, C, H, W)\n        \n        # 特征融合\n        x_combined = torch.cat([x_att, x_3d_agg], dim=1)  # (B, C+8, H, W)\n        \n        # 主干网络特征提取\n        features = self.backbone.forward_features(x_combined)\n        \n        # 分类\n        output = self.classifier(features)\n        return output\n\n# ====================================================\n# 损失函数（保持不变）\n# ====================================================\nclass LabelSmoothingLoss(nn.Module):\n    def __init__(self, classes, smoothing=0.05):\n        super().__init__()\n        self.confidence = 1.0 - smoothing\n        self.smoothing = smoothing\n        self.classes = classes\n\n    def forward(self, pred, target):\n        pred = pred.log_softmax(dim=-1)\n        with torch.no_grad():\n            true_dist = torch.zeros_like(pred)\n            true_dist.fill_(self.smoothing / (self.classes - 1))\n            true_dist.scatter_(1, target.long().data, self.confidence)\n        return torch.mean(torch.sum(-true_dist * pred, dim=-1))\n\n# ====================================================\n# 训练和验证函数 - 优化数据加载流程\n# ====================================================\ndef train_one_chunk(model, train_df, optimizer, criterion, scheduler, scaler, epoch, chunk_idx, total_chunks):\n    \"\"\"训练一个数据块\"\"\"\n    model.train()\n    running_loss = 0.0\n    all_preds = []\n    all_labels = []\n    \n    # 创建数据集和加载器 - 优化参数\n    dataset = RSNADataset(\n        train_df,\n        CFG.train_dir,\n        img_size=(128,128),\n        num_slices=64,\n        transforms=train_transform_3d\n        # cache=train_cache  # if you have a cache, otherwise omit\n    )\n    \n    loader = DataLoader(\n        dataset,\n        batch_size=CFG.batch_size,\n        shuffle=True,\n        num_workers=CFG.num_workers,\n        pin_memory=True,\n        drop_last=True,\n        prefetch_factor=CFG.prefetch_buffer,  # 预加载\n        persistent_workers=True,  # 保持工作进程\n        worker_init_fn=lambda _: np.random.seed(torch.initial_seed() % 2**32)\n    )\n    \n    print(f\"\\nEpoch {epoch+1} - 数据块 {chunk_idx+1}/{total_chunks}\")\n    start_time = time.time()\n    \n    for batch_idx, batch in enumerate(loader):\n        images = batch['image'].to(device, non_blocking=True)\n        labels = batch['labels'].to(device, non_blocking=True)\n        \n        optimizer.zero_grad(set_to_none=True)\n        \n        with torch.amp.autocast('cuda', enabled=CFG.use_amp):\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n        \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        if scheduler is not None:\n            scheduler.step()\n        \n        # 记录损失和预测\n        batch_size = images.size(0)\n        running_loss += loss.item() * batch_size\n        \n        # 定期记录预测结果\n        if batch_idx % 50 == 0 or batch_idx == len(loader) - 1:\n            preds = torch.sigmoid(outputs).detach().cpu().numpy()\n            all_preds.append(preds)\n            all_labels.append(labels.detach().cpu().numpy())\n        \n        # 打印进度\n        if batch_idx % 20 == 0:\n            print(f\"  批次 {batch_idx+1}/{len(loader)} - 损失: {loss.item():.4f}\")\n        \n        # 清理\n        del images, labels, outputs, loss\n    \n    # 计算指标\n    epoch_loss = running_loss / len(loader.dataset)\n    all_preds = np.concatenate(all_preds, axis=0) if all_preds else np.array([])\n    all_labels = np.concatenate(all_labels, axis=0) if all_labels else np.array([])\n    \n    mean_auc = 0.0\n    if len(all_preds) > 0 and len(all_labels) > 0:\n        auc_scores = []\n        for i in range(NUM_CLASSES):\n            try:\n                if np.sum(all_labels[:, i]) > 0:\n                    auc = roc_auc_score(all_labels[:, i], all_preds[:, i])\n                else:\n                    auc = 0.5\n                auc_scores.append(auc)\n            except:\n                auc_scores.append(0.5)\n        mean_auc = np.mean(auc_scores)\n    \n    # 输出块训练时间\n    chunk_time = time.time() - start_time\n    print(f\"  数据块训练时间: {chunk_time:.2f}秒 - 平均损失: {epoch_loss:.4f} - AUC: {mean_auc:.4f}\")\n    \n    # 清理\n    del loader, dataset, all_preds, all_labels\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    return epoch_loss, mean_auc\n\ndef validate(model, val_df, criterion):\n    \"\"\"验证模型\"\"\"\n    model.eval()\n    running_loss = 0.0\n    all_preds = []\n    all_labels = []\n    \n    # 创建验证数据集和加载器\n    dataset = RSNADataset(\n        val_df,\n        CFG.train_dir,\n        img_size=(128,128),\n        num_slices=64,\n        transforms=train_transform_3d,\n        #cache=val_cache  # if you have a cache, otherwise omit\n    )\n    \n    loader = DataLoader(\n        dataset,\n        batch_size=CFG.val_batch_size,\n        shuffle=False,\n        num_workers=CFG.num_workers,\n        pin_memory=True,\n        prefetch_factor=CFG.prefetch_buffer,\n        persistent_workers=True\n    )\n    \n    start_time = time.time()\n    \n    with torch.no_grad():\n        for batch in loader:\n            images = batch['image'].to(device, non_blocking=True)\n            labels = batch['labels'].to(device, non_blocking=True)\n            \n            with torch.amp.autocast('cuda', enabled=CFG.use_amp):\n                outputs = model(images)\n                loss = criterion(outputs, labels)\n            \n            running_loss += loss.item() * images.size(0)\n            preds = torch.sigmoid(outputs).detach().cpu().numpy()\n            all_preds.append(preds)\n            all_labels.append(labels.detach().cpu().numpy())\n            \n            del images, labels, outputs, loss\n    \n    # 计算指标\n    val_loss = running_loss / len(loader.dataset)\n    all_preds = np.concatenate(all_preds, axis=0) if all_preds else np.array([])\n    all_labels = np.concatenate(all_labels, axis=0) if all_labels else np.array([])\n    \n    mean_auc = 0.0\n    if len(all_preds) > 0 and len(all_labels) > 0:\n        auc_scores = []\n        for i in range(NUM_CLASSES):\n            try:\n                if np.sum(all_labels[:, i]) > 0:\n                    auc = roc_auc_score(all_labels[:, i], all_preds[:, i])\n                else:\n                    auc = 0.5\n                auc_scores.append(auc)\n            except:\n                auc_scores.append(0.5)\n        mean_auc = np.mean(auc_scores)\n    \n    # 输出验证时间\n    val_time = time.time() - start_time\n    print(f\"验证时间: {val_time:.2f}秒 - 损失: {val_loss:.4f} - AUC: {mean_auc:.4f}\")\n    \n    # 清理\n    del loader, dataset, all_preds, all_labels\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    return val_loss, mean_auc\n\n# ====================================================\n# 主训练函数 - 增加预加载步骤\n# ====================================================\ndef train_fold(fold, train_df, val_df):\n    print(f\"\\n{'='*50}\")\n    print(f\"开始训练 Fold {fold}\")\n    print(f\"训练样本: {len(train_df)}, 验证样本: {len(val_df)}\")\n    print(f\"{'='*50}\\n\")\n    \n    # 预加载当前fold的缓存\n    print(\"预加载训练集缓存...\")\n    train_cache = preload_dicom_volumes(train_df, CFG.train_dir, img_size=(128,128), num_slices=64)\n    print(\"预加载验证集缓存...\")\n    val_cache = preload_dicom_volumes(val_df, CFG.train_dir, img_size=(128,128), num_slices=64)\n    \n    # 初始化模型\n    model = RSNA_Model(\n        model_name=CFG.model_name,\n        num_classes=NUM_CLASSES,\n        in_chans=CFG.in_channels,\n        pretrained=CFG.pretrained\n    )\n    model.to(device)\n    \n    # 数据并行（如果有多个GPU）\n    if torch.cuda.device_count() > 1:\n        model = nn.DataParallel(model)\n    \n    # 损失函数和优化器\n    if CFG.label_smoothing > 0:\n        criterion = LabelSmoothingLoss(classes=NUM_CLASSES, smoothing=CFG.label_smoothing)\n    else:\n        criterion = nn.BCEWithLogitsLoss()\n    \n    optimizer = AdamW(\n        model.parameters(),\n        lr=CFG.lr,\n        weight_decay=CFG.weight_decay\n    )\n    \n    scheduler = CosineAnnealingWarmRestarts(\n        optimizer,\n        T_0=CFG.T_0,\n        eta_min=CFG.min_lr\n    )\n    \n    scaler = torch.amp.GradScaler('cuda', enabled=CFG.use_amp)\n    \n    # 训练记录\n    best_auc = 0.0\n    patience_counter = 0\n    history = {\n        'train_loss': [], 'train_auc': [],\n        'val_loss': [], 'val_auc': []\n    }\n    \n    # 计算数据块数量\n    total_chunks = max(1, len(train_df) // CFG.chunk_size)\n    print(f\"数据分为 {total_chunks} 块，每块最多 {CFG.chunk_size} 样本\")\n    \n    # 开始训练\n    for epoch in range(CFG.epochs):\n        start_time = time.time()\n        train_loss = 0.0\n        train_auc = 0.0\n        \n        # 按块训练\n        for chunk_idx in range(total_chunks):\n            # 获取当前块数据\n            start = chunk_idx * CFG.chunk_size\n            end = min(start + CFG.chunk_size, len(train_df))\n            chunk_df = train_df.iloc[start:end].copy()\n            \n            # 训练当前块\n            chunk_loss, chunk_auc = train_one_chunk(\n                model, chunk_df, optimizer, criterion, scheduler,\n                scaler, epoch, chunk_idx, total_chunks\n            )\n            \n            # 累加\n            train_loss += chunk_loss * len(chunk_df)\n            train_auc += chunk_auc * len(chunk_df)\n            \n            # 清理\n            del chunk_df\n        \n        # 计算平均训练指标\n        train_loss /= len(train_df)\n        train_auc /= len(train_df)\n        \n        # 验证\n        val_loss, val_auc = validate(model, val_df, criterion)\n        \n        # 记录历史\n        history['train_loss'].append(train_loss)\n        history['train_auc'].append(train_auc)\n        history['val_loss'].append(val_loss)\n        history['val_auc'].append(val_auc)\n        \n        # 打印 epoch 结果\n        epoch_time = time.time() - start_time\n        print(f\"\\nEpoch {epoch+1}/{CFG.epochs} - 总耗时: {epoch_time:.2f}秒\")\n        print(f\"训练损失: {train_loss:.4f} - 训练AUC: {train_auc:.4f}\")\n        print(f\"验证损失: {val_loss:.4f} - 验证AUC: {val_auc:.4f}\")\n        \n        # 保存最佳模型\n        if val_auc > best_auc:\n            best_auc = val_auc\n            patience_counter = 0\n            model_path = CFG.model_dir / f\"model_fold{fold}_best.pth\"\n            torch.save(model.state_dict(), model_path)\n            print(f\"保存最佳模型到 {model_path} (AUC: {best_auc:.4f})\")\n        else:\n            patience_counter += 1\n            if patience_counter >= CFG.patience:\n                print(f\"早停: {CFG.patience} 个epoch未改善\")\n                break\n    \n    # 绘制训练曲线\n    plt.figure(figsize=(12, 5))\n    plt.subplot(1, 2, 1)\n    plt.plot(history['train_loss'], label='训练损失')\n    plt.plot(history['val_loss'], label='验证损失')\n    plt.title(f'Fold {fold} 损失曲线')\n    plt.xlabel('Epoch')\n    plt.ylabel('损失')\n    plt.legend()\n    \n    plt.subplot(1, 2, 2)\n    plt.plot(history['train_auc'], label='训练AUC')\n    plt.plot(history['val_auc'], label='验证AUC')\n    plt.title(f'Fold {fold} AUC曲线')\n    plt.xlabel('Epoch')\n    plt.ylabel('AUC')\n    plt.legend()\n    \n    plt.tight_layout()\n    plt.savefig(CFG.output_dir / f\"fold_{fold}_metrics.png\")\n    plt.close()\n    \n    # 清理\n    del model, optimizer, scheduler, scaler\n    gc.collect()\n    torch.cuda.empty_cache()\n    \n    return best_auc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-07T11:05:04.805911Z","iopub.status.idle":"2025-10-07T11:05:04.806139Z","shell.execute_reply.started":"2025-10-07T11:05:04.806033Z","shell.execute_reply":"2025-10-07T11:05:04.806043Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ====================================================\n# 主执行\n# ====================================================\nif __name__ == \"__main__\":\n    print(\"加载训练数据...\")\n    train_df = pd.read_csv(CFG.train_csv)\n    print(f\"训练数据形状: {train_df.shape}\")\n\n    # 查看标签分布\n    print(\"\\n标签分布:\")\n    print(train_df[LABEL_COLS].sum().sort_values(ascending=False))\n\n    # 创建交叉验证\n    skf = StratifiedKFold(n_splits=CFG.fold, shuffle=True, random_state=42)\n    train_df['fold'] = -1\n    for fold, (_, val_idx) in enumerate(skf.split(train_df, train_df['Aneurysm Present'])):\n        train_df.loc[val_idx, 'fold'] = fold\n\n    train_df.to_csv(CFG.output_dir / 'train_with_folds.csv', index=False)\n\n    fold_aucs = []\n    for fold in CFG.selected_folds:\n        print(f\"\\nFold {fold} 数据预加载中...\")\n        train_fold_df = train_df[train_df['fold'] != fold].reset_index(drop=True)\n        val_fold_df = train_df[train_df['fold'] == fold].reset_index(drop=True)\n\n        # 使用多进程加速预加载\n        train_cache = preload_dicom_volumes_parallel(\n            train_fold_df, CFG.train_dir, img_size=(128,128), num_slices=64, num_workers=CFG.num_workers\n        )\n        val_cache = preload_dicom_volumes_parallel(\n            val_fold_df, CFG.train_dir, img_size=(128,128), num_slices=64, num_workers=CFG.num_workers\n        )\n\n        # 创建 Dataset 和 DataLoader\n        train_dataset = RSNADataset(train_fold_df, CFG.train_dir, img_size=(128,128), num_slices=64, transforms=train_transform_3d, cache=train_cache)\n        val_dataset = RSNADataset(val_fold_df, CFG.train_dir, img_size=(128,128), num_slices=64, transforms=train_transform_3d, cache=val_cache)\n        train_loader = DataLoader(train_dataset, batch_size=CFG.batch_size, shuffle=True, num_workers=CFG.num_workers, pin_memory=True)\n        val_loader = DataLoader(val_dataset, batch_size=CFG.val_batch_size, shuffle=False, num_workers=CFG.num_workers, pin_memory=True)\n\n        # 训练\n        fold_auc = train_fold(fold, train_fold_df, val_fold_df)\n        fold_aucs.append(fold_auc)\n\n        # 清理缓存为下一个fold做准备\n        del train_cache, val_cache, train_dataset, val_dataset, train_loader, val_loader\n        gc.collect()\n        torch.cuda.empty_cache()\n\n    print(\"\\n\" + \"=\"*50)\n    print(\"所有Fold训练结果:\")\n    for i, auc in enumerate(fold_aucs):\n        print(f\"Fold {i}: {auc:.4f}\")\n    print(f\"平均AUC: {np.mean(fold_aucs):.4f} ± {np.std(fold_aucs):.4f}\")\n    print(\"=\"*50)\n    \n    # 保存总结\n    with open(CFG.output_dir / \"training_summary.txt\", \"w\") as f:\n        f.write(\"RSNA颅内动脉瘤检测训练总结\\n\")\n        f.write(\"=\"*50 + \"\\n\")\n        for i, auc in enumerate(fold_aucs):\n            f.write(f\"Fold {i}: {auc:.4f}\\n\")\n        f.write(f\"平均AUC: {np.mean(fold_aucs):.4f} ± {np.std(fold_aucs):.4f}\\n\")\n    \n    print(\"训练完成!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-07T11:05:04.806896Z","iopub.status.idle":"2025-10-07T11:05:04.807211Z","shell.execute_reply.started":"2025-10-07T11:05:04.807027Z","shell.execute_reply":"2025-10-07T11:05:04.807043Z"}},"outputs":[],"execution_count":null}]}