ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

基于Matlab与机器学习的心电疲劳检测:HRV特征提取与分类实战

基于Matlab与机器学习的心电疲劳检测:HRV特征提取与分类实战 简介这套基于 MATLAB 与机器学习的心电疲劳检测项目面向毕业设计、课程设计和项目开发场景以心电信号为输入完成疲劳状态的识别与评估。压缩包共二十四个文件总计约四点一七 MB其中 .m 与 .mlx 脚本覆盖信号预处理、特征提取、模型训练与结果可视化.xlsx 提供心电采集数据和时域特征数据.mat 存放训练好的模型.md 与 .txt 则给出说明和注释整体结构清晰便于按模块研读。项目源码经过严格测试可放心参考并在其基础上扩展。内容涉及小波阈值去噪、Pan-Tompkins QRS 检测、ARMA 系数、LSTM 建模及混淆矩阵评估等关键环节能帮助学习者快速理解从原始心电数据到疲劳识别模型的完整链路也适合作为相关课程设计或答辩展示的落地模板。目前已有四十四人浏览学习便于在真实项目场景中对照使用与二次开发。1. 心电疲劳检测为什么说这是一条“能毕业又不劝退”的技术路线如果你正为毕业设计或者课程设计选题发愁又不想一上来就碰脑电、眼动这些昂贵设备那基于 Matlab 机器学习实现的心电疲劳检测是一个性价比很高的方向。核心思路并不复杂把心电ECG信号切成若干时间窗从每个窗口里提取心率变异性HRV等特征再用分类器输出“疲劳 / 清醒”的判别结果。你需要的材料也很常规一个带疲劳标签的心电数据集、Matlab 2019b 及以上版本、以及基本的信号处理常识。这个方向适合两类人一类是需要快速跑通“采集—特征—模型—评估”全流程的在校学生另一类是打算在可穿戴场景里做疲劳状态监测的开发者。它真正的难点其实不在分类器而在两个容易被忽略的问题疲劳标签怎么定义才算合理以及特征提取和验证方式怎么绕开数据泄漏。这篇文章把这两件事的前因后果讲透。2. 疲劳为什么能在心电信号里露馅HRV特征与生理学解释2.1 疲劳检测的两个前提标签怎么定信号从哪来做疲劳检测的第一个问题是什么叫疲劳这是一个主观状态没有金标准。常见的打标方式有三种主观量表如 KSS 嗜睡量表、疲劳自评问卷、任务时长连续工作 2 小时以上视为疲劳、客观行为指标反应时变长、眼动变慢。毕业设计阶段最稳妥的做法是让受试者在清醒和疲劳两种状态下各采集一段心电然后按“段”打标签而不是按“秒”打标签。因为疲劳是一个缓慢变化的过程逐秒标注既不可靠也没必要。第二个问题是为什么选心电而不是脑电脑电对设备要求高眼动需要摄像头和图像算法而心电只需要贴片电极或胸带Matlab 里读入数据直接就能处理。更重要的是心电信号虽然看起来只是一串 QRS 波形但 R 峰之间的时间间隔RR 间期隐含着自主神经系统的调控信息而疲劳恰恰会改变这种调控模式。这就是心电能做疲劳检测的生理学基础。2.2 自主神经系统的痕迹HRV 为什么跟着疲劳变化心率变异性HRV衡量的是相邻心跳间隔的波动程度。你可能直觉上认为心跳越规律越好实际上恰恰相反健康的、清醒状态下的心脏RR 间期有丰富的波动而疲劳或压力状态下交感神经占优势副交感神经被抑制RR 间期的波动幅度会明显下降。换句话说疲劳的人心跳可能并不快但心跳间隔变得“机械而单调”。HRV 特征可以分成三组时域特征直接对 RR 间期做统计频域特征把 RR 间期序列变换到频谱上观察不同频段的能量非线性特征则用熵或散点图描述序列的复杂程度。其中时域特征最容易理解也是入门时优先复现的。疲劳发生时SDNNRR 间期标准差和 RMSSD相邻 RR 差异的均方根通常会下降pNN50 也会变小这些趋势在很多公开研究里都被反复验证过。2.3 用 Matlab 提取 HRV 特征的最小代码下面的代码演示了从一段 ECG 信号到 HRV 特征的全过程。这里假设你已经完成了基础的带通滤波ecg是滤波后的心电列向量fs是采样率。%% 1. 检测 R 峰位置 % MinPeakDistance 设置为 0.6s对应最大心率 100 bpm防止把 T 波误检为 R 峰 [~, locs] findpeaks(ecg, ... MinPeakHeight, 0.3 * max(ecg), ... MinPeakDistance, round(0.6 * fs), ... MinPeakProminence, 0.15 * max(ecg)); %% 2. 计算 RR 间期单位秒并转成毫秒序列 rr diff(locs) / fs; rr_ms rr * 1000; %% 3. 时域 HRV 特征 sdnn std(rr_ms); % RR 间期标准差 rmssd sqrt(mean(diff(rr_ms).^2)); % 相邻 RR 差异的均方根 nn50 sum(abs(diff(rr_ms)) 50); % 相邻差超过 50ms 的个数 pnn50 nn50 / length(rr_ms) * 100; % pNN50 百分比这里MinPeakProminence的作用比预期更重要。ECG 中 T 波的幅度有时会接近 R 峰的 50%仅靠高度阈值区分不了 R 峰和 T 波。MinPeakDistance是从时间上限制两个 R 峰的最小间距MinPeakProminence则要求候选峰相对于周围波形有足够的突出程度。采样率 250 Hz 时round(0.6 * 250)就是 150 个采样点。如果你的数据包含运动员或高心率受试者需要把 0.6s 调小到 0.45s 左右否则会把真实心跳漏检。2.4 建议优先提取的特征清单下面这张表列出的特征是疲劳检测里最常被论文引用、也最容易复现的。建议第一个版本先做这些。特征名计算方式疲劳时的一般趋势备注SDNNRR 间期标准差下降反映整体变异性RMSSD相邻 RR 差的均方根下降反映副交感活性最稳定pNN50相邻差 50ms 的占比下降对短时窗敏感窗口太短会失真LF / HF频段能量0.04–0.15 Hz / 0.15–0.4 HzLF/HF 常升高需要 RR 间期重采样SampEn样本熵下降非线性特征量级小但区分度高频域特征的坑在 RR 间期是非等间隔采样不能直接做 FFT必须先重采样成等间隔序列再算功率谱这一步在下一章详细展开。3. 从原始心电到机器学习输入R 峰检测、滑窗切片的 Matlab 路线图3.1 数据集怎么组织先想清楚再动手很多人在跑通模型之后才发现数据文件夹结构不合理导致训练脚本里全是if strcmp(subject, subj07)这样的补丁。我的建议是一开始就按“受试者—条件—时间段”三级目录组织。比如data/subj01/fatigue/seg1.mat表示 1 号受试者在疲劳状态下采集的某一段心电。Matlab 里用dir批量遍历这三个层级脚本逻辑会干净很多。数据集的原始信号一般要存成单独的.mat文件里面至少包含三个变量ecg原始信号、fs采样率、label0 表示清醒1 表示疲劳。标签放在每一段信号的 meta 里而不是放在全局变量里这样后续做滑窗切分的时候每个窗口能继承它所属时间段的标签。3.2 预处理三步去基线、带通滤波、重采样ECG 原始信号里通常混着三样东西50 Hz 工频干扰、肌电噪声、以及因呼吸和电极移动造成的基线漂移。疲劳检测关心的是 RR 间期所以预处理的核心目标是让 R 峰检测稳定而不是把波形还原得多漂亮。%% 预处理基线漂移 带通滤波 % 假设 ecg_raw 是原始信号fs 是采样率 % 1) 用局部中位数估计基线减法去漂移 base movmedian(ecg_raw, round(0.2 * fs)); ecg_detrend ecg_raw - base; % 2) 0.5~40 Hz 带通保留 QRS 主能量去掉肌电和高频噪声 ecg_filt bandpass(ecg_detrend, [0.5 40], fs);movmedian的窗口长度按 0.2 秒设置这个长度远小于一个心搏周期因此估计出的“基线”不会把 QRS 波吞掉只跟踪缓慢变化的漂移。bandpass的低频设 0.5 Hz是因为呼吸造成的基线变化频率通常在 0.2~0.5 Hz 之间低于 0.5 Hz 的成分丢掉不影响 QRS高频设 40 Hz 是为了保留 QRS 的陡峭边沿同时滤掉大部分肌电噪声。如果你的数据采样率只有 125 Hz上限就要降到 30 Hz 左右否则滤波结果会出现边缘抖动。3.3 R 峰检测闭着眼睛用 findpeaks 一定会翻车用findpeaks直接找 R 峰第一个版本一定会遇到两类误检把 T 波当成 R 峰或者把运动伪迹当成 QRS。处理思路是在检测之后加一个合理性校验而不是盲目调阈值。%% R 峰检测 合理性校验 [~, locs] findpeaks(ecg_filt, ... MinPeakDistance, round(0.5 * fs), ... MinPeakProminence, 0.2 * max(ecg_filt)); rr diff(locs) / fs; % 剔除 RR 间期小于 0.4s心率150bpm或大于 2s心率30bpm的异常点 valid rr 0.4 rr 2.0; locs_valid locs([false; valid]); % 第一个 R 峰没有对应的 RR 间期直接保留参数说明MinPeakDistance取 0.5 秒是下限对应 120 bpm这个值可以保证不会在同一心搏里检出两个峰同时不会漏掉心率稍快的受试者。MinPeakProminence取 0.2 倍幅度理由是 T 波的突出程度通常小于这个比例。合理性校验这一步绝对不能省运动伪迹常常会让某一段的 RR 间期直接掉到 0.2 秒如果不剔除后面算出来的 SDNN 会虚高。3.4 滑窗切片与标签继承特征矩阵的构造R 峰检测完成后下一步是构造机器学习需要的特征矩阵。疲劳检测一般不用整段信号做一条样本而是把信号切成 30~60 秒的窗口每个窗口算一组特征特征矩阵的一行就是一个样本。%% 滑动窗口切分 RR 间期序列构造特征矩阵 win_len 30; % 窗口长度秒 step 10; % 步长秒窗口重叠 2/3 % 先把 R 峰时间转成连续时间轴 t_r locs_valid / fs; features zeros(0, 5); % 5 个特征SDNN, RMSSD, pNN50, LF/HF, SampEn labels []; for t0 0 : step : (max(t_r) - win_len) idx t_r t0 t_r t0 win_len; rr_win rr(valid) * 1000; % 对应窗口内的 RR 间期ms if length(rr_win) 20 continue; % 窗口内心搏太少特征不可靠 end f extract_hrv_features(rr_win); % 自定义函数见下方 features(end1, :) f; labels(end1, :) seg_label; % 整段时间段共用一个标签 end窗口长度选 30 秒是经验值太短20 秒时 RMSSD 和 pNN50 的统计稳定性很差太长5 分钟则丧失了疲劳随时间变化的检测能力。步长 10 秒让相邻窗口有重叠一定程度上能平滑预测结果的抖动。extract_hrv_features里做的事情就是把上一章提到的 5 个特征全部算出来RD 间期标准差、RMSSD、pNN50、LF/HF 以及样本熵。频域特征这一步经常有同学直接对 RR 间期序列调用fft得到的结果完全没有意义因为 RR 间期是事件序列时间间隔不均匀。正确做法是先按 4 Hz 重采样成均匀序列再算功率谱。Matlab 里用interp1就能实现重采样频率选 4 Hz 即可覆盖 0.04~0.4 Hz 的 HRV 频带。4. 用 Matlab 训练疲劳分类模型特征筛选、分类器对比与交叉验证参数4.1 特征矩阵准备好了先做两件事归一化与正确划分特征矩阵的行是窗口列是特征。第一个容易犯的错是把所有样本混在一起做归一化然后再划分训练集和测试集。这看起来只是顺序问题实际上会造成数据泄漏测试集的均值和标准差已经通过归一化参与到了训练过程中导致交叉验证分数虚高。正确做法是先划分、再对训练集算归一化参数、最后应用到测试集上。第二个容易犯的错是随机划分样本。同一个受试者的相邻时间窗高度相关随机划分会把同一个人的窗口同时分进训练集和测试集模型很容易“认出”这个人。正确的划分粒度是受试者保证同一个人的所有窗口要么全在训练集要么全在测试集。这是拿去做毕设答辩时评委最常追问的一个点。4.2 按受试者划分数据的 Matlab 写法%% 按受试者分组划分而不是按样本行随机划分 subjects unique(subject_id); % subject_id 是和特征矩阵等长的列向量 cv cvpartition(numel(subjects), KFold, 5); for k 1 : cv.NumTestSets test_subjects subjects(cv.test(k)); test_idx ismember(subject_id, test_subjects); train_idx ~test_idx; X_train features(train_idx, :); y_train labels(train_idx, :); X_test features(test_idx, :); y_test labels(test_idx, :); % 只对训练集估计归一化参数 mu mean(X_train); sigma std(X_train); X_train_norm (X_train - mu) ./ sigma; X_test_norm (X_test - mu) ./ sigma; % 在这里训练模型并记录每一折的指标 endcvpartition(numel(subjects), KFold, 5)的对象是受试者编号的列表而不是特征矩阵的行数。ismember负责把受试者编号映射回样本行的逻辑索引。归一化参数mu和sigma必须在每一折里重新计算这个细节决定了交叉验证结果是否可信。4.3 一个能当默认基线的分类器集成树对于特征数不多几十个、样本量不大几百到几千行的疲劳检测任务集成树是一个很好的默认基线。它不需要精细调参对特征量纲不敏感还能天然处理特征之间的非线性关系。%% 集成树分类5 折交叉验证 rng(42); % 固定随机种子结果可复现 mdl fitcensemble(X_train_norm, y_train, ... Method, Bag, ... NumLearningCycles, 50, ... Learners, templateTree(MinLeafSize, 3)); % 查看训练集上的交叉验证准确率 cvmdl crossval(mdl); loss kfoldLoss(cvmdl, LossFun, ClassifError); acc 1 - loss;Method选Bag即随机森林的 bagging 形式NumLearningCycles是树的数量。50 棵树对疲劳检测这个规模的数据足够再多收益很小且训练时间变长。MinLeafSize设 3 是防止单棵树的叶子太大导致欠拟合这个值可以视样本量调整样本量越小叶子应该越小否则模型学不到细节。4.4 特征太多时先用 relieff 做一轮筛选如果做完特征工程后特征数量超过几十个我一般会先做一次有监督的特征筛选而不是直接全部丢进分类器。疲劳检测里有些特征之间存在强相关性比如 SDNN 和总功率谱能量经常高度相关同时放进去不会提升精度却会让模型更难解释。%% 用 relieff 做特征重要性排序 [idx, weights] relieff(features, labels, 10); % 输出按重要性排序的特征索引取前 8~12 个 top_features idx(1:min(12, numel(idx)));relieff的第三个参数是最近邻数量默认 10 即可。它的原理是随机选样本在同类和异类中各找最近邻比较特征在这些邻居上的差异差异越大的特征权重越高。筛选之后重新跑 4.3 的集成树你会发现交叉验证分数要么持平要么略微上升但模型解释起来轻松得多。4.5 分类器对比给毕业设计准备的基线表疲劳检测的公开数据集一般没有大到能支撑深度学习经典的机器学习分类器已经够用。下面是四种常见分类器在这个任务上的定位方便你在论文里做对比实验时参考。分类器Matlab 函数适合场景需要关注的参数集成树fitcensemble默认基线鲁棒NumLearningCycles, MinLeafSize支持向量机fitcsvm特征维度高、样本量小KernelScale, BoxConstraint线性判别fitcdiscr特征近似高斯分布、追求可解释无特别参数K 近邻fitcknn数据量小、不做实时部署NumNeighbors, DistanceSVM 在小样本场景经常表现不错但要用KernelScale, auto让它自动估计核宽度否则手动给定的尺度往往不合适。疲劳检测的输出如果只需要“疲劳 / 清醒”二分类SVM 和集成树的差距通常不大没必要过度纠结分类器选型。真正决定上限的是特征质量和验证方式。5. 心电疲劳检测的常见问题排查5 条翻车实录与修复方法5.1 数据泄漏交叉验证准确率 98%答辩现场被打脸现象模型在交叉验证里准确率接近满分但换成新受试者数据后准确率跌到 60% 左右。原因这是疲劳检测里最隐蔽的坑。大多数情况下是因为归一化或特征筛选在整个数据集上完成测试集的信息混入了训练流程另一个常见原因是同受试者的窗口被随机分到了训练集和测试集模型实际上在记忆受试者身份而不是在识别疲劳状态。解决严格按照 4.2 节的“先按受试者分组、再在训练集内部做归一化”的流程重写评估脚本。检查方法很简单打印出每一折训练集和测试集的受试者 ID确认没有重叠。这个动作花十分钟但能避免答辩时被评委一句话问住。5.2 R 峰误检T 波被当成 R 峰HRV 特征全线失真现象提取出的 RR 间期序列里频繁出现 0.3 秒左右的“心跳间隔”SDNN 和 RMSSD 异常偏大疲劳趋势完全反了。原因findpeaks只认峰值不认波形形态。在疲劳后期心电信号形态会因肌肉紧张和呼吸变化而变形T 波幅度升高很容易满足峰值条件。如果同时没有设置MinPeakDistance就会把 T 波误检成 R 峰。解决除了加MinPeakDistance我在预处理里额外加了一步 RR 间期合理性校验剔除小于 0.4 秒和大于 2.0 秒的间隔并把被剔除间隔两侧的 R 峰一并标记为可疑。同时在特征提取前打印一个简单的直方图肉眼确认 RR 间期的分布峰值在 0.6~1.0 秒之间这属于心电分析的直觉检查能发现大多数误检问题。5.3 样本不平衡模型永远输出“清醒”准确率还挺高现象疲劳样本只占总样本的 20%训练出的模型在测试集上准确率 80% 以上但混淆矩阵显示所有的疲劳样本都被判成了清醒。原因这是典型的类别不平衡问题。分类器默认优化整体准确率对占比大的类别更“友好”少数类样本被牺牲了。疲劳数据采集成本高、受试者配合度有限疲劳段天然少于清醒段。解决先在训练时给少数类加权重。集成树可以用fitcensemble后处理或者直接换用fitcsvm并设置Weights, w把疲劳类的权重设为清醒类的 3 倍左右。评价指标也要改不要只看准确率要看疲劳类的召回率和 F1-score。Matlab 里用confusionchart画混淆矩阵一眼就能看出问题在哪。5.4 频域特征频谱混乱直接用 FFT 处理 RR 间期现象用fft(rr_ms)算出的 LF/HF 忽大忽小完全没有规律。原因RR 间期是事件序列时间间隔不均匀。休息时心跳间隔约 1 秒但每一秒都不完全一致直接对原始 RR 序列做 FFT采样时间点不固定频谱已经失真了。解决先用interp1把 RR 间期重采样到 4 Hz 等间隔序列再做功率谱估计。Matlab 里pburg或pwelch都行pburg对短数据更友好。注意重采样前先去除异常 RR 间期否则一个 2 秒的伪迹会被插值成大片低频能量。5.5 特征全部堆上去维度高、样本少、过拟合现象特征工程做了 30 个特征交叉验证分数反而比只用 5 个特征时低测试集波动很大。原因样本量窗口数可能只有几百而特征维度到了几十模型进入过拟合区间。尤其是非线性特征之间相关性高重复信息被模型当成相互独立的证据放大了噪声。解决先做相关性矩阵把相关性大于 0.85 的特征去掉一组再用 4.4 节的relieff排序只保留前 10~12 个。在疲劳检测这个任务上精简特征后的模型在跨受试者测试里通常比全特征模型更稳。5.6 避坑自查清单做完一轮实验后按下面这个清单逐项检查能拦截大多数翻车事故检查项通过标准受试者分组每一折的训练集与测试集受试者 ID 零重叠归一化时机每一折内先 fit 训练集再 transform 测试集RR 间期分布直方图主峰位于 0.5~1.2 秒无大量 0.4 秒以下短间隔类别不平衡混淆矩阵中疲劳类召回率不是 0频域特征重采样为等间隔序列后再算功率谱6. 把疲劳检测模型真正用起来滑动平滑、置换检验与最小可行验证毕设交稿或者课程设计验收之前我建议花半天时间做三件小事它们能显著提升项目的说服力。第一给预测结果加滑动平均。模型的输出是每个窗口的疲劳概率相邻窗口概率波动往往很大直接按 0.5 阈值硬切会产生频繁的状态跳变。我在部署时一般对概率序列做长度为 3~5 的滑动平均再套阈值状态切换就平滑了。这个技巧不改变模型本身却让演示效果完全不同。第二做一次置换检验。把标签随机打乱 100 次每次重新训练模型并记录准确率如果真实准确率落在随机打乱后的分布边缘说明模型并没有学到有效信息。Matlab 里用一个for循环加上randperm就能实现这个实验放在论文里是很有说服力的基线验证。第三用最小可行模型验证整个流程。我会先用 SDNN、RMSSD、pNN50 这 3 个最基础的时域特征跑通一遍确认疲劳组的特征值确实低于清醒组再做频域和非线性特征。如果基础特征连趋势都反了问题大概率不在模型而在 R 峰检测或标签定义上这时候继续加特征只会让错误更隐蔽。我自己的习惯是每次拿到一份新的心电数据先打印一页报告RR 间期直方图、两个窗口的时域特征对比、以及分类器的混淆矩阵三样东西花不了两分钟却能避免在错误的方向上调试一整天。做这类项目慢不是问题方向错了才是。希望这些经验能在你的心电疲劳检测项目里派上用场祝顺利。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表