
简介一份基于Python语言的中医脉象识别系统源码包面向医疗健康领域开发者、中医信息化研究人员及高校相关专业学生。系统围绕脉象信号的数字化识别流程展开涵盖数据采集、去噪滤波、特征提取、模型训练与测试输出等环节并采用卷积神经网络CNN、循环神经网络RNN等深度学习方法进行建模与调优实现对不同脉象类型的自动分类有助于理解智能医疗诊断系统的完整落地路径。压缩包共61个文件主体为47个Python脚本负责系统主逻辑、API接口、服务启动及工具封装另含8个CSV数据文件用于样本存储、1个H5模型权重文件用于已训练模型加载以及少量Markdown/TXT说明文档包体仅1.24MB结构轻量、易于部署与二次开发。当前已有158人学习下载适合作为中医数字化课题的参考实现或作为深度学习信号处理项目的入门样例。资源目录划分清晰可直接运行核心模块便于读者按需阅读数据预处理、特征工程和模型推理等关键代码。1. 脉象识别系统从零搭难在信号不漂亮很多刚接触这个题目的人以为脉象识别是深度学习问题导入TensorFlow就能端到端出结果。真正跑过一个周期会发现90%的精力不在模型而在把一段带着呼吸干扰、基线漂移、皮肤接触抖动的波形整理成机器能理解的特征。中医脉象有浮、沉、迟、数、弦、滑等分类落到传感器上其实是压力波或光电容积波采样率不高有用信息集中在前十阶谐波里。Python的优势在于科学计算生态完整从scipy滤波到sklearn分类一行行可验证比纯C原型的迭代快得多。这篇会按“数据组织→预处理→特征提取→训练→封装服务”的顺序把一套可直接复用的源码结构和参数讲清楚适合有Python基础、想自己跑通一套脉象识别流程的工程师。2. 脉象信号采集与数据组织先定义你的正样本和负样本2.1 传感器选型与采样率设定常见脉象采集设备有三类压阻式腕带、压电薄膜、光电容积脉搏波PPG。医用标准多采用压阻式因为它能反映脉位深浅但压阻信号易受温度漂移影响。PPG传感器如MAX30102成本低在智能手环里大量使用适合做初版验证。这段流程与传感器型号无关核心是确认采样率与位深传感器类型典型采样率优点主要干扰压阻式500 Hz压力波形真实基线漂移、接触压力不稳压电薄膜1 kHz灵敏度高工频干扰、震动噪声PPGMAX30102100 Hz低成本、易集成环境光、运动伪差我一般会把采样率固定在500 Hz以上因为脉象识别需要保留重搏波切迹。这是判定弦滑脉的重要形态点对应频率大约在515 Hz按奈奎斯特定理只要30 Hz就够但过零检测和峰值定位需要更高时间分辨率。数据保存时统一转成单精度浮点省一半存储空间。2.2 数据目录结构与标注规范脉象数据如果不做一致化组织后面预处理脚本要反复改路径。推荐用以下目录结构dataset/ person_01/ girdle_file_01.csv pg_file_02.csv person_02/ labels.csvlabels.csv里每行列一条样本的元信息filename,label,age,gender,sample_rate person_01/girdle_file_01.csv,float,34,M,500 person_01/pg_file_02.csv,wiry,34,M,500标注字段建议用英文标识避免中文路径在旧版Python库里的编码问题。如果有医生标注也要保留原始诊断文本后续可以映射成多标签。这里有一个常见坑一个文件里可能包含多个脉搏周期直接整条文件标一个标签会让模型学到呼吸节奏。因此要按单周期或固定时间窗切分每个切分片段单独标注我习惯切成5秒窗口重叠2秒确保每个窗至少包含4个完整周期。2.3 用Python写数据加载器这段代码不依赖深度学习框架只做数据切分和基础校验保证后续训练脚本拿到的是干净数组。import numpy as np import pandas as pd from pathlib import Path def load_pulse_file(filepath, sample_rate): 读取单通道脉象信号返回均一化的时间序列 df pd.read_csv(filepath, headerNone) signal df.iloc[:, 0].to_numpy(dtypenp.float64) # 去掉采样率异常造成的跳变点 signal np.nan_to_num(signal, nan0.0) return signal def make_windows(signal, window_sec5.0, overlap_sec2.0, sample_rate500): 滑窗生成训练样本返回窗口列表和起始索引 window_len int(window_sec * sample_rate) step int((window_sec - overlap_sec) * sample_rate) if step 0: raise ValueError(overlap_sec 必须小于 window_sec) windows [signal[i:i window_len] for i in range(0, len(signal) - window_len 1, step)] return windows逻辑说明make_windows的第一个参数signal是经过pd.read_csv读出的波形滑窗步长由window_sec与overlap_sec的差决定。重叠的作用是为了让相邻窗口在模型预测时更平滑但训练时如果样本高度重叠要注意划分验证集时按时间顺序切分不能随机乱分否则会数据泄漏。np.nan_to_num用来处理传感器偶尔输出的NaN因为后续滤波函数对NaN非常敏感。3. 脉象信号预处理去噪、基线漂移与周期切分的Python实现3.1 用scipy设计带通滤波器原始脉象信号里最影响分类能力的是基线漂移和高频毛刺。基线漂移由呼吸运动和手部微动引起频率多在0.5 Hz以下高频毛刺来自肌肉震颤频率在40 Hz以上。脉搏有效分量一般在0.820 Hz之间所以用Butterworth带通滤波可以同时压掉两端噪声。from scipy.signal import butter, filtfilt, find_peaks def bandpass_filter(signal, fs, low0.8, high20.0, order4): 零相位带通滤波避免相位偏移影响峰值定位 nyquist 0.5 * fs low_n low / nyquist high_n high / nyquist if not (0 low_n high_n 1): raise ValueError(归一化频率超界请检查 fs/low/high) b, a butter(order, [low_n, high_n], btypeband) # filtfilt 对数据做前后向滤波零相位 return filtfilt(b, a, signal)注意filtfilt比lfilter更适合脉象识别因为相位偏移会改变重搏波切迹的位置导致特征点定位不准。但filtfilt在信号首尾有暂态效应我的做法是滤波前截掉前500个点避免传感器刚开始接触时的压力突跳。通带下限设为0.8 Hz并不是绝对如果你用的是100 Hz采样率的PPG低频漂移更重可以提高到1.2 Hz但要小心切掉一部分真实慢波信息。3.2 基于峰值检测切割单个脉搏周期周期切分是脉象识别里最容易出错的一步。常规方法是找R波一样的波峰但脉象波形的主峰后往往跟着一个重搏波单纯找峰值会把重搏波误当成新周期。更稳的做法是先找主波峰的包络再设置最小间距。def detect_pulse_peaks(signal, fs, min_distance0.4): 检测主波峰min_distance 为两个相邻主峰的最小秒数 dist_samples int(min_distance * fs) peaks, props find_peaks(signal, distancedist_samples, prominence0.1) return peaks, props参数说明distance对应相邻主峰的最小采样点数人体静息心率一般在60100次/分周期约0.61.0秒所以0.4秒是一个比较安全的下界。prominence是峰突出度用来抑制小重搏波值等于信号归一化幅度的0.1倍。如果信号幅度已经标准化到010.1即可如果传感器增益不同这个值要按每个文件重新计算不能全局写死。3.3 归一化与数据增强预处理后每个脉象波形的幅度可能差很多为了特征可比我采用逐周期幅度归一化而不是全局归一化。逐周期归一化会破坏脉象“浮沉”的绝对压力信息所以如果要识别浮脉/沉脉需保留原始幅度特征如果只识别弦滑迟数可以只做周期内部归一化。def normalize_cycle(cycle): 按周期内部最大最小归一化保留形态特征 peak_to_peak np.ptp(cycle) if peak_to_peak 1e-8: return np.zeros_like(cycle) return (cycle - np.min(cycle)) / peak_to_peak数据增强部分我一般只做两种幅值缩放0.9~1.1倍和时域拉伸±5%重采样。不要做加性白噪声因为脉象分类对噪声敏感加了噪声会让模型误把噪声当成真实波动。增强后的样本要放到同一个时间索引里避免在验证集出现同一周期的变形体。4. 特征提取与模型训练脉象识别分类器的参数调优4.1 时域、频域与形态学特征参数表模型选型前先确定特征集合。脉象识别常用的特征可以分成三类我用一个表把参数含义和计算方式列清楚特征类别特征名计算公式或来源脉象含义时域主波幅值max(signal)浮沉趋势时域上升支斜率主峰前10%到90%时间段的平均斜率动脉硬化程度时域重搏波切迹相对深度切迹幅值 / 主波幅值血管弹性弦脉常较浅频域频谱能量占比FFT后0.820Hz能量/总能量信号质量指标形态周期长度变异性std(periods) / mean(periods)迟数、结代脉形态波形面积比周期曲线下面积 / 周期幅值滑脉与弦脉区分重搏波切迹检测是特征工程的难点我通常用二阶导数过零法对滤波后的信号求二阶导数主峰之后的负向最低点就是切迹。这个方法对噪声敏感所以特征提取前要再做一次平滑滤波。4.2 随机森林与基线模型实现深度学习框架在脉象小样本场景下容易过拟合一个中等规模数据集也就几千条记录随机森林或梯度提升树往往效果更好。下面是一个用sklearn训练随机森林的最小闭环from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import StratifiedKFold from sklearn.metrics import classification_report class PulseClassifier: def __init__(self, n_estimators300, max_depth8): self.clf RandomForestClassifier( n_estimatorsn_estimators, max_depthmax_depth, min_samples_leaf3, n_jobs-1, random_state42 ) def fit_eval(self, X, y): skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for train_idx, val_idx in skf.split(X, y): self.clf.fit(X[train_idx], y[train_idx]) pred self.clf.predict(X[val_idx]) print(classification_report(y[val_idx], pred, digits3))参数说明min_samples_leaf控制叶子节点最少样本数脉象数据类间不平衡常见设置3~5可以减小噪声分支。max_depth不需要太大脉象特征维度通常在20~50之间深度8能捕获交互信息又不会完全记住样本。n_jobs-1在多核机器上并行训练但注意特征集不大时树数300棵和100棵差距很小主要看验证标准差。4.3 混淆矩阵与识别率解读训练后只看准确率不够尤其是弦脉和滑脉容易互相误判。要重点看以下两类迟脉和数脉是按心率区分若采样率不统一分类器会被采样率特征带偏弦脉和紧脉波形相似仅靠幅值特征无法区分需要加入谐波相位。实战中我会打印每个类别的精确率和召回率并检查分类错误的样本是不是同一个被试者如果是说明跨个体泛化没做好需要按人划分训练集而不是按窗口划分。import matplotlib.pyplot as plt def show_confusion(cm, labels): fig, ax plt.subplots(figsize(6, 5)) im ax.imshow(cm, cmapBlues) ax.set_xticks(range(len(labels))) ax.set_yticks(range(len(labels))) ax.set_xticklabels(labels, rotation45) ax.set_yticklabels(labels) for i in range(cm.shape[0]): for j in range(cm.shape[1]): ax.text(j, i, cm[i, j], hacenter, vacenter) plt.colorbar(im) plt.savefig(confusion_matrix.png, dpi150)这段代码把混淆矩阵存成图片方便后续和医生标注逐条核对。如果矩阵显示弦脉大量误判为滑脉先回头检查重搏波切迹特征是否提取正确而不是急着换模型结构。5. 把模型封装成可调用的脉象识别服务模型训练完下一步是让队友能直接传一段波形文件得到分类结果。常见做法是把预处理和模型权重打包成一个Python类提供离线推理接口。5.1 用Joblib保存模型并提供预测接口import joblib class PulsePredictor: def __init__(self, model_path, feature_extractor): self.model joblib.load(model_path) self.feature_extractor feature_extractor def predict_wave(self, signal, fs): 输入原始波形输出标签与概率 filtered bandpass_filter(signal, fs) features self.feature_extractor.extract(filtered, fs) proba self.model.predict_proba([features])[0] label self.model.classes_[np.argmax(proba)] return label, dict(zip(self.model.classes_, proba))逻辑说明joblib.dump保存随机森林时会把Cython编译的底层结构一并序列化加载速度比pickle快。predict_proba返回的数组顺序与classes_对应用字典形式输出方便前端展示。启动服务时一次性加载模型不要每个请求重复加载否则I/O开销会拖垮推理时间。5.2 滑动窗口实时预测技巧如果要接实时数据流不能等整段采集完再分类。我会用双缓冲队列一边采集一边丢入长度为5秒的窗口每2秒做一次预测相邻窗口重叠。预测时只取队列末尾的5秒数据避免窗口边界效应。这里有个关键点滤波函数每次都要重新计算整个窗口不能对队列增量滤波否则相位响应会变化。5.3 参数自检清单把模型交付出去前可以按照下面的清单快速验证一遍检查项期望表现失败时的排查方向主波峰值检出率每5秒窗口检出4~7个峰检查滤波器通带下限是否过高类别概率和每样本概率和1检查模型是否被重复加载或篡改同段信号重复预测两次结果一致特征提取器是否依赖随机数换一个人的信号准确率下降但不过半确认训练集是否按人划分验证集最后再提一个我自己踩过的坑预处理里的滤波器参数和训练时不一致通常是因为上线代码里改了一个采样率常量。解决办法是把滤波器参数和模型一起打包进配置文件中每次加载都打印一份指纹确保训练和部署用的是同一套信号处理链路。本文还有配套的精品资源点击获取