简介:这份Python脉象识别系统源码面向中医数字化与智能医疗方向的开发者、学生及科研人员,提供一套可运行的脉诊信号自动识别与分析方案。项目融合生物医学工程与机器学习思路,借助Numpy、Pandas、Scikit-learn、TensorFlow等库完成脉象数据的去噪、归一化、特征提取与分类建模,可用于课程设计、毕业项目或医疗AI入门实践。压缩包为zip格式,共61个文件,约1.26MB,其中47个py源码构成主程序与各功能模块,8个csv存放脉象数据集,另有3个md与1个txt说明文档、1个h5预训练模型及gitignore配置,目录涵盖数据预处理、模型训练、预测评估与可视化等环节。已有300人学习下载。读者可据此理解传感器数据采集到模型推理的完整链路,参考预训练权重快速验证效果,并在此基础上改进算法或迁移到其他生理信号识别任务。
1. 从一张手腕照片到一份体质报告:脉象识别系统到底在做什么
很多人第一次听到「Python脉象识别系统源码」,脑子里浮现的是中医馆里三根手指搭在手腕上的画面,然后下意识觉得这事没法用代码做。其实拆开看,它要解决的问题非常工程化:把一段手腕桡动脉处的压力或图像信号采下来,经过预处理、特征提取、分类判别,最后输出一个「弦脉」「滑脉」「细脉」之类的标签,甚至给出一份体质倾向报告。整套流程里,Python 负责的是信号处理、特征工程、模型训练和推理服务这几块,硬件负责把物理世界的搏动变成数字。
这个方向适合两类人:一类是做过 Python 数据处理、想找一个有真实物理背景的项目练手的人;另一类是手里已经有脉搏传感器或指压式采集设备、想把数据跑通成可用系统的开发者。它不需要你懂中医辨证,但需要你理解信号采样、滤波、时频特征和分类模型的基本套路。源码的价值不在于「识别得多准」,而在于它把从原始波形到分类结果的完整链路摊开给你看,让你能替换数据、替换特征、替换模型,而不是被一个黑匣子卡住。
2. 脉象信号的采集与预处理:把噪声从波形里剥出来
2.1 为什么脉象识别第一步不是建模而是滤波
脉搏信号本质上是一个低频、微弱、易受干扰的生理信号。典型桡动脉脉搏波的基频在 1~1.5 Hz 左右,主要能量集中在 0.5~10 Hz,但采集时混进来的东西非常多:工频干扰、基线漂移、呼吸调制、手部抖动、传感器接触压力变化。如果你直接把原始波形丢进模型,模型学到的很可能是「这个人手抖得厉害」而不是「这个人脉象弦」。
常见做法是先做带通滤波把 0.5~10 Hz 之外的成分压掉,再用小波或经验模态分解处理基线漂移。我一般会先用 Butterworth 带通做粗滤,因为它的相位特性在离线分析里够用,实现也简单。如果要做实时系统,就得考虑零相位滤波或者用 FIR 线性相位方案,否则滤波带来的延迟会让后续的峰值检测对不上。
import numpy as np from scipy.signal import butter, filtfilt def bandpass_filter(signal, fs=200, low=0.5, high=10.0, order=4): """ signal: 一维脉搏信号数组 fs: 采样率,常见 100~500 Hz low/high: 通带边界,脉象主要能量区间 order: 滤波器阶数,太高会振铃 """ nyq = 0.5 * fs low_cut = low / nyq high_cut = high / nyq b, a = butter(order, [low_cut, high_cut], btype='band') # filtfilt 做零相位滤波,避免波形整体平移 return filtfilt(b, a, signal)这段代码里fs必须和你的采集设备一致,如果设备标称 200 Hz 但实际有丢包,滤波后峰值位置会漂。order不要超过 5,阶数越高过渡带越陡,但振铃也越明显,脉象波形里的重搏波切迹很容易被振铃吃掉。filtfilt适合离线,实时场景换成lfilter并接受相位延迟。
2.2 峰值检测与周期切分:一个容易被低估的环节
滤波之后要做的是找到每个心动周期的起点,也就是主波峰位置。脉象特征里很多指标依赖周期对齐,比如主波高度、重搏波相对位置、周期时长变异。如果峰值检测漏掉一个或者多检一个,后面所有统计特征都会歪。
我一般用scipy.signal.find_peaks配合两个约束:最小峰间距和最小峰高。最小峰间距按生理上限设,成年人静息心率一般不超过 120 次/分,对应周期 0.5 秒,所以distance至少设成0.4 * fs。最小峰高用信号自身的中位数加一个系数,避免把噪声尖峰当峰。
from scipy.signal import find_peaks def detect_peaks(filtered_signal, fs=200): # 最小间距 0.4 秒,对应最高 150 次/分 min_distance = int(0.4 * fs) # 峰高阈值取中位数 + 0.5 倍标准差,可按数据调整 height_threshold = np.median(filtered_signal) + 0.5 * np.std(filtered_signal) peaks, props = find_peaks( filtered_signal, distance=min_distance, height=height_threshold ) return peaks, propsheight_threshold这个系数是经验值,数据质量差就调低,噪声大就调高。检测完一定要画图看一眼,把原始波形和峰值标记叠在一起,漏检和多检肉眼能看出来。这一步偷懒,后面特征全废。
2.3 周期归一化与特征提取的衔接
拿到峰值位置后,把每个周期切成等长片段,再做幅值归一化。归一化的目的是去掉不同人、不同接触压力带来的绝对幅值差异,让模型关注波形形状而不是绝对大小。常见做法是每个周期除以该周期的最大值,或者除以整段信号的中位数。
切分时要注意边界:第一个峰之前和最后一个峰之后的片段直接丢掉,不要补零,补零会引入虚假的低频成分。每个周期长度可以统一重采样到固定点数,比如 200 点,这样后续做统计特征或者送进卷积网络都方便。
3. 脉象特征工程:时域、频域和时频域怎么选
3.1 时域特征:最直观也最容易被误用
时域特征包括主波幅值、上升支时间、下降支时间、重搏波相对高度、周期均值与标准差等。这些指标和中医脉象描述有对应关系,比如弦脉主波陡、重搏波不明显,滑脉波形圆滑、重搏波位置靠前。但直接用手工时域特征有个问题:不同采集设备、不同按压力度会让同一脉象的绝对数值差很多。
我一般会做比值化处理,比如重搏波高度除以主波高度,上升支时间除以整个周期时长。这样特征对幅值和时间尺度都不敏感,跨设备迁移会好一些。下面是一个周期内提取几个核心时域特征的函数。
def extract_time_features(cycle, fs=200): """ cycle: 单个周期的归一化波形,长度固定 返回:主波位置、上升时间、重搏波相对高度等 """ main_peak = np.argmax(cycle) # 上升支时间:从周期起点到主波峰 rise_time = main_peak / fs # 下降支找局部极大值作为重搏波候选 from scipy.signal import find_peaks after_main = cycle[main_peak:] dicrotic_candidates, _ = find_peaks(after_main) if len(dicrotic_candidates) > 0: dicrotic_idx = main_peak + dicrotic_candidates[0] dicrotic_ratio = cycle[dicrotic_idx] / cycle[main_peak] else: dicrotic_ratio = 0.0 return { 'rise_time': rise_time, 'dicrotic_ratio': dicrotic_ratio, 'main_amplitude': cycle[main_peak] }dicrotic_candidates[0]取的是主波之后第一个局部极大值,但实际重搏波可能不是第一个,如果波形里有噪声小峰,这里会取错。稳妥做法是限定搜索范围,比如主波后 0.15~0.4 秒之间找最大值。rise_time依赖周期起点对齐,如果切分时起点没对准,这个值会漂。
3.2 频域特征:功率谱能补时域看不到的信息
脉象波形的频域能量分布和血管弹性、外周阻力有一定关联。常见做法是对每个周期做 FFT,取 0~10 Hz 的功率谱,然后算几个频段的能量比,比如 1~3 Hz 和 3~6 Hz 的比值。频域特征对周期长度变化不敏感,但要求信号平稳,如果呼吸调制明显,谱会糊掉。
实际用的时候我不会只靠频域,而是把时域和频域特征拼成一个向量送进分类器。特征维度控制在 20~40 之间,太多容易过拟合,尤其当你样本量只有几百条的时候。
3.3 时频域:小波变换在脉象里的实际用法
时频域方法里,连续小波变换用得比较多,因为它能同时给出时间和频率信息,适合看脉象波形里各个成分什么时候出现。但小波的结果是一个二维矩阵,直接送分类器维度太高,通常要做统计聚合,比如每个尺度上的能量均值、方差、最大值。
我一般用 Morlet 小波,尺度范围覆盖 0.5~10 Hz 对应的尺度。小波变换的计算量比 FFT 大,如果做实时系统要评估一下延迟。离线分析无所谓,Python 的pywt库够用。
import pywt def wavelet_features(cycle, fs=200): scales = np.arange(1, 64) coeffs, freqs = pywt.cwt(cycle, scales, 'morl', sampling_period=1/fs) # 每个尺度取能量均值,压成一维 energy_per_scale = np.mean(np.abs(coeffs) ** 2, axis=1) return energy_per_scalescales的上限 64 对应最低频率大约fs / (2 * 64),200 Hz 采样下约 1.5 Hz,如果你关心更低频成分,尺度要加大。morl小波没有尺度函数,重构不方便,但做特征提取没问题。
4. 分类模型与训练流程:从传统机器学习到轻量卷积网络
4.1 样本量决定你该选 SVM 还是 CNN
脉象识别公开数据集很少,自己采的话,一个受试者能贡献几十到几百个周期,但标注是个大问题。中医脉象标注依赖医师经验,不同医师之间一致性有限。所以实际项目里,样本量往往在几百到几千条周期级别。
这个量级下,我优先用 SVM 或者随机森林配合手工特征,因为小样本下深度模型很容易过拟合。如果样本能到上万条,再考虑一维卷积网络直接吃原始周期波形。源码里如果直接上 CNN 而没提数据量,大概率是演示性质,别指望直接用到自己的数据上。
4.2 用 sklearn 搭一个可复现的基线分类器
下面是一个完整的基线流程:读特征、划分训练测试、标准化、训练 SVM、输出分类报告。特征矩阵的每一行是一个周期,每一列是一个特征。
import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.metrics import classification_report # X: shape (n_cycles, n_features) # y: shape (n_cycles,),标签为脉象类别整数 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) clf = SVC(kernel='rbf', C=1.0, gamma='scale', class_weight='balanced') clf.fit(X_train_scaled, y_train) y_pred = clf.predict(X_test_scaled) print(classification_report(y_test, y_pred))stratify=y保证训练测试集类别比例一致,小样本下很重要。class_weight='balanced'处理类别不均衡,脉象数据里某些类别可能只有几十条。C和gamma用网格搜索调,但别在测试集上调,要单独划验证集。标准化必须用训练集的均值和方差去变换测试集,否则数据泄漏。
4.3 一维卷积网络的输入组织与训练注意点
如果要用 CNN,输入通常是(n_cycles, length, 1),length 是归一化后的周期点数。网络结构不用太深,两到三层卷积加全局平均池化就够,参数量控制在几万以内。训练时用早停和 Dropout,验证集损失不降就停。
import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(input_length, n_classes): model = models.Sequential([ layers.Input(shape=(input_length, 1)), layers.Conv1D(16, 5, activation='relu', padding='same'), layers.MaxPooling1D(2), layers.Conv1D(32, 5, activation='relu', padding='same'), layers.MaxPooling1D(2), layers.GlobalAveragePooling1D(), layers.Dropout(0.3), layers.Dense(n_classes, activation='softmax') ]) model.compile( optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) return modelGlobalAveragePooling1D替代全连接层能大幅减少参数,小样本下更稳。Dropout(0.3)是经验值,数据越少可以适当加大。训练时batch_size不要太大,32 或 64 即可,太大梯度更新次数少,小样本下收敛慢。
5. 避坑与排查:脉象识别系统最常见的五个翻车点
5.1 现象:模型在训练集上准确率 99%,测试集只有 40%
原因:数据泄漏。同一个受试者的周期被同时分到训练集和测试集,模型记住了这个人的波形特征而不是脉象类别。解决:按受试者划分数据集,同一个人要么全在训练集,要么全在测试集。如果受试者数量太少,考虑留一受试者交叉验证。
5.2 现象:换一台采集设备后,所有特征分布都变了
原因:不同设备的采样率、滤波器、接触压力标定不一样,绝对幅值和频率响应有差异。解决:做设备间的归一化,比如用同一段静息信号做幅值基准,或者用对抗域适应方法。最省事的做法是每台设备单独训练一个标准化参数。
5.3 现象:峰值检测在部分样本上漏检,导致周期数对不上
原因:信号幅值整体偏低或者噪声太大,固定阈值失效。解决:改用自适应阈值,比如滑动窗口内的中位数加倍数,或者先做包络提取再检测。检测完加一个人工复核环节,把异常周期标出来剔除。
5.4 现象:小波特征维度太高,训练极慢且过拟合
原因:直接把小波系数矩阵展平送分类器,维度上千。解决:对每个尺度做统计聚合,只保留能量均值、方差等少数指标,把维度压到几十。或者用主成分分析降维,但主成分要在训练集上拟合。
5.5 现象:实时推理延迟高,界面卡顿
原因:滤波用了filtfilt,它需要整段信号,实时场景下每次都要重算历史数据。解决:实时链路改用lfilter或者状态空间滤波,维护滤波器状态。小波变换也换成滑动窗口版本,不要每次从头算。
6. 把源码跑成自己的系统:替换数据与验证的实操技巧
拿到一份脉象识别源码,最容易犯的错是直接跑 demo 然后觉得「不过如此」。真正有价值的是把数据替换成你自己的,然后看哪些环节会崩。我一般会先做一个最小验证:用自己采集的 50 个周期,跑通预处理到特征提取,把特征画出来看分布。如果特征在类别之间有重叠,先别急着换模型,回去检查滤波和峰值检测。
验证分类器是否靠谱,不要只看准确率。脉象类别不均衡时,看混淆矩阵和每类的召回率。如果某个类别召回率极低,要么是样本太少,要么是特征对这个类别不敏感。这时候可以单独看这个类别的波形,和医师标注对照,确认是标注问题还是特征问题。
还有一个实用技巧:把模型输出的概率值留下来,做阈值调整。比如系统输出「弦脉 0.6,滑脉 0.3」,你可以设一个置信度阈值,低于阈值就输出「不确定」,而不是硬分到某一类。这在医疗相关场景里比强行分类更负责任。
import numpy as np def predict_with_rejection(model, X_scaled, threshold=0.5): probs = model.predict_proba(X_scaled) max_probs = np.max(probs, axis=1) preds = np.argmax(probs, axis=1) # 低于阈值的样本标记为 -1,表示不确定 preds[max_probs < threshold] = -1 return preds, max_probsthreshold根据你的应用场景调,宁可拒识也不要错分。这个机制在源码里经常被忽略,但实际部署时非常有用。
我自己做这类项目的习惯是:每换一次数据,先把预处理链路单独跑一遍,把中间结果存成文件,确认每一步的输出符合预期,再往下走。这样出问题的时候能快速定位是滤波、切分还是模型的问题,而不是对着一个端到端脚本干瞪眼。脉象识别系统源码的价值,最终取决于你能不能把它拆开、替换、再组装成适合自己数据和场景的版本。希望帮到你。
本文还有配套的精品资源,点击获取