☰
基于Matlab与机器学习的心电疲劳检测:HRV特征提取与分类实战
2026/10/13 1:40:43 网站建设 项目流程

简介:这套基于 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 间期序列变换到频谱上观察不同频段的能量,非线性特征则用熵或散点图描述序列的复杂程度。其中时域特征最容易理解,也是入门时优先复现的。疲劳发生时,SDNN(RR 间期标准差)和 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 Hz)LF/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(采样率)、label(0 表示清醒,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(end+1, :) = f; labels(end+1, :) = 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; % 在这里训练模型并记录每一折的指标 end

cvpartition(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, Distance

SVM 在小样本场景经常表现不错,但要用'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 间期直方图、两个窗口的时域特征对比、以及分类器的混淆矩阵,三样东西花不了两分钟,却能避免在错误的方向上调试一整天。做这类项目,慢不是问题,方向错了才是。希望这些经验能在你的心电疲劳检测项目里派上用场,祝顺利。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询