686. PhysioNet - Digitization of ECG Images | physionet-ecg-image-digitization
我要感谢组织者和 Kaggle 工作人员举办并运行了这场出色的竞赛。我也向 @hengck23 致以诚挚的谢意,感谢他发布了出色的 baseline 笔记本和讨论。
我的 pipeline 在阶段 0 和阶段 1 未经修改地使用了 @hengck23 的 公开实现。因此,本解决方案主要侧重于阶段 2 的分割和后处理技术。
关键创新:
MyCoordUnetDecoder 的全图分割模型(整体模型)注意:在本解决方案说明中,我将标准 12 导联 ECG 的每一行称为“序列 (0-3)"。
竞赛 metric 计算每张图像的 SNR,在线性 SNR 域中对其进行平均,然后转换为 SNR(dB) (讨论)。
因此,提高中等至高 SNR 图像的分数往往比在最难的低 SNR 案例上花费精力更能提高最终得分。在分析折外 (OOF) 预测后,我专注于改进中等难度和简单的图像,这些图像数量更多且更容易改进。
为了通过靠近采样点来最小化预测误差,我在三个关键领域改进了我的方法:
竞赛数据是 PTB-XL 数据集 的子集。
虽然 PTB-XL 数据集以 500Hz 采样频率提供所有时间序列数据,但竞赛数据使用多种频率:250、256、500、512、1000 和 1025Hz。这表明竞赛组织者将原始的 500Hz 信号重采样到了这些不同的频率。
我通过计算相关系数将竞赛数据匹配回原始的 PTB-XL 时间序列,并将所有信号替换为其 500Hz 原始信号。
这种替换提高了采样位置的一致性,从而在训练期间带来了明显的 CV 改进。单折模型的 LB 分数从 21.67dB 提高到 22.49dB。
由于仅使用竞赛数据就取得了令人满意的分数,且合成数据的进一步改进微乎其微,我没有在额外的数据合成上投入太多时间(使用 PTB-XL 数据集和 ECG-Image-Kit)。
考虑到私人数据集可能包含困难的 0015 类型褶皱数据,我在每个 epoch 的训练集中添加了少量随机选择的 0015 数据。
创建分割掩码的方法至关重要。这是因为通过 pipeline(时间序列 → 分割掩码 → 时间序列)重建信号时实现的 SNR 直接与模型的上限性能相关。
密集掩码(覆盖整个信号线)没有产生高的重建 SNR。相反,我创建了稀疏掩码,每列最多标注 2 个像素。
为了在后处理阶段实现亚像素精度的重建,我根据 y 坐标的小数部分将标签分布到两个像素(整数部分和整数部分 + 1)。在重建期间,我通过使用标签值作为权重计算 y 坐标的加权平均将其转换为时间序列数据。
为了在不重采样的情况下从 500Hz (sig_len=5000) 映射到掩码,我将掩码宽度设置为 5600 像素,并在 [301:5301] 范围内绘制掩码,覆盖 5000 像素。
以下是创建的掩码和在此掩码上训练的模型的 OOF 预测结果(黄色:GT,绿色:预测)。
我使用了两种类型的模型:
MyCoordUnetDecoder 的 U-Net 模型。我基于 @hengck23 的模型构建了整体模型架构。我将编码器更改为来自 segmentation_models_pytorch 的 timm 编码器。我裁剪了图像的顶部并将其调整大小为 (1280, 5600)。
特定的 ECG 导联具有很强的相关性(例如,爱因托芬定律)。为了将这些关系纳入模型,我构建了一个接受四个序列作为输入的 2.5D 模型。
我在以零 mV y 坐标位置为中心的 ±3mV (240 像素) 范围内裁剪每个序列。共享的 U-Net 编码器处理每个序列图像,然后我在每个 U-Net 解码器层的连接路径中融合跨序列的特征。解码器提取特征,分割头将其转换为掩码预测。
我测试了三种融合模块变体:
通常,2.5D 模型使用 conv3d、LSTM 或 transformers 来提取深度信息。LSTM 效果不佳(可能是由于参数设置不当)。由于经验有限,我没有尝试 transformers。
由于我想混合所有深度(序列堆叠顺序)信息,我还尝试了使用 conv2d 进行特征融合。虽然变体之间的差异很小,但 conv2d 显示了最好的 CV 结果。
我应用 conv2d 块 (Conv2d→BatchNorm2d→ReLU) 进行特征缩减和特征融合。
为了节省参数,共享 conv2d 在所有序列之间共享缩减 conv2d 块。
我将特征重塑为 (C, 4, H, W) 并多次应用 conv3d 块 (Conv3d→BatchNorm3d→LeakyReLU)。
为了验证融合模块的有效性,我比较了预测结果。
顶行显示具有未掩码信号和 GT 掩码的图像。中间行和底行显示具有掩码区域(模拟图像伪影)的图像,以及分别来自整体模型和序列模型的预测。
虽然整体模型无法预测掩码区域,但序列模型跨导联共享信息,使其能够合理预测峰值相位和振幅。
提示:梯度检查点 (Gradient checkpointing) 对于减少高分辨率图像的激活内存消耗非常有效
image_only_aug = A.Compose([
A.RandomBrightnessContrast(brightness_limit=(-0.1,0.1), contrast_limit=(-0.1, 0.1), p=0.2),
A.RandomShadow(p=0.2),
A.GaussianBlur(p=0.2),
A.CoarseDropout(num_holes_range=(1,8), hole_height_range=(0.01, 0.1), hole_width_range=(0.01, 0.05), fill=0, p=0.1),
A.ToGray(p=0.25),
])
image_and_mask_aug = A.HorizontalFlip(p=0.5)
我使用预测结果计算掩码每一列的加权平均,以获得亚像素级别的预测。
我测试了:
scipy.signal.resample 给出了最好的结果。
完整的掩码→时间序列转换 pipeline 代码,包括掩码后处理和重采样:
def pixel_to_series(pixel, length):
_, H, W = pixel.shape
eps=1e-8
y_idx = np.arange(H, dtype=np.float32)[:, None]
series = []
for j in [0, 1, 2, 3]:
p = pixel[j]
denom = p.sum(axis=0)
y_exp = (p * y_idx).sum(axis=0) / (denom + eps)
series.append(y_exp)
series = np.stack(series).astype(np.float32)
if length!=W:
resampled_series = []
for s in series:
rs = signal.resample(s, length).astype(np.float32)
resampled_series.append(rs)
series = np.stack(resampled_series)
return series
公共 LB: 23.37, 私人 LB: 23.27
公共 LB: 23.34, 私人 LB: 23.23
| 编码器 | 模型 (融合模块) | 合成数据 | 公共 LB | 私人 LB | 提交 A | 提交 B |
|---|---|---|---|---|---|---|
| EfficientNet B7 | 整体 | 22.93 | 22.65 | ☑️ | ☑️ | |
| EfficientNetV2 L | 整体 | ✅ | 22.60 | 22.55 | ☑️ | ☑️ |
| EfficientNetV2 M | 整体 | 22.52 | 22.38 | ☑️ | ||
| EfficientNetV2 M | 整体 | ✅ | 22.58 | 22.39 | ☑️ | |
| EfficientNet B6 | 序列 (共享 conv2d) | ✅ | 23.10 | 22.92 | ☑️ | ☑️ |
| EfficientNet B6 | 序列 (共享 conv2d) | 23.00 | 22.81 | ☑️ | ☑️ | |
| EfficientNet B4 | 序列 (共享 conv2d) | ✅ | 22.93 | 22.73 | ☑️ | |
| EfficientNetV2 L | 序列 (conv3d) | 22.92 | 22.77 | ☑️ | ☑️ | |
| EfficientNetV2 L | 序列 (conv2d) | 22.85 | 22.70 | ☑️ | ☑️ | |
| EfficientNetV2 M | 序列 (conv3d) | ✅ | 22.73 | 22.49 | ☑️ | |
| EfficientNetV2 M | 序列 (conv2d) | ✅ | 22.76 | 22.55 | ☑️ |