{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":10338,"databundleVersionId":862042,"sourceType":"competition"},{"sourceId":311450,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":264153,"modelId":285251}],"dockerImageVersionId":30919,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# RSNA肺炎挑战\n\n## 安装依赖&导入库","metadata":{}},{"cell_type":"code","source":"!pip install pydicom albumentations\n!pip install pydicom albumentations timm\n\nimport os\nimport cv2\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torchvision\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.models.detection import FasterRCNN\nfrom torchvision.ops import FeaturePyramidNetwork\nfrom torchvision.models.detection.rpn import AnchorGenerator\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm import tqdm\nimport timm  # 用于加载 EfficientNet 模型\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nimport seaborn as sns\nfrom sklearn.metrics import confusion_matrix\nfrom sklearn.metrics import precision_recall_curve, average_precision_score\n\nimport warnings\nwarnings.filterwarnings('ignore')\n\nos.environ[\"CUDA_LAUNCH_BLOCKING\"] = \"1\"\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T09:58:28.421633Z","iopub.execute_input":"2025-03-31T09:58:28.421913Z","iopub.status.idle":"2025-03-31T09:58:48.737075Z","shell.execute_reply.started":"2025-03-31T09:58:28.421890Z","shell.execute_reply":"2025-03-31T09:58:48.736287Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 设置数据路径\n","metadata":{}},{"cell_type":"code","source":"TRAIN_DIR = \"../input/rsna-pneumonia-detection-challenge/stage_2_train_images/\"\nTEST_DIR = \"../input/rsna-pneumonia-detection-challenge/stage_2_test_images/\"\nTRAIN_LABELS = \"../input/rsna-pneumonia-detection-challenge/stage_2_train_labels.csv\"\nDETAILED_CLASS_INFO = \"../input/rsna-pneumonia-detection-challenge/stage_2_detailed_class_info.csv\"\nOUTPUT_DIR = \"/kaggle/working/\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T09:58:48.743295Z","iopub.execute_input":"2025-03-31T09:58:48.743606Z","iopub.status.idle":"2025-03-31T09:58:48.766552Z","shell.execute_reply.started":"2025-03-31T09:58:48.743575Z","shell.execute_reply":"2025-03-31T09:58:48.765736Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 定义数据集&数据预处理\n","metadata":{}},{"cell_type":"code","source":"torch.backends.cudnn.benchmark = True\nprint(\"Imports and configuration completed.\")\n\nclass PneumoniaDataset(Dataset):\n    def __init__(self, dataframe, image_dir, transforms=None, mode='train', class_info=None):\n        \"\"\"\n        初始化数据集类\n        :param dataframe: 数据集的 DataFrame，包含标注信息\n        :param image_dir: 图像所在目录\n        :param transforms: 数据增强函数\n        :param mode: 'train' 或 'test'\n        :param class_info: 类别信息 (从 stage_2_detailed_class_info.csv 读取)\n        \"\"\"\n        self.class_info = class_info  # 将 class_info 赋值给实例属性\n        self.dataframe = self._preprocess(dataframe, mode)\n        self.image_dir = image_dir\n        self.transforms = transforms\n        self.mode = mode\n        # 创建一个映射字典，确保标签从1开始（0通常是背景类）\n        self.label_map = {\"No Lung Opacity / Not Normal\": 1, \"Normal\": 2, \"Lung Opacity\": 3}\n        # 过滤掉没有有效边界框的患者ID\n        self.valid_patient_ids = self.dataframe.patientId.unique()\n\n    def _preprocess(self, df, mode):\n        \"\"\"处理重复标注和路径问题\"\"\"\n        # 合并类别信息\n        if self.class_info is not None:\n            df = df.merge(self.class_info[['patientId', 'class']], on='patientId', how='left')\n            # 只保留肺部异常的样本\n            df = df[df['class'] != 'No Lung Opacity / Not Normal'].reset_index(drop=True)\n        \n        # 添加边界框的右下角坐标\n        df['x_max'] = df['x'] + df['width']\n        df['y_max'] = df['y'] + df['height']\n\n        # 丢弃包含 NaN 的样本\n        df = df.dropna(subset=['x', 'y', 'x_max', 'y_max'])\n        \n        # 过滤无效边界框 - 确保所有坐标都是有效的\n        df = df[(df['x'] >= 0) & (df['y'] >= 0) & (df['x_max'] > df['x']) & (df['y_max'] > df['y'])]\n        \n        # 确保每个患者至少有一个有效边界框\n        if mode == 'train':\n            return df.groupby('patientId').filter(lambda x: len(x) > 0)\n        return df\n\n    def __getitem__(self, idx):\n        patient_id = self.valid_patient_ids[idx].strip()  # 去除空格\n        dicom_path = os.path.join(self.image_dir, f\"{patient_id}.dcm\")\n        \n        if not os.path.exists(dicom_path):\n            raise FileNotFoundError(f\"DICOM 文件不存在: {dicom_path}\")\n            \n        dicom = pydicom.dcmread(dicom_path)\n        image = dicom.pixel_array.astype(np.float32)\n        # 归一化\n        image = (image - image.min()) / (image.max() - image.min())\n        image = np.stack([image] * 3, axis=-1)\n        \n        if self.mode == 'train':\n            # 获取当前患者的所有边界框\n            patient_df = self.dataframe[self.dataframe.patientId == patient_id]\n            boxes = patient_df[['x', 'y', 'x_max', 'y_max']].values.astype(np.float32)\n            \n            # 确保边界框有效（非空且没有NaN）\n            if len(boxes) == 0 or np.any(np.isnan(boxes)):\n                # 创建一个虚拟的边界框和标签，避免返回None\n                boxes = np.array([[10.0, 10.0, 50.0, 50.0]], dtype=np.float32)\n                labels = np.array([1], dtype=np.int64)  # 使用1作为默认标签（非背景）\n            else:\n                # 获取标签并映射\n                if 'class' in patient_df.columns:\n                    labels = patient_df['class'].map(self.label_map).values\n                else:\n                    # 如果没有类别信息，默认使用标签1\n                    labels = np.ones(len(boxes), dtype=np.int64)\n            \n            # 应用数据增强\n            if self.transforms:\n                transformed = self.transforms(image=image, bboxes=boxes, labels=labels)\n                image = transformed['image']\n                boxes = np.array(transformed['bboxes'])\n                \n                # 确保变换后的边界框非空\n                if len(boxes) == 0:\n                    boxes = np.array([[10.0, 10.0, 50.0, 50.0]], dtype=np.float32)\n                    labels = np.array([1], dtype=np.int64)\n                else:\n                    labels = np.array(transformed['labels'])\n            \n            # 创建目标字典\n            target = {\n                \"boxes\": torch.as_tensor(boxes, dtype=torch.float32),\n                \"labels\": torch.as_tensor(labels, dtype=torch.int64),\n                \"image_id\": torch.tensor([idx])\n            }\n            return image, target\n        \n        return image, patient_id\n\n    def __len__(self):\n        return len(self.valid_patient_ids)\n\n\n# 数据增强操作配置\ntrain_transform = A.Compose([\n    A.HorizontalFlip(p=0.5),\n    A.RandomResizedCrop(\n        size=(512, 512),\n        scale=(0.08, 1.0),\n        ratio=(0.75, 1.33)\n    ),\n    A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.2, rotate_limit=15, p=0.5),\n    ToTensorV2()\n], bbox_params=A.BboxParams(\n    format='pascal_voc',\n    label_fields=['labels'],\n    min_visibility=0.3\n))\nprint(\"Dataset and augmentations defined.\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T09:58:48.785666Z","iopub.execute_input":"2025-03-31T09:58:48.785896Z","iopub.status.idle":"2025-03-31T09:58:48.805984Z","shell.execute_reply.started":"2025-03-31T09:58:48.785878Z","shell.execute_reply":"2025-03-31T09:58:48.805106Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 定义 自定义 EfficientNet-B3 + FPN 模型","metadata":{}},{"cell_type":"code","source":"class EfficientNetBackboneWithFPN(nn.Module):\n    \"\"\"\n    利用 EfficientNet-B3 预测多个层次特征，再用 FPN 融合输出字典形式的特征，\n    符合 Faster R-CNN 对 backbone 的要求。\n    \"\"\"\n    def __init__(self, model_name='efficientnet_b3', out_channels=256, pretrained=True, out_indices=None):\n        \"\"\"\n        :param model_name: EfficientNet 模型名称\n        :param out_channels: FPN 输出的通道数\n        :param pretrained: 是否加载预训练权重\n        :param out_indices: 指定哪些特征层用于 FPN，默认为最后4个特征层\n        \"\"\"\n        super().__init__()\n        self.backbone = timm.create_model(model_name, features_only=True, pretrained=pretrained)\n        if out_indices is None:\n            out_indices = [1, 2, 3, 4]\n        self.out_indices = out_indices\n        in_channels_list = [self.backbone.feature_info[i]['num_chs'] for i in out_indices]\n        self.fpn = FeaturePyramidNetwork(\n            in_channels_list=in_channels_list,\n            out_channels=out_channels\n        )\n        \n    def forward(self, x):\n        features = self.backbone(x)\n        features = [features[i] for i in self.out_indices]\n        features_dict = {str(i): feature for i, feature in enumerate(features)}\n        x = self.fpn(features_dict)\n        return x\n\ndef get_model(num_classes=4, score_thresh=0.5):  # 增加到4类，包括背景类\n    # 构造自定义 backbone\n    custom_backbone = EfficientNetBackboneWithFPN(model_name='efficientnet_b3', out_channels=256, pretrained=True)\n    custom_backbone.out_channels = 256  # 必须设置\n\n    # 定义 AnchorGenerator，为每个 FPN 层指定尺寸，注意这里 4 个元素对应 4 个特征图\n    anchor_generator = AnchorGenerator(\n        sizes=((32,), (64,), (128,), (256,)),\n        aspect_ratios=((0.5, 1.0, 2.0),) * 4\n    )\n\n    # 构造 Faster R-CNN 模型时传入自定义的 rpn_anchor_generator\n    model = FasterRCNN(\n        custom_backbone, \n        num_classes=num_classes,  # 包括背景类\n        rpn_anchor_generator=anchor_generator,\n        min_size=512,  # 确保输入图像大小一致\n        max_size=512\n    )\n    model.roi_heads.score_thresh = score_thresh  # 设置候选框过滤阈值\n    return model\n\nprint(\"Custom model defined.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T09:58:48.827035Z","iopub.execute_input":"2025-03-31T09:58:48.827292Z","iopub.status.idle":"2025-03-31T09:58:48.843089Z","shell.execute_reply.started":"2025-03-31T09:58:48.827272Z","shell.execute_reply":"2025-03-31T09:58:48.842208Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 训练评估和预测函数","metadata":{}},{"cell_type":"code","source":"def collate_fn(batch):\n    filtered_batch = list(filter(lambda x: x is not None, batch))\n    if len(filtered_batch) == 0:\n        return [], []\n    images = [item[0] for item in filtered_batch]\n    targets = [item[1] for item in filtered_batch]\n    return images, targets\n\ndef train_one_epoch(model, optimizer, loader, device):\n    model.train()\n    total_loss = 0\n    valid_batches = 0\n    progress = tqdm(loader, desc='Training')\n    \n    for images, targets in progress:\n        # 跳过空批次\n        if len(images) == 0:\n            continue\n            \n        try:\n            images = [img.to(device) for img in images]\n            targets = [{k: v.to(device) for k, v in t.items()} for t in targets]\n            \n            # 检查边界框和标签\n            valid_batch = True\n            for target in targets:\n                # 确保每个目标至少有一个边界框\n                if len(target['boxes']) == 0:\n                    valid_batch = False\n                    break\n                # 确保标签值在有效范围内\n                if torch.any(target['labels'] <= 0):\n                    valid_batch = False\n                    break\n            \n            if not valid_batch:\n                continue\n                \n            outputs = model(images, targets)\n            losses = sum(loss for loss in outputs.values())\n            \n            optimizer.zero_grad()\n            losses.backward()\n            optimizer.step()\n            \n            total_loss += losses.item()\n            valid_batches += 1\n            progress.set_postfix(loss=total_loss / (valid_batches))\n            \n        except Exception as e:\n            print(f\"Error in batch: {e}\")\n            continue\n    \n    return total_loss / max(1, valid_batches)\n\n\ndef predict(model, test_dir, device, threshold=0.3):\n    model.eval()\n    submission = []\n    test_files = [f.replace(' ', '') for f in os.listdir(test_dir) if f.endswith('.dcm')]\n    progress = tqdm(test_files, desc='Predicting')\n    for filename in progress:\n        patient_id = filename[:-4]\n        dicom_path = os.path.join(test_dir, filename)\n        dicom = pydicom.dcmread(dicom_path)\n        image = dicom.pixel_array.astype(np.float32)\n        image = (image - image.min()) / (image.max() - image.min())\n        image = np.stack([image] * 3, axis=-1).transpose(2, 0, 1)\n        image = torch.from_numpy(image).unsqueeze(0).to(device)\n        \n        with torch.no_grad():\n            preds = model(image)\n        boxes = preds[0]['boxes'].cpu().numpy()\n        scores = preds[0]['scores'].cpu().numpy()\n        keep = scores > threshold\n        boxes = boxes[keep]\n        scores = scores[keep]\n        \n        if len(scores) > 0:\n            order = np.argsort(-scores)\n            boxes = boxes[order]\n            scores = scores[order]\n            boxes[:, 2] = boxes[:, 2] - boxes[:, 0]\n            boxes[:, 3] = boxes[:, 3] - boxes[:, 1]\n            pred_str = ' '.join([f\"{s:.4f} {x:.1f} {y:.1f} {w:.1f} {h:.1f}\" \n                                 for s, (x, y, w, h) in zip(scores, boxes)])\n        else:\n            pred_str = \"\"\n        submission.append({'patientId': patient_id, 'PredictionString': pred_str})\n    return pd.DataFrame(submission)\n\nprint(\"Training and prediction functions defined.\")\n\ndef visualize_samples(model, dataset, indices, device, save_path=None, show=True):\n    \"\"\"可视化几个样本的预测结果，并可选择显示和保存\"\"\"\n    fig, axes = plt.subplots(2, 3, figsize=(15, 10))\n    axes = axes.flatten()\n    \n    for i, idx in enumerate(indices):\n        if i >= len(axes):\n            break\n            \n        image, target = dataset[idx]\n        \n        # 预测\n        image_tensor = image.unsqueeze(0).to(device)\n        with torch.no_grad():\n            preds = model(image_tensor)\n        \n        # 转回numpy显示\n        image_np = image.permute(1, 2, 0).cpu().numpy()\n        \n        # 显示图像\n        axes[i].imshow(image_np[:,:,0], cmap='gray')\n        \n        # 绘制真实边界框\n        for box, label in zip(target['boxes'], target['labels']):\n            x, y, x_max, y_max = box.cpu().numpy()\n            width = x_max - x\n            height = y_max - y\n            rect = patches.Rectangle((x, y), width, height, linewidth=2, edgecolor='g', facecolor='none')\n            axes[i].add_patch(rect)\n            axes[i].text(x, y-5, f'GT: {label.item()}', color='g', fontsize=8, backgroundcolor='w')\n        \n        # 绘制预测边界框\n        for box, score, label in zip(preds[0]['boxes'], preds[0]['scores'], preds[0]['labels']):\n            if score > 0.5:  # 只显示高置信度的预测\n                x, y, x_max, y_max = box.cpu().numpy()\n                width = x_max - x\n                height = y_max - y\n                rect = patches.Rectangle((x, y), width, height, linewidth=2, edgecolor='r', facecolor='none')\n                axes[i].add_patch(rect)\n                axes[i].text(x, y+height+5, f'Pred: {label.item()} ({score:.2f})', color='r', fontsize=8, backgroundcolor='w')\n        \n        axes[i].set_title(f\"Sample {idx}\")\n        axes[i].axis('off')\n    \n    plt.tight_layout()\n    \n    # 保存图片（如果提供了路径）\n    if save_path:\n        plt.savefig(save_path)\n        print(f\"图像已保存至: {save_path}\")\n    \n    # 显示图片（如果需要）\n    if show:\n        plt.show()\n    else:\n        plt.close()\n\n    return fig  # 返回图形对象，便于进一步处理\n\ndef visualize_confusion_matrix(model, dataset, device, save_path=None):\n    \"\"\"绘制混淆矩阵\"\"\"\n    model.eval()\n    all_preds = []\n    all_labels = []\n    \n    # 使用DataLoader批量处理数据\n    test_loader = DataLoader(dataset, batch_size=8, shuffle=False, collate_fn=collate_fn)\n    \n    with torch.no_grad():\n        for images, targets in test_loader:\n            images = [img.to(device) for img in images]\n            outputs = model(images)\n            \n            # 获取预测结果\n            for i, output in enumerate(outputs):\n                pred_boxes = output['boxes'].cpu().numpy()\n                pred_labels = output['labels'].cpu().numpy()\n                pred_scores = output['scores'].cpu().numpy()\n                \n                # 获取得分最高的预测\n                if len(pred_scores) > 0:\n                    best_idx = np.argmax(pred_scores)\n                    all_preds.append(pred_labels[best_idx])\n                else:\n                    all_preds.append(0)  # 背景类\n                \n                # 获取真实标签\n                true_boxes = targets[i]['boxes'].cpu().numpy()\n                true_labels = targets[i]['labels'].cpu().numpy()\n                \n                if len(true_labels) > 0:\n                    all_labels.append(true_labels[0])  # 取第一个标签\n                else:\n                    all_labels.append(0)  # 背景类\n    \n    # 计算混淆矩阵\n    cm = confusion_matrix(all_labels, all_preds)\n    \n    # 绘制混淆矩阵\n    plt.figure(figsize=(10, 8))\n    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n                xticklabels=['Background', 'Class 1', 'Class 2', 'Class 3'],\n                yticklabels=['Background', 'Class 1', 'Class 2', 'Class 3'])\n    plt.xlabel('Predicted Label')\n    plt.ylabel('True Label')\n    plt.title('Confusion Matrix')\n    \n    if save_path:\n        plt.savefig(save_path)\n        print(f\"混淆矩阵已保存至: {save_path}\")\n    \n    plt.show()\n\ndef visualize_precision_recall(model, dataset, device, save_path=None):\n    \"\"\"绘制精确率-召回率曲线\"\"\"\n    model.eval()\n    class_scores = {1: [], 2: [], 3: []}  # 每个类别的得分\n    class_labels = {1: [], 2: [], 3: []}  # 每个类别的真实标签\n    \n    test_loader = DataLoader(dataset, batch_size=8, shuffle=False, collate_fn=collate_fn)\n    \n    with torch.no_grad():\n        for images, targets in test_loader:\n            images = [img.to(device) for img in images]\n            outputs = model(images)\n            \n            for i, output in enumerate(outputs):\n                pred_boxes = output['boxes'].cpu().numpy()\n                pred_labels = output['labels'].cpu().numpy()\n                pred_scores = output['scores'].cpu().numpy()\n                \n                true_boxes = targets[i]['boxes'].cpu().numpy()\n                true_labels = targets[i]['labels'].cpu().numpy()\n                \n                # 为每个类别收集预测分数和真实标签\n                for cls in [1, 2, 3]:\n                    # 找到该类别的预测\n                    cls_indices = np.where(pred_labels == cls)[0]\n                    if len(cls_indices) > 0:\n                        max_score_idx = cls_indices[np.argmax(pred_scores[cls_indices])]\n                        class_scores[cls].append(pred_scores[max_score_idx])\n                    else:\n                        class_scores[cls].append(0.0)\n                    \n                    # 检查真实标签中是否有该类别\n                    class_labels[cls].append(1 if cls in true_labels else 0)\n    \n    # 绘制每个类别的PR曲线\n    plt.figure(figsize=(12, 8))\n    \n    for cls in [1, 2, 3]:\n        precision, recall, _ = precision_recall_curve(class_labels[cls], class_scores[cls])\n        ap = average_precision_score(class_labels[cls], class_scores[cls])\n        \n        plt.plot(recall, precision, lw=2, \n                 label=f'Class {cls} (AP = {ap:.2f})')\n    \n    plt.xlabel('Recall')\n    plt.ylabel('Precision')\n    plt.title('Precision-Recall Curve')\n    plt.legend(loc='best')\n    plt.grid(True)\n    \n    if save_path:\n        plt.savefig(save_path)\n        print(f\"PR曲线已保存至: {save_path}\")\n    \n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T09:58:48.864254Z","iopub.execute_input":"2025-03-31T09:58:48.864629Z","iopub.status.idle":"2025-03-31T09:58:48.886593Z","shell.execute_reply.started":"2025-03-31T09:58:48.864594Z","shell.execute_reply":"2025-03-31T09:58:48.885680Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 主函数，数据加载&训练","metadata":{}},{"cell_type":"code","source":"if __name__ == '__main__':\n    # 适用于多进程环境（例如 Windows）\n    torch.multiprocessing.freeze_support()\n    # 初始化设备（Kaggle Notebook 上用 GPU）\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    print(f\"Using device: {device}\")\n\n    # 加载类别数据信息\n    class_info_df = pd.read_csv(DETAILED_CLASS_INFO)\n    train_df = pd.read_csv(TRAIN_LABELS, encoding='utf-8')\n    \n    # 构造训练数据集（仅用于可视化）\n    train_dataset = PneumoniaDataset(train_df, TRAIN_DIR, train_transform, 'train', class_info=class_info_df)\n    \n    # 初始化模型\n    model = get_model(num_classes=4).to(device)  # 4类包括背景类\n    \n    # 加载训练好的模型\n    checkpoint_path = \"/kaggle/input/efficientnet-b3-fpn/pytorch/default/1/model_epoch_20.pth\"  # 指定要加载的模型文件\n    if os.path.exists(checkpoint_path):\n        # 添加weights_only=True以避免安全警告\n        model.load_state_dict(torch.load(checkpoint_path, weights_only=True))\n        print(f\"Loaded model from {checkpoint_path}\")\n    else:\n        print(f\"Error: Model file {checkpoint_path} not found!\")\n        exit()\n    \n    # 设置模型为评估模式\n    model.eval()\n    \n    # 可视化一些样本\n    random_indices = np.random.choice(len(train_dataset), 6, replace=False)\n    visualize_samples(model, train_dataset, random_indices, device, \n                 save_path=os.path.join(OUTPUT_DIR, \"sample_predictions.png\"),\n                 show=True)\n    \n    #  混淆矩阵可视化\n    visualize_confusion_matrix(model, train_dataset, device, \n                              os.path.join(OUTPUT_DIR, \"confusion_matrix.png\"))\n\n    #  精确率-召回率曲线\n    visualize_precision_recall(model, train_dataset, device, \n                              os.path.join(OUTPUT_DIR, \"pr_curve.png\"))\n    \n    # 生成提交文件\n    submission_df = predict(model, TEST_DIR, device, threshold=0.3)\n    submission_df.to_csv(\"submission3.csv\", index=False)\n    print(\"Submission file saved!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-31T09:58:48.887629Z","iopub.execute_input":"2025-03-31T09:58:48.887911Z","execution_failed":"2025-03-31T10:01:01.204Z"}},"outputs":[],"execution_count":null}]}