☰
Python脉象识别系统源码全解析:从信号预处理到分类模型
2026/10/3 10:47:49 网站建设 项目流程

简介:Python脉象识别系统源码是一套完整的中医脉象自动识别项目,面向医疗信息化开发者、AI学习者及科研人员,通过传感器采集脉搏信号并结合机器学习与深度学习技术,实现脉位、脉率、脉力等特征的分析与分类,辅助临床诊断。压缩包共61个文件,大小约1.26MB,以47个Python脚本为主,覆盖界面、中间件、接口服务、数据处理等模块;8个CSV文件存储脉象数据集;H5文件为训练好的模型权重;另有README等文档辅助上手。源码大量运用NumPy、Pandas、Scikit-learn与TensorFlow,分别承担数值运算、数据清洗、模型训练和深度网络构建。目前已有300人学习下载,适合熟悉Python并希望切入智能医疗方向的开发者研读。通过学习可掌握从脉象数据预处理、特征提取到模型训练与预测的完整流程,并复用其中可视化与接口模块,快速搭建自己的脉象识别原型。

1. Python脉象识别系统源码:拿到手先拆数据流,再谈跑通

拿到这套Python脉象识别系统源码,我的第一反应不是双击运行 train.py,而是先盯着目录结构看了十分钟。脉象识别不是那种“跑个模型就完事”的 Demo,它是一条完整的信号处理链路:从桡动脉压力波形或 PPG 信号出发,依次要去基线漂移、滤掉工频干扰、切出单周期、提特征,最后才能落到浮、沉、迟、数这类分类标签上。这套源码恰好把这个链路完整打通了,自带样本数据、可视化脚本和模型导出工具,既能当课程设计交差,也能作为中医数字化项目的地基。

适合两类人:一类是想拿“脉象识别”做机器学习练手的学生,另一类是做可穿戴设备或健康监测的工程师——他们关心的不是中医理论本身,而是波形怎么变成可用的分类特征。如果只盯着模型训练,很容易忽略前面 80% 的代码。我拆过不少生物信号项目,凡是“准确率很高但换个样本就翻车”的情况,基本都是预处理或特征工程埋了雷。所以这篇文章从数据流开始,把每个阶段的关键参数和坑都过一遍。你可以边读边对照源码,也可以先跑通整体流程再回头调参数——两条路都行,但别跳过预处理直接看预测结果。

2. 脉象识别系统架构与信号预处理:滤波参数决定实验成败

2.1 先搞清模块边界:源码的数据流不是从训练开始的

任何信号类的源码包,第一步都是梳理输入输出。这套源码的目录结构大致是这样:

pulse_detect/ ├── data/ │ ├── sample_fu.csv │ ├── sample_chen.csv │ └── labels.csv ├── config.py ├── preprocess.py ├── features.py ├── train.py ├── predict.py └── visualize.py

data 目录下存放的是原始波形 CSV,每一行是一个采样点,至少包含 timestamp 和 voltage 两列;labels.csv 记录每条波形的类别标签。preprocess.py 做滤波和去基线漂移,features.py 负责切周期、算特征,train.py 训练分类器并导出 pkl 模型,predict.py 加载模型对单条波形做推理。visualize.py 是我比较看重的脚本——它能把原始波形、滤波后波形和峰值点画在同一张图上,排查问题时比盯数字直观得多。

这个结构是典型的流水线设计:每个模块只做一件事,模块之间通过 CSV 或 pkl 文件解耦。我第一次拆这套源码时,直接把 preprocess.py 和 features.py 混在一起改,结果调参时根本分不清是滤波影响了特征,还是特征计算本身有 bug。后来我坚持按模块跑,每一步输出中间文件,定位问题就快多了。如果你拿到的源码结构不完全一样,我建议按这个边界自己拆一遍——哪怕只是加几行 print,也能让数据流向变得清晰。

拿到源码第一件事,打开 config.py 看这几个关键参数:

参数默认值说明
SAMPLE_RATE100采样率(Hz),所有滤波系数都依赖它
DETREND_CUTOFF0.5高通截止频率(Hz)
NOTCH_FREQ50工频陷波频率(Hz)
PEAK_DISTANCE0.4脉搏主峰最小间距(秒)
MIN_CYCLE_LEN0.3单周期最短长度(秒)

其中 SAMPLE_RATE 是命根子。如果采集设备是 500Hz,而配置文件里还是 100,那么后面的 butter 系数、find_peaks 的 distance 全部按错误频率换算,结果就是波形看着对,特征全错。我一般会先在 visualize.py 里加一行打印,输出实际读取到的采样率和数据长度,确认无误再往下走。

2.2 去基线漂移:高通滤波器的截止频率不是随便填的

脉象波形采集时,传感器和人体接触会让信号叠加一个缓慢漂移的基线,呼吸也会造成低频起伏。如果不去掉这部分,后续峰值检测会把基线波动当成脉搏波,特征全乱。常见做法是使用二阶巴特沃斯高通滤波器,截止频率设在 0.5Hz 左右。以 100Hz 采样率为例:

import numpy as np from scipy.signal import butter, filtfilt def detrend_signal(signal, fs, cutoff=0.5): """零相位高通滤波,去除基线漂移。 cutoff: 截止频率(Hz),脉搏波主要能量在 0.5-10Hz, 基线漂移和呼吸起伏通常低于 0.5Hz。 """ nyquist = 0.5 * fs b, a = butter(2, cutoff / nyquist, btype='high') filtered = filtfilt(b, a, signal) return filtered

这里用 filtfilt 而不是 lfilter,因为它做零相位滤波,不会让波峰位置产生偏移。cutoff 这个参数非常敏感:设成 0.2Hz,基线漂移可能去除不干净;设成 1Hz,会把脉搏信号的上升沿削掉,主波高度特征直接失真。我一般先设 0.5,然后看可视化波形,观察基线是否还在缓慢起伏;如果还有,就逐步降到 0.3。注意 fs 必须和实际采样率一致,否则一切白搭。

基线是否去干净,不能只靠眼睛。我习惯加一个量化检查:

def check_detrend(original, filtered): """计算滤波前后的 RMS 变化,判断基线漂移占比""" rms_diff = np.sqrt(np.mean((original - filtered) ** 2)) rms_original = np.sqrt(np.mean(original ** 2)) print(f"基线漂移占比: {rms_diff / rms_original * 100:.1f}%") return rms_diff / rms_original

如果这个占比超过 30%,说明原始信号中基线漂移过重,应该优先检查传感器佩戴是否稳定,而不是继续调截止频率。这个思路也被我用到其他生理信号项目里,比反复换滤波器参数高效得多。

2.3 工频干扰:陷波器与滑动窗口的分工

50Hz 工频干扰是另一个大麻烦,尤其当采集设备没有做硬件滤波时。预处理脚本里通常同时提供两种方案:一是针对单一频率的陷波器,二是滑动平均滤波。陷波器的参数要小心:

from scipy.signal import iirnotch, filtfilt def remove_powerline(signal, fs, notch_freq=50.0, quality=30.0): """50Hz 陷波,quality 越大带宽越窄""" w0 = notch_freq / (0.5 * fs) b, a = iirnotch(w0, quality) return filtfilt(b, a, signal)

quality(品质因数)决定陷波带宽,quality=30 时大约能覆盖 50Hz 附近 1Hz 以内的频段。如果采样率是 100Hz,奈奎斯特频率正好是 50Hz,陷波器会失效——这种情况只能靠硬件抗混叠或降采样前滤波解决。滑动平均则适用于电源不稳定导致的宽频噪声,但窗口长度要小于一个脉搏周期。比如 100Hz 采样下窗口设为 10 个点(100ms),否则会把主波尖峰抹平。

我的经验是:先做高通去漂移,再做陷波,最后做一次滑动平均平滑,这个顺序不能反。如果反了,去掉基线漂移之前先滑动平均,会把基线漂移变成阶梯状,高通滤波后反而引入新的低频段伪迹。预处理做完后,波形已经比较干净,接下来要切分单个脉搏周期。

2.4 脉搏周期切分:find_peaks 的两个关键参数

切分周期的核心是找主波峰。scipy.signal.find_peaks 的 distance 和 prominence 必须按心率和信号幅度来设:

from scipy.signal import find_peaks def extract_cycles(signal, fs, min_period=0.4, min_prominence=0.1): """按脉搏周期切分信号,返回每个单周期的起止索引""" peaks, props = find_peaks( signal, distance=int(fs * min_period), prominence=np.max(signal) * min_prominence ) cycles = [] for i in range(len(peaks) - 1): start, end = peaks[i], peaks[i+1] cycles.append((start, end)) return cycles, peaks, props

distance 参数是峰与峰之间的最小样本数,按 0.4 秒意味着允许最高 150 次/分的心率。如果被测试者心率只有 50 次/分,0.4 秒的间距足够,因为单个周期是 1.2 秒,不会误检出一个主波里的副峰。但如果心率很快,比如 120 次/分,周期只有 0.5 秒,distance 就得相应缩小。prominence 设为最大幅度的 10%,是为了把那些幅度过小的噪声峰滤掉。切分后,首尾不完整的周期必须丢弃,否则特征里的周期时长失真。我一般会在代码里加一个长度判断:周期长度小于 0.3 秒或大于 2 秒的直接跳过。

提示:每次修改完预处理参数,都要重新跑一遍 extract_cycles,并且看一眼可视化图里的峰值点是否准确落在主波峰顶部。峰值点位置一旦偏移,后续所有时域特征都会跟着错。

3. 特征提取与模型识别:手工特征加集成分类器,比盲上神经网络更稳

3.1 时域特征:主波高度、上升时间与重搏波比值

脉象识别最常用的时域特征都围绕一个脉搏周期展开。拿到一个干净的周期后,我通常提取主波高度 h1、上升时间 t1、重搏波高度 h2,以及它们之间的相对关系:

import numpy as np def time_features_from_cycle(cycle, fs): """输入单个脉搏周期信号,返回时域特征 dict""" cycle = cycle - np.mean(cycle) # 去直流 peak_idx = np.argmax(cycle) h1 = cycle[peak_idx] t1 = peak_idx / fs # 从起点到主波峰的上升时间 # 找重搏波:主波之后至少 0.1s 的最大局部峰 offset = int(0.1 * fs) tail = cycle[peak_idx + offset:] if len(tail) < offset: h2 = 0.0 else: h2 = np.max(tail) period = len(cycle) / fs return { 'h1': h1, 't1': t1, 't1_over_period': t1 / period, 'h2_over_h1': h2 / h1 if h1 != 0 else 0.0, 'pulse_rate': 60.0 / period, }

h1 是主波高度,代表脉搏压力最大值;t1 是上升时间,与动脉弹性和外周阻力有关;h2/h1 这个比值对弦脉和滑脉的区分度很高。计算时有两个坑:一是 cycle 必须先减去均值,否则直流偏置会污染 h1 和 t1;二是找重搏波时要从主波峰后至少 0.1 秒开始扫描,否则可能把主波下降沿上的一个抖动当成重搏波。period 由切分长度决定,所以上一章切分不准,这里的 pulse_rate 也会跟着错。

3.2 频域特征:频谱能量比与频谱熵

时域特征看的是波形的形状,频域特征看的是波形的节律成分。脉搏信号的功率谱主要分布在 0.5-8Hz,低频部分与血流缓冲能力有关,高频部分与动脉壁硬度有关。我用 periodogram 提取两块特征:

from scipy.signal import periodogram def freq_features_from_cycle(cycle, fs): freqs, psd = periodogram(cycle, fs) low_mask = (freqs >= 0.5) & (freqs < 4) mid_mask = (freqs >= 4) & (freqs < 8) low_energy = np.sum(psd[low_mask]) mid_energy = np.sum(psd[mid_mask]) total_energy = np.sum(psd[psd > 0]) + 1e-12 p_positive = psd[psd > 0] probs = p_positive / total_energy spectral_entropy = -np.sum(probs * np.log(probs)) return { 'low_mid_ratio': low_energy / mid_energy, 'spectral_entropy': spectral_entropy, }

low_mid_ratio 能体现不同脉象在频带能量上的差异,spectral_entropy 则反映信号的复杂程度。注意:如果周期长度太短,比如采样率只有 20Hz,那么频率分辨率太低,这两个特征基本不可信。我个人的底线是采样率不低于 100Hz,且每个周期至少 256 个采样点。

把所有周期都过一遍特征提取后,把每个周期的时域和频域特征拼接成一行向量,同时带上当前周期的标签。这里我会顺手做一个特征相关性检查,用 pandas 计算各特征之间的相关系数,如果发现 h1 和 h1_over_period 的相关性超过 0.95,就只保留其中一个,减少冗余维度。

3.3 分类器选择:为什么先选随机森林而不是 CNN

样本量只有几十到几百条时,随机森林和 SVM 通常比神经网络更稳。随机森林能处理少量样本、对特征尺度不敏感,还能直接输出特征重要性。训练代码大致是这样:

from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score clf = RandomForestClassifier( n_estimators=300, max_depth=10, min_samples_leaf=3, class_weight='balanced', random_state=42, n_jobs=-1 ) scores = cross_val_score(clf, X_features, y_labels, cv=5) print(f'Cross-val accuracy: {scores.mean():.3f} ± {scores.std():.3f}')

n_estimators=300 是因为样本量不大,多一些树不会明显过拟合;max_depth=10 控制每棵树深度;min_samples_leaf=3 防止叶子节点分裂到单个样本;class_weight='balanced' 自动处理类别不平衡。cv=5 意味着每一折要拿 20% 的样本做验证,如果某个类别的样本数少于 5,交叉验证很容易出现某一折里根本没有这个类别。所以训练前要先用 value_counts 检查标签分布。

为什么不上 CNN?总样本几百条,每条周期长度还不固定,卷积核很难学到稳定的局部模式。手工特征把一条波形压缩成几十个有物理意义的维度,模型更容易拟合。如果你有上千条样本、且长度经过对齐,再试一维 CNN 不迟,我最后一章会说怎么预留模型切换接口。

4. 源码复现:从环境安装到训练推理的完整流程

4.1 环境准备:别让第三方库版本玩死你

老生常谈,但每次都会遇到有人卡在这里。建议用虚拟环境:

python -m venv pulse_env source pulse_env/bin/activate # Windows 使用 pulse_env\Scripts\activate pip install numpy scipy scikit-learn matplotlib pandas

如果你熟悉 Python 环境配置,这一节可以快进;但网上 python 安装教程里踩过的坑,在这里同样会出现——系统自带 Python 和虚拟环境混用最容易导致库冲突。我的建议是 Python 3.8-3.10,scipy 1.7+,scikit-learn 1.0+。在 Python 3.12 上跑老代码,很可能遇到 numpy 的 API 变动问题,到那时排查成本远高于一开始固定版本。

依赖安装完成后,先跑通一个最小测试:

python -c "import numpy, scipy, sklearn; print(numpy.__version__, scipy.__version__, sklearn.__version__)"

如果 scipy 版本过低,iirnotch 可能存在兼容性问题;如果 sklearn 版本过高,部分旧代码里的模型参数名字可能失效。看到版本号正常输出,再继续下一步。

4.2 先跑可视化,确认数据能读

环境没问题后,第一个要跑的是 visualize.py,不要直接训练:

python visualize.py --input data/sample_fu.csv --detrend --cutoff 0.5

这个命令会打开一个窗口,显示三条曲线:原始波形、滤波后波形、周期边界。如果曲线不平滑或者有毛刺,需要回到 config.py 调整滤波参数。如果脚本报错提示找不到文件,先检查 CSV 的路径和列名——不少拿到源码改过目录的人,最后发现是路径分隔符问题。我一般习惯在 visualize.py 里加一段:

import pandas as pd def load_wave(path): df = pd.read_csv(path) print(f"Loaded {len(df)} samples, columns: {list(df.columns)}") return df['voltage'].values

先打印列名和样本数,能省下很多排查时间。如果 CSV 里根本没有 timestamp 列,而是直接一列波形,那就不需要时间戳,采样率依然由 config 里的 SAMPLE_RATE 决定。

4.3 训练模型与单样本预测:命令与输出说明

确认可视化正常后,开始训练:

python train.py --data data/labels.csv --feature-type both --model rf --output models/pulse_rf.pkl

参数含义如下:

参数可选值说明
--data路径标签文件,需含 wave_path 和 label 两列
--feature-typetime / freq / both使用的特征类型
--modelrf / svm分类器类型
--output路径模型保存位置,含 .pkl 后缀

训练过程中会打印每一折交叉验证的准确率,最后会把模型、特征列名、类别列表一起写进 pkl 文件。这个 pkl 是后续 predict.py 的唯一依赖,它包含的可不只是clf对象,还有预处理参数,所以别只挑模型那一部分复制。

预测单条波形:

python predict.py --input data/sample_chen.csv --model models/pulse_rf.pkl --output results/prediction.json

输出 JSON 大概是:

{ "label": "chen", "confidence": 0.87, "features": {"h1": 0.12, "pulse_rate": 72.0} }

predict.py 内部会自动完成预处理、切周期、提取特征,再对每个周期分别预测,取多数投票作为最终标签。如果同一个波形文件里多个周期预测结果不一致,说明信号本身不稳定或特征边界模糊,输出里也会给出各周期的置信度列表。这个细节比只给一个标签有价值——它能帮你判断当前预测到底可不可信。

5. 脉象识别避坑指南:从数据采集到模型评估的四个常见翻车现场

5.1 训练集准确率 99%,测试集只有 60%:数据划分的随机种子问题

现象:用train_test_split划分数据后,模型在训练集上几乎满分,测试集上却掉到及格线以下。

原因:最常见的是数据划分前没有打乱,或者没有按受试者分组。同一个人的连续波形在 CSV 文件里是相邻的,如果不打乱,训练集和测试集会包含同一波形的不同周期,模型记住了样本而不是规律。另一个隐蔽原因是预处理阶段使用了整段信号的统计量做归一化,导致数据泄漏。

解决:训练前确认 labels.csv 的排列顺序,必要时先shuffle。如果样本来自多个受试者,用GroupKFold代替KFold。我在源码里见过好几个人把随机种子固定成 42 就以为万事大吉,但 GroupKFold 不看随机种子,它看 group 划分。把受试者 ID 作为 group 传进去,才能保证一组波形不会同时出现在训练集和测试集。

5.2 同一受试者隔 10 分钟测两次,分类结果一浮一沉:采集状态没控制

现象:模型训练完后,拿同一个人在不同时间点采集的波形验证,两次结果完全相反。

原因:脉象受情绪、呼吸、肢体位置甚至饭前饭后影响。采集状态不一致时,同一个体的波形差异可能大于不同脉象之间的差异。很多人把这种个体波动当成噪声,其实是信号质量的最大源头。

解决:在采集规范里固定测量姿势,静息时间至少 3 分钟,传感器压力保持一致。如果源码没有记录元数据,我会在 CSV 的额外列里加上“采集时间、受试者、姿势”,训练时把受试者信息作为分组依据。这一步不是算法问题,是数据管理问题,但它直接决定模型能不能泛化到新样本。

5.3 模型永远预测“平脉”:类别不平衡被忽视

现象:分类报告里平脉的 recall 是 0.92,但浮脉只有 0.3,模型几乎把所有样本都判断成平脉。

原因:训练数据里平脉占 70%,模型学到“全猜平脉”就能有七成准确率。这是医疗数据的常见分布,正常人群本来就占多数,但诊断目标恰恰要抓住少数异常脉象。

解决:随机森林里设置class_weight='balanced',SVM 里同样有class_weight参数,也可以用 SMOTE 过采样少数类。训练前打印标签分布,一旦发现某个类别数量少于总体的 10%,就需要提醒补充数据。评估时不要只看准确率,要打印每个类别的 precision、recall 和 F1,尤其是少数类的表现。

5.4 直接把原始波形喂给 CNN,效果不如手工特征

现象:有人觉得“端到端更先进”,把一维 CNN 模型搬到这套源码里,结果交叉验证准确率比随机森林低十几个点。

原因:总共几百个样本,每条周期长度不一致,CNN 很难学到稳定的卷积核。手工特征已经把信号压缩成十几个有物理意义的维度,模型更容易拟合。CNN 的随机性和调参成本在这种数据规模下不占优势。

解决:数据量达到上千条、长度统一且做好增强之前,先用手工特征加集成模型。如果一定要试 CNN,至少保证每条样本长度一致,用np.pad或裁剪把周期对齐,并且加早停和 dropout。源码的 features.py 已经提供了特征导出接口,换模型只需要替换样本生成方式,不需要推倒整个流水线。

6. 进阶:把脉象识别系统封装成 Python 模块,加一个自检波形验证流程

6.1 用类封装预测流水线

predict.py 是命令行脚本,复用起来不直接。我会把它重构成一个PulseClassifier类,加载一次模型,可以多次预测:

import joblib class PulseClassifier: def __init__(self, model_path, config): self.model = joblib.load(model_path) self.config = config def predict_wave(self, signal, fs): """传入原始波形和采样率,返回标签、置信度与特征""" filtered = detrend_signal(signal, fs, self.config['cutoff']) filtered = remove_powerline(filtered, fs) cycles, _, _ = extract_cycles(filtered, fs) features = [extract_pulse_features(c, fs) for c in cycles] if not features: return {'error': 'no valid pulse cycles'} probs = self.model.predict_proba(features) final_idx = np.argmax(probs.mean(axis=0)) return { 'label': self.model.classes_[final_idx], 'confidence': float(probs.mean(axis=0)[final_idx]), }

这样可以直接丢进 Flask 接口或者一个简单的 Tkinter 界面里做演示。注意 config 必须保存预处理的全部参数,否则换一台设备采样率不同,滤波系数就对不上。

6.2 生成一份简易脉象报告

对课程设计或项目答辩来说,不需要复杂前端,一张图加几行文字就够。用 matplotlib 画出原始波形、滤波后波形和峰值点,再用matplotlib.pyplot.text把脉象标签、置信度和关键特征值写到图下方。我习惯把特征值也显示出来,比如“主波高度=0.31,上升时间=0.14s”,这会让结果看起来有依据,也方便别人复核你的工作。

6.3 自检验证技巧:永远留一条“已知答案”的波形

这个习惯救过我很多次。我会在 data 目录下放一个 verify_fu.csv,是我手工标注为“浮脉”的稳定波形。每次改动预处理或特征计算代码后,先对这条波形预测,确认结果还是“浮”,置信度变化也在合理范围,再跑完整训练。因为预处理参数稍有变化,特征就会偏移,自检波形能第一时间暴露回归问题。

从那以后,每次拿到任何信号处理类的开源源码,我都会先构造一条已知标签的样本放进“全家桶”,强制走一遍完整流程,确认每个环节都正常,再开始调参数或换模型。脉象识别这类系统尤其如此——数据的坑比模型的坑多得多,有了自检样本,起码能分清到底是算法变了还是数据漂了。希望帮到你。

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

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

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

立即咨询