ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

脉象识别系统毕业设计实战:从信号预处理到分类模型

脉象识别系统毕业设计实战:从信号预处理到分类模型 简介一套基于Python与Django框架开发的脉象识别系统毕业设计源码面向计算机相关专业学生适用于毕业设计、课程设计及期末大作业等场景。压缩包共61个文件包含47个Python源码、8个CSV数据文件、1个H5模型文件及Markdown/TXT说明文档源码覆盖Django后端配置、数据预处理、模型预测、JWT权限认证等模块H5文件为训练好的模型权重CSV为脉象样本数据整体仅1.26MB。已有201人学习下载。项目内含完整可运行的Django工程结构、数据库脚本与部署教程代码附带注释逻辑清晰新手也能轻松上手。系统功能完善、界面美观经过严格调试确保可运行能帮助读者快速掌握脉象识别算法与Web应用整合方法是高分通过项目的优质参考。1. 当毕业设计遇上“脉象识别”不是玄学是信号处理把“脉象识别系统”作为毕业设计题目第一次听到的人多半会愣一下。它既不像“基于Spring Boot的商城系统”那样一听就是CRUD也不像“基于YOLOv8的目标检测”那样有现成模型可抄。脉象是中医里靠手指感受的桡动脉搏动而“识别”意味着要把这个主观感受变成可量化的分类结果——这正好是一个完整的Python数据科学项目信号采集、预处理、特征提取、模式识别每步都有明确的技术选型和可验证的指标。对于计算机、电子或生物医学工程背景的学生来说它比普通管理系统更有区分度又能覆盖从数据处理到模型部署的完整链路。这个题目的真正难点不在“机器学习”而在数据本身。脉象信号是典型的非平稳、低信噪比生理信号采集环境、传感器压力、受试者状态都会让波形漂移。所以我写这篇文章的目的很直接把从原始数据到最终分类精度的完整工程路径讲清楚包括每一步的参数怎么设、为什么这么设、跑出来的特征怎么看。如果你正在做或准备做这个题目跟着这个流程走能省下大量试错时间。2. 脉象识别系统的数据基础先搞懂信号从哪来、怎么存2.1 脉象信号的本质与数字化表示脉象不是“心电图”那种电生理信号它本质上是桡动脉内压力随心脏搏动产生的周期性波动。传统中医用食、中、无名三指按压寸、关、尺三个位置感受“浮、沉、迟、数、弦、滑”等属性。到了数字化阶段我们用压电式传感器或应变式传感器把桡动脉搏动的压力变化转换成电压变化再经ADC采样成离散序列。一条完整的脉象记录通常包含三路信号对应寸、关、尺三个位置的传感器采样频率常见为125Hz、250Hz、500Hz。以250Hz为例心跳一次约0.8秒单周期内包含约200个采样点足以保留脉搏波的主波、重搏波等细节形态。原始信号以CSV或TXT格式存储每列是一路通道再加一列时间戳结构如下timestamp,ch1_cun,ch2_guan,ch3_chi 0.000,0.421,0.398,0.402 0.004,0.425,0.402,0.403 0.008,0.419,0.399,0.400这是最常见的文本存储格式。如果传感器厂商给了专用软件可能导出为EDF或MAT格式但大多数毕业设计场景下CSV已经够用。拿到数据后的第一件事永远是看一眼采样率、通道数和时长这三个参数决定了后续所有滤波和特征提取的边界。2.2 数据预处理带通滤波、去基线漂移和分段原始信号不能直接喂给模型。受呼吸、肌肉紧张和传感器滑移影响信号里混有低频基线漂移和高频噪声。常见做法是级联两个阶段先做0.5Hz到45Hz的带通滤波去除基线漂移和高频干扰再对滤波后的信号做滑动窗口分段以心跳周期为最小单位。滤波用scipy.signal的butter和filtfilt实现。注意filtfilt是零相位滤波不会引起波形相位偏移这对后续提取脉搏波形态特征很重要import numpy as np from scipy.signal import butter, filtfilt def bandpass_filter(data, fs250.0, low0.5, high45.0, order4): 零相位带通滤波用于去除基线漂移和高频噪声 :param data: 单通道一维信号 :param fs: 采样率单位Hz :param low: 低截止频率单位Hz :param high: 高截止频率单位Hz :param order: 滤波器阶数阶数越高衰减越陡但计算量增大 b, a butter(order, [low, high], btypebandpass, fsfs) return filtfilt(b, a, data)参数取值上low0.5是为了滤掉呼吸引起的基线漂移呼吸频率约0.2-0.4Hzhigh45是在保留脉搏波主要谐波分量的同时抑制工频干扰50Hz和肌肉噪声。order4是折中值阶数太高会引入振铃现象太低则阻带衰减不够。分段这一步决定了“一条样本”怎么定义。常见策略是检测每个脉搏周期的主波波峰以波峰为锚点截取前一个周期的末端到后一个周期的起始得到固定长度的周期样本。波峰检测用scipy.signal.find_peaks即可参数要特别留意distance必须大于以采样率为基准的最小心率间隔from scipy.signal import find_peaks # 假设fs250正常心率区间60-120次/分最小峰间距为0.5秒 min_distance int(0.5 * fs) # 即125个采样点 peaks, properties find_peaks(filtered, distancemin_distance, height0.1)这里height0.1是一个经验阈值用于排除幅度过小的伪峰。实际项目中我会在检测前先对信号做z-score归一化让幅度统一到同一量纲再调阈值。检测结果可以用一段可视化脚本快速确认把原始信号和波峰位置画在同一张图上如果错检漏检明显优先调distance和height。2.3 样本平衡与数据增强大部分毕设都会栽在这里脉象数据的标注是一大痛点。临床采集时通常由资深中医师根据指感标注脉象类型比如“弦滑脉”“细沉脉”然后同步记录传感信号。这带来两个问题一是标注的主观性不同医师可能对同一条信号的判断不一致二是类别分布极不平衡“平脉”正常脉象样本往往数倍于病理性脉象。不平衡问题如果不处理模型会倾向于把所有样本都预测为多数类宏观F1惨不忍睹。常用的缓解手段有两个方向第一对少数类做过采样最简单的就是复制少数类样本但容易过拟合第二用数据增强合成新样本。对脉象信号来说最稳妥的增强手段是“时域抖动”和“幅度抖动”——对原始信号施加微小的时间平移和增益扰动这不会破坏脉搏波的形态结构def augment_signal(signal, shift_max10, scale_range(0.95, 1.05)): 时域平移加幅度缩放生成新样本 :param signal: 单周期脉象信号 :param shift_max: 最大平移采样点数 :param scale_range: 幅度缩放范围 shift np.random.randint(-shift_max, shift_max) shifted np.roll(signal, shift) if shift 0: shifted[:shift] 0 elif shift 0: shifted[shift:] 0 scale np.random.uniform(*scale_range) return shifted * scale另一个被忽视的问题是分段后的样本长度不一致。采集时受试者的心率不同一个完整脉搏周期可能有80个采样点也可能有120个。直接拿来训练模型需要每个样本长度一致通常采用插值重采样到固定长度比如128或256点。用scipy.interpolate的interp1d做线性插值即可不建议用高阶样条容易在波峰处产生过冲。预处理步骤常用工具关键参数作用带通滤波scipy.signal.butter filtfilt0.5-45Hz, order4去基线漂移和高频噪声波峰检测scipy.signal.find_peaksdistance0.5*fs, height0.1定位脉搏周期起点周期分段numpy切片每周期一段生成单样本长度对齐interp1d线性插值目标长度128或256统一样本维度数据增强自定义函数shift_max10, scale0.95-1.05平衡类别、防过拟合这是一套完整可行的预处理管线任何人拿到原始CSV都能照此处理。做完这一步你会得到一组干净的、等长的、带标签的脉搏周期样本它们才是后续特征提取和模型训练的原材料。注意数据对齐前先随机抽10个样本画图确认分段质量比做完整个管线再回头查要高效得多。3. 特征提取的三种层次从手工特征到自动学习3.1 时域与频域特征把中医术语翻译成数字脉象识别早期靠手工特征这些特征至今仍然有效尤其当训练样本不多时。时域特征直接从波形幅度和时间轴上计算对应中医里“脉率”“脉势”等概念。常见的有主波幅度一个周期内的最大幅值对应“脉力”的强弱主波上升时间从周期起点到主波波峰的时间差反映“来势”的缓急重搏波幅度比重搏波波峰与主波波峰幅度的比值用于区分某些动脉硬化表征周期时长两次主波波峰之间的间隔对应“脉率”。频域特征用功率谱描述信号的能量分布。快速傅里叶变换FFT不是直接对整条长信号做的而是对每一个分段后的周期样本做然后提取频谱峰值频率、频谱质心、子带能量比等。这里给一个完整的特征提取函数它同时输出时域和频域特征import numpy as np from scipy.fft import rfft, rfftfreq def extract_features(segment, fs250.0): 提取单周期脉象信号的时域频域特征 :param segment: 一个完整的脉搏周期样本 :param fs: 采样率 :return: dict包含全部特征 seg np.asarray(segment, dtypefloat) n len(seg) t np.arange(n) / fs # 时域特征 peak_idx np.argmax(seg) amp seg[peak_idx] # 主波幅度 rise_time t[peak_idx] # 上升时间 cycle_len n / fs # 周期时长 # 计算重搏波在下降支上找局部峰值 # 下降支从主波峰之后取后半段局部最大 offset peak_idx int(0.3 * n) if peak_idx int(0.3 * n) n else peak_idx dicrotic_idx offset np.argmax(seg[offset:]) dicrotic_ratio seg[dicrotic_idx] / amp # 重搏波幅度比 # 频域特征 spectrum np.abs(rfft(seg)) freqs rfftfreq(n, d1/fs) valid spectrum[freqs 0.5] valid_freqs freqs[freqs 0.5] peak_freq valid_freqs[np.argmax(valid)] # 频谱峰值频率 total_power np.sum(valid**2) if total_power 0: centroid 0.0 else: centroid np.sum(valid_freqs * valid**2) / total_power # 频谱质心 return { amplitude: amp, rise_time: rise_time, cycle_len: cycle_len, dicrotic_ratio: dicrotic_ratio, peak_freq: peak_freq, spectral_centroid: centroid, }这段代码体现了脉象信号特征提取的典型思路先用np.argmax找到主波峰再根据主波峰位置分割出下降支在下降支上找重搏波峰用来计算形态比例频谱部分用rfft得到单边频谱排除0.5Hz以下的低频成分后取峰值频率和质心。spectral_centroid是频谱能量分布的重心能在一定程度上表达“弦脉”“滑脉”在高频成分上的差异。3.2 小波包分解非平稳信号的多尺度视角周期样本的形态会随呼吸和血压调节而变化时域特征和FFT特征都是对整个周期的全局描述局部形态细节容易丢失。小波变换的优势在于同时在时间轴和频率轴上做刻画对一个脉动周期高频段对应脉波上升支的陡峭程度和低频段对应整体张力可以被分开观察。小波包分解比离散小波更进一步它对低频和高频都做逐层分解。常用db4小波、分解层数3到4层。每一层分解得到一组系数对系数求能量、均值、标准差作为特征。这里要点在于小波包特征维度随着层数指数增长3层分解会产生8个子带4层是16个这很容易导致特征维度超过样本数量。因此一般做法是只选能量占比靠前的子带而不是全部塞进模型。计算每个子带能量占比的示例代码如下import pywt def wavelet_energy_features(segment, waveletdb4, level4): 小波包分解后计算各子带能量占比 :param segment: 单周期脉象信号 :param wavelet: 小波基db4适合脉搏波这类平滑信号 :param level: 分解层数4层含16个子带 :return: 子带能量占比向量 wp pywt.WaveletPacket(datasegment, waveletwavelet, modesymmetric) energy_ratios [] subbands [node.path for node in wp.get_level(level, freq)] total_energy np.sum(segment ** 2) for path in subbands: coeff wp[path].data energy np.sum(coeff ** 2) ratio energy / total_energy if total_energy 0 else 0 energy_ratios.append(ratio) return np.array(energy_ratios)modesymmetric是边界扩展方式因为脉象周期样本的两端不一定对齐零点对称扩展能减少边界效应。用小波包特征做分类时有一个实用的筛选技巧先训练一个随机森林查看特征重要性排序把靠后的子带特征剔除通常能从16维缩到8维左右分类准确率不降反升。3.3 深度学习特征端到端的波形学习手工特征加小波包特征在中小数据集上表现稳健但有一个天花板特征是由人定义的遇到没见过的脉象形态变化时自适应能力有限。深度学习的路线是把波形本身作为输入让模型自己学特征。对一维脉象信号来说主流选择是1D CNN和LSTM的组合网络——CNN提取局部形态特征类似脉搏波的主波、重搏波形态LSTM捕获周期内的时序依赖。这里给出一个轻量级模型结构输入为256点的单通道周期样本输出为5分类如平、弦、滑、细、数用TensorFlow/Keras接口实现from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense, Dropout def build_pulse_net(input_len256, num_classes5): 脉象分类模型1D CNN LSTM :param input_len: 输入信号长度与预处理阶段重采样目标一致 :param num_classes: 分类数 model Sequential([ Conv1D(filters32, kernel_size5, activationrelu, paddingsame, input_shape(input_len, 1)), MaxPooling1D(pool_size2), Conv1D(filters64, kernel_size5, activationrelu, paddingsame), MaxPooling1D(pool_size2), LSTM(units32, return_sequencesFalse), Dropout(0.3), Dense(num_classes, activationsoftmax) ]) model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy]) return model模型设计的一个细节第一个Conv1D层的kernel_size5意味着卷积核覆盖5个采样点在250Hz采样率下对应约20毫秒的时间窗口正好能捕捉脉搏波上升支的陡峭变化。paddingsame保证卷积后序列长度不变方便后续池化和LSTM层处理。LSTM坍缩序列维度后接Dropout是防止全连接层过拟合的低成本手段。训练时批次大小取32学习率默认Adam参数即可但如果准确率震荡严重建议把学习率降到1e-4并加上ReduceLROnPlateau回调。卷积核的个数32、64和LSTM的神经元数32是针对小数据集的保守设计。如果样本量在数千量级模型参数量超过十万就很容易过拟合这点是毕设答辩时最常被追问的问题。4. 识别模型选型与参数调优从SVM到CNN的工程取舍4.1 传统机器学习路径特征矩阵加分类器当特征提取完成后脉象识别就退化为一个标准的表格分类任务。手工特征通常是几十维的小矩阵最合适的分类器是随机森林、XGBoost或带RBF核的支持向量机SVM。这三者在小样本上的表现普遍优于深度学习原因是归纳偏置更强不需要大量数据来约束参数量。SVM在脉象识别上有特殊优势它只依赖支持向量做决策对高维小样本的适应力强。用scikit-learn训练并做网格搜索调参的做法如下from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV, train_test_split from sklearn.preprocessing import StandardScaler # features是特征矩阵(n_samples, n_features)labels是标签向量 X_train, X_test, y_train, y_test train_test_split( features, labels, test_size0.2, random_state42, stratifylabels ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) param_grid { C: [0.5, 1, 5, 10], gamma: [scale, 0.01, 0.05, 0.1], } svm SVC(kernelrbf, class_weightbalanced, random_state42) grid GridSearchCV(svm, param_grid, cv5, scoringf1_macro, n_jobs-1) grid.fit(X_train_scaled, y_train) print(fBest params: {grid.best_params_}) print(fCV F1: {grid.best_score_:.4f})class_weightbalanced这一行的作用是根据类别频率自动调整惩罚权重防止多数类主导决策边界。如果你的预处理已经做过过采样或数据增强这个参数可以不加两者选一种方式处理不平衡即可叠加有时会导致少数类被过度纠正。网格搜索的参数范围并不大C取[0.5, 1, 5, 10]已经足够。C太大容易过拟合到噪声样本C太小则决策边界过于平滑欠拟合。gamma控制RBF核的影响半径在线性不可分但局部形态差异明显的数据集上gamma取0.05到0.1效果较好。4.2 深度学习路径数据量决定模型复杂度当样本量超过5000或者你希望论文里有一个“自动特征学习”的卖点时深度学习路线才值得选。用小数据集硬跑CNN结果往往不如SVM加手工特征这是大多数毕设翻车的原因。我的建议是一种折中架构把手工特征和波形输入一起送入模型形成多输入结构。波形分支用轻量CNN学习形态特征手工特征分支直接用全连接层学习两分支在最后一层拼接后分类。这样在数据量有限的前提下模型既有自动学习能力又不丢失人工先验。训练深度模型时必须监控训练集和验证集的loss曲线。脉象数据量小常见的信号是训练集loss持续下降、验证集loss在第5到10个epoch后反向上升这是过拟合的标准特征。应对办法优先级先加Dropout和L2正则化再降低模型容量减少卷积核数最后才考虑用早停。from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) # 训练时传callbacks[early_stop]即可patience10的含义是如果验证集loss连续10个epoch没有改善就终止训练并回滚到历史最优的权重。这个设置对脉象数据比较合适数据量小导致训练曲线抖动大patience太小如3会过早停止。4.3 评价指标怎么选准确率会骗人脉象数据天然不平衡“平脉”类样本可能占40%以上一个把所有样本都预测为“平脉”的模型准确率也能达到40%。即使做了类别平衡准确率仍然不适合作为唯一衡量标准——它对多数类的正确性过于敏感。更合理的指标组合是macro-F1和混淆矩阵。macro-F1把每个类别的F1单独算出后取平均每个类别的权重相同能直观反映模型在少数类上的表现。混淆矩阵则能看出类别之间的混淆模式比如“弦脉”和“滑脉”经常互相分错说明这两个类别的波形形态确实相近这类confusion在答辩时能成为很好的分析素材。from sklearn.metrics import classification_report, confusion_matrix, f1_score y_pred grid.predict(X_test_scaled) print(classification_report(y_test, y_pred, digits3)) cm confusion_matrix(y_test, y_pred) print(cm)打印出的classification_report会给出每个类别的精确率、召回率和F1。我通常在removesubmissions标准基础上再加一个要求macro-F1不低于0.75才认为模型有临床应用讨论价值。如果你的结果低于这个数优先检查预处理分段的正确性而不是换模型。场景推荐方案理由样本量小于2000手工特征 SVM/随机森林低维特征、强归纳偏置不易过拟合样本量5000以上1D CNN LSTM自动特征学习能发现人未定义的波形模式类别严重不平衡class_weightbalanced 或过采样防止模型被多数类别主导需要可解释性随机森林特征重要性 混淆矩阵答辩时能讲清楚哪些特征在起作用5. 模型部署与毕业设计的高阶验证技巧5.1 用交叉验证替代固定划分很多毕设只做一次train_test_split就交差这在脉象识别这种小样本场景下极不可靠。原始70%训练、30%测试划分一次得到0.8的准确率换一次随机种子可能跌到0.6。正确的做法是用分层K折交叉验证K设为5或10。每一折都做完整的预处理加特征提取是不现实的因为分段逻辑独立于模型训练所以通常做法是先完成所有样本的分段和特征提取再用分层K折评估模型稳定性。如果K折结果的方差过大比如标准差超过0.1说明数据本身不够干净或样本量太少。这时候不要继续调模型回头检查是否存在受试者级别的数据泄漏——同一个人的多个脉搏周期样本被同时分到训练集和测试集模型会通过记忆个体特征获得虚高的准确率。严格的评估应当按受试者分组划分数据同一人的样本只出现在同一折里。5.2 一个打包的脉象识别工具类把预处理、特征提取、分类器和评估统一封装方便毕设报告里展示完整流程也方便后续换模型对比。我建议用dataclass组织配置参数用sklearn的Pipeline组织特征变换与分类器from dataclasses import dataclass dataclass class PulseConfig: fs: int 250 # 采样率 lowcut: float 0.5 # 带通下限 highcut: float 45.0 # 带通上限 seg_len: int 256 # 周期重采样长度 test_size: float 0.2 random_state: int 42 config PulseConfig() # 后续所有函数统一使用config参数数据路径和标签文件另用字典传入这样做的实际好处是答辩演示时只需要改config里的一个参数再重跑脚本就能展示不同采样率或滤波器参数对结果的影响比在代码里反复搜索硬编码参数要省时间得多。5.3 波形可视化的可视化证据链毕设论文和答辩评审最认可的是“眼见为实”。训练完成后把原始信号、滤波后波形、波峰标注、特征提取效果比如频谱或小波子带能量画成图按顺序粘贴到论文的实验部分。这里有一个具体的检查方法对一个典型的“滑脉”样本画重搏波检测结果如果重搏波峰位置被正确标注说明你的分段和特征提取逻辑自洽。使用matplotlib的plt.subplots把多张图排列在一个画布上分别展示原始信号和滤波结果的对照图。这一步对查错极有帮助如果你发现滤波后波形在某些周期出现明显畸变多半是带通滤波器阶数过高或分段时边界处出现了截断误差回到第二步调整即可。脉象识别系统从数据到模型的可信度判断最终不取决于某个指标有多高而是每一层的输出是否经得起人工检查。波形清洗干净、特征分布合理、混淆矩阵中的错误模式能够解释这样的系统在答辩时才会被认可为一个完整的、有工程闭环的毕业设计而不是一个只要复制代码就能跑通的demo。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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