
简介基于Python开发的脉象识别系统毕业设计项目面向需要完成毕业设计、课程设计或期末大作业的计算机及相关专业学生尤其适合中医信息化、机器学习与Web开发方向。系统采用Django框架搭建后端Web应用集成训练好的深度学习模型model.h5可对脉象数据进行预处理、特征提取与分类识别功能覆盖数据管理、用户认证、权限控制、模型推理与API接口等完整环节。代码中配有详细注释并采用模块化结构划分app_main、app_common、app_data、utils、middleware等目录包含模型定义、视图逻辑、序列化器、URL路由、后台管理、登录测试等规范实现新手也能快速理解项目架构与业务逻辑。压缩包共61个文件以47个Python源码文件为主辅以8个CSV脉象数据文件、3个Markdown说明文档及H5模型文件整体仅1.26MB轻量精炼。目前已有201人学习参考项目经过严格调试确保可运行并附带部署教程、项目说明与数据库脚本读者可快速搭建环境掌握从数据处理到模型预测的完整实现是一份高分通过的毕业设计参考方案。1. 为什么脉象识别系统非 Python 不可拿到一份基于 Python 开发的脉象识别系统源码先别急着解压跑 demo。脉象识别本质上是一条从传感器到诊断结论的信号处理链路采集桡动脉搏动波形做滤波去噪切分单个脉搏周期再提取能区分浮、沉、迟、数、弦、滑等脉象的特征最后交给分类器输出结果。这条链路里的每个环节都有成熟的 Python 库可用NumPy 处理数组、SciPy 做滤波、PyWavelets 做小波分解、scikit-learn 训传统分类器、TensorFlow/PyTorch 上深度学习模型串起来比 C 少写一半代码。对毕业设计而言Python 最大的价值不是性能而是把从波形到脉象标签的完整逻辑压缩在几千行内你能在答辩前把每个环节都调通。但这套源码绝不是装个库就能跑的玩具。脉象识别的难点集中在三处一是信号质量差压阻式或光电式传感器的输出里混着基线漂移和 50Hz 工频干扰二是脉搏周期不规整同一个人的 60 秒数据里每个单波的起点和时长都在变三是脉象标签本身有主观性同一个波形老中医和算法标注可能不一致。下面的内容按实际开发顺序展开每一章对应源码里的一层从数据预处理到模型调参最后给出验证和排错的具体方法。2. 信号采集与预处理先把波形洗干净再谈识别2.1 脉象数据从哪来传感器选型与采样率设置脉象识别系统首先要解决数据来源。市面上常见的方案有三种压阻式脉搏传感器如 HK-2000A/B 系列通过压力变化反映桡动脉搏动光电式容积脉搏波PPG传感器用红外光检测血流变化还有直接用医疗级脉象采集仪导出 CSV 的离线方案。毕业设计里用最多的是压阻式因为它输出的是模拟电压信号接一个 ADC 模块如 USB-6009 或 STM32 采集板就能进计算机。源码里通常封装了一个数据读取类负责从串口或 USB 读取原始电压值。采样率是第一个需要确认的参数。一般脉象信号的主要能量集中在 0.5Hz 到 20Hz 之间按照奈奎斯特定理采样率至少 40Hz 才能保留完整信息但实际工程里建议 500Hz 到 1000Hz。原因有两个一是过低的采样率会让脉搏波的上升沿变钝特征点主波峰、重搏波谷、重搏波峰定位不准确二是后续做小波分解时高频细节系数需要足够的采样点数支撑。源码里常见的配置是# 采集参数配置 SAMPLE_RATE 1000 # 采样率 1000Hz即每秒 1000 个点 CHANNEL_COUNT 1 # 单通道采集 ADC_RESOLUTION 12 # ADC 分辨率 12 位 VREF 3.3 # 参考电压 3.3V BUFFER_SECONDS 10 # 每次采集 10 秒数据 # 读取原始数据以串口为例 import serial import numpy as np ser serial.Serial(COM3, baudrate115200, timeout2) raw_data [] for _ in range(SAMPLE_RATE * BUFFER_SECONDS): line ser.readline().decode(utf-8).strip() if line: raw_data.append(float(line)) ser.close() signal np.array(raw_data)这段代码做了三件事配置采样参数、从串口逐行读取电压值、把字符串列表转成 NumPy 数组。SAMPLE_RATE 1000是关键参数如果采集设备实际输出频率是 500Hz这里必须改成 500否则后面所有的时间轴计算如脉率都会偏大一倍。BUFFER_SECONDS 10表示每次分析 10 秒数据这个长度足够包含 10 到 20 个完整脉搏周期。2.2 去除基线漂移高通滤波与移动平均的取舍原始脉搏信号里最明显的干扰是基线漂移。呼吸运动、传感器按压松紧变化、肢体微小移动都会让信号的整体幅度产生缓慢的上下波动。这种漂移的频率通常在 0.5Hz 以下比脉搏基频约 1Hz 到 1.7Hz低得多因此用高通滤波器就能解决。源码里常见的做法是 butterworth 高通滤波截止频率设在 0.5Hz。看下面的实现from scipy.signal import butter, filtfilt def highpass_filter(signal, cutoff0.5, fs1000, order4): 高通滤波去除基线漂移 cutoff: 截止频率 0.5Hz低于此频率的成分被滤掉 fs: 采样率 order: 滤波器阶数越高过渡带越陡但相位失真越明显 nyquist 0.5 * fs # 奈奎斯特频率 500Hz normal_cutoff cutoff / nyquist b, a butter(order, normal_cutoff, btypehigh, analogFalse) filtered filtfilt(b, a, signal) # 零相位滤波避免偏移 return filtered clean_signal highpass_filter(signal, cutoff0.5, fsSAMPLE_RATE)为什么用filtfilt而不是lfilter因为lfilter是因果滤波会引入相位延迟导致脉搏波的波峰位置发生偏移后续检测收缩期起点时误差会变大。filtfilt对信号做正向和反向两次滤波相位延迟相互抵消输出波形和原始波形在时间轴上对齐。代价是计算量翻倍但 10 秒的数据量完全不成问题。order4是经验值阶数越高对 0.5Hz 以下成分的衰减越彻底但过高会让滤波器在截止频率附近产生振铃效应反而在波形上叠加人工振荡。2.2.1 移动平均法的适用场景有些源码不用高通滤波而是先算信号的移动平均作为漂移估计再从原始信号里减去def remove_baseline_with_moving_average(signal, window_size500): 移动平均窗口 500ms估计基线漂移并去除 window_size: 窗口大小采样点数500ms 对应 500 个点1000Hz 采样率 kernel np.ones(window_size) / window_size baseline np.convolve(signal, kernel, modesame) return signal - baseline这种做法的优点是直观缺点是窗口大小不好设窗口太小会把低频的脉搏成分一起当基线消掉窗口太大则对缓慢漂移的反应迟钝。实际对比下来butterworth 高通滤波更稳定建议优先使用。如果拿到的源码里用的是移动平均注意看window_size是否和采样率匹配——很多改版代码在这里栽跟头。2.3 工频干扰与高频噪声带阻滤波和小波去噪的组合50Hz 工频干扰是另一个常见问题。电源线和采集设备之间通过电磁耦合引入的噪声在频谱上表现为 50Hz 及谐波处的尖峰。虽然脉搏信号主要成分低于 20Hz但 50Hz 干扰会掩盖重搏波切迹等细节特征。处理方法是陷波器带阻滤波from scipy.signal import iirnotch, filtfilt def notch_filter(signal, notch_freq50, fs1000, quality_factor30): 50Hz 陷波滤波去除工频干扰 quality_factor: 品质因数越大带宽越窄对邻近频率影响越小 nyquist 0.5 * fs w0 notch_freq / nyquist # 归一化中心频率 b, a iirnotch(w0, quality_factor) filtered filtfilt(b, a, signal) return filtered signal_after_notch notch_filter(clean_signal, notch_freq50, fsSAMPLE_RATE)quality_factor30表示带宽约为 50/30 ≈ 1.67Hz在 49.2Hz 到 50.8Hz 之间有明显衰减而对 20Hz 以下的脉搏信号基本无影响。如果采集环境里工频干扰特别严重可以在陷波后再叠加一次低通滤波截止频率设在 30Hz 或 40Hz把高频噪声一并去掉。注意低通截止不能低于 30Hz否则重搏波切迹通常在 30Hz 以内但有陡峭的转折会被磨平。小波去噪是更精细的第二步。它对信号做多尺度分解把高频细节系数里的噪声成分置零或收缩再重构信号。对于脉搏信号这种非平稳信号小波比傅里叶滤波更能保留时域突变特征import pywt def wavelet_denoise(signal, waveletdb4, level4): 小波去噪保留脉搏波细节 wavelet: 小波基db4 是 Daubechies 4 阶常用于生物医学信号 level: 分解层数4 层对应把 0-500Hz 频带分成 4 段 coeffs pywt.wavedec(signal, wavelet, levellevel) # 估算噪声标准差使用第一层细节系数 sigma np.median(np.abs(coeffs[-level])) / 0.6745 threshold sigma * np.sqrt(2 * np.log(len(signal))) # 对细节系数做软阈值处理 coeffs_thresh list(coeffs) for i in range(1, level 1): coeffs_thresh[i] pywt.threshold(coeffs[i], threshold, modesoft) # 重构信号 denoised pywt.waverec(coeffs_thresh, wavelet) return denoised[:len(signal)]小波去噪的效果比单纯低通滤波好但level不能设太大。level4在 1000Hz 采样率下近似系数对应的频带是 0 到 31.25Hz正好覆盖脉搏信号的主要能量范围细节系数带里的噪声被阈值收缩。如果level6近似频带变成 0 到 7.8Hz反而会把 8Hz 以上的真实细节如重搏波切迹处的转折当作噪声滤掉。db4小波基是生物电信号处理的标准选择比haar光滑比sym8计算量小。2.3.1 预处理顺序的验证方法滤波顺序不能乱。正确顺序是先高通去基线漂移再陷波去工频最后小波去噪。每一步做完都画图确认把原始信号和滤波后信号叠在一张图里检查基线是否在零附近、波峰是否明显、重搏波切迹是否可见。如果直接跳到特征提取等模型准确率上不去时才回头查信号质量排查成本会翻几倍。3. 脉搏周期切分与单波对齐识别精度的分水岭3.1 为什么周期切分比特征提取更影响准确率脉象特征如脉率、脉压、主波幅值都是基于单个脉搏周期计算的。如果周期的起点找不准后面的特征全部失真。最常见的错误是用固定长度滑窗切分比如每 0.8 秒切一段但人的心率不可能恒定呼吸、情绪都会导致周期波动。正确做法是先找到每个脉搏波的起点即收缩期上升沿的起始点再按起点把信号切成独立的心动周期。周期起点检测的算法很多源码里常见的是基于自适应阈值的峰值检测先找到所有局部极大值作为候选主波峰再根据主波峰位置向前回溯找最低点作为周期起点。看这个实现from scipy.signal import find_peaks def detect_cycle_start(signal, fs1000): 检测每个脉搏周期的起点 思路先找主波峰再在主波峰前找局部最小值作为起点 # 1. 找主波峰局部极大值 # height 参数动态设置取信号最大值的 60% 作为最小峰值高度 peak_height 0.6 * np.max(signal) # distance 限制相邻峰值最小间距防止把重搏波峰误检为主波 min_distance int(0.4 * fs) # 400ms对应 150 次/分钟的最高心率 peaks, properties find_peaks(signal, heightpeak_height, distancemin_distance) # 2. 在每个主波峰前找起点局部最小值 starts [] for peak_idx in peaks: # 搜索窗口主波峰前 300ms 到峰值的这段区域 search_start max(0, peak_idx - int(0.3 * fs)) segment signal[search_start:peak_idx] if len(segment) 0: continue local_min search_start np.argmin(segment) starts.append(local_min) return np.array(starts), peaks starts, peaks detect_cycle_start(signal_after_denoise, fsSAMPLE_RATE)find_peaks是 SciPy 的信号峰值检测函数关键参数有三个。height设为信号最大值的 60%避免把重搏波峰幅度通常为主波的 30% 到 50%误检为主波。distance0.4*fs即 400ms这个值根据最高可接受心率计算150 次/分钟对应周期 400ms如果两个峰间距小于 400ms说明其中一个是重搏波峰而不是主波。最后一步的主波峰前 300ms 内找局部最小值利用了生理学先验收缩期开始前有一段舒张期末的低压期是真正的周期分界点。3.2 切波后的对齐与统一长度检测出周期起点后每个周期的长度不一样因为心率波动但分类器的输入通常是固定维度的特征向量这就需要把每个周期对齐到统一长度。两种常见方案第一种是重采样到固定长度。把所有周期重采样到 200 个点这样既保留了波形的形态信息又统一了输入维度。第二种是先提取特征再用特征向量输入模型不需要对齐原始波形。源码里通常是两套流程都保留特征提取部分对齐是硬性的from scipy.signal import resample def align_cycles(signal, starts, target_length200): 把每个脉搏周期重采样到统一长度 starts: 周期起点索引数组 target_length: 统一后的采样点数200 点足以保留波形形态 cycles [] for i in range(len(starts) - 1): seg signal[starts[i]:starts[i1]] if len(seg) 50: # 小于 50ms 的段太短判定为噪声跳过 continue # 重采样到固定长度 aligned resample(seg, target_length) # 归一化减去均值再除以标准差消除个体差异 aligned (aligned - np.mean(aligned)) / np.std(aligned) cycles.append(aligned) return np.array(cycles) aligned_cycles align_cycles(signal_after_denoise, starts, target_length200)resample用 FFT 方法重采样会产生轻微的振铃效应Gibbs 现象但对脉搏波的影响很小。target_length200是经验值太长会放大重采样引入的插值误差太短会丢失波形细节。归一化这一步容易被忽略但它对模型泛化至关重要——不同人的脉搏波幅值差异很大不归一化的话分类器会倾向于按幅值分类而不是按形态分类。3.2.1 异常周期的剔除周期切分后不是直接进入特征提取要先做异常检测。呼吸性窦性心律不齐、传感器滑动、偶发早搏都会产生异常的周期段。源码里常见的剔除规则是周期长度在平均周期的 70% 到 130% 之外就丢弃峰值幅度小于平均幅度 50% 的丢弃起点到主波峰的时间异常收缩期时长超过周期 70%也丢弃。这一步能明显提升后续特征提取的稳定性。4. 特征提取与分类建模从波形到脉象标签的最后一公里4.1 时域特征脉率、脉压、主波幅值与重搏波参数特征提取是脉象识别的核心直接决定了分类模型的上限。时域特征直接从对齐后的周期波形中计算与中医脉象的位、数、形、势有对应关系。看下面的特征提取函数def extract_time_features(cycle): 从单个脉搏周期提取时域特征 cycle: 重采样后的单周期信号长度 200 features {} # 1. 主波峰位置和幅值 peak_idx np.argmax(cycle) features[main_wave_amp] cycle[peak_idx] # 主波幅值 # 2. 重搏波切迹主波峰后第一个局部最小值 after_peak cycle[peak_idx:] if len(after_peak) 5: notch_idx peak_idx np.argmin(after_peak[:int(len(after_peak)*0.6)]) features[dicrotic_notch_depth] cycle[notch_idx] # 切迹深度 # 3. 上升支斜率从起点到主波峰的平均斜率 if peak_idx 0: features[rising_slope] cycle[peak_idx] / peak_idx # 4. 周期长度对应的脉率次/分钟 features[pulse_rate] 60.0 * SAMPLE_RATE / len(cycle) # 5. 主波宽度半幅值宽度 half_max cycle[peak_idx] / 2 above_half np.where(cycle half_max)[0] if len(above_half) 0: features[main_wave_width] above_half[-1] - above_half[0] return features这些特征和中医脉象的对应关系需要理解清楚。脉率对应数脉和迟脉的判据——数脉脉率超过 90 次/分钟迟脉低于 60 次/分钟。主波幅值和脉压相关幅值高往往对应洪脉或实脉。上升支斜率反映脉势的来势——斜率陡峭对应紧脉、弦脉的紧张感。重搏波切迹深度是区分滑脉和弦脉的关键滑脉的重搏波明显且切迹位置较低弦脉的重搏波不明显甚至消失。4.2 频域与小波特征补充时域之外的判别信息时域特征不能覆盖全部脉象信息。频域特征通过 FFT 把脉搏波形分解为不同频率成分的叠加其中高频成分的占比反映血管弹性和外周阻力。小波特征则是多尺度的时频分析能捕捉到波形在局部时刻的突变模式。看下面的组合提取import numpy as np import pywt def extract_frequency_features(cycle, fs_resampled1000): 从单个周期提取频域和小波特征 cycle: 重采样后的单周期信号 features {} # FFT 频谱特征 fft_vals np.fft.rfft(cycle) fft_mag np.abs(fft_vals) freqs np.fft.rfftfreq(len(cycle), d1.0/fs_resampled) # 低频段0-5Hz能量占比 low_mask freqs 5 features[low_freq_ratio] np.sum(fft_mag[low_mask]**2) / np.sum(fft_mag**2) # 中频段5-10Hz能量占比 mid_mask (freqs 5) (freqs 10) features[mid_freq_ratio] np.sum(fft_mag[mid_mask]**2) / np.sum(fft_mag**2) # 频谱质心反映频谱能量集中区域的频率位置 features[spectral_centroid] np.sum(freqs * fft_mag) / np.sum(fft_mag) # 小波分解特征每层细节系数的能量 coeffs pywt.wavedec(cycle, db4, level3) for i, coeff in enumerate(coeffs[1:], start1): features[fwavelet_detail_{i}_energy] np.sum(coeff**2) / len(coeff) return features频域特征的意义在于补充时域特征对滑脉和涩脉的区分能力。滑脉的频谱能量更集中频谱质心偏低涩脉的频谱能量分散高频成分占比高。小波三层分解的细节系数能量则反映了不同频带上的信号活动量对血管顺应性变化敏感。4.3 分类模型对比KNN、SVM 与 CNN 的适用边界特征提取完成后选择分类模型。毕业设计最常见的选择是支持向量机SVM因为它对小样本、高维特征的表现好如果数据量大且波形形态特征明显一维 CNN 也值得尝试。看一个 SVM 分类的完整流程from sklearn.svm import SVC from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report def train_svm_classifier(feature_matrix, labels): 训练 SVM 分类器识别脉象 feature_matrix: (样本数, 特征维度) 的特征矩阵 labels: 脉象标签如 浮脉, 沉脉, 迟脉, 数脉 # 特征标准化SVM 对特征尺度敏感 scaler StandardScaler() X_scaled scaler.fit_transform(feature_matrix) # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split( X_scaled, labels, test_size0.3, random_state42, stratifylabels ) # 网格搜索找到最优 C 和 gamma param_grid { C: [0.1, 1, 10, 100], gamma: [0.01, 0.1, 1], kernel: [rbf] } grid_search GridSearchCV( SVC(), param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid_search.fit(X_train, y_train) print(f最优参数: {grid_search.best_params_}) print(f交叉验证准确率: {grid_search.best_score_:.4f}) print(classification_report(y_test, grid_search.predict(X_test))) return grid_search.best_estimator_, scaler best_svm, scaler train_svm_classifier(feature_matrix, labels)SVM 的C和gamma是关键参数。C是误分类惩罚系数越小越容忍误分类但泛化能力越强越大越容易过拟合。gamma控制 RBF 核的影响半径越小决策边界越平滑越大越容易在每个样本周围形成孤立区域。网格搜索在C[0.1,1,10,100]和gamma[0.01,0.1,1]的 12 组组合里找最优配合 5 折交叉验证能避免过拟合。4.3.1 一维 CNN 的输入与构造如果样本量超过 500一维 CNN 是比 SVM 更强的选项。它直接吃对齐后的波形不需要手工特征import tensorflow as tf from tensorflow.keras import layers, models def build_1d_cnn(input_length200, num_classes4): 一维 CNN 直接对脉搏波形分类 input_length: 对齐后的波长度 num_classes: 脉象类别数 model models.Sequential([ layers.Input(shape(input_length, 1)), layers.Conv1D(filters16, kernel_size7, activationrelu, paddingsame), layers.MaxPooling1D(pool_size2), layers.Conv1D(filters32, kernel_size5, activationrelu, paddingsame), layers.MaxPooling1D(pool_size2), layers.Conv1D(filters64, kernel_size3, activationrelu, paddingsame), layers.GlobalAveragePooling1D(), layers.Dense(32, activationrelu), layers.Dropout(0.5), # 防止过拟合 layers.Dense(num_classes, activationsoftmax) ]) model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] ) return modelkernel_size在第一层设为 7对应 7 个采样点约 35ms 的窗口能覆盖脉搏上升支的主要形态变化。Dropout(0.5)放在全连接层之前训练时随机丢弃一半神经元强制网络学习冗余特征。CNN 需要的数据量比 SVM 大样本不足时优先用数据增强——对波形做小幅度的幅度缩放0.9 到 1.1 倍和时间拉伸0.95 到 1.05 倍。5. 系统架构与源码组织让代码跑起来的关键路径5.1 从数据流看源码的模块划分拿到源码后先看目录结构不要直接运行主程序。一个规范的脉象识别系统源码通常按数据流组织成四个模块数据采集模块、信号处理模块、特征与模型模块、界面展示模块。典型的目录如下pulse_recognition/ ├── data/ │ ├── raw/ # 原始采集数据 CSV │ ├── processed/ # 预处理后的干净信号 │ └── features/ # 提取的特征文件 ├── src/ │ ├── acquisition/ # 串口/文件数据读取 │ ├── preprocessing/ # 滤波、去噪、切波 │ ├── features/ # 时域频域特征提取 │ ├── models/ # 分类器训练与保存 │ ├── visualization/ # 波形与频谱绘图 │ └── ui/ # 桌面界面 ├── models/ │ └── svm_model.pkl # 训练好的模型文件 ├── main.py # 入口文件 └── requirements.txt # 依赖库清单main.py是入口典型流程是先加载数据经过预处理和特征提取然后加载已训练好的模型做预测最后在界面上显示波形和脉象结果。源码里可能有两种运行模式一种是训练模式有标签数据训练模型并保存另一种是识别模式加载模型并预测新数据。先看main.py里if __name__ __main__之后的逻辑判断当前版本跑的是哪种模式。5.2 配置文件的参数对照表源码里的关键参数散落在不同模块里建议先整理成参数表再根据实际设备调整。下表列出了最常见的参数及其作用参数名位置默认值作用调整依据SAMPLE_RATE采集模块1000采样率根据采集设备实际输出频率CUTOFF_HIGH预处理0.5高通截止频率基线漂移严重时降为 0.3NOTCH_FREQ预处理50陷波频率美标 60Hz 时改为 60QUALITY_FACTOR预处理30陷波品质因数工频干扰带宽宽时降为 20MIN_DISTANCE切波0.4*fs主波峰最小间距儿童脉率快降为 0.3*fsTARGET_LENGTH切波200重采样长度波形细节不足时增为 256C模型1SVM 惩罚系数网格搜索自动调优GAMMA模型0.1RBF 核宽网格搜索自动调优修改参数时一次只改一个改完重新跑预处理并画图对比避免多个参数互相影响导致无从排查。比如改了CUTOFF_HIGH后要确认基线漂移是否清除、波形是否出现衰减再决定下一步。5.3 环境搭建与依赖管理脉象识别系统依赖较多建议用虚拟环境隔离避免与系统 Python 冲突。准备环境的标准流程是# 创建虚拟环境 python -m venv pulse_env source pulse_env/bin/activate # Linux/Mac # 或 pulse_env\Scripts\activate (Windows) # 安装依赖 pip install numpy scipy scikit-learn matplotlib pywavelets # 如果源码用了深度学习模型额外安装 pip install tensorflow注意 SciPy 和 NumPy 的版本兼容性。SciPy 1.10 以上版本要求 NumPy 1.22 以上如果源码是在老版本 Python3.7下写的先检查requirements.txt里的版本范围再安装不要无脑pip install -r requirements.txt装最新版。常见报错如ImportError: cannot import name butter from scipy.signal通常是 SciPy 版本过旧butter在 0.18 之后就存在遇到这种错误看下是不是环境变量指向了别的 Python。6. 验证方法与调参技巧判断你的识别到底准不准最后一章落地到一个具体的验证流程。用混淆矩阵看每个脉象类别的具体表现而不是只看整体准确率。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt # 假设已有测试集预测结果 y_true [...] y_pred [...] cm confusion_matrix(y_true, y_pred) class_names [浮脉, 沉脉, 迟脉, 数脉, 弦脉, 滑脉] # 打印归一化混淆矩阵 cm_normalized cm.astype(float) / cm.sum(axis1, keepdimsTrue) print(归一化混淆矩阵每行总和为1:) print(np.round(cm_normalized, 2)) # 可视化 plt.figure(figsize(8, 6)) sns.heatmap(cm_normalized, annotTrue, fmt.2f, xticklabelsclass_names, yticklabelsclass_names, cmapBlues) plt.title(脉象识别混淆矩阵) plt.xlabel(预测标签) plt.ylabel(真实标签) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)看混淆矩阵时关注对角线旁边的值。如果弦脉和滑脉高频混淆说明特征里缺少区分这两类脉象的关键信息优先在重搏波切迹深度和频谱质心上下功夫而不是盲目调 SVM 的C和gamma。如果某个类别的召回率特别低比如沉脉召回率只有 0.6检查标注数据是否足够以及预处理阶段有没有把弱信号的波形过度滤波。一个值得掌握的技巧是保留原始波形做可解释性验证。模型输出某个标签时把对应波形和特征值打印出来让懂脉象的人确认特征值是否符合该类脉象的生理表现def explain_prediction(sample_idx, waveform, features, label, model): 解释单条样本的预测依据 sample_idx: 样本编号 waveform: 对齐后的脉搏波形 features: 提取的特征字典 label: 预测标签 print(f样本 {sample_idx} 预测结果: {label}) print(关键特征值:) for key in [pulse_rate, main_wave_amp, rising_slope, dicrotic_notch_depth, spectral_centroid]: if key in features: print(f {key}: {features[key]:.4f}) # 画出波形并标注主波峰和切迹 plt.figure(figsize(10, 4)) plt.plot(waveform) peak_idx np.argmax(waveform) plt.plot(peak_idx, waveform[peak_idx], ro, label主波峰) plt.title(f脉搏波形 (预测: {label})) plt.legend() plt.show()这种可解释性调试在毕业设计答辩时尤其有用。评委问你为什么认为这是数脉时你能指着波形图和特征值说脉率 96 次/分钟超过阈值 90主波峰尖锐、上升支斜率大比空谈模型准确率有说服力得多。最后再提一个容易忽略的细节模型训练前要固定随机种子np.random.seed(42)否则每次运行结果不一样答辩现场出问题会很被动。本文还有配套的精品资源点击获取