Python脉象识别系统实战:脉搏波特征提取与分类模型搭建

发布时间:2026/9/28 6:28:03
Python脉象识别系统实战:脉搏波特征提取与分类模型搭建 简介面向医疗健康领域与AI开发者的Python脉象识别系统源码专注中医脉象数字化识别基于深度学习完成不同脉象类型的自动分类适用于智能医疗、信号处理及相关课题研究也适合医学信息工程、计算机视觉方向的学生与算法工程师参考学习。压缩包共61个文件以47个py源码为主体辅以8个csv数据文件、h5模型文件及README说明文档整体约1.24MB目录覆盖数据采集、预处理、特征提取、模型训练、测试与结果输出等完整模块。目前已有160人学习下载。通过源码可掌握CNN/RNN在脉象信号分类中的完整工程流程理解波形、幅度、频率等关键特征的提取方法并熟悉去噪、滤波等信号处理细节同时可直接运行或二次开发结合传感器与设备采集的脉象数据训练高精度识别模型为医生和患者提供智能诊断参考。1. 拿到 Python 脉象识别系统源码第一件事不是跑模型第一次拿到「Python脉象识别系统源码.rar」这份压缩包的人通常有两种反应一种是觉得中医玄学终于被 Python 收编了解压就想跑另一种是已经在这个方向踩过坑知道脉象识别真正难的不是分类模型而是从传感器到特征矩阵那一段看不见的路。这个系统干的事说穿了是把中医切脉时手指感受到的桡动脉搏动换成一条数字波形再经过滤波、切分、提特征、分类最后输出浮、沉、迟、数、弦、滑这类脉象标签。它最适合三类人做课程设计或毕业设计的学生、想给中医数字化找个落地切口的研究者以及做可穿戴健康监测但苦于体征信号分析没突破的开发者。它的价值不在分类准确率能拼到 99%而在给你一条现成的、可复现的信号处理与模式识别链路省掉从零搭管线的时间。2. 脉象数据怎么备采集方式、标注规范与数据集选型2.1 三种采集方式与信号特点先搞清楚你的波形是哪来的脉象识别系统的输入端不是图片是一维脉搏波序列。不同采集方式拿到的波形形态差异极大直接影响后续滤波和特征提取的参数所以开工第一件事是确认数据来源。常见做法有三种。第一种是压阻式压力传感器贴在桡动脉搏动最强处直接测压力变化。这类波形最接近中医手指的触感主波、重搏波、降中峡都比较清晰适合做脉象分类研究但佩戴位置敏感稍微移位波形就变形。第二种是光电容积脉搏波PPG多见于手环和指夹式血氧仪测的是血管内血容量变化。PPG 采集方便、设备便宜但波形形态和压力波有明显差异重搏波特征偏弱且容易受指尖温度和个人血管状态干扰。第三种是直接使用公开的脉搏波数据集这类数据的好处是已经有人标注过脉象类别省掉最痛苦的标注环节坏处是采样率、设备型号、受试者人群各异跨数据集训练时容易翻车。三种方式的采样率差异很大压力传感器常见 100 Hz 到 500 HzPPG 设备通常 25 Hz 到 125 Hz。采样率低于 50 Hz 时重搏波信息严重丢失很多频域特征算出来没有参考价值。我一般会在解压源码后先确认代码里写死的采样率再对照自己的数据源不一致就先重采样不然后面全是白算。2.2 公开数据集现状与自采数据的标注流程标注一致性比样本量更重要脉象识别最尴尬的现实是公开可用的、带脉象标注的脉搏波数据集少得可怜而且多数样本量在几百到几千条之间。放到深度学习里这点数据不够看但在传统机器学习加手工特征的框架下几千条数据足够做一轮像样的实验。如果决定自采数据标注是最容易埋雷的环节。脉象标注不像图像分类那样边界清晰「弦脉」和「紧脉」在很多波形上外观高度接近同一个医生隔一周再标注同一段波形结果都可能不一致。所以标注规范要先定死只保留明确可判别的类别模糊样本直接丢弃而不是硬塞进某一类。我见过一个项目强行把模糊样本标成「平脉」结果平脉类里混入大量弦脉特征模型训练时损失函数怎么都降不下去。标注流程上比较可靠的做法是双人独立标注加仲裁。至少要有两名有中医背景的人分别标注同一批波形计算标注一致性Cohens Kappa低于 0.7 的类别直接砍掉或重新讨论标准。没有中医背景的团队做自采标注建议先拿公开数据集做预训练或特征提取把自采数据留作测试集不要一开始就自己造标签。2.3 统一存储格式采样率、标签编码与文件组织无论数据来自哪里进入代码前必须先统一格式。源码里常见的存储方案是每条脉搏波单独存一个 CSV 或 NumPy 的 .npy 文件外加一个标签文件。解压后先把数据规整成下面这种结构pulse_data/ ├── raw/ │ ├── subject_001_wave1.csv │ ├── subject_001_wave2.csv │ └── ... ├── labels.csv └── meta.jsonlabels.csv至少需要subject_id、sample_id、pulse_type、sampling_rate四列。sampling_rate一定要存进元数据因为不同设备的采样率不同后续重采样和特征提取都要用到。标签编码我建议直接用中文或拼音字符串存比如fu、chen、xuan到训练前再映射成整数索引这样每次查看标注时不用对着数字猜含义。import json, os import pandas as pd def unify_dataset(label_csv_path, out_dir): df pd.read_csv(label_csv_path) meta {} for subject_id, group in df.groupby(subject_id): sr group[sampling_rate].mode()[0] meta[subject_id] {sampling_rate: int(sr), n_samples: len(group)} with open(os.path.join(out_dir, meta.json), w, encodingutf-8) as f: json.dump(meta, f, ensure_asciiFalse, indent2) return meta这段代码的作用是把按受试者分组的采样率和样本数汇总成一份元数据文件。意义在于训练前先看清楚每个受试者贡献了多少样本防止某个人的数据占了大头导致模型偷懒只记住这个人没学会脉象本身。数据规整这一步虽然不产生任何模型效果但因为原始数据里一个受试者的采集时间可能跨好几天设备设置也可能被动过靠元数据能在后续排查问题时快速定位到异常样本来自哪台设备、哪个时间点。3. 把原始脉搏波变成特征矩阵预处理三步与四组特征计算3.1 去除基线漂移与高频噪声滤波器参数别照抄原始脉搏波不能直接进模型。最典型的两个问题是基线漂移和工频干扰。基线漂移来自呼吸运动、传感器压紧程度的缓慢变化表现是整条波形像坐在一个缓慢起伏的坡道上工频干扰则来自市电环境固定在 50 Hz部分地区 60 Hz。脉象信号的频率范围基本在 0.5 到 10 Hz 之间主要能量集中在 1 到 5 Hz所以滤波思路很清晰高通去掉 0.5 Hz 以下的漂移低通去掉 10 Hz 以上的噪声若任务需要可以加一个 50 Hz 陷波。from scipy.signal import butter, filtfilt def bandpass_filter(signal, fs, low0.5, high10.0, order3): nyq 0.5 * fs b, a butter(order, [low / nyq, high / nyq], btypeband) return filtfilt(b, a, signal)注意这里用的是filtfilt而不是lfilter前者是零相位滤波能避免波形相位偏移。相位偏移对脉搏波的影响很隐蔽它不会让波形看起来明显异常但特征点主波峰、降中峡、重搏波的位置会整体平移导致后续周期切分错位。滤波器的阶数我一般用 3 到 4 阶阶数越高衰减越陡但相位畸变也更明显。如果滤波后的波形首尾出现大幅摆荡通常是因为原始序列太短filtfilt的边缘效应放大优先检查信号长度是不是小于滤波器的 3 倍滤波器脉冲响应长度。3.2 单周期切分与幅度归一化让样本对齐才能进入同一套特征标准一整段 30 秒的波形不能直接作为一个样本脉象分类的基本单元是单拍脉搏波。切分方法主流是峰值检测找到每个周期的主波峰然后从峰前取一段到峰后取一段组成一个完整周期。这里最大的坑是传感器移动或手法变化导致波形局部变形峰值检测会漏检或误检。from scipy.signal import find_peaks def segment_cycles(wave, fs, min_distance_seconds0.4): min_distance int(fs * min_distance_seconds) peaks, props find_peaks(wave, distancemin_distance, prominence0.05 * (wave.max() - wave.min())) cycles [] for i in range(len(peaks) - 1): start max(0, peaks[i] - int(0.15 * fs)) end peaks[i 1] - int(0.05 * fs) cycles.append(wave[start:end]) return cycles, peaks, propsmin_distance_seconds是最小周期长度正常成人静息心率 60 到 100 次/分对应周期 0.6 到 1.0 秒取 0.4 秒可以排除心率过快情况下的误检。prominence是峰值突出度阈值设为波形峰谷幅值差的 5%能滤掉那些幅度很小的伪峰。切出来每个周期的长度不一样后续要么统一重采样到固定长度比如 256 点要么提取特征时不依赖长度。我一般会把每个周期重采样到 256 点再做归一化这样特征矩阵的维度是固定的后面进机器学习模型不用做额外处理。3.3 四组特征的完整计算时域、频域、小波与形态学特征提取是这类系统的核心也是最能体现工程经验的地方。特征要能刻画脉象的物理本质「浮脉」轻取即得波形幅度大但重按衰减快「沉脉」需要重按才能感受到幅度偏小「弦脉」脉管张力大波形上升陡峭、重搏波位置偏高「滑脉」往来流利主波宽大、重搏波明显。把这些形态差异量化成数值常用的四组特征如下。时域特征包括一个周期内的最大值主波幅值、最小值、峰谷差、上升支平均斜率、下降支平均斜率、周期时值比主波时间占整个周期的比例。频域特征用 FFT 后取频谱峰值频率和 0.5 到 5 Hz 频带能量占比。小波特征通过pywt对单周期信号做 4 层 db4 小波分解取各层细节系数能量作为特征。形态学特征里最有区分度的是降中峡深度主波峰到降中峡的幅度差与主波幅值的比值和重搏波幅值比。import numpy as np from scipy.fft import rfft, rfftfreq import pywt def extract_features(cycle, fs): # 归一化 cycle (cycle - np.min(cycle)) / (np.max(cycle) - np.min(cycle) 1e-8) n len(cycle) t np.linspace(0, n / fs, n) # 时域特征 main_peak_idx np.argmax(cycle) main_amp cycle[main_peak_idx] rising_part cycle[:main_peak_idx] rise_slope main_amp / (len(rising_part) / fs 1e-8) # 频域特征 freqs rfftfreq(n, 1 / fs) spectrum np.abs(rfft(cycle)) valid freqs 5 dominant_freq freqs[valid][np.argmax(spectrum[valid])] # 小波特征 coeffs pywt.wavedec(cycle, db4, level4) detail_energy [np.sum(np.square(c)) for c in coeffs[1:]] return { main_amp: main_amp, rise_slope: rise_slope, dominant_freq: dominant_freq, wavelet_energy_0: detail_energy[0], wavelet_energy_1: detail_energy[1], wavelet_energy_2: detail_energy[2], wavelet_energy_3: detail_energy[3], }这段代码把单个周期浓缩成 7 维向量。时域特征抓的是一拍波的形态频域特征抓的是节奏感小波特征抓的是波形的局部突变四类特征互补。特征维度不是越多越好脉象分类项目里 20 到 50 维手工特征是合理区间再往上加就容易把噪声也当成信号。算完所有样本后用StandardScaler做标准化然后把特征矩阵(样本数, 特征维度)和标签数组存成.npz后面训练直接读取不用每次重算。np.savez(pulse_features.npz, XX_scaled, yy_encoded, feature_namesnp.array(feature_names))4. 分类模型选型与训练从 SVM 到轻量 CNN 的参数设置与评估口径4.1 特征矩阵进传统机器学习SVM 与随机森林的调参基线数据量在几千级别时传统机器学习比深度学习更稳这是脉象识别项目的普遍结论。因为手工特征已经做了大量信号压缩剩下的分类问题边界相对清晰SVM 和随机森林完全够用而且训练快、好调试。SVM 我一般首选 RBF 核两个必调参数是C和gamma。C控制误分类惩罚C太大容易过拟合太小欠拟合gamma控制 RBF 核的影响半径gamma过大导致决策边界太曲折模型几乎在背样本gamma过小则特征拉不开。from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV param_grid_svm { C: [0.1, 1, 10, 100], gamma: [0.001, 0.01, 0.1, 1], } svm SVC(kernelrbf, probabilityTrue, class_weightbalanced) grid GridSearchCV(svm, param_grid_svm, cv5, scoringf1_macro, n_jobs-1) grid.fit(X_train, y_train) print(best params:, grid.best_params_)class_weightbalanced是必须写的脉象数据集里各类别数量往往差异很大平脉样本可能是弦脉的三倍不加这个参数模型会倾向把一切预测成平脉。GridSearchCV的scoring用f1_macro而不是accuracy原因也是类别不平衡场景下准确率没有参考意义。随机森林的调参重点在n_estimators和min_samples_leaf前者在 100 到 500 之间通常够用后者设在样本总数的 1% 左右能有效防止过拟合。4.2 端到端的轻量 CNN什么时候值得上深度学习如果手工特征配上传统机器学习在测试集上已经能到 85% 以上的 F1深度学习通常不会带来质的提升反而增加训练成本和复现难度。但有一类情况值得上 CNN你觉得手工特征丢掉了波形里的关键信息比如重搏波与降中峡的相对位置这类空间结构特征没被手工特征刻画到位。这时候可以用一个参数量很小的 1D-CNN 直接在归一化后的单周期波形256 点上做端到端分类。import torch import torch.nn as nn class PulseCNN(nn.Module): def __init__(self, n_classes): super().__init__() self.conv1 nn.Conv1d(1, 16, kernel_size7, padding3) self.conv2 nn.Conv1d(16, 32, kernel_size5, padding2) self.pool nn.MaxPool1d(2, 2) self.flatten nn.Flatten() self.fc nn.Linear(32 * 64, 64) self.out nn.Linear(64, n_classes) self.dropout nn.Dropout(0.5) def forward(self, x): x torch.relu(self.conv1(x)) x self.pool(x) x torch.relu(self.conv2(x)) x self.pool(x) x self.dropout(self.flatten(x)) x torch.relu(self.fc(x)) return self.out(x)这个网络只有两层卷积加两层全连接参数量不到 5 万在小数据集上不容易过拟合。输入x的 shape 是(batch, 1, 256)256 是单周期重采样后的固定长度。kernel_size取 7 和 5 是为了覆盖脉搏波中的局部形态结构太小的卷积核只看到相邻几个点抓不到一次完整的上升支太大的卷积核则把整个周期糊在一起特征失去局部性。训练时学习率取 1e-3批量大小 32配合 Adam 优化器一般 30 到 50 个 epoch 就能收敛。如果训练集准确率一路上升但验证集抖动剧烈八成是数据量不够先把 Dropout 提到 0.6 试一下再不行就退回手工特征方案。4.3 评估口径按受试者分组才是真实水平脉象识别项目里最常见的虚假繁荣来自随机划分训练集和测试集。同一个受试者的几十个连续脉搏周期被切成了独立样本随机划分会把同一人的数据同时分进训练集和测试集模型实际上记住了这个人而不是学会了脉象。测试指标虚高 5 到 10 个百分点是常态。正确的做法是按受试者分组划分数据训练集和测试集的受试者完全没有重叠。from sklearn.model_selection import GroupShuffleSplit groups df[subject_id].values gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X, y, groupsgroups))用GroupShuffleSplit保证同一个subject_id的所有样本只出现在训练集或测试集之一。第一次跑出来的指标如果比之前随机划分低不少不用惊讶这是真实水平。建议汇报指标时同时给出宏平均 F1、每类别的精确率和召回率因为弦脉、滑脉这类形态接近的类别整体准确率高不代表难分的那几类也被学会了。5. 跑通脉象识别项目的五个常见坑波形错位、样本不平衡与过拟合排查5.1 滤波后波形首尾出现大幅摆荡现象是滤波后的信号两端出现明显的大幅振荡中间正常周期切分后有一批样本特征异常。原因是filtfilt的边缘效应特别是在信号长度较短或滤波器阶数较高时序列首尾的滤波窗口内数据不足边界条件被放大。另一个隐蔽原因是原始波形开头有一段设备启动噪声幅值远超正常脉搏波。解决方法是预处理前去掉每段记录的前 2 秒和设备停机前的最后 1 秒。如果信号本身就只有 5 到 8 秒可以先用中值滤波估计基线减去基线后再做带通滤波。判断滤波是否正常的标准很简单滤波后的波形在首尾 0.5 秒内不应出现超过整体幅度 20% 的波动。5.2 峰值检测漏掉或错位导致周期切分混乱现象是切出来的周期长度忽长忽短有些周期里包含两个主波峰有些只有一个半波时域特征严重偏离正常范围。原因是find_peaks的prominence参数设得太低把重搏波甚至噪声尖峰也当成了主波峰。重搏波在某些脉象滑脉中幅度接近主波峰的 70%很容易被误检。解决方法是先看波形分布把所有检测到的峰间隔做成直方图正常情况应该集中在 0.6 到 1.0 秒附近。如果直方图在 0.3 秒左右出现第二个峰说明存在大量误检。此时提高prominence阈值到峰谷差的 10% 到 15%并加入约束两个相邻主波峰的时间间隔不能低于 0.4 秒。再不行就改用基于模板匹配的切分算法用人工挑出的一个标准周期做滑动相关取相关性峰值作为周期边界。5.3 样本不平衡让模型只会猜「平脉」现象是训练集准确率很高测试时发现几乎所有样本都被预测为样本量最大的类别比如平脉其他类别的召回率趋近于零。原因是数据集里平脉样本占 60% 以上模型发现全猜平脉就能拿到不错的准确率不需要学习其他类别。这是脉象识别项目里最常见也最容易被忽视的问题尤其是自采数据时采集者倾向于找身体健康的人平脉自然占比高。解决方法是先把类别分布打印出来看超过 3:1 的类别比例就要处理。算法层面用class_weightbalanced或损失函数里给少数类加权重数据层面可以用时间缩放、幅度扰动、加少量高斯噪声的方式对少数类做数据增强——注意不要对波形做时间反转那会破坏脉搏波的物理语义上升支和下降支顺序一变脉象含义完全不同。真正讲究的做法是重新采集数据把少数类的样本量补到与平脉类差距不超过 2 倍。5.4 自采数据与公开数据混用导致域偏移现象是单独在公开数据上训练表现不错加入自采数据后整体指标反而下降自采部分的测试结果尤其差。原因是两个数据源的采集设备型号、传感器压力、采样率、受试者年龄段分布都不同波形幅度和形态存在系统性差异。这不是模型的问题是数据域的问题专业说法叫域偏移。不同设备采集的 PPG 信号光基线水平就可能差一个量级。解决方法是先做设备间的归一化验证取两个数据源的各 100 个样本分别算主波幅值的均值和标准差如果分布差异明显要么对每段信号做独立的幅度归一化要么干脆分开建模一个模型专门处理一类设备。最务实的路线是公开数据用作预训练或特征提取自采数据只做最终评估不再混入训练集。5.5 标注不一致比噪声更致命现象是模型训练时损失函数下降缓慢或者验证集指标震荡得厉害排查了特征和模型结构都没找到问题最后去看标注发现同一类别的波形形态差异极大。原因是不同标注者对「弦脉」和「紧脉」的理解不同或者同一个标注者在不同时间对同一批波形给出了不同标签。标注噪声对分类任务的影响远大于信号噪声信号噪声是特征层面的扰动标注噪声直接改写了学习目标。解决方法是整理一份标注一致性报告计算每个类别的标注置信度剔除置信度低于 0.6 的样本。如果项目里根本没有第二个人能标注就退一步缩小类别集合把最难的类别合并——比如把「弦脉」和「紧脉」合并成「弦类脉」虽然分类粒度变粗但每一类的标签更可靠。宁可分类少而准不要多而乱。6. 进阶给识别结果一个可解释的交代再谈落地模型跑通只是第一步。脉象识别系统要让人信服难点不在指标而在解释模型说这是弦脉依据是什么用特征重要性分析可以打开这个黑匣子。随机森林训练后直接输出feature_importances_通常会发现rise_slope和wavelet_energy_2排在最前面这符合脉象的物理直觉——弦脉的特征就是上升支陡峭。SVM 没有直接的特征重要性可以用permutation_importance把某个特征的值打乱后观察指标下降幅度下降越大说明该特征越关键。这两个方法都不需要改训练代码是投入产出比最高的解释手段。落地方面我见过把源码跑通后做成一个小工具的常见路径用 PyQt5 或 Gradio 包一个界面左边显示加载的脉搏波形右边实时显示预测的脉象类别和置信度。如果目标是嵌入可穿戴设备需要把模型导出成轻量格式传统机器学习模型可以用sklearn的joblib直接序列化PyTorch 模型导出为 TorchScript 后体积和推理速度都有保障。最后说一个个人习惯我会在项目根目录放一个config.yaml把采样率、滤波器通带、特征开关、模型超参数全部写进去每次实验改配置不本文还有配套的精品资源点击获取