做可穿戴设备或生理信号研究的人,几乎都会遇到同一个问题:单看 ECG 很好,单看 PPG 也还行,但只要人一动、传感器一松、信号一丢,结果就完全没法用。CardioFusion-AI 这个方向做的事情,就是把 ECG 和 PPG 两条信号放在同一个模型里做融合,让系统在信号退化条件下还能稳定输出心率、血氧、呼吸率这类生理参数。适合正在做多模态生理监测、可穿戴算法、运动健康场景的开发者阅读。最值得先关注的不是网络结构有多花哨,而是退化模拟、融合策略和评估指标这三个环节是否闭环。
我从复现思路出发,把整个流程拆成了数据准备、预处理、融合模型、退化模拟、批量评测和落地边界六块。这篇文章不会给你一堆无法验证的性能数字,而是告诉你每一步应该怎么判断:输入长什么样、参数调到哪里、输出怎么算对、退化条件下模型到底有没有变稳。
1. 先想清楚:ECG-PPG 融合解决的是“信号互相救场”,不是简单取平均
1.1 两条信号为什么互补
ECG 测量的是心脏电活动,通过电极接触皮肤获得,波形里有明显的 QRS 波群,可以用来算心率、心率变异性,形态稳定,但对电极贴放位置和肌电干扰比较敏感。
PPG 测量的是血液容积变化,通过光电容积描记获得,传感器通常放在手腕、手指或耳垂,能用来算心率、血氧饱和度和脉搏率。它不需要贴电极,佩戴更舒适,但很容易受环境光、运动伪迹和低灌注状态影响。
两条信号虽然都能提供心率或者脉搏相关信息,但它们的物理来源不同、干扰模式也不同。ECG 怕电极脱落和肌电干扰,PPG 怕运动和光学噪声。这种互补性正是做融合的价值:一路信号退化时,另一路还能提供参考;两路同时退化时,也可以通过时序一致性和先验约束把误差压下去。
如果只是把两条信号拼在一起取平均,那不能叫融合。真正有用的融合,是让模型学会动态判断当前哪种信号更可信,并输出一个带置信度的估计结果。
1.2 信号退化到底退化在哪
常见退化场景可以分成五类,实际测试时最好逐类模拟,不要只加一点高斯噪声就结束:
- 运动伪迹:手臂摆动、走路、跑步时,PPG 波形会发生明显基线漂移和幅度抖动;ECG 也可能因肌肉活动产生高频干扰。
- 传感器接触不良:电极或光电传感器松动,信号会出现间歇性断裂、突变和饱和。
- 数据丢包与缺失:无线传输不稳或采集中断,时间序列上出现随机缺失段,单条样本可能缺掉一整段。
- 环境干扰:PPG 受环境光影响,ECG 受工频干扰和静电放电影响。
- 低灌注或传感器位置偏差:PPG 信号幅度变小、波峰不明显,ECG 也可能因位置偏移出现低振幅或多个伪峰。
这些退化不是单一存在的。做 CardioFusion-AI 这类方案时,训练集里应该同时包含干净片段、单一退化片段和混合退化片段,让模型见过足够多组合,而不是只处理“干净输入加少量噪声”的理想情况。
1.3 单模态方案为什么容易崩
单模态方案的问题不是精度低,而是缺少交叉验证。ECG 一段信号被肌电干扰后,算法可能把干扰峰值当成心跳;PPG 一段运动伪迹后,峰值检测可能数出两倍心跳。这时候如果没有另一路信号做约束,模型自己很难知道“我可能算错了”。
融合模型可以解决一部分问题,但有一个前提:不能在两路输入中都把退化信息当成有效特征。所以我更倾向把融合模型做成退化感知的,比如对每一路输出一个噪声估计或者质量系数,而不是直接让特征在最后一层拼接。
2. 复现 CardioFusion-AI 前,先把数据、环境和判断标准定好
2.1 数据形态:两条同步时间序列,而不是两张图
ECG 和 PPG 融合处理的输入是同步时间序列。你在数据准备阶段就要确认三件事:
- 两条信号是否在同一时间轴采集,时间戳是否对齐。
- 采样率是否一致。很多公开数据集里 ECG 是 250Hz,PPG 是 64Hz 或 125Hz,需要重采样到同一采样率。
- 标签是什么。按心跳逐拍标注、按窗口标注心率,还是按事件段标注?不同任务决定后续模型输出设计。
窗口化时,我一般先按 10 秒长度切段,重叠率 50%。这样既能看到足够多的心跳周期,又不会让样本太长。如果只测心率,5 秒也能跑,但心率变异性相关的指标就偏短了。
不要一开始就切 60 秒的窗口。样本是变多了,但计算量变大,而且很多数据段会包含多种退化状态,标签不好定义。
2.2 公开数据与标注方式
原始材料没有指定数据集。实际做这类实验时,可以优先调研常用的生理信号公开数据集,比如 WESAD、PPG-DaLiA、MIMIC 中带 ECG 和 PPG 的记录等。每个数据集的传感器位置、采集设备、受试者状态不一样,先读一遍 README 比直接跑模型更重要。
需要注意几个坑:
- 公开数据集可能只提供了原始信号,没有逐拍标签,你需要自己用峰值检测或半自动标注生成标签。
- 部分数据集按维度拆分:受试者睡眠、压力、运动任务等,不要把所有状态混合进训练集。
- 同一受试者的数据不要同时出现在训练集和测试集,否则会造成数据泄漏,指标虚高。一定要按受试者划分。
2.3 软件环境和硬件门槛
这类项目的依赖通常包括:Python、PyTorch 或 TensorFlow、NumPy、SciPy、信号处理库、可视化库。我建议先建一个干净的环境,只装必要依赖,不要直接复制整个公版环境。实测时最容易翻车的不是模型,而是 scipy 版本和 librosa 版本之间互相打架。
硬件上,单入双通道短时生理信号的数据量并不大。CPU 可以训练一个很小的 CNN 模型,但要做退化模拟和批量交叉验证,建议准备一块显存至少 6GB 的 GPU。如果你的机器只有 CPU,也不用急着放弃,把窗口长度缩小、模型层数减少、样本量控制在几百段,仍然能跑通流程。
2.4 先定义“算成功”:误差、退化鲁棒性、可复现性
没有验收标准,训练再久也说不清效果。我在动手前会先写一个评估清单:
- 心率估计误差:常用 MAE、RMSE,目标是多少要先定好。比如正常段 MAE 小于 3 bpm,退化段可以放宽到 5 bpm,但如果退化后误差翻倍,要具体分析。
- 血氧估计误差:SpO2 通常看绝对误差,误差超过 2% 时就需要关注。
- 单模态缺失下的表现:把 ECG 置零、PPG 置零分别测试,看模型输出是不是仍然在合理范围。
- 可复现性:同样随机种子,重复训练两次,测试集指标差异应在可接受范围内。
这里不写死具体数字,因为不同数据集、不同退化强度差别很大。关键是先定一个你能接受的上限,再把模型和评估脚本一起调。
3. 一条可落地的融合流程:预处理、退化模拟、模型训练
3.1 预处理:滤波、分段、归一化的顺序不能乱
预处理顺序建议固定成这样:
- 读取同步后的 ECG 和 PPG 原始信号。
- 去除明显异常段:峰峰值超过物理范围、值全部为常数、时间戳缺失严重的片段直接丢弃。
- 带通滤波。ECG 常用 0.5Hz 到 45Hz 左右,PPG 常用 0.5Hz 到 10Hz 左右。具体频率边界要看数据集的采样率,不要照搬。
- 重采样到统一采样率,比如 125Hz 或 250Hz。
- 分窗口,并按窗口做归一化。归一化不能全数据集一起算,否则训练样本里会混入测试集统计信息。
这里最容易忽略的是归一化的顺序。很多人在滤波之前做归一化,结果滤波后的信号幅度分布被改变。正确做法是先滤波,再分窗,最后按窗口做标准化。标准化可以用 z-score,也可以缩放到 [0,1],两种方式我都试过,只要全流程一致问题都不大。
3.2 退化模拟是鲁棒训练的关键,不是可有可无
如果训练数据里全是干净信号,融合模型学到的只是“两个输入都很干净时怎么融合”,一旦遇到信号退化,输出会迅速劣化。
退化模拟建议放在训练时在线做,不要提前固化到磁盘。原因是每个 epoch 可以随机生成不同退化组合,模型能见过更多变化。常见做法包括:
- 给 ECG 或 PPG 添加高斯白噪声,信噪比从 5dB 到 20dB 随机选。
- 用一段随机基线漂移叠加到 PPG 上,模拟运动伪迹。
- 随机将某一路部分时间点设为 NaN 或 0,模拟数据丢包。
- 对两条信号做相对时间偏移,模拟采样不同步,但偏移量不宜超过 50 毫秒,否则标签会错位。
训练时我一般设置 50% 概率不退化,50% 概率随机选一种或多种退化。这样模型既保留干净输入下的准确性,又增加退化下的稳定性。
3.3 融合结构:双分支提取加模态 Dropout
融合模型的结构可以有多种选择,但比较稳妥的基线是这样:
- ECG 分支:1D CNN 加池化,提取心拍特征。
- PPG 分支:同样用 1D CNN 加池化,提取脉搏波形特征。
- 两路特征拼接到一起,送入一个 Transformer 或 BiLSTM 层,输出心率或血氧估计值。
- 训练时随机把一路特征置零,称为模态 Dropout。这样模型不会对某一路特征形成绝对依赖。
这种结构代码上比较简单,也不容易出问题。示例结构可以写成下面这样,注意这是示意代码,实际参数要按你的数据和任务调整。
import torch import torch.nn as nn class ECGPPGFusion(nn.Module): def __init__(self, input_height=100, hidden_dim=64, output_dim=1): super().__init__() # 这里把每个模态当作一维信号处理 self.ecg_encoder = nn.Sequential( nn.Conv1d(1, 16, kernel_size=15, stride=2), nn.ReLU(), nn.Conv1d(16, 32, kernel_size=9, stride=2), nn.ReLU(), nn.AdaptiveAvgPool1d(32), ) self.ppg_encoder = nn.Sequential( nn.Conv1d(1, 16, kernel_size=15, stride=2), nn.ReLU(), nn.Conv1d(16, 32, kernel_size=9, stride=2), nn.ReLU(), nn.AdaptiveAvgPool1d(32), ) self.fusion = nn.Sequential( nn.Linear(32 * 2, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, output_dim), ) def forward(self, ecg, ppg, drop_modal=False): if drop_modal: # 随机丢弃一路,模拟模态缺失 if torch.rand(1).item() < 0.5: ecg = ecg * 0 else: ppg = ppg * 0 ecg_feat = self.ecg_encoder(ecg).flatten(1) ppg_feat = self.ppg_encoder(ppg).flatten(1) feat = torch.cat([ecg_feat, ppg_feat], dim=-1) return self.fusion(feat)需要强调的是,模态 Dropout 只在训练时使用,推理阶段如果某一路确实缺失,不能直接置零,应该通过显式的质量掩码告诉模型缺失位置。否则模型在训练和推理之间会出现不一致。
3.4 损失函数和评价指标怎么选
损失函数上,回归心率或血氧这类连续值时,Huber 损失比 MSE 更稳,因为它对大误差不那么敏感。若你的标签是逐拍 R 峰位置,需要额外的峰值匹配策略,不能只比较数量。
评估指标至少包含四个:
- MAE 和 RMSE:看整体误差。
- 绝对误差百分比:看相对误差。
- 模态缺失时误差上升幅度:看融合是否真的互补。
- Bland-Altman 一致性:看是否有系统性偏差。
训练时要把测试集按退化类型分组。不要只报告一个平均 MAE,要分别看干净段、ECG 退化段、PPG 退化段、双路退化段的表现。如果平均 MAE 很低,但 ECG 退化段误差很高,说明模型还是过度依赖 ECG。
4. 从单条样例到批量评测:先跑通,再跑全量
4.1 用最小样例验证数据通路
不要一上来就跑全部数据集。我建议先取两条样本:一条 ECG 和 PPG 都干净,一条故意把 PPG 加严重运动伪迹。然后运行预处理脚本,画出波形,确认数据加载、滤波、分段、标签对齐都正常。
这一步能筛掉大量低级问题:文件路径错误、采样率不一致、标签偏移、数组维度不同。很多人直接跑训练脚本,loss 一直不降,最后发现是 loader 返回的 PPG 和标签没有对齐。
验证数据通路时,只看两件事:
- 输出张量的形状是否符合模型输入要求。
- 标签值与对应波形的手工核对是否一致。
4.2 单条推理与可视化
模型训练好后,先做单条推理。载入 checkpoint,输入一段测试窗口,输出预测值。然后画三张图:
- 原始 ECG 和 PPG 波形,标注退化区域。
- 模型预测值和真实值在时间轴上的对比曲线。
- 误差分布散点图或 Bland-Altman 图。
单条推理的意义是确认模型在真实退化样本上不是预测均值。如果模型对所有输入都输出同一个值,说明训练没收敛,或者测试集和训练集的归一化方式不一致。
4.3 批量评估:文件命名、失败重试、结果汇总
单条没问题后,再写批量评估脚本。批量评估不只是遍历文件,你要提前处理好三个问题:
- 输出命名:建议用
受试者ID_窗口索引_退化类型.csv或 JSON,避免默认文件名覆盖。 - 失败重试:某条数据解码失败时,要记录错误原因,跳过但不中断整个流程。
- 结果汇总:每个窗口的预测值、真实值、误差、退化类型、是否缺失一路模态都要落到一张表里,方便后面分组统计。
可以用一个很轻量的脚本结构,比如:
python evaluate.py --test_dir ./data/test --model_path ./checkpoints/best.pt --output ./results/test_results.csv脚本内部按窗口循环,单窗口出错就写入日志继续跑下一段。批量跑完后,再按退化类型分组统计 MAE 和 RMSE。
4.4 批量跑完之后应该看四个数
评估结果不能只看平均 MAE。我建议至少看这四组数值:
- 干净段 MAE:确认基础性能没有退化。
- PPG 退化段 MAE:看模型是否还依赖 PPG。
- ECG 退化段 MAE:看模型是否过度依赖 ECG。
- 双路退化段 MAE:看模型在最差情况下的兜底能力。
如果第二组或第三组数值明显恶化,要回看退化模拟和后处理。如果双路退化段误差反而很低,也要警惕,可能是测试集里双路退化样本太少,统计不可靠。
5. 参数调节和典型崩溃点排查
5.1 窗口长度、重叠率、采样率
窗口长度和采样率直接影响模型能看到多少心跳周期。
- 采样率 100Hz 时,10 秒窗口包含 1000 个点;250Hz 时包含 2500 个点。窗口不宜过短,至少包含 5 到 6 个心跳周期。
- 重叠率影响样本数量和相邻窗口的连续性。50% 到 75% 都常见,重叠太高训练会变慢,但能提升稳定性。
- 如果你的目标是实时监测,窗口越长意味着输出延迟越高。实验阶段可以先用 10 秒窗口,部署时要考虑滑窗叠加和输出平滑。
5.2 退化强度、模态缺失比例的平衡
退化强度太弱,模型学不到鲁棒性;退化强度太强,模型把噪声当信号。我一般用信噪比随机取值而不是固定值,比如 ECG 退化信噪比在 5dB 到 15dB,PPG 退化信噪比在 3dB 到 10dB。这样模型能适应不同程度的退化。
模态缺失比例不要设太高。我训练时用 15% 到 30% 的概率随机将某一路置零,这样模型不至于完全忽视某一路,也不会为了应对缺失而削弱两路融合的好处。
测试时不要使用同样的随机退化。建议固定测试退化强度,比如 10dB 噪声,这样结果可复现。
5.3 训练不稳定的通用排查顺序
如果训练时 loss 出现 NaN、不下降、或者反复振荡,按这个顺序排查:
- 看输入数据:先确认样本里没有 NaN、Inf,不要只检查原始数据,预处理后也要检查。
- 看归一化:不同模态的输出尺度差异过大,会导致梯度不稳定。检查 ECG 和 PPG 分支的输出范围。
- 看学习率:模型参数不大,但学习率太高很容易让 loss 变 NaN。先降到 1e-4 试跑 5 个 epoch。
- 看损失函数:如果标签范围是 0 到 1,损失函数可以用 BCE 或 Huber。如果用 MSE,输出层要接 Sigmoid 或做标签缩放。
- 看训练集样本数量:样本太少时,模型容易过拟合,而不是完全学不会。
卡住时先看日志和数据,不要急着改网络结构。
5.4 模态缺失时结果离谱,先别改模型
推理时如果某一路缺失,结果直接飞到不合理范围,问题通常出在训练阶段和推理阶段的不一致。比如训练时用零替换缺失值,推理时用均值替换;或者训练时做了正态噪声,推理时直接置零。这种不一致会让模型看到一种训练时没见过的输入分布。
更稳妥做法是在预处理函数里统一处理缺失策略。缺失段在训练和推理都用同一个函数生成掩码,模型输入增加一个缺损失的二进制通道,而不是直接拿 0 填充模型。这样可以避免模型把 0 理解成正常低幅值信号。
6. 从实验到可穿戴场景落地,还有几道坎
6.1 计算量和实时性
实验室训练出的模型如果直接搬到手表或手环上,往往跑不动。落地时要考虑模型大小和推理时延:
- 量化:把模型从 FP32 转成 int8,模型体积和推理速度会改善,但要验证量化后误差是否在接受范围内。
- 剪枝:删除一些不重要的卷积核,能减小体积,但需要重新微调。
- 流式处理:不要每次等满一个完整窗口再推理,可以设置 1 到 2 秒的滑窗步长,再对输出做中值滤波或卡尔曼滤波。
实时场景里还要考虑首包延迟。如果你需要在一个窗口内持续追踪心率,那模型每 10 秒输出一次可能不够,至少不会在运动剧烈变化时及时响应。
6.2 传感器差异和跨设备迁移
不同品牌手环、不同佩戴位置的 PPG 信号分布差异很大。一个模型在实验室传感器上效果好,换到另一台设备上可能完全不能用。所以要特别注意:
- 训练集包含多台设备时,按设备拆分测试,而不是混在一起。
- 如果只有单台设备,可以在预处理阶段加入随机幅度缩放和基线漂移,模拟设备差异。
- 部署前至少要采集少量目标设备数据做校准,重新评估一次。
不要因为实验室离线测试表现好,就直接声称能跨设备稳定工作。跨设备迁移是单独的一道评估流程。
6.3 数据隐私与使用边界
生理数据属于健康相关数据,无论做研究还是做产品,都要注意隐私和合规。原始数据不能明文传到第三方服务器;本地处理优先级高于云端处理;数据集采集时要明确授权范围。
技术文章里可以不展开法务细节,但工程实现上要留下脱敏、加密和日志审计的模块。如果你准备发布代码或数据集,要仔细检查是否包含可识别受试者身份的信息。
6.4 落地验证的优先级
真实落地时,我会先把重心放在三项验证上:
- 单受试者连续佩戴测试:至少跑 2 到 3 小时,看心率、血氧输出是否稳定,有没有周期性跳变。
- 运动状态切换测试:静止、走路、跑步、上下楼,每种状态至少 5 分钟,重点看过渡阶段的误差。
- 极端脱落恢复测试:故意让传感器松动 10 秒再贴紧,观察模型多久恢复到正常范围。
这类测试不能只看平均误差,要看时间曲线。如果脱落恢复后模型需要 30 秒才回到正常,说明后处理平滑参数太重,需要调低平滑系数或者引入质量检测。
我个人的建议是:先别急着把融合结构做复杂。把预处理、退化模拟、评估脚本跑通以后,再逐步增加模型容量和鲁棒策略。真正难的往往不是网络结构,而是当一路信号被污染时,模型还能不能给出可解释的置信判断。CardioFusion-AI 这类方案的价值,也正在于把这个问题从“分别处理好两条信号”变成“在信号彼此不可靠时也能协作输出一个稳定结果”。