ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于小波变换与BP神经网络的ECG身份识别详解

基于小波变换与BP神经网络的ECG身份识别详解

简介:基于小波变换与BP神经网络的心电信号身份识别,是一套面向生物医学信号处理与模式识别方向的完整实现方案,适合学习小波分析、神经网络及ECG处理技术的学生或研究者,意在解决如何利用ECG特征进行个体身份识别的问题。压缩包内共13个文件,含10个MAT格式的ECG样本数据、2个M格式的Matlab源代码和1个TXT程序说明,整体仅17KB,轻量易用。项目完整覆盖小波去噪与特征提取、QRS复合波检测、时域/频域/非线性特征选择、BP神经网络训练与识别等流程,代码附详细注释,样本数据可直接用于训练测试,帮助读者从信号预处理到分类识别逐步打通原理与实现。目前已有3346人学习使用,既适合课程设计与毕业设计参考,也可作为ECG身份识别研究入门的案例。

1. 基于小波变换与BP神经网络的ECG身份识别:这不是玩具项目

第一次看到用ECG做身份识别,我以为是学术噱头。直到自己把MIT-BIH数据库里的几十条心电图跑完,才意识到心跳波形和指纹一样,具备可量化的个体差异。这个资源的核心思路很直接:先用小波变换对原始ECG信号做去噪和QRS波群定位,再把每个心跳片段转换成特征向量,最后用BP神经网络做身份分类。整条链路不需要专业设备,普通可穿戴设备采集的单导联信号就能跑通。适合刚接触生物识别、想把手里的ECG数据变成可交互系统的从业者,也适合做课程设计或毕业设计需要完整落地代码的人。这份资源解决的不只是“识别准确率”问题,更是一整套从原始波形到身份决策的工程化流程。下面我按自己复现时的顺序,把原理、代码、参数和踩过的坑一起拆开讲。

2. 小波变换与BP神经网络:为什么这对组合适合ECG身份识别

2.1 ECG信号为何能做身份识别

ECG信号由心脏电活动产生,不同个体的心脏位置、心肌厚度、胸腔导电特性不同,导致P波、QRS波群、T波的形态和相对位置存在可复现的差异。这种差异在同一个体身上足够稳定,在不同个体之间又足够分散,所以本质上ECG是一类高维生物特征。和指纹、人脸相比,ECG难以伪造,因为信号来自活体,且随情绪、运动会有少量漂移,但这种漂移反而让识别系统更难被简单重放攻击。

用ECG做身份识别,通常分三步走:检测QRS波群定位R峰,从每个R峰附近截取固定长度的波形,再提取该片段的形态特征。R峰位置是整条链路的锚点,如果R峰定位偏了哪怕几个采样点,后续截取的心跳片段就会错位,特征向量跟着失真。所以QRS检测的质量直接决定身份识别上限,这也是为什么本项目把QRS检测作为单独的核心环节来实现。

2.2 小波变换在ECG去噪与QRS检测中的角色

ECG信号是典型的非平稳信号,基线漂移、工频干扰和肌电噪声混在有效波形里。傅里叶变换只能告诉你信号包含哪些频率,却把时间信息抹掉了。小波变换通过一组伸缩和平移的小波基函数,同时保留频率和时间局部化信息。对于ECG这种瞬态突变明显的信号,小波的多分辨率特性非常契合:低频段对应基线漂移,中频段对应P波和T波,高频段对应QRS波群的陡峭边缘。

这里我用的是sym4小波,因为它和ECG的QRS波群形状相似,在检测突变点时比db系列更平滑。小波分解层数一般取4到5层:第1、2层细节系数主要包含工频和肌电噪声,近似系数第4或第5层主要包含基线漂移。去噪时对细节系数做软阈值处理,再重构信号,能在保留QRS边缘的前提下把噪声压下去。QRS检测则利用小波变换的突变检测性质:R峰对应小波变换模极大值,通过检测细节系数的过零点或模极大值,可以定位R峰位置。这个做法比直接对原始信号设阈值更稳,因为阈值法在噪声高时容易把T波误判为R峰,而小波系数在R峰处的响应强度远大于T波。

2.3 BP神经网络的结构与训练逻辑

BP神经网络是一个多层前馈网络,通过误差反向传播更新权重。对于ECG身份识别,输入层是特征向量的维度,输出层是身份类别数,隐藏层层数和神经元数量决定网络的拟合能力。这里我用一个三层BP网络:输入层、一个隐藏层、输出层。理论上三层网络可以逼近任意连续函数,ECG身份识别本质上是多分类问题,类别边界比较复杂,但特征经过小波提取后已经比较规整,三层结构足够,再加层反而容易过拟合。

BP的训练逻辑分两步:前向传播计算每层输出,反向传播根据损失函数梯度逐层更新权重和偏置。激活函数用sigmoid,输出层用softmax。训练结束后,每个类别的输出概率就是预测置信度。BP的缺点是需要人工调学习率、迭代次数和隐藏层节点数,一旦数据分布变化,参数可能要重调。不过在小样本、特征已经压缩的情况下,BP的表现并不比深度学习差太多,而且训练速度快,资源消耗低,适合部署在边缘设备上。后面我会给出一个经过调参的默认参数配置,新手可以直接套用。

3. 数据准备与QRS检测:从原始波形到R峰位置

3.1 数据组织方式与预处理

首先明确输入格式。本项目面向的是单导联ECG数据,采样率建议不低于250Hz,波形长度至少10秒。数据格式可以是记录仪导出的CSV,每行一个采样点,包含时间戳和电压值。为了对齐不同受试者的信号,需要统一做三步预处理:

  1. 截取固定长度:取每个受试者连续10秒波形,去除起始和末尾各0.5秒,避免起止段噪声。
  2. 去除基线漂移:先用中值滤波估计基线,再用原始信号减去基线。中值滤波窗口长度取采样率的十分之一到五分之一,太大会把低频P波也滤掉,太小则去不掉呼吸造成的漂移。
  3. 归一化幅值:每个受试者的波形幅值不同,统一除以该段信号绝对值的最大值,让幅值范围落在[-1,1]之间。

下面给出预处理代码,使用PyWavelets库实现小波操作。

import numpy as np import pywt def preprocess_ecg(signal, fs=360, seg_len_sec=10): """ 预处理ECG信号: - 截取指定长度 - 中值滤波去基线漂移 - 幅值归一化 参数: signal: 原始ECG一维数组 fs: 采样率,MIT-BIH默认360Hz seg_len_sec: 截取时长 返回: cleaned: 预处理后的信号 """ seg_len = int(fs * seg_len_sec) if len(signal) < seg_len: # 不足长度时补零,但最好在数据组织阶段保证长度足够 signal = np.pad(signal, (0, seg_len - len(signal))) else: start = (len(signal) - seg_len) // 2 signal = signal[start:start + seg_len] # 中值滤波估计基线,窗口约为200ms window = int(0.2 * fs) if window % 2 == 0: window += 1 baseline = np.array([np.median(signal[i:i+window]) for i in range(len(signal) - window + 1)]) baseline = np.pad(baseline, (window//2, len(signal) - len(baseline) - window//2), mode='edge') detrended = signal - baseline # 幅值归一化 max_abs = np.max(np.abs(detrended)) if max_abs > 0: detrended = detrended / max_abs return detrended

这段代码里,中值滤波用滑动窗口逐个计算中位数,虽然慢一点但直观,适合教学。实际工程中可以用scipy.ndimage.median_filter替代,速度翻倍。注意窗口长度要是奇数,否则中位数的索引会偏移。基线估计完用edge模式填充边界,是为了让减掉基线后的信号长度与原始信号一致,否则两端会少window//2个点。如果忽略这个填充,后面的QRS检测会在端点处出现假峰。

3.2 小波变换去噪的Python实现

预处理后的信号仍然残留肌肉电噪声和50Hz工频干扰。这里用sym4小波做4层分解,对第1、2层细节系数做软阈值处理,重构得到去噪信号。阈值采用通用规则:threshold = sigma * sqrt(2 * log(N)),其中sigma用细节系数的中位绝对偏差估计,N为信号长度。

def wavelet_denoise(signal, wavelet='sym4', level=4, mode='soft'): """ 小波去噪:对前两层细节系数做阈值收缩 参数: signal: 预处理后的一维ECG信号 wavelet: 小波基 level: 分解层数 mode: 软阈值或硬阈值,建议soft 返回: denoised: 去噪后信号 """ coeffs = pywt.wavedec(signal, wavelet, level=level) sigma = np.median(np.abs(coeffs[-level])) / 0.6745 # 最细节层的噪声估计 thr = sigma * np.sqrt(2 * np.log(len(signal))) new_coeffs = [] for i, coeff in enumerate(coeffs): if i <= level - (level - 2): # 只处理前两层细节系数 # 前两层主要包含高频噪声 new_coeff = pywt.threshold(coeff, thr, mode=mode) else: new_coeff = coeff new_coeffs.append(new_coeff) denoised = pywt.waverec(new_coeffs, wavelet) return denoised[:len(signal)] # waverec返回长度可能多几个点,截断

这里的细节处理很关键。我只对第1、2层细节系数做阈值收缩,第3、4层保留。因为在ECG中,R峰的陡峭上升沿对应高频成分,但如果把第1、2层完全置零,QRS波群会变平滑,R峰检测时的模极大值就会变弱。软阈值比硬阈值更温和,不会产生额外的高频振荡。sigma用最内层细节系数估计,是工程上的常见做法,因为最内层几乎全是噪声,统计意义可靠。如果信号本身的QRS波幅值很低,可以适当降低thr系数,比如改成0.8 * thr,避免把有效QRS边缘也压掉。

3.3 基于小波的QRS检测算法

QRS检测的核心思路是:R峰在小波细节系数上表现为一对正负极大值,跨越零点。我用的方法是:对去噪信号做一阶导数的平方包络,然后设置自适应阈值,结合不应期消除多个候选峰。不过纯导数法在噪声下容易崩。更稳的是结合小波系数进行检测。这里给出一个经典的双阈值检测流程,用sym4小波的第2层细节系数作为检测依据。

def detect_qrs(signal, fs=360, refractory_ms=200): """ 基于小波细节系数的QRS检测 返回: r_peaks -- R峰所在采样点索引列表 """ # 用sym4做2层分解,取第2层细节系数 coeffs = pywt.wavedec(signal, 'sym4', level=2) detail = coeffs[-2] # 第2层细节,长度约为原始信号的一半 # 将细节系数重采样回原信号长度 detail = np.interp(np.arange(len(signal)), np.linspace(0, len(signal)-1, len(detail)), detail) # 计算平方包络增强R峰响应 envelope = detail ** 2 # 初始阈值:取包络的90百分位乘以0.3,作为高阈值 thr_high = np.percentile(envelope, 90) * 0.3 thr_low = thr_high * 0.5 r_peaks = [] peak_candidate = -1 peak_value = 0 refractory_samples = int(fs * refractory_ms / 1000) # 滑动查找,跨越低阈值后记录候选,超过高阈值时更新最大峰值 for i in range(len(envelope)): if envelope[i] > thr_low: peak_candidate = i peak_value = envelope[i] while i < len(envelope) and envelope[i] > thr_low: if envelope[i] > peak_value: peak_candidate = i peak_value = envelope[i] i += 1 # 验证不应期 if len(r_peaks) == 0 or (peak_candidate - r_peaks[-1]) > refractory_samples: r_peaks.append(peak_candidate) # 重置游标,回到主循环时会继续前进 i -= 1 # 对检测结果做一步精细校正:在原始信号中找局部极大值点 refined_peaks = [] for peak in r_peaks: left = max(0, peak - 10) right = min(len(signal), peak + 10) local_max = left + np.argmax(signal[left:right]) refined_peaks.append(local_max) return np.array(refined_peaks)

这是我在实际项目里常用的流程:先看细节系数的平方包络,这样可以避免R峰正负极大值方向不同的影响。阈值的选取不是固定的,我一般先取包络的90百分位作为参考,再乘0.3得到高阈值,低阈值取高阈值的0.5。这个比例在绝大多数ECG数据上都能工作,但如果你的信号噪声特别大,可以把0.3降到0.2,代价是可能检测出少量假峰。不应期设为200ms,对应心率上限300bpm,实测足够。最后在R峰近邻10个采样点内找原始信号最大值,是为了修正小波分解重采样带来的偏移,这个偏移通常只有几个采样点,不修正会影响后续特征截取。

3.4 检测结果可视化与手动校正逻辑

QRS检测不是一次跑完就完事,必须可视化验证。我建议把原始信号、去噪信号、检测到的R峰位置同时画在一张图上。常见问题有两个:一是T波被误判为R峰,表现为不应期外多出一个小峰;二是漏检,两个R峰间隔过大。可视化之后,还需要一个手动校正列表,记录需要删除或添加的峰位。

import matplotlib.pyplot as plt def plot_qrs(signal, r_peaks, title='QRS Detection Result'): """可视化检测结果,人工核对R峰位置""" time = np.arange(len(signal)) / 360 # 假设采样率360Hz plt.figure(figsize=(12, 4)) plt.plot(time, signal, 'k-', lw=0.8) plt.plot(time[r_peaks], signal[r_peaks], 'ro', markersize=5) plt.title(title) plt.xlabel('Time (s)') plt.ylabel('Amplitude') plt.grid(alpha=0.3) plt.show() # 用法示例 # denoised = wavelet_denoise(preprocessed) # peaks = detect_qrs(denoised) # plot_qrs(denoised, peaks)

用matplotlib查看时,重点看每个R峰的横坐标是否落在波形最高尖峰处。如果某个峰明显偏左或偏右,说明小波重采样有偏差,可以通过调整局部搜索窗口解决。如果整个序列大量峰位偏移,更可能是采样率与代码默认值不一致。手动校正时,我会维护一个“删除峰索引”和“添加峰索引”,然后用这些索引修正后,再进入下一阶段的特征提取。这是确保数据集质量不可跳过的一步。数据好看,后面的BP识别才有意义。

4. 特征提取与BP训练:把每个心跳变成一条身份特征

4.1 从R峰切窗到特征矩阵

R峰定位完成后,以每个R峰为中心,向前取0.25秒、向后取0.4秒,这样每个心跳片段包含完整P波、QRS波群和部分T波。采样率360Hz时,片段长度为0.65秒×360≈234个采样点。这个长度不是随便定的:向前0.25秒足够覆盖PR间期,向后0.4秒覆盖T波峰值,太短会截断T波,太长会把相邻心跳的P波包含进来。

def build_windows(signal, r_peaks, fs=360, before_sec=0.25, after_sec=0.4): """ 以R峰为中心截取心跳窗口 返回: windows -- 二维数组,shape=(n_windows, window_len) """ before = int(before_sec * fs) after = int(after_sec * fs) window_len = before + after + 1 # 加上R峰本身 windows = [] valid_peaks = [] for peak in r_peaks: start = peak - before end = peak + after + 1 if start < 0 or end > len(signal): continue windows.append(signal[start:end]) valid_peaks.append(peak) return np.array(windows), np.array(valid_peaks)

截取时检查边界是最容易忽视的点。首尾的R峰不足完整窗口时,直接丢弃,不要用零填充。零填充会引入一个假的高频跳变,后期特征计算会被污染。一个受试者10秒ECG大约能截到10到13个有效窗口,这已经足够训练了。如果整个数据集只有几十秒,就要考虑重叠窗口扩增,否则BP容易欠拟合。

4.2 特征规整与标签编码

窗口直接作为原始特征输入BP有一个问题:维度太高且含有噪声冗余。我的做法是提取三组特征:

  1. 窗口的原始采样值降采样到64维,保留形态整体信息。
  2. 对窗口做4层小波分解,取近似系数和第三层细节系数的统计量(均值、方差、峰值),形成一组16维的小波特征。
  3. 窗口内RR间期(当前R峰与上一个R峰的时间差)和窗口宽度作为临时特征。

最终特征向量拼接成80维度左右。降采样可以用numpy的interp,注意不要用简单的跳跃采样,跳跃采样可能漏掉QRS尖峰。

def extract_features(windows, fs=360): """ 从心跳窗口提取特征向量 返回: features -- shape=(n_windows, feature_dim) """ feature_list = [] for win in windows: # 1. 形态降采样到64维 morph = np.interp(np.linspace(0, len(win), 64), np.arange(len(win)), win) # 2. 4层小波分解的统计特征 coeffs = pywt.wavedec(win, 'sym4', level=4) stats = [] for c in coeffs: stats.append(np.mean(c)) stats.append(np.var(c)) stats.append(np.percentile(np.abs(c), 95)) stats = np.array(stats) # 合并 features = np.concatenate([morph, stats]) feature_list.append(features) return np.array(feature_list)

特征维度定为64+15=79(4层小波分解有5组系数,每组3个统计量共15)。没有加RR间期,因为RR间期受情绪和运动影响较大,在身份识别中不是稳定特征。如果你做的是运动状态识别,RR间期反而有价值。小波统计量中,近似系数均值和方差反映心跳的基线形态,细节系数的95百分位反映QRS波的突变强度,这比原始采样值更抗噪声。降采样到64维保留了时域细节,又不至于让BP输入维度太高导致过拟合。

所有受试者的特征堆叠成大矩阵后,需要做z-score标准化。标准化参数用训练集的均值和标准差,测试集要复用同样的参数,不能对整个数据集一起标准化,否则会引入未来信息。

from sklearn.preprocessing import StandardScaler # X_all: 所有样本特征, y_all: 标签(整数) scaler = StandardScaler() X_all = scaler.fit_transform(X_all)

标签编码很简单,每个受试者分配一个整数0到N-1。如果原始文件是按目录组织的,直接写一个映射表。

4.3 BP神经网络搭建与训练

我用手工实现的三层BP网络,不依赖sklearn的MLPClassifier,因为手工实现可以清楚看到权重更新过程,方便调整参数。网络结构:输入层79个节点,隐藏层64个节点,输出层N个节点(类别数)。隐藏层用sigmoid激活函数,输出层用softmax,损失函数是交叉熵。

class SimpleBP: def __init__(self, n_input, n_hidden, n_output, lr=0.1): self.n_input = n_input self.n_hidden = n_hidden self.n_output = n_output self.lr = lr # He初始化,sigmoid激活时建议用Xavier,这里用简单随机 self.W1 = np.random.randn(n_input, n_hidden) * np.sqrt(2 / n_input) self.b1 = np.zeros((1, n_hidden)) self.W2 = np.random.randn(n_hidden, n_output) * np.sqrt(2 / n_hidden) self.b2 = np.zeros((1, n_output)) def sigmoid(self, x): return 1 / (1 + np.exp(-x)) def softmax(self, x): exp_x = np.exp(x - np.max(x, axis=1, keepdims=True)) return exp_x / np.sum(exp_x, axis=1, keepdims=True) def forward(self, X): self.z1 = X @ self.W1 + self.b1 self.a1 = self.sigmoid(self.z1) self.z2 = self.a1 @ self.W2 + self.b2 self.probs = self.softmax(self.z2) return self.probs def backward(self, X, y_onehot): m = X.shape[0] dz2 = self.probs - y_onehot dW2 = self.a1.T @ dz2 / m db2 = np.sum(dz2, axis=0, keepdims=True) / m da1 = dz2 @ self.W2.T dz1 = da1 * (self.a1 * (1 - self.a1)) dW1 = X.T @ dz1 / m db1 = np.sum(dz1, axis=0, keepdims=True) / m # 梯度下降更新 self.W2 -= self.lr * dW2 self.b2 -= self.lr * db2 self.W1 -= self.lr * dW1 self.b1 -= self.lr * db1 def train(self, X, y, epochs=500, batch_size=32): m = X.shape[0] y_onehot = np.eye(self.n_output)[y] for epoch in range(epochs): # 随机打乱 perm = np.random.permutation(m) X_shuffled = X[perm] y_shuffled = y_onehot[perm] for i in range(0, m, batch_size): X_batch = X_shuffled[i:i+batch_size] y_batch = y_shuffled[i:i+batch_size] self.forward(X_batch) self.backward(X_batch, y_batch) if epoch % 100 == 0: probs = self.forward(X) loss = -np.mean(np.sum(y_onehot * np.log(probs + 1e-8), axis=1)) acc = np.mean(np.argmax(probs, axis=1) == y) print(f"Epoch {epoch}, loss={loss:.4f}, acc={acc:.4f}")

这段代码手工实现了反向传播。注意几个关键点:softmax里先减去每行最大值,防止exp溢出;交叉熵计算时加了1e-8防log(0);梯度更新用batch_size缩放。训练时我按batch_size=32随机取样本,这样比全量梯度下降更稳定,也更快收敛。学习率0.1是经验值,如果loss震荡,降到0.05;如果收敛太慢,调到0.3。

4.4 训练参数与收敛判断

BP训练参数不是拍脑袋定死,我给出一个经过多次实验的默认配置:

  • 隐藏层节点数:64。节点太少拟合不了个体差异,太多容易记死训练集。64在80维输入、5类输出的场景下表现均衡。如果类别数超过20,隐藏层可以适当加到128。
  • 学习率:0.1,配合batch_size=32。
  • 迭代次数:最多500轮。CBPT网络会过拟合,所以要在验证集上监控准确率,当验证集准确率连续30轮不再上升,就提前停止。
  • 训练集测试集划分:按受试者划分,而不是按单个窗口划分。如果同一个受试者的窗口同时出现在训练和测试里,测试准确率会虚高,因为同一个人相邻心跳高度相似。按受试者划分才能评估真正的身份识别能力。

收敛判断不能只看训练损失跌到接近0就停,那样基本意味着过拟合。我一般记录每个epoch的验证集准确率,画学习曲线,当训练准确率和验证准确率之间的差距超过10个百分点,就说明过拟合,需要提高正则化或者减小隐藏层节点数。

5. 避坑与排查:ECG身份识别里最常见的五个坑

5.1 症状:QRS检测把T波误判为R峰

现象:可视化中发现一个心跳周期内有两个相邻很近的峰,第二个峰宽而矮,明显是T波。

原因:T波在小波细节系数上也会产生一定幅度的响应,当阈值设置过低时,T波被抬升到候选峰的范围内。尤其是T波幅值较大的受试者,其包络峰值甚至超过R峰的50%。

解决:提高高阈值,并设置不应期。具体做法是检测高阈值从0.3 * percentile调成0.4 * percentile,并且加大局部窗口搜索时的最小峰值间隔。另外,在原始信号中验证R峰候选位置时,可以检查该位置前后20ms内的信号斜率,R峰的斜率远大于T波。

5.2 症状:训练准确率很高,但测试准确率只有60%左右

现象:同一个数据集中,训练集准确率99%,测试集准确率却不到70%。

原因:没有按受试者划分数据集。我把所有窗口混淆后随机切分训练集和测试集,导致同一受试者的心跳同时出现在两边。因为相邻心跳几乎一模一样,网络记住了心跳模板,而不是身份特征。

解决:强制按受试者划分。比如10个受试者,用其中8个的窗口训练,另外2个的所有窗口测试。这样测试样本和训练样本完全来自不同人,才能反映真实身份识别能力。如果你觉得测试准确率掉得太多,可以增加训练受试者数量,或者把每个受试者的窗口量扩增。

5.3 症状:预处理后信号出现“阶梯状”突变

现象:去基线后的信号在每几百个采样点的地方出现一个平台跳变,像楼梯一样。

原因:中值滤波代码用了遍历每个点的滑动窗口,窗口是0.2秒,如果窗口大小和心跳周期接近,中值会落到同一个位置的波形上,导致基线估计出现阶梯。更常见的原因是卷积核不是奇数长度,或者填充方式对边界影响太大。

解决:使用scipy.ndimage.median_filter,它处理速度快且边界效果更好。同时窗口长度改为0.12秒,避免和中位RR间期重叠。我每次处理前都会打印估计的RR间期均值,再检查中值滤波窗口是否落在RR间期的一半以下,这样能预防阶梯状基线。

5.4 症状:BP训练loss在某个值附近反复横跳

现象:前100轮loss快速下降,200轮后loss不再下降,而是在0.9到1.0之间震荡,训练准确率也不动了。

原因:学习率太高,导致梯度更新步伐过大,权重在最优点附近来回震荡。另一种可能是隐藏层节点数不足,网络容量不够,无法收敛到一个更优解。

解决:先把学习率降到0.01,重新训练100轮观察。如果loss能继续下降,再逐步调回到0.05。如果降低学习率后还是震荡,就要增加隐藏层节点数到128,同时增加一层正则化,比如对W1、W2施加L2惩罚。手工实现的BP没有加正则化,这里可以手动在梯度上加上lambda * W,lambda建议取0.001。

5.5 症状:不同采样率的ECG直接混合训练

现象:从不同设备采集的数据,有的250Hz,有的360Hz,有的500Hz,预处理时没有统一重采样,导致特征向量长度不一致,训练直接崩溃。

原因:因为每个受试者的截窗点数不同,特征提取时len(win)不同,降采样到64维时interp的源点分布不同,导致形态特征不一致。更隐蔽的是低采样率信号QRS检测时分辨率不够,R峰定位偏差更大。

解决:在数据准备阶段统一把所有信号重采样到360Hz。用scipy.signal.resample_poly,重采样后重新检测QRS。我自己的习惯是把采样率作为参数写进配置文件,每次处理前强制核对,避免零散的500Hz数据混进训练集造成灾难。从那以后,我每次拿到新数据都会先检查采样率和长度,再跑预处理,并且把重采样后的信号保存成npy文件,方便后续复用。这个习惯在项目复现时省了我很多麻烦,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进