HexMIL:基于层级注意力与多实例学习的CT影像篡改检测可解释框架
在医学影像进入深度学习时代之后大家的目光大多集中在“如何让模型看得更准”却很少思考另一个同样关键的问题如果 CT 影像本身被人为篡改过模型还能不能给出可信的诊断结论更棘手的是当模型给出“伪造”判断时它能不能告诉医生自己到底依据的是哪些体素、哪些切片、哪些区域HexMIL 这篇工作正是围绕这一场景展开的。本文会从问题背景、多实例学习与层级注意力原理、可解释性设计、PyTorch 风格实现到工程落地注意事项完整拆解这套方案。1. 背景AI 篡改 CT 影像是一个真实存在的风险1.1 医学影像安全容易被忽略CT、MRI、X 光这类医学影像是医生诊断、分期、疗效评估的重要依据。过去我们默认这些影像来自正规设备采集后进入 PACS 系统中间链路相对可信。但随着生成模型和图像编辑技术的进步基于深度学习的医学影像篡改已经不再停留在理论层面。攻击者可以利用生成模型在 CT 影像中合成或移除病灶也可以对影像做局部修改让诊断结果发生偏移。比如在肺部 CT 中把一个小结节“P 掉”或者把正常组织区域“画”出一个疑似病灶这类细微变化如果只靠肉眼检查很容易被忽略。更危险的是如果这类修改后的影像进入 AI 辅助诊断流程模型给出的置信度可能依然很高但结论已经是经过操控的。这正是 HexMIL 要解决的问题在一个影像序列被交给医生之前系统能否自动判断它是否经过 AI 篡改如果能能否定位到具体被修改的切片或区域1.2 为什么 CT 篡改检测比普通图像检测更难普通自然图像的真伪检测往往关注全局纹理、噪声分布、JPEG 痕迹这些特征。但 CT 影像有几个特殊性第一CT 是断层扫描一张检查会产生几十到上千张横断面切片整体是一个三维体数据。判断“整个检查是否被改过”既需要看单张切片的局部异常也需要看切片与切片之间在空间上的连续性。第二篡改往往集中在某些局部区域而不是整张图。比如只改了某个肺结节的边界或者只替换了肝脏某一段的纹理。如果我们把整组切片当作一个整体来分类局部异常很容易被正常区域的统计特征淹没。第三医学场景对“为什么”极度敏感。模型说“这份 CT 被篡改过”医生一定会追问哪个层面、哪个位置、依据是什么。如果模型只能给出一个概率数字在临床上是很难被接受的。这几个特点决定了问题不适合直接用普通 3D 分类网络来硬解而是需要一套能够“先看整体、再盯局部、最后给出证据”的建模思路。HexMIL 选择的就是这个方向。1.3 这里说的 MIL 不是 Simulink 里的 MIL顺带提一个容易混淆的概念。在系统工程和汽车电子领域MIL 常指 Model-in-the-Loop也就是模型在环测试。Simulink 里常说的 MIL 测试就是这种。而在本文场景中MIL 是 Multiple Instance Learning多实例学习。两者英文缩写一样含义完全不同讨论时最好根据上下文明确指代避免沟通成本。2. 核心概念包、实例、注意力与可解释性2.1 多实例学习从“一张图”到“一组图”多实例学习的基本单位是包Bag和实例Instance。一个包包含多个实例但只有“包级标签”没有实例级标签。放到 CT 篡改检测场景中一个包 一份 CT 检查的全部切片或者整组 3D Patch包级标签 这份 CT 是否被篡改实例 每一张 2D 切片或每一个 3D Patch实例级标签 某张切片是否被篡改这些通常没有标注。传统监督学习需要每个样本都有标签但多实例学习只需要包级标签就能训练。模型要做的是在训练过程中自动找出“哪些实例与包级标签关系最大”。这个性质非常适合 CT 篡改检测我们不太可能拿到大量逐像素标注的篡改数据但比较容易拿到“这份检查被改过 / 没被改过”的粗粒度标签。MIL 的关键决策点在聚合方式。早期方法用 max-pooling 或 mean-pooling 聚合实例特征但前者只关注最极端的实例后者会稀释局部信息。注意力机制出现后实例聚合从“硬编码统计量”变成了“可学习的加权求和”。每个实例的注意力权重就代表了它对包级判断的贡献程度。2.2 注意力机制为什么要“层级化”单层注意力在实例数量较少时效果不错但 CT 体数据非常大。假设一个检查有 300 张切片每张切片切分成 16 个 Patch那就有 4800 个实例。如果在这 4800 个实例上直接做注意力模型要同时权衡大量实例之间的全局关系训练难度高权重也容易分散到无关区域。HexMIL 的核心思路是“层级注意力”。它不在一层之内完成所有实例的权衡而是分成多个层级每个层级关注不同粒度的结构。底层关注切片内部的小区域判断某个局部 Patch 是否异常中层关注切片之间的上下文判断哪些切片整体上更可疑顶层在整个体数据层面做最终聚合输出“是否被篡改”。这种结构的优势是显著降低了单层注意力的决策负担。每一层只需要在自己负责的范围内做相对简单的判断上一层拿到下一层的输出后再做进一步筛选。这个过程更接近医生的工作方式先快速浏览所有切片锁定可疑层面再放大到层面内的局部区域仔细确认。2.3 Ante-Hoc 可解释 vs Post-Hoc 可解释在可解释性研究里经常出现两个词Post-Hoc 和 Ante-Hoc。Post-Hoc 是指在模型训练完成后再用外部工具去解释模型行为比如 LIME、SHAP、Grad-CAM。这类方法的优点是不需要修改模型结构缺点也很明显解释工具本身就存在近似误差而且解释结果并不能保证忠实反映模型的真实决策路径。Ante-Hoc 则是在模型设计阶段就把可解释性内置进去。模型的输出不仅包含分类结果还包含结构化的解释信息比如每个实例的注意力权重。这些权重不是事后用工具猜出来的而是模型在产生预测结果时自然形成的中间产物。HexMIL 的可解释性属于后者。它的层级注意力权重天然可以作为“证据”某个区域权重高说明模型在判断篡改时确实重点关注了这个区域。这种解释在结构上是忠实的因为它就是模型内部计算流程的一部分。3. HexMIL 整体设计思路3.1 从 CT 体数据到包-实例结构在设计模型之前需要先确定“实例”的粒度。实践中常见两种切法一种是切片级把 CT 体数据按 Z 轴拆成一组 2D 切片每个切片是一个实例。优点是结构简单可以直接复用成熟的 2D 预训练模型。缺点是丢失了相邻切片之间的空间上下文。另一种是 Patch 级或块级先把体数据切成若干 3D Patch或者先切 2D 切片再切成 2D Patch。优点是空间粒度更细缺点是实例数量暴增计算开销明显上升。HexMIL 这类层级方法更合理的做法是混合粒度底层用 Patch 级特征中层用切片级聚合顶层在序列级别做最终判断。这样既保留了局部细节又控制了注意力层需要处理的数量级。3.2 三个层级的注意力流从信息流动的角度看HexMIL 可以分为三层第一层是局部感知层。它的输入是切片内若干 Patch 的特征向量输出是每个 Patch 的注意力权重以及聚合后的切片特征。这一层的意义在于找出切片内部的可疑小区域。第二层是切片感知层。它的输入是整份检查所有切片的特征向量输出是每个切片的注意力权重以及聚合后的体数据特征。这一层的意义在于找出哪些切片更值得关注。第三层是决策层。拿到体数据特征后通过一个分类头输出“篡改 / 正常”的二分类结果。同时由于前两层的注意力权重都被完整保留模型最终可以同时提供“哪张切片权重高”和“该切片中哪个 Patch 权重高”两个层级的解释信息。这个结构最值得借鉴的地方在于可解释性不是挂在模型外面的附属品而是模型内部的必需组件。要得到最终的体数据特征就必然要经过两级注意力加权要得到两级注意力权重就必然产生切片级和区域级的证据信息。3.3 模型输出与可解释热图在推理阶段HexMIL 的输出可以分为三个部分一是篡改概率。也就是模型判断这份 CT 被 AI 篡改的可能性。二是切片注意力序列。每张切片对应一个注意力权重权重高的切片说明模型认为它包含更明显的篡改线索。三是区域注意力热图。在被高权重切片内进一步还原出每个 Patch 的注意力分数重排为二维热图后可以直接叠加在原始切片上。有了这三部分输出医生看到的就不再只是“异常”这两个字而是一份可回溯的解释材料。这正是 Ante-Hoc 可解释检测的核心价值。4. 环境准备与版本说明HexMIL 本质上是一个深度学习模型实现落地时高度依赖 PyTorch 生态。以下环境以常见配置为例实际版本需要根据你的项目情况调整操作系统Ubuntu 20.04 / 22.04Windows 也可以但建议使用 Linux 服务器进行训练Python3.8 或 3.10深度学习框架PyTorch 1.10 以上2.x 更佳视觉库OpenCV、SimpleITK 或 nibabel用于读取 CT 数据数值计算NumPy可视化Matplotlib、SimpleITKGPU建议至少 11GB 显存例如 RTX 2080Ti / 3090 / A5000 级别。需要注意CT 数据的读取库各家的轴顺序定义不太一样。SimpleITK 的 GetArrayFromImage 返回的 shape 一般是 (z, y, x)而部分模型期望的输入顺序是 (c, d, h, w)这里需要做好约定避免训练和推理时维度错乱。如果只是复现理解算法流程可以先不用完整 CT 数据而是把流程跑在公开的医学影像数据集上或者先用普通 3D 数据验证模型结构是否正确。重点是先把 MIL 包结构和层级注意力模块跑通再替换为真实 CT 数据。5. 实战HexMIL 风格模型搭建PyTorch 示意这里不提供论文原文的完整复现代码因为论文通常还包含数据增强、预训练权重、损失函数调节等细节。下面给出一个 HexMIL 风格模型的骨架实现帮助你理解模块之间的数据流和层级注意力结构。你可以在此基础上按自己的数据集调整。5.1 项目结构建议以下目录结构适合中小型研究与实验hexmil_demo/ ├── data/ │ └── preprocess_ct.py ├── models/ │ ├── feature_extractor.py │ ├── attention_layers.py │ └── hexmil.py ├── utils/ │ └── metrics.py ├── train.py ├── inference.py └── requirements.txt将数据预处理、模型定义、训练脚本和推理脚本分开便于后续维护。5.2 数据预处理把 CT Volume 转成实例包这里以“切片内 Patch 为第一层实例”的设计为例。首先读取 CT 体数据然后沿着 Z 轴取出一组 2D 切片再把每张切片切分成固定大小的 Patch。import SimpleITK as sitk import numpy as np import torch def load_ct_volume(path): 读取 CT 体数据返回 shape 为 (z, h, w) 的数组。 注意SimpleITK 返回的 axis order 与 raw array 方向有关 生产代码中务必确认与训练时保持一致。 image sitk.ReadImage(path) volume sitk.GetArrayFromImage(image) return volume.astype(np.float32) def extract_patches_from_slice(slice_2d, patch_size64, stride64): 将一张 2D 切片切分为多个 patch。 如果切片尺寸不能被 patch_size 整除使用 stride 控制重叠程度。 h, w slice_2d.shape patches [] positions [] for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): patch slice_2d[y:y patch_size, x:x patch_size] patches.append(torch.from_numpy(patch).unsqueeze(0).float()) positions.append((y, x)) if not patches: # 如果切片比 patch 还小直接整体作为 patch patch torch.from_numpy(slice_2d).unsqueeze(0).float() patches.append(patch) positions.append((0, 0)) return torch.stack(patches), positions def build_bag_from_volume(volume, slice_indices, patch_size64, stride64): 从一组切片索引构建一个包bag。 返回 bag_patches: shape (num_slices, num_patches, 1, patch_size, patch_size) slice_indices: 原始切片的索引方便映射回体数据空间。 slice_patches [] for idx in slice_indices: slice_2d volume[idx] patches, _ extract_patches_from_slice(slice_2d, patch_size, stride) slice_patches.append(patches) bag_patches torch.stack(slice_patches, dim0) return bag_patches, slice_indices这段代码的重点是build_bag_from_volume它把一份 CT 体数据转换成了一个“切片-补丁”两层嵌套的包结构。第一个维度是切片第二个维度是切片内部的 Patch后面两个维度是单通道图像。这个结构会直接喂给后面模型。需要注意的是真实 CT 的像素值范围通常是 -1024 到 3071 之间需要根据窗宽窗位截断并归一化。这里没有处理归一化因为不同数据集的最佳范围不一样建议在自己数据上验证。5.3 特征提取模块对每个 Patch 提取特征向量。为了简化这里用一个小型 2D CNN 代替预训练大模型核心目的是说明流程。import torch.nn as nn import torch.nn.functional as F class PatchFeatureExtractor(nn.Module): 输入 shape: (batch, 1, patch_size, patch_size) 输出 shape: (batch, feature_dim) def __init__(self, in_channels1, feature_dim128): super().__init__() self.features nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size3, stride2, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(32, 64, kernel_size3, stride2, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, 128, kernel_size3, stride2, padding1), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d((1, 1)), ) self.fc nn.Linear(128, feature_dim) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.fc(x) return x特征提取器的设计比较自由。如果你处理的 Patch 较大或者希望利用 ImageNet 预训练权重也可以换成 ResNet18 的 stem 或者完整网络。需要注意的是CT 是单通道灰度图如果需要使用 RGB 预训练模型需要把单通道复制成三通道并且对输入尺寸做适配。5.4 层级注意力模块这一步是核心中的核心。第一层注意力聚合切片内 Patch第二层注意力聚合切片序列。class AttentionLayer(nn.Module): 通用注意力聚合层。 输入特征 shape: (batch, num_instances, feature_dim) 输出聚合特征 shape: (batch, feature_dim) 同时返回注意力权重 shape: (batch, num_instances) def __init__(self, feature_dim128, hidden_dim64): super().__init__() self.attention nn.Sequential( nn.Linear(feature_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, 1), ) def forward(self, features): # features: (batch, num_instances, feature_dim) scores self.attention(features) # (batch, num_instances, 1) scores scores.squeeze(-1) # (batch, num_instances) weights F.softmax(scores, dim1) # 加权聚合 context torch.sum(weights.unsqueeze(-1) * features, dim1) return context, weights注意力层本身并不复杂关键在于它被使用了两次。第一层传入切片内 Patch 特征得到切片级特征第二层传入所有切片的特征得到体数据级特征。下面把它们组装成层级结构class HierarchicalAttentionLayer(nn.Module): 层级注意力 第一层切片内部 Patch 特征 - 切片特征 第二层切片特征序列 - 体数据特征 def __init__(self, feature_dim128, hidden_dim64): super().__init__() self.patch_attention AttentionLayer(feature_dim, hidden_dim) self.slice_attention AttentionLayer(feature_dim, hidden_dim) def forward(self, patch_features): patch_features shape: (batch, num_slices, num_patches, feature_dim) 返回 volume_feature: (batch, feature_dim) slice_weights: (batch, num_slices) patch_weights: (batch, num_slices, num_patches) batch_size, num_slices, num_patches, feature_dim patch_features.shape # 第一层对每个切片内部的 patch 做注意力 slice_features [] patch_weights [] for s in range(num_slices): slice_patch_features patch_features[:, s, :, :] # (batch, num_patches, feature_dim) slice_feature, weights self.patch_attention(slice_patch_features) slice_features.append(slice_feature) patch_weights.append(weights) slice_features torch.stack(slice_features, dim1) # (batch, num_slices, feature_dim) patch_weights torch.stack(patch_weights, dim1) # (batch, num_slices, num_patches) # 第二层对切片序列做注意力 volume_feature, slice_weights self.slice_attention(slice_features) return volume_feature, slice_weights, patch_weights这里使用了循环遍历切片的方式代码可读性更好适合理解原理。在实际工程中如果切片数量较多可以考虑把第一层注意力也向量化避免 Python 循环带来的性能损耗。5.5 组装 HexMIL 模型有了特征提取器和层级注意力层就可以组装完整模型了。class HexMIL(nn.Module): HexMIL 风格模型 输入一个 CT 包的 patch 序列 输出篡改概率、切片注意力权重、patch 注意力权重 def __init__(self, in_channels1, feature_dim128, hidden_dim64): super().__init__() self.feature_extractor PatchFeatureExtractor(in_channels, feature_dim) self.hierarchical_attention HierarchicalAttentionLayer(feature_dim, hidden_dim) self.classifier nn.Sequential( nn.Linear(feature_dim, 64), nn.ReLU(inplaceTrue), nn.Linear(64, 1), ) def forward(self, bag_patches): bag_patches shape: (batch, num_slices, num_patches, 1, patch_size, patch_size) 返回 logits: (batch, 1) slice_weights: (batch, num_slices) patch_weights: (batch, num_slices, num_patches) batch_size, num_slices, num_patches bag_patches.shape[:3] # 提取每个 patch 的特征 patch_features [] for b in range(batch_size): batch_patch_features [] for s in range(num_slices): patches bag_patches[b, s] # (num_patches, 1, patch_size, patch_size) features self.feature_extractor(patches) # (num_patches, feature_dim) batch_patch_features.append(features) patch_features.append(torch.stack(batch_patch_features, dim0)) patch_features torch.stack(patch_features, dim0) # (batch, num_slices, num_patches, feature_dim) # 层级注意力聚合 volume_feature, slice_weights, patch_weights self.hierarchical_attention(patch_features) # 分类 logits self.classifier(volume_feature) return logits, slice_weights, patch_weights这一步完成了模型的整体搭建。输入输出结构如下输入一份 CT 的 Patch 包shape 为(batch, num_slices, num_patches, 1, patch_size, patch_size)。输出logits用于计算分类损失slice_weights和patch_weights用于生成可解释性热图。因为注意力权重是在前向传播中自然产生的所以解释信息不依赖任何外部工具。这是 Ante-Hoc 可解释性的代码体现。5.6 训练与验证训练逻辑和普通分类模型类似但额外需要把返回的注意力权重记录下来。import torch import torch.nn as nn import torch.optim as optim def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0.0 for batch in dataloader: bag_patches batch[bag_patches].to(device) labels batch[label].to(device) logits, slice_weights, patch_weights model(bag_patches) labels labels.float().unsqueeze(1) loss criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(dataloader)在验证阶段除了计算 AUC 和 Accuracy 之外还应该保存每一份验证样本的切片注意力权重方便后续可视化。单纯看分类指标并不能体现 HexMIL 的价值重点还是要检查注意力位置是否合理。6. 运行验证与结果解读6.1 先跑通小规模数据建议先用一个非常小的数据集验证模型结构正确性。比如取 8 份 CT每份取 16 张切片每张切片切分成 4 个 Patch跑一个 Batch 看能否顺利前向和反向传播。如果出现显存不足优先减小patch_size、减少切片数量、减小feature_dim。结构没跑通之前不要盲目加大数据规模。6.2 评估模型不能只看准确率对于篡改检测任务正负样本往往不平衡准确率并不是最可靠的指标。建议重点关注AUC衡量模型把篡改样本排在正常样本前面的能力。敏感度与特异度临床场景中漏报篡改样本的危害远大于误报所以需要关注敏感度。注意力稳定性同一份检查在多次推理中高权重切片位置是否稳定。如果注意力分布在随机切片上反复横跳说明模型没有学到稳定的篡改线索。最后这一点非常重要。HexMIL 的优点在于它天然提供了可量化的解释信息但这也意味着模型被“看得更清楚”了。如果注意力位置不合理模型内部的决策依据就会暴露问题这时候需要回头检查数据预处理和训练策略。6.3 如何把注意力权重还原成热图slice_weights可以直接按索引还原到切片序列。patch_weights需要结合positions信息在原始切片上恢复出二维热图。import matplotlib.pyplot as plt def visualize_slice_attention(slice_2d, patch_weights, positions, patch_size64): slice_2d: 原始 2D 切片shape (h, w) patch_weights: 该切片内每个 patch 的注意力分数 positions: 每个 patch 在原始切片中的左上角坐标列表 h, w slice_2d.shape heatmap np.zeros((h, w), dtypenp.float32) for (y, x), weight in zip(positions, patch_weights): y_end min(y patch_size, h) x_end min(x patch_size, w) heatmap[y:y_end, x:x_end] max(heatmap[y:y_end, x:x_end], weight) # 归一化到 0-1 方便显示 heatmap (heatmap - heatmap.min()) / (heatmap.max() - heatmap.min() 1e-8) plt.figure(figsize(12, 5)) plt.subplot(1, 2, 1) plt.imshow(slice_2d, cmapgray) plt.title(Original CT Slice) plt.subplot(1, 2, 2) plt.imshow(slice_2d, cmapgray) plt.imshow(heatmap, cmapjet, alpha0.4) plt.title(Attention Heatmap) plt.show()注意这里使用了最大叠加而不是平均叠加目的是让高权重 Patch 在热图上更突出。如果采用平均叠加小区域的强信号会被周围低权重区域稀释视觉上不够清晰。7. 常见问题与排查思路问题现象常见原因解决思路训练时显存溢出切片数和 Patch 数过多特征图太大减小 patch_size / stride减少单次样本数或使用梯度累积注意力权重全部均匀分布模型没有学到有效特征或数据规模太小先加入预训练特征提取器检查标签是否有误查看训练 loss 是否下降预测结果接近随机正负样本不平衡或特征提取能力不足评估 AUC 而不是 accuracy尝试增加难负样本挖掘或数据增强热图位置与直觉不符预处理不一致或模型关注了拍摄伪影等混淆特征检查窗宽窗位处理、数据归一化方式可视化中间特征确认推理速度过慢循环遍历切片和 Patch 导致效率低向量化第一层注意力使用更大的 batch 推理提前缓存特征训练和验证表现差异大数据划分不在同一分布或存在数据泄漏按患者划分数据不要将同一患者的不同切片分到训练和验证集特别提醒一点CT 数据划分必须按“患者”维度切分不能按“切片”维度切分。同一患者的切片高度相关如果一部分在训练集、一部分在验证集模型相当于看见了“熟面孔”评估结果会虚高。8. 最佳实践与工程建议8.1 数据层面CT 数值范围和窗宽窗位直接决定模型能看到的灰度细节。肺窗、纵隔窗、骨窗对应的 CT 值范围不同训练前必须统一。常见做法是截断到固定范围再做 min-max 归一化或 z-score 标准化。如果篡改样本数量有限可以先做切片级预训练再在完整包结构上微调。比如先用 2D CNN 区分“正常切片 / 篡改切片”把特征提取器训好后再接上层级注意力。这样可以减少端到端训练的压力。8.2 模型层面注意力层的设计有多个变体值得尝试在注意力打分时加入门控机制让模型可以显式忽略信息量低的区域在损失函数中加入注意力正则项比如鼓励注意力分布的熵不要过高如果篡改区域总是很小可以考虑 top-k 注意力聚合只保留权重最高的若干 Patch。无论哪种变体都要保证注意力权重的“可解释性”不被破坏。不要为了让模型精度更高而引入完全黑盒的子模块否则前面做的 Ante-Hoc 设计就失去意义了。8.3 工程落地层面真正的落地场景中一份 CT 可能有上千张切片完整跑一次推理耗时较长。可以考虑两阶段策略先用一个轻量级切片级筛选器过滤掉大概率正常的切片再对可疑切片做 Patch 级精细分析。这样既保证了效率又保留了注意力证据。部署时还要考虑 DICOM 元信息的核对。模型判断为“被篡改”时不仅输出热图还应该同时输出检查时间、设备、切片索引、重建参数等上下文信息方便医生回溯。任何自动化标记都需要人工复核流程不能把模型输出直接当作最终结论。9. 总结与学习路线HexMIL 的核心贡献不是简单地把注意力机制套用到 CT 数据上而是设计了一套“层级注意力 多实例学习 Ante-Hoc 可解释”的整体方案。它把 CT 体数据看成包结构用两级注意力逐步定位可疑切片和可疑区域最终让模型在输出篡改结论的同时提供切片级和区域级的证据信息。如果你想继续深入研究建议按下面的顺序推进先跑通本文的简化模型理解包构建、两级注意力、热图可视化三条主线阅读多实例学习的基础论文重点理解 max-pooling、mean-pooling 与注意力聚合的差异把特征提取器换成更大的 2D 或 3D 网络在真实 CT 数据上验证效果尝试不同的注意力变体例如 gated attention、top-k attention结合对抗攻击方法生成更多样的篡改样本测试模型的鲁棒性。在动手实现之前建议先明确自己的切入点是想复现完整论文还是想借鉴 HexMIL 解决新的检测问题。前者需要对照论文原文的每一处超参数和实验设置后者则可以把重点放在数据预处理、注意力层级设计和可解释性评估上。无论选择哪条路都要牢记一点医学影像 AI 的可解释性不是用来“展示”的而是用来支持临床决策的。模型给不出可信证据时宁可不给判断也不能给一个无法解释的结论。