简介:这份教程面向具备一定MATLAB编程基础的科研人员、医疗工作者与技术开发者,系统讲解生物医学信号处理从预处理到深度学习的完整流程。内容围绕ECG、EEG、EMG等生理信号展开,涵盖数据导入、滤波去噪、时域与频域分析、时频分析以及信号分类识别,并配有大量可直接运行的代码示例,如切比雪夫II型滤波器设计、filtfilt滤波、buffer信号分割与resample重采样等,帮助读者在实践中掌握MATLAB信号处理工具箱的使用技巧。资源包为1个docx文档,约31KB,结构清晰,按绪论、信号预处理、时域分析、频域分析、时频分析、分类识别等模块组织,便于按需查阅。目前已有3316人学习,适合希望系统入门或查漏补缺的读者,也能为有经验的研究者提供机器学习与深度学习在生物医学领域应用的参考思路。
1. 从一段心电信号说起:MATLAB 在生物医学信号处理里到底扮演什么角色
很多做生物医学信号处理的工程师,第一次真正被 MATLAB 折服,往往不是因为它画图好看,而是因为一段被工频干扰和基线漂移糊住的心电信号,在几行滤波代码之后突然露出了清晰的 R 波。生物医学信号——心电、脑电、肌电、光电容积脉搏波——天生就是低幅值、强噪声、非平稳的,采样率从几百赫兹到几千赫兹不等,直接拿去做分类或检测,模型学到的多半是噪声。MATLAB 在这个链条里的价值,是把「读数据、去噪、分段、提特征、训练模型」串成一条可复现的流水线,而且每一步都有现成的函数和可视化手段让你确认自己没做错。这篇笔记面向的是已经拿到数据、准备动手的从业者:如果你手上有一批生理信号记录,想从预处理一路做到深度学习分类,又不想在工具链上反复翻车,那接下来的内容就是给你准备的。我会按真实项目的推进顺序讲,先立住预处理,再谈特征和模型,最后落到几个能直接抄的参数和排错习惯。
2. 预处理先立住:滤波、去基线、分段的 MATLAB 落地
生物医学信号处理里,预处理做不好,后面全是玄学。我见过太多人把原始信号直接丢进分类器,然后抱怨准确率上不去,回头一看,50 Hz 工频干扰还在,基线漂移把整段信号的均值拉偏了。这一章把预处理拆成三个必须做扎实的动作:滤波、去基线、分段,每个动作都给可运行的 MATLAB 代码和参数说明。
2.1 读数据与采样率对齐:别让第一步就埋雷
拿到数据的第一件事不是滤波,是确认采样率和时间轴。不同设备导出的记录,采样率可能标在文件头,也可能只写在文档里,甚至同一批数据里混着 250 Hz 和 360 Hz 两种。采样率搞错,后面所有频率参数都是错的。
% 读取一段生理信号记录,假设是文本或 mat 格式 % 常见做法是先探测文件结构,再决定读取方式 raw = load('subject01_ecg.mat'); % 假设内部变量名为 ecg sig = raw.ecg(:); % 强制成列向量,避免行列混用 fs = 360; % 采样率,必须从数据说明里确认 % 构造时间轴,方便后续按秒定位 t = (0:numel(sig)-1) / fs; % 先看一眼原始波形,确认没有整段饱和或断点 figure; plot(t, sig); xlabel('Time (s)'); ylabel('Amplitude (mV)'); title('Raw ECG');这段代码的关键不在读取,而在两个习惯:一是把信号强制转成列向量,避免后面做矩阵运算时维度对不上;二是先画原始波形。很多数据问题——整段削顶、中间丢包、通道接反——在原始波形上一眼就能看出来,等到滤波之后再看就晚了。采样率fs这个参数必须来自数据说明,不能靠猜,猜错的话后面设计滤波器时截止频率会整体偏移。
2.2 工频干扰与基线漂移:两类噪声要用两套滤波器
生理信号里最常见的两类噪声,处理思路完全不同。工频干扰是窄带噪声,集中在 50 Hz 附近,用陷波器(notch)精准打掉;基线漂移是低频漂移,通常在 0.5 Hz 以下,用高通滤波器压掉。把这两件事混在一个滤波器里做,往往两头不讨好。
% 设计 50 Hz 陷波器,品质因数 Q 决定陷波带宽 wo = 50 / (fs/2); % 归一化频率 bw = wo / 35; % 带宽,Q 值约 35 [bn, an] = iirnotch(wo, bw); % 设计高通滤波器去基线漂移,截止 0.5 Hz fc = 0.5; [bh, ah] = butter(2, fc/(fs/2), 'high'); % 依次滤波,注意 filtfilt 做零相位滤波 sig_notch = filtfilt(bn, an, sig); sig_clean = filtfilt(bh, ah, sig_notch); % 对比滤波前后 figure; subplot(2,1,1); plot(t, sig); title('Raw'); subplot(2,1,2); plot(t, sig_clean); title('After notch + highpass');这里有两个参数值得说清楚。陷波器的品质因数 Q 通过bw = wo/35间接设定,Q 越大陷波越窄,对 50 Hz 打得越准,但如果信号本身在 50 Hz 附近有有用成分,窄陷波更安全;Q 太小会把周围频段一起削掉。高通滤波器的阶数用 2 阶起步,阶数越高过渡带越陡,但相位失真和数值不稳定风险也越大,filtfilt做前向加后向两次滤波,能把相位失真抵消掉,代价是等效阶数翻倍,所以原始阶数不要设太高。截止频率 0.5 Hz 是心电信号的常见选择,脑电如果关心更慢的成分,可能要降到 0.1 Hz 甚至更低。
2.3 分段与 R 波定位:把连续信号切成可训练的样本
滤波之后的信号还是连续的,要送进模型就得切成一段一段的样本。心电按心动周期切,脑电按固定时间窗切,肌电按动作段切。切分的关键是定位事件点,心电里就是 R 波。
% 用 Pan-Tompkins 思路做 R 波检测,MATLAB 有现成函数 % 先做带通,突出 QRS 复合波 [bq, aq] = butter(2, [5 15]/(fs/2), 'bandpass'); sig_qrs = filtfilt(bq, aq, sig_clean); % 微分、平方、移动窗积分,增强 QRS 能量 diff_qrs = [0; diff(sig_qrs)]; sq_qrs = diff_qrs .^ 2; win = round(0.15 * fs); % 150 ms 积分窗 integ = filter(ones(1,win)/win, 1, sq_qrs); % 自适应阈值找峰,这里用简化版:超过均值若干倍即候选 thr = 0.6 * max(integ); [pks, locs] = findpeaks(integ, 'MinPeakHeight', thr, ... 'MinPeakDistance', round(0.25*fs)); % 把候选点映射回原始信号,取局部极大值作为 R 波位置 r_locs = zeros(size(locs)); for k = 1:numel(locs) lo = max(1, locs(k)-round(0.05*fs)); hi = min(numel(sig_clean), locs(k)+round(0.05*fs)); [~, idx] = max(sig_clean(lo:hi)); r_locs(k) = lo + idx - 1; end % 按 R 波切分,每段取 R 波前 0.25 s 到后 0.4 s pre = round(0.25*fs); post = round(0.4*fs); beats = []; for k = 1:numel(r_locs) if r_locs(k)-pre > 0 && r_locs(k)+post <= numel(sig_clean) beats(:, end+1) = sig_clean(r_locs(k)-pre : r_locs(k)+post); end end这段代码里,MinPeakDistance设成 0.25 秒是为了防止把 T 波误判成 R 波,正常心率下两个 R 波间隔不会短于这个值。积分窗 150 ms 是 QRS 复合波的典型宽度。切分时前后各取一段,保证每段都包含完整的 QRS 和部分 ST 段,具体前后长度取决于你要分析什么——做心律失常分类,后段可以取长一点;做心率变异性,只需要 R 波位置序列就够了。切完之后建议随机抽几段画出来看,确认没有切偏或者把噪声段切进来。
3. 特征工程还是端到端:MATLAB 里两条路怎么选
预处理做完,接下来是决定用传统特征工程加浅层分类器,还是直接上深度学习做端到端。这两条路在 MATLAB 里都有成熟支持,但适用场景差别很大,选错了要么精度上不去,要么算力白烧。
3.1 时频域特征提取:小样本场景的稳妥选择
如果你的数据量在几百到几千段这个量级,特征工程加 SVM 或随机森林往往比深度学习更稳。生理信号的可解释特征集中在时域和频域:时域的均值、方差、峰峰值、过零率,频域的功率谱密度、各频带能量占比。
% 对每段心电信号提取时频域特征 feat = []; for k = 1:size(beats, 2) x = beats(:, k); % 时域特征 f_mean = mean(x); f_std = std(x); f_ptp = max(x) - min(x); f_zcr = sum(abs(diff(sign(x)))) / (2*numel(x)); % 频域特征:Welch 功率谱 [pxx, f] = pwelch(x, [], [], [], fs); band_lf = sum(pxx(f>=0.5 & f<5)); % 低频段能量 band_hf = sum(pxx(f>=5 & f<15)); % QRS 主频段 band_total = sum(pxx); feat(:, end+1) = [f_mean; f_std; f_ptp; f_zcr; ... band_lf; band_hf; band_total]; end feat = feat'; % 行是样本,列是特征pwelch的默认窗长是信号长度的八分之一左右,对短样本可能偏短,可以显式指定窗长,比如pwelch(x, round(0.2*fs), [], [], fs)。频带划分不是固定的,心电的 QRS 主频在 5 到 15 Hz,脑电的节律划分是 delta/theta/alpha/beta,肌电的有效频段更高,要根据信号类型调整。特征提取完记得做标准化,不同特征的量纲差好几个数量级,不标准化的话距离度量会被大量纲特征主导。
3.2 一维 CNN 端到端:数据够多时的省心方案
当样本量上万,或者你不想手工设计特征,一维卷积网络是生理信号分类的常见选择。MATLAB 的 Deep Learning Toolbox 支持直接定义一维卷积层,输入是「时间点 × 通道数」的序列。
% 定义一维 CNN,输入为单通道心电片段 layers = [ sequenceInputLayer(1) convolution1dLayer(5, 16, 'Padding', 'same') batchNormalizationLayer reluLayer maxPooling1dLayer(2, 'Stride', 2) convolution1dLayer(5, 32, 'Padding', 'same') batchNormalizationLayer reluLayer maxPooling1dLayer(2, 'Stride', 2) globalAveragePooling1dLayer fullyConnectedLayer(5) softmaxLayer classificationLayer]; options = trainingOptions('adam', ... 'MaxEpochs', 30, ... 'MiniBatchSize', 64, ... 'InitialLearnRate', 1e-3, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 10, ... 'LearnRateDropFactor', 0.5, ... 'ValidationData', {XVal, YVal}, ... 'Plots', 'training-progress');卷积核大小 5 是生理信号的常用起点,对应采样率 360 Hz 下约 14 ms,能覆盖 QRS 的上升沿。第一层通道数 16 到 32 起步,太大会在小数据集上过拟合。globalAveragePooling1dLayer替代全连接展平,能显著减少参数量,对信号分类这种任务通常比直接展平更稳。学习率用分段衰减,每 10 个 epoch 减半,比固定学习率更容易收敛到好的解。训练时一定要留验证集,生理信号数据往往有类别不平衡,验证集上的混淆矩阵比准确率更能说明问题。
3.3 两条路的取舍:看样本量也看可解释性需求
选特征工程还是端到端,我一般看两个维度。样本量低于五千,优先特征工程,因为深度学习在这个量级上很难学到比手工特征更好的表示,反而容易过拟合。样本量上万且标注质量高,端到端更省心,尤其是多分类任务,CNN 能自动学到不同类别的形态差异。另一个维度是可解释性:医疗场景经常需要说清楚「为什么判成这一类」,特征工程加树模型能给出特征重要性,端到端网络就得靠 Grad-CAM 之类的归因方法,解释成本更高。如果项目对可解释性有硬要求,别硬上深度学习。
4. 避坑与排查:生物医学信号处理里最容易翻车的五件事
这一章是我自己踩过和见别人踩过的坑,按「现象 → 原因 → 解决」写,每条都对应真实项目里出现过的故障。
4.1 滤波后信号反而更差:相位失真在作怪
现象:做完带通滤波,R 波位置整体偏移,或者波形出现明显振铃。原因:用了filter而不是filtfilt,单次滤波引入相位延迟,阶数越高延迟越大。解决:预处理阶段一律用filtfilt做零相位滤波;如果必须实时处理不能用filtfilt,就改用线性相位 FIR 滤波器,把群延迟算出来做补偿。
4.2 训练准确率很高但测试崩了:数据泄漏藏在分段里
现象:训练集准确率 99%,测试集掉到 60%。原因:把同一段记录切出来的样本同时分进了训练集和测试集,模型记住了这段记录的噪声特征。解决:按记录划分训练测试集,同一受试者的数据只能出现在一边;如果受试者数量少,用留一受试者交叉验证。
4.3 模型对某个类别完全不响应:类别不平衡没处理
现象:五分类任务里,占比最小的类别召回率接近零。原因:损失函数被多数类主导,模型倾向于全预测多数类。解决:在classificationLayer里设置类别权重,或者对少数类做重采样;MATLAB 的trainingOptions不直接支持类别权重,需要在自定义损失或数据层处理。
4.4 频谱图上出现不该有的峰:采样率或频率轴搞错
现象:功率谱在 50 Hz 附近有峰,但陷波之后还在。原因:采样率设错,导致归一化频率算错,陷波器实际打在了别的频点上。解决:滤波前先确认fs,用pwelch画原始信号频谱,确认干扰峰的实际位置,再设计滤波器。
4.5 训练损失不下降:输入没标准化或学习率太大
现象:训练几个 epoch 后损失卡住不动,或者震荡发散。原因:输入信号幅值范围差异大,或者初始学习率超过稳定范围。解决:对输入做逐样本标准化(减均值除标准差),学习率从 1e-3 起步,观察前几个 epoch 的损失曲线,发散就降到 1e-4。
5. 从预处理到部署:一个可复现的验证习惯
走到这里,预处理、特征、模型都跑通了,最后一步是把整条链路固定下来,确保换一批数据还能复现。我自己的习惯是写一个顶层脚本,把采样率、滤波参数、分段长度、模型超参数全部集中成配置结构体,每次实验只改配置不改代码。
% config.m —— 所有可调参数集中管理 cfg.fs = 360; cfg.notch_freq = 50; cfg.notch_Q = 35; cfg.hp_cutoff = 0.5; cfg.hp_order = 2; cfg.seg_pre = 0.25; cfg.seg_post = 0.4; cfg.cnn_kernel = 5; cfg.cnn_filters = [16 32]; cfg.lr = 1e-3; cfg.batch = 64;配置集中之后,验证方法也要固定。我一般做三层验证:第一层是单元验证,对单段信号跑预处理,画滤波前后对比图,确认没有相位偏移和幅值异常;第二层是交叉验证,按受试者划分,看模型在不同受试者上的方差,方差大说明模型学到了个体特征而不是通用模式;第三层是外部验证,留一批完全没参与调参的数据,只跑一次,这个结果才是能写进报告的。
还有一个具体技巧:把预处理后的信号和原始信号叠在一起画,用半透明显示,这样能直观看到滤波改变了哪些成分。如果滤波后的波形和原始波形在 QRS 主峰上明显错位,说明相位没处理好;如果基线被压平的同时 QRS 幅值也被削了,说明高通截止频率设高了。这个可视化习惯帮我省了很多次返工。
最后说一个我自己的教训。早期做心电分类,我花了两周调 CNN 结构,准确率卡在 85% 上不去,后来发现是预处理阶段陷波器的 Q 值设得太小,把 QRS 的部分高频成分一起削掉了,模型拿到的输入本身就缺信息。把 Q 从 10 调到 35 之后,同样的网络结构直接到了 93%。这件事让我养成了一个习惯:模型效果不好时,先回头查预处理,别急着改网络。希望帮到你。
本文还有配套的精品资源,点击获取