686. PhysioNet - Digitization of ECG Images | physionet-ecg-image-digitization
我们最好的提交作品通过运行三个独立的解决方案,然后计算它们预测值的加权平均来实现。我们在比赛后期组建了团队,因此我们的流程是在相对较少的共享假设下开发的。这种独立性增加了模型的多样性,从而使我们的集成模型能够显著超越 individual 分数。
| 解决方案 | 集成权重 | 公共 LB 分数 | 私有 LB 分数 |
|---|---|---|---|
| Imanishi 的流程 | 45% | 21.58 | 21.43 |
| James 的流程 | 32% | 21.17 | 20.98 |
| Liu 的流程 | 23% | 20.84 | 20.74 |
| 集成模型 | N/A | 22.16 | 22.03 |
总的来说,我们所有的解决方案都是通过校正输入图像以纠正失真,然后使用分割模型和 softmax 操作来提取电压信号。然而,我们在预处理方法(1 阶段 vs 2 阶段)、模型架构(CNN vs Transformers)、分割损失函数(二元交叉熵 vs 自定义"coord"损失)以及信号后处理方法(异常抑制技术和信号锐化变换)方面存在一些实质性差异。因此,我们预测中的不准确之处 largely 不相关,加权平均大大提高了信噪比。
我们的 individual 解决方案将在下面详细描述。
使用 hengck23 优秀的 notebook 作为基线,我引入了以下改进:
F.grid_sample() 单步变换原始图像。savgol_filter,根据 sig_len 动态调整 window_length。在 Liu 的 Coord loss 实现中,分割损失和 Coord loss 都应用于相同的 logits。在我的情况下,这效果不好,所以我 model head 分成两个 heads(分割 head,y 坐标 head),并分别应用 分割损失 (dice loss + BCE loss) 和 Coord loss。
两个 heads 具有相同的输出形状:[height, width, 4ch]。
分割损失在训练期间充当辅助损失,但分割输出也用于推理期间掩蔽 x 方向的低置信度区域以及稍后描述的 y 聚类。
虽然 Liu 的部分已经解释了 Coord Loss,但我会描述我认为重要的部分。
使用像 hengck23 基线那样的基于分割的方法,其中预测曲线掩码然后使用 argmax 获取 y 坐标,很难估计最佳 y 位置。查看两个 heads 的 ground truth 时这一点变得很明显(下图)。
使用 Coord Loss,logits 输出(height × width × 4 通道)与分割 head 相同,直到某一点,但随后在 y 维度上应用 softmax 以直接预测 y 坐标。
如果我们使用只有一个像素的 one-hot 标签作为 ground truth,模型会对小坐标偏移非常敏感。相反,ground truth 沿 y 轴表示为 高斯形状的概率分布。高斯参数(sigma 和 radius)与 Liu 的保持一致,因为我没有时间调整它们。
损失函数是标准的 交叉熵损失,将问题视为 y 位置上的多类分类。
通过在 Coord Loss head 输出上沿 y 方向应用 argmax,我们可以获得更准确的 y 坐标。添加 Coord Loss 使我的公共 LB 分数提高了约 +1.0 dB,这是一个巨大的增益。
出于某种原因,我的 stage2 模型偶尔会错误分类导联标签,这可能会显著降低 SNR。我想在训练期间防止这种情况,但最终未能做到,所以我用 使用 y 方向聚类的后处理步骤 来处理它。
对于 y 聚类,我使用了分割 head 的输出,生成的掩码随后应用于 y 坐标 head。
在提交 notebook 中,我将整个测试集分为两部分,并为每个进程分配一个 T4 GPU,如下面的代码所示,以加快推理速度。
实际上,我的这部分流程运行大约需要 70 分钟,这是团队成员中最快的。
env1 = os.environ.copy()
env2 = os.environ.copy()
env1['CUDA_VISIBLE_DEVICES'] = '0'
env2['CUDA_VISIBLE_DEVICES'] = '1'
cmd1 = f'python run.py --half 0'
proc1 = subprocess.Popen(cmd1.split(' '), env=env1)
cmd2 = f'python run.py --half 1'
proc2 = subprocess.Popen(cmd2.split(' '), env=env2)
_ = proc1.communicate()
_ = proc2.communicate()
我的推理流程有 3 个主要步骤:
步骤 1 和 2 都使用微调版本的 DINOv2-base。我总共使用了 6 个模型的集成,步骤 1 中 2 个,步骤 2 中 4 个。一半的模型以故意翻转的方向处理图像,作为测试时数据增强的一种形式。
我基于 PTB-XL 数据集中的约 22K 独特 ECG 记录生成了约 175K 训练示例。
这是通过 ECG-image-kit 生成“干净”的 ECG 图像,然后故意用一些自定义数据增强代码破坏它们,试图大致模仿主机数据中出现的图像类型(严重损坏的纸张手机照片、电脑屏幕手机照片、黑白扫描等)。
每种图像类型大致使用以下组合模拟(顺序不分先后):
额外的增强是在我的训练脚本中动态执行的,那些只是我在训练前应用的。
在约 175K 额外训练示例中,只有约 50K 用于训练我的最终模型。主要是因为 (1) 关键点检测器的准确性似乎在约 17K 训练示例后没有 meaningful 提高,(2) 在主机图像上微调之前在合成数据上预训练导联分割模型的增益相对于消耗的 GPU 时间来说相当小(额外计算超过两天 RTX 5090 仅获得 +0.17 dB 增益有点令人失望……我想尝试其他事情,而不是让硬件被困在那个方向上 pushing further)。
这是通过检测图像中的关键点,然后使用那些关键点位置计算单应性矩阵来工作的,该矩阵可用于校正相机角度、缩放和旋转的差异。它与 @hengck23 的 stage 0 相当相似,主要区别是我使用了视觉 transformer 而不是 CNN,以及我自己生成的训练数据。

关键点在 1036x1036 分辨率下检测,然后用于直接将图像从其 native 分辨率校正为 --> 1694x2198。
训练详情:
albumentations 应用):
RandomRotate90GridDistortionElasticTransformOpticalDistortionGaussNoiseGaussianBlurRandomBrightnessContrastColorJitterCoarseDropout我最终集成中的两个关键点检测模型都在 17.4K 我的合成训练示例上训练(没有主机数据)。它们主要在数据增强设置和训练 epoch 计数上不同。一个用中等重度增强和 24 epochs 训练,另一个用更重增强和 48 epochs 训练。
将训练数据 triple 到约 50K 示例在针对其他合成图像测试时提高了交叉验证,但在针对主机提供的 ECG 图像测试我的完整流程时没有提高分数,因此只有约 10% 的可用合成数据用于训练我最终集成中的关键点检测器。
我使用 ground-truth 信号数据生成“完美”热力图,描述如果图像完全无失真,导联应该位于图像中的何处,然后微调 DINOv2-base 以基于具有 realistic 失真的图像预测那些热力图。这教会它自动校正任何穿过我预处理的扭曲。

我发现使用高于 ECG plots native 分辨率的水平分辨率非常有益,因此这使用 1694x4396 的输入分辨率(比 native 宽约 2 倍),我在 transformer 骨干网络和预测 head 之间插入了一个 ConvTranspose2d 层,这在产生输出前不久将分辨率又提高了 2 倍。结果,热力图的分辨率为 1694x8792。与仅使用 naive 分辨率相比,这提供了 巨大的分数改进,早期实验中约 4.1 dB 增益。使用比 native 高 2 倍的输入分辨率和比 native 高 4 倍的输出分辨率似乎是最优的,调整其中任何一个数字 2 倍都会使分数变差。
训练分 2 个阶段进行:
训练详情:
A.GridDistortion(num_steps=5, distort_limit=0.2, p=0.15),并且在微调期间没有任何 显式 数据增强。然而,微调故意使用来自我的预处理流程较旧、较不准确版本的校正图像,因此模型在训练期间比测试时暴露于更多校正错误。对训练数据使用更准确的校正使我的分数变差,所以我相信校正不准确在微调期间充当了一种 sneaky 数据增强。我的流程使用单独的关键点检测模型将每个图像校正两次,然后翻转其中一个结果图像,并将它们 fed 到 4 个导联分割模型的集合中,其中一半训练为处理翻转方向的图像。然后将生成的热力图通过平均像素 logits 混合。

上述方法基于以下观察:
分数可能通过以更多方向处理图像、应用其他顶级解决方案和公共 notebook 中与旋转和翻转无关的一些测试时增强,并找出为什么在校正前应用 TTA 没有帮助(也许我有 bug?)来进一步提高,但我没有时间非常 extensively 实验这个。
我通过以下步骤将原始预测导联位置热力图转换为信号:
……像往常一样,许多对我效果不佳的事情如果付出额外努力和 GPU 时间进行调整,潜在地可能对其他人效果良好。当早期结果看起来没有希望时,我经常继续测试其他想法。许多上述“坏”想法最终对其他顶级竞争对手效果良好 😅
我要向 Kaggle 和比赛组织者提供这个宝贵机会表示衷心感谢。特别感谢 @hengck23 分享他的强基线,这为我工作 served 了 crucial 基础。
我的解决方案优化了 @hengck23 基线的 Stage 2。关键洞察是将波形提取视为 每列坐标回归,而不是纯粹依赖“分割 → 后处理”流程。
我保留 U-Net 风格的热力图预测以保持稳定性,但添加 CoordLoss:一个 GT 中心的 局部高斯交叉熵,直接监督每列中心线 y 坐标。在我的本地验证 split 上,这一单个改变将 SNR 提高了 超过 +1 dB,它是整体质量的主要贡献者。
为了进一步减少训练 - 推理不匹配,验证/推理使用相同的 旧子像素兼容 y 提取器(NaN + 插值行为)。我还添加轻量级“安全”正则化项强制执行物理上合理的导联关系。
Stage 0/1 (基线): 检测并将 ECG sheets 校正为规范坐标(校正条)。
Stage 2 (本工作):
架构
timm resnet34.a3_in1kBatchNorm 冻结
所有 BN 层在训练期间强制进入 eval 模式,这在 batch size 1 带梯度累积下稳定优化。
损失
p(y|x,c)=softmax(z_c[:,x]/T)y^*(x,c) 周围构建 GT 中心局部高斯目标:
w_k ∝ exp(-(k-y^*)^2/(2σ^2)) 在窗口 ±R 内(II_corr=(1-w)*II+w*(I+III)),带 warmup/ramp 以保持稳定。II ≈ I + III。这些 intentionally 轻量级;它们主要帮助避免 implausible 输出并减少边界伪影,而 CoordLoss 提供主要准确性提升。
我们通过以下方式集成我们的预测:
这比我们最好的 individual 提交提供了 0.6 dB 的增益 😁。