资讯动态

Python脉象识别系统源码全解析:从信号预处理到分类模型

发布时间:2026/10/3 10:48:01 来源:尧图企业网站定制
简介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.pydata 目录下存放的是原始波形 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高通截止频率HzNOTCH_FREQ50工频陷波频率HzPEAK_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, cutoff0.5): 零相位高通滤波去除基线漂移。 cutoff: 截止频率(Hz)脉搏波主要能量在 0.5-10Hz 基线漂移和呼吸起伏通常低于 0.5Hz。 nyquist 0.5 * fs b, a butter(2, cutoff / nyquist, btypehigh) 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_freq50.0, quality30.0): 50Hz 陷波quality 越大带宽越窄 w0 notch_freq / (0.5 * fs) b, a iirnotch(w0, quality) return filtfilt(b, a, signal)quality品质因数决定陷波带宽quality30 时大约能覆盖 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_period0.4, min_prominence0.1): 按脉搏周期切分信号返回每个单周期的起止索引 peaks, props find_peaks( signal, distanceint(fs * min_period), prominencenp.max(signal) * min_prominence ) cycles [] for i in range(len(peaks) - 1): start, end peaks[i], peaks[i1] cycles.append((start, end)) return cycles, peaks, propsdistance 参数是峰与峰之间的最小样本数按 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_estimators300, max_depth10, min_samples_leaf3, class_weightbalanced, random_state42, n_jobs-1 ) scores cross_val_score(clf, X_features, y_labels, cv5) print(fCross-val accuracy: {scores.mean():.3f} ± {scores.std():.3f})n_estimators300 是因为样本量不大多一些树不会明显过拟合max_depth10 控制每棵树深度min_samples_leaf3 防止叶子节点分裂到单个样本class_weightbalanced 自动处理类别不平衡。cv5 意味着每一折要拿 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.10scipy 1.7scikit-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(fLoaded {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_weightbalancedSVM 里同样有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(axis0)) return { label: self.model.classes_[final_idx], confidence: float(probs.mean(axis0)[final_idx]), }这样可以直接丢进 Flask 接口或者一个简单的 Tkinter 界面里做演示。注意 config 必须保存预处理的全部参数否则换一台设备采样率不同滤波系数就对不上。6.2 生成一份简易脉象报告对课程设计或项目答辩来说不需要复杂前端一张图加几行文字就够。用 matplotlib 画出原始波形、滤波后波形和峰值点再用matplotlib.pyplot.text把脉象标签、置信度和关键特征值写到图下方。我习惯把特征值也显示出来比如“主波高度0.31上升时间0.14s”这会让结果看起来有依据也方便别人复核你的工作。6.3 自检验证技巧永远留一条“已知答案”的波形这个习惯救过我很多次。我会在 data 目录下放一个 verify_fu.csv是我手工标注为“浮脉”的稳定波形。每次改动预处理或特征计算代码后先对这条波形预测确认结果还是“浮”置信度变化也在合理范围再跑完整训练。因为预处理参数稍有变化特征就会偏移自检波形能第一时间暴露回归问题。从那以后每次拿到任何信号处理类的开源源码我都会先构造一条已知标签的样本放进“全家桶”强制走一遍完整流程确认每个环节都正常再开始调参数或换模型。脉象识别这类系统尤其如此——数据的坑比模型的坑多得多有了自检样本起码能分清到底是算法变了还是数据漂了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑