简介基于Python的脉象识别系统源码是一套面向中医脉诊与现代医学诊断场景的程序融合信号处理、模式识别与机器学习技术旨在通过分析人体脉搏信号辅助医生进行健康评估与疾病诊断。压缩包共包含61个文件体积仅1.26MB其中47个Python脚本构成系统核心逻辑8个CSV文件存放样本数据3个Markdown文档记录开发说明另附H5格式的模型权重文件目录结构清晰便于按模块查看与修改。目前已有176人浏览/学习适用于医学信号处理、人工智能与软硬件结合方向的开发者及学生。除完整可运行的源码外还覆盖数据采集、预处理、特征提取、分类识别到结果输出的全流程模块设计并配有测试脚本与接口示例方便读者快速理解脉象识别的工程实现也可作为毕业设计或课程开发的参考基础。1. 拿到的这套Python脉象识别系统源码先搞清它识别什么手里这份Python脉象识别系统源码.zip解压之后是一套典型的「信号处理 机器学习」工程输入是脉搏波信号输出是弦脉、滑脉、平脉这类脉象标签。它解决的是中医脉诊客观化里最基础的一环——把医生手指感知的脉象转成可计算的特征再交给分类器判断。适用人群很明确正在做课程设计或毕业设计的本科生、想给健康监测设备加脉象识别功能的嵌入式开发以及刚入门生理信号处理、想找一个完整管线练手的Python使用者。这套系统真正的门槛不在算法而在信号质量控制和特征对齐后文会按这个顺序展开。2. 解压源码.zip的第一步目录结构、数据集与训练标签的约定拿到压缩包先别急着看模型代码。脉象识别这类系统数据格式的坑远多于算法本身。本章先把解压、环境、数据格式这三件事说清楚任何一步没对齐后面的特征提取都是白做。2.1 解压与运行环境检查先确认能跑再看代码怎么写压缩包解压这一步看起来简单实际经常卡在编码上。Windows 下打包的 zip内部文件名可能带着 GBK 编码直接unzip解压会出现乱码目录名。我一般先试不带任何参数解压看到乱码再用-O指定编码# 静默解压到指定目录避免在当前目录散落一堆文件 unzip -q Python脉象识别系统源码.zip -d pulse_system # 如果文件名乱码说明压缩包内编码是 GBKLinux/unzip 新版本支持指定编码 unzip -q -O GBK Python脉象识别系统源码.zip -d pulse_system解压之后先看目录结构再核对依赖。大多数同类项目会按src、data、models、features分目录可能还带一个requirements.txt。源码包通常长这样pulse_system/ ├── data/ │ ├── raw/ # 原始脉搏波 CSV │ └── meta.csv # 样本与标签对照表 ├── src/ │ ├── preprocess.py # 滤波、去基线漂移 │ ├── segments.py # 单周期切分 │ ├── features.py # 特征提取 │ ├── train.py # 训练与调参与评估 │ └── predict.py # 对新样本做预测 └── requirements.txt依赖一般集中在numpy、scipy、pandas、scikit-learn、pywt、joblib这几个库上。建议直接用 Python 3.9 以上的版本装依赖时不要对照网上的“教程”逐个安装直接读包里自带的requirements.txt一次装完python -m pip install -r requirements.txt如果仓库没有requirements.txt就用下面这个最小集合版本号不必锁死能跑通即可python -m pip install numpy scipy pandas scikit-learn PyWavelets joblib flask提示如果代码是用老版本 TensorFlow 写的那就麻烦了。建议先把import行全部扫一遍发现深度学习的依赖再决定是重装老框架还是把模型改成 scikit-learn。脉象识别并不必须上深度学习传统特征加 SVM 在小样本场景下往往更稳。2.2 脉象数据从哪来公开数据集与自行采集两条路线没有数据整个系统就是空壳。常见做法是两种找论文公开的脉搏波数据集或者用脉搏传感器自己采一批。前者省事后者更贴合真实部署。公开数据集通常提供按压传感器或光电容积脉搏波的原始信号采样率从 125 Hz 到 1000 Hz 不等标签可能是中文脉象名或拼音缩写。拿到手第一件事不是训练而是确认三件事采样率是多少、信号是否经过滤波、标签和信号文件有没有错位。这三项任何一项不明确后面做出的模型都不值得信任。自行采集的话常见传感器是压阻式脉搏传感器或多普勒脉搏波模块接树莓派或单片机通过串口输出采样率由采集代码决定。采的时候务必把采样率写进文件名或数据库字段例如S012_xian_250hz.csv。我见过太多人采完数据忘了记采样率最后全靠猜特征里的频率成分全废了。2.3 数据格式约定CSV 列名与标签映射别嫌啰嗦无论数据来自哪条路线最终都要整理成统一的 CSV 格式。推荐的最小格式是四列时间戳、脉搏波幅值、受试者编号、脉象标签。timestamp,wave,subject_id,label 0.000,0.152,S001,ping 0.004,0.163,S001,ping 0.008,0.170,S001,ping标签建议用拼音或英文中文标签虽然直观但 sklearn 对字符串标签也能处理只是在保存模型和做混淆矩阵时不够清爽。常见映射是平脉ping弦脉xian滑脉hua细脉xi。如果数据集自带的是中文标签先统一转成拼音别让同一脉象出现“弦脉”和“xian”两种写法这是新手最容易忽略的数据卫生问题。有了 meta 表之后还要做一步非常关键的操作按受试者划分训练集和测试集而不是按行随机切。同一受试者的前后几个周期高度相关一旦同一个人的数据同时出现在训练集和测试集里验证指标会虚高一大截。正确做法是先按subject_id分层切分import pandas as pd from sklearn.model_selection import train_test_split meta pd.read_csv(data/meta.csv) subjects meta[subject_id].unique() # test_size0.2 表示留 20% 受试者做测试random_state 固定保证可复现 train_sub, test_sub train_test_split(subjects, test_size0.2, random_state42) train_idx meta[subject_id].isin(train_sub) test_idx meta[subject_id].isin(test_sub) meta.loc[train_idx].to_csv(data/train.csv, indexFalse) meta.loc[test_idx].to_csv(data/test.csv, indexFalse)逻辑说明这里切分的单位是subject_id而不是单条样本确保一个受试者的所有周期只会出现在训练或测试中的一侧模拟“新来一个人”的真实场景。random_state42让每次划分结果一致。划分之后马上检查每个类在训练集和测试集里的占比。如果某类脉象样本特别少切出来的 test 里一个样本都没有那就得考虑留出法换成交叉验证或对该类做更保守的分层。这一步做完数据准备才算完成。3. 信号预处理做到位带通滤波、基线漂移与单周期切分脉象识别系统的成败八成在预处理。同一个分类器信号干净时准确率能到九成信号带漂移时直接崩到六成。本章从滤除噪声、去基线、切分周期三步走每一步给出可复现的代码和参数经验值。3.1 带通滤波与工频陷波先把脉搏波洗出能看的样子脉搏波信号的噪声来源主要是三块人体呼吸引起的低频漂移、肌肉紧张产生的低频扰动、以及 50 Hz 工频干扰。低频漂移留在下一节单独处理这里先做带通滤波和工频陷波。脉搏波的有效能量集中在 0.5 Hz 到 30 Hz 之间心率 60 次/分对应基频 1 Hz谐波能到十几次。因此带通下限取 0.5 Hz 可以压掉一部分呼吸分量上限取 30 Hz 不至于把波形轮廓削平。用scipy.signal实现零相位滤波from scipy import signal def filt_band(sig, fs250.0): 带通 0.5~30Hz 50Hz 陷波返回干净信号 # 4 阶 butterworthfiltfilt 保证零相位偏移 b_band, a_band signal.butter(4, [0.5, 30], btypebandpass, fsfs) filt signal.filtfilt(b_band, a_band, sig.astype(float)) # Q30 表示陷波带宽较窄只切工频附近避免伤到脉搏波谐波 b_notch, a_notch signal.iirnotch(50.0, 30.0, fs) filt signal.filtfilt(b_notch, a_notch, filt) return filt参数说明fs必须和采集时实际采样率一致这是最容易踩坑的地方。butter的 4 阶在工程上是平衡选择阶数再高容易引入振铃filtfilt双向滤波不会产生相位移动这对后续周期切分很重要。iirnotch的 Q 值取 30陷波带宽大约不到 2 Hz如果你把 Q 调到 1等于把整个信号能量都削掉一块重搏波可能会被滤平。滤波后建议画一次时域图确认形态主波、重搏波应该清楚可见切出来的波形不应该出现振铃尾巴。如果某个样本波形异常宁可删掉不要硬塞进训练集。3.2 去掉基线漂移中值滤波与多项式拟合选哪个带通滤波已经把极低频部分衰减了一部分但呼吸造成的基线起伏仍然存在。常见做法有两种中值滤波拟合法和多项式拟合法。中值滤波的思路是把信号视为“基线 细节”用一个窗口约 1 秒的中值估计当前基线。脉搏波周期通常在 0.6 秒到 1.2 秒之间中值窗口取 1 秒能较好贴着基线走from scipy.ndimage import median_filter def remove_baseline_median(sig, fs250.0): # kernel 取 1 秒长度窗口太小会把主波一起滤掉 kernel int(fs) baseline median_filter(sig, sizekernel, modereflect) return sig - baseline多项式拟合则是把整段信号拟合一条低阶曲线当基线import numpy as np def remove_baseline_poly(sig, fs250.0): n len(sig) t np.arange(n) / fs # deg 取 4~6太高会把主波形态带进基线 coef np.polyfit(t, sig, deg5) baseline np.polyval(coef, t) return sig - baseline两种方法怎么选如果每个样本只有几秒多项式拟合更稳中值滤波在边缘处会有反射误差。如果样本是连续采集的几十秒长信号中值滤波更贴合呼吸节律因为呼吸漂移不是严格多项式形态。我一般先用中值滤波如果发现重搏波附近出现异常的负向鼓包再换多项式拟合对比。3.3 单周期切分与归一化给特征提取准备标准输入整段信号不能直接进模型特征描述的是“单个脉搏周期”。切分的常规做法是先找主波峰值再从峰值后固定位置截取固定长度窗口。from scipy.signal import find_peaks def segment_signal(clean, fs250.0, win_sec1.0): # 主波通常占信号幅值较高height 设 60 分位能滤掉小杂波 peaks, _ find_peaks(clean, distanceint(0.4 * fs), heightnp.percentile(clean, 60)) segs [] for p in peaks: start p int(0.1 * fs) # 峰后 0.1 秒 end start int(win_sec * fs) # 固定 1 秒窗口 if end len(clean): seg clean[start:end] # 幅值归一化让不同人的波形幅度可比较 seg (seg - seg.min()) / (seg.ptp() 1e-9) segs.append(seg) return segs逻辑说明起点取主波峰后 0.1 秒而不是主波峰本身是为了把上升支的完整形态排除在外让窗口主要包含降支和重搏波。窗口固定 1 秒基本能覆盖一个完整周期如果心率偏低、周期超过 1.2 秒就得把win_sec调大。切分后需要做质量筛查。脉搏波切分常见问题是窗口里混进了下一个周期的上升沿或者幅值太低导致归一化后全是噪声。最简单粗暴的过滤规则是窗口内最大值出现在最后 20% 位置、或归一化前幅值小于整段信号幅值 20% 的窗口直接丢弃。这个规则不用太精细目的是把明显损坏的样本清掉。切分完成的波形段建议直接存成npy格式并带上标签样本量不大时一次全部加载进内存即可。如果波形段有几千条建议按(样本数, 窗口长度)的矩阵存成一个文件后续特征提取会快很多。4. 特征提取与模型训练把波形变成十来个数字再交给 SVM预处理做干净之后下一步是把每条脉搏波段转成一组有生理意义的数值特征。特征设计决定了模型的性能上限模型只是在逼近这个上限。本章先讲特征怎么设计再给出训练与调参的具体做法。4.1 时域特征主波、重搏波与脉率的信息压缩时域特征直接描述波形形态。中医脉象里弦脉表现为脉体挺直、按之如琴弦反映在波形上是主波高耸、降支快、重搏波不明显滑脉则“往来流利”重搏波清晰且与主波间隔紧凑。这些形态差异用几个数值就能量化。我常用的一组时域特征如下表特征名计算方式生理含义rise_time主波峰值位置对应时间反映动脉弹性dicrotic_ratio重搏波峰值 / 主波峰值区分弦脉与滑脉的核心特征period相邻主波峰间隔均值对应脉率systolic_area_ratio窗口前段面积 / 总面积反映收缩期压力占比实现代码def extract_time_features(seg, fs250.0): n len(seg) peak_idx int(np.argmax(seg)) rise_time peak_idx / fs # 重搏波在主波峰值之后通常出现在峰值后 0.15~0.4 秒 tail seg[peak_idx int(0.05 * fs):] if len(tail) int(0.2 * fs): dicrotic np.max(tail[:int(0.3 * fs)]) else: dicrotic 0.0 dicrotic_ratio dicrotic / (seg[peak_idx] 1e-9) # 前 60% 面积近似收缩期 split int(0.6 * n) systolic_area_ratio seg[:split].sum() / (seg.sum() 1e-9) return [rise_time, dicrotic_ratio, systolic_area_ratio]需要注意dicrotic_ratio的定位窗口。如果把整个尾巴都最大化重搏波可能被下一次主波的上升沿污染。限制在峰后 0.05 秒到 0.35 秒之间是经验值心率偏低的人群重搏波会晚一些可以适当拉长到 0.45 秒。4.2 频域与小波特征补上时域看不到的频谱信息单纯时域特征对形态相似的脉象难以区分。频谱特征能反映能量在不同频段的分布弦脉往往在低频段集中度更高滑脉的高频成分相对更丰富。def extract_fft_features(seg, fs250.0): n len(seg) f np.fft.rfftfreq(n, d1.0 / fs) spectrum np.fft.rfft(seg - seg.mean()) power np.abs(spectrum) ** 2 valid f 12.0 fv f[valid] pv power[valid] dominant_freq fv[np.argmax(pv)] # 低频段(0~2Hz)能量占比 low_band pv[fv 2.0].sum() total pv.sum() 1e-9 low_ratio low_band / total return [dominant_freq, low_ratio]rfftfreq计算频率轴rfft只取正频率部分适合真实信号。valid f 12.0排除高频噪声段如果采样率是 1000 Hz这段逻辑不用改因为频率轴归一化由rfftfreq自动完成。小波特征的作用是同时保留时域和频域信息。脉搏波是典型的非平稳信号小波比纯 FFT 更适合描述主波到重搏波之间的瞬态变化。常见参数是 db4 小波、3 层分解import pywt def extract_wavelet_features(seg): coeffs pywt.wavedec(seg, db4, level3) feats [] for detail in coeffs[1:]: # 每层的能量和标准差描述不同频段的能量分布 feats.append(float((detail ** 2).mean())) feats.append(float(detail.std())) return feats逻辑说明wavedec返回的第 0 项是近似系数保留整体趋势这里只用细节系数因为近似系数和 FFT 低频信息高度重叠。level3 对 1 秒窗口比较合适如果窗口更长level 可以加到 4 或 5但特征维度会跟着涨小样本下容易过拟合。4.3 训练 SVM 与随机森林参数搜索与交叉验证特征不一定要多但必须和标签有对应关系。把上面的时域、频域、小波特征拼成一个向量就能交给分类器了。脉象识别训练样本通常只有几十到几百例SVM 的核方法在这个规模下比深度模型稳定得多随机森林则更适合特征里有冗余或噪声的场景。from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV, StratifiedKFold # X 是 (n_samples, n_features) 的特征矩阵y 是对应的标签数组 param_grid { C: [1, 10, 100], gamma: [scale, 0.01, 0.001], kernel: [rbf] } base_svc SVC(probabilityTrue, class_weightbalanced) cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) grid GridSearchCV(base_svc, param_grid, cvcv, scoringf1_macro, n_jobs-1) grid.fit(X, y) best_svc grid.best_estimator_参数说明C控制误分类惩罚脉象数据噪声大太大会把噪声当信号学进去太小则欠拟合gamma影响 RBF 核的作用半径。class_weightbalanced会在训练时自动增大少数类的权重对脉象数据集的类别不平衡很有用。scoringf1_macro是对各脉象类别的 F1 求平均比 accuracy 更能反映每个类是否都被照顾到。对比实验也不要省随机森林可以当作第二模型同时训练最后选验证集上表现好的一个。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier(n_estimators300, max_depth8, min_samples_leaf4, class_weightbalanced, random_state42)min_samples_leaf4防止树过深导致小样本过拟合n_estimators300 棵在脉象数据这种规模下已经足够多再多训练时间上去但精度提升很有限。模型训练完成后立刻用joblib保存import joblib joblib.dump(best_svc, models/pulse_svm.joblib)后面预测阶段只需要加载这个文件配合同样的预处理和特征提取函数就能对新信号做分类。5. 避坑脉象识别最容易翻车的 5 个环节脉象识别系统的坑一半在数据一半在压缩包本身。这些坑不看案例很难提前预料我按自己遇到过的真实顺序列出来按「现象 → 原因 → 解决」给全。5.1 伪加密导致源码包解压时要求输入密码现象拿到一个毫无密码的源码 zip 包解压时却弹出密码输入框所有文件都无法提取。原因zip 目录区有一个加密标志位正常情况下未加密文件该位为 0。某些压缩工具或恶意分享者会把该标志位改成 1造成“伪加密”文件内容其实并没有加密。这在论坛和网盘分享里非常常见。解决用 Python 的zipfile读取时手动把该标志位清掉再解压import zipfile with zipfile.ZipFile(Python脉象识别系统源码.zip, r) as zin: for info in zin.infolist(): # 0x01 是加密标志伪加密时文件并未真正加密清掉即可 info.flag_bits ~0x0001 zin.extractall(pulse_system)说明infolist()返回的ZipInfo对象持有flag_bits移动端压缩工具生成的伪加密文件用这种方式通常能绕过。如果清掉标志位后仍能正常提取且文件体积与实际内容一致说明确实只是伪加密。如果真加密解出来的文件会损坏或直接报错那就不能强行处理。5.2 采样率不统一导致特征全乱现象同一套模型在 A 设备采样数据上准确率 92%换 B 设备数据后掉到 67%且未报任何错误。原因A 设备采样率 250 HzB 设备采样率 1000 Hz。周期切分时int(0.4 * fs)算出的距离不同频率轴位置不同FFT 主频计算更是完全错位。解决在预处理入口强制统一采样率用scipy.signal.resample_poly重采样from scipy.signal import resample_poly def resample_signal(sig, src_fs, target_fs250.0): # 先算升降采样倍数保证整数比以减少误差 up int(round(target_fs)) down int(round(src_fs)) return resample_poly(sig, up, down, axis0).astype(float)逻辑说明resample_poly内部做抗混叠滤波比直接np.interp或resample更稳。重采样之后务必将所有下游调用里的fs统一替换成target_fs检查代码里有没有写死的250之类的魔法数字。5.3 标签不平衡导致模型变成“全都预测平脉”现象训练完成后整体准确率 91%但看混淆矩阵弦脉和细脉的召回率都是 0模型把所有样本都判成了平脉。原因平脉样本占 70% 以上模型在优化全局准确率时发现“全猜平脉”代价最小于是学会了走捷径。解决换用class_weightbalanced或对少数类做上采样验证指标不再看accuracy改看每个类别的recall和f1_macro。一个可落地的做法是给每条样本算权重from sklearn.utils.class_weight import compute_class_weight classes np.unique(y) weights compute_class_weight(balanced, classesclasses, yy) sample_weight np.array([weights[list(classes).index(label)] for label in y])说明compute_class_weight返回每个类一个权重少数类权重自动放大。把sample_weight传给 SVM 的fit参数比单独调class_weight更直观也适用于随机森林。记住在这种不平衡数据集上准确率高不等于模型可用。5.4 滤波过度把重搏波滤平了现象特征提取代码逻辑没问题但dicrotic_ratio几乎全是接近 0 的常量模型怎么训都分不开弦脉和滑脉。原因带通滤波上限设成了 10 Hz或陷波滤波 Q 值设成 1导致 10 Hz 附近的谐波能量被过度衰减重搏波形态被抹平。解决把带通上限重新调回 25~30 Hz陷波 Q 回到 30。检查方法是画出滤波前后波形对比重搏波应当是主波降支上的一个清晰小波峰而不是完全平滑的曲线。如果使用的是savgol_filter做平滑window_length别超过 31否则同样会把重搏波抹平。5.5 分组泄漏同一个人的数据同时出现在训练和测试集现象交叉验证 F1 达到 0.95部署时换一批新人数据效果骤降。原因切分训练测试时直接用train_test_split(X, y)同一受试者的多个周期同时落在训练集和测试集模型学的是“识别这个人”而不是“识别这类脉象”。解决交叉验证也要按受试者分组直接用GroupKFoldfrom sklearn.model_selection import GroupKFold group_kfold GroupKFold(n_splits5) for train_idx, val_idx in group_kfold.split(X, y, groupssubject_ids): clf.fit(X[train_idx], y[train_idx]) val_score clf.score(X[val_idx], y[val_idx])逻辑说明groupssubject_ids保证同一个受试者的所有样本只出现在同一个折中。这是脉象识别里最容易忽略的验证错误也是模型上线后翻车的最常见原因。特征提取和调参做得再好验证协议错了结果就是一场空。6. 部署与验证混淆矩阵、留出法和最小预测接口模型训练完不算完还得证明它可靠并且能被别人调用。先看每个脉象类别的预测质量再封装一个能接收单条信号并输出标签的接口这套源码才算真正落地。评估不能只看准确率。脉象数据天然不平衡要打印混淆矩阵逐类看召回率和精确率from sklearn.metrics import confusion_matrix, classification_report, ConfusionMatrixDisplay y_pred best_svc.predict(X_test) print(classification_report(y_test, y_pred)) cm confusion_matrix(y_test, y_pred) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labelsbest_svc.classes_) disp.plot()注意输出表格里每个类别的recall是否都大于 0.6。只要有一个类别低于 0.5这个模型就不具备部署条件回去加样本或调整class_weight。验证通过后最小可用接口一般做成一次预测一个文件。把预处理到特征提取的流程串起来加载保存好的模型文件import joblib import numpy as np model joblib.load(models/pulse_svm.joblib) def predict_wave(raw_signal, fs250.0): clean filt_band(raw_signal, fs) clean remove_baseline_median(clean, fs) segs segment_signal(clean, fs) feats np.array([extract_time_features(s, fs) extract_fft_features(s, fs) extract_wavelet_features(s) for s in segs]) probs model.predict_proba(feats) return model.classes_[np.argmax(probs.mean(axis0))]逻辑说明多个周期各自预测后对概率取平均比单周期拍板更稳能避免个别噪声周期干扰结论。如果后续要接成 web 服务用 Flask 包一个 POST 接口前端传 JSON 数组返回脉象标签即可。我自己第一次做这套系统时调 SVM 调到了 95% 的正确率后来发现是分组泄漏造成的假象改掉验证方式后降回 78%那之后我对指标的第一反应都是先看验证协议是否严谨。脉象识别没有银弹把预处理、特征和对齐做好剩下的就是踏实调参。上面这些步骤走完你手里这份源码就能从“能跑”变成“可信”。希望帮到你。本文还有配套的精品资源点击获取