0 引言
《中国心血管健康与疾病报告2023概要》显示,我国高血压患病人数已达2.45亿1。高血压是指以体循环动脉血压(收缩压/舒张压)增高为主要特征,可伴有心、脑、肾等器官的功能或器质性损害的临床综合征。高血压是导致各类CVD的主要原因之一,然而高血压早期可以完全没有自觉症状,人们往往因此忽视而错过早期干预的时机。若能在早期阶段通过筛查及时发现并进行有效干预,便是最为有效的预防策略。且高血压可防可控。研究表明,血压是测定心血管性能特征的定量指标,在血压不低于115/75 mmHg(1 mmHg=0.133 kPa)的范围内,收缩压每降低10 mmHg,或舒张压每降低5 mmHg,死亡风险降低10%~15%,脑卒中风险降低35%,冠心病风险降低20%,心力衰竭风险降低40%2。因此,如果早期阶段无创精准地的估计血压,则对CVD的早期预防和治疗具有重要意义。
传统血压检测方式多基于袖带式充气测量,测量精度高,但存在操作繁琐、无法连续监测、易造成肢体压迫不适等缺陷,难以满足日常居家健康监测、动态连续生理采集的应用需求。随着可穿戴传感技术与生物信号检测技术的快速发展,光体积描记法(PPG)凭借无创、便携、采集成本低、可实时连续采集等优势,成为无创血压预测领域的主流研究信号,为无袖带式血压动态检测提供了新的技术思路3。PPG信号蕴含丰富的脉搏波动信息,其波形峰值、谷值、上升沿、下降沿及周期变化规律与人体收缩压、舒张压、平均动脉压存在关联性。传统PPG血压预测方法多依赖人工特征工程,通过手动提取波形时域、频域特征构建回归模型,不仅对研究者的先验知识依赖度高,且人工特征泛化能力差,难以适配个体差异带来的波形波动。随着深度学习技术的不断发展,卷积神经网络(CNN)凭借优异的局部特征提取能力,被广泛应用于时序生理信号分析,能够自动挖掘PPG波形的隐性深层特征,有效规避人工特征提取的局限性4。因此,本文将探索基于卷积神经网络的 PPG 特征自动学习方法,实现无需人工特征干预的无创血压估计。
实验流程图:

目录
[0 引言](#0 引言)
[1 数据](#1 数据)
[1.1 数据介绍](#1.1 数据介绍)
[1.2 数据预处理](#1.2 数据预处理)
[1.2.1 PPG质量评估](#1.2.1 PPG质量评估)
[1.2.2 滤波](#1.2.2 滤波)
[1.2.3 单周期提取](#1.2.3 单周期提取)
[2 方法与实验](#2 方法与实验)
[2.1 一维卷积神经网络](#2.1 一维卷积神经网络)
[2.2 实验设置](#2.2 实验设置)
[2.2.1 基于一维卷积神经网络的血压回归模型构建](#2.2.1 基于一维卷积神经网络的血压回归模型构建)
[2.2.2 数据集加载与划分策略](#2.2.2 数据集加载与划分策略)
[2.2.3 模型训练策略](#2.2.3 模型训练策略)
[2.3 评价指标](#2.3 评价指标)
[3 结果分析](#3 结果分析)
数据
1.1 数据介绍
本文实验数据取自 2018 年发表于《Scientific Data》的桂林医院 PPG 血压数据集(PPG-BP Database)5。该数据集共纳入 219 名年龄区间为 21~86 岁的成年受试者,样本覆盖正常血压、高血压前期、各级高血压、糖尿病等心血管相关病症;数据集总计采集 657 段PPG波形数据。实验同步采用医用袖带式血压仪记录每名受试者的收缩压(SBP)与舒张压(DBP),作为血压预测真值,同时配套采集年龄、性别、身高、体重、身体质量指数(BMI)、心率等人口统计学与生理指标,相关标签信息统一存放于**PPG-BP dataset.xlsx** 文件中。原始 PPG 信号以 1000 Hz 采样率采集,单段信号时长 2.1 s,所有波形数据按受试者分类存储于**0_subject**文件夹内,采用 TXT 格式保存,每名受试者对应 3 组独立 PPG 波形样本。下图1为单段典型 PPG 信号波形示例,图2为SBP和DBP的分布情况。
此数据链接如下:Item - PPG-BP Database - figshare - Figshare
图1 单段典型 PPG 信号波形
图2 SBP和DBP的直方图
为了更好的处理数据,实验基于 MATLAB 批量读取文件夹内全部 PPG 波形 TXT 文件,以文件名为索引构建结构体存储各段 PPG 时序信号,批量整合所有波形数据后导出为.mat格式文件,完成原始文本数据向 MATLAB 专用矩阵数据格式的转换,命名为**:PPG-BP.mat,其代码如下:**
Matlab
clear; clc;
dataFolder = 'PPG-BP_Database\Data File\0_subject\';
cd(dataFolder);
txtFiles = dir('*_*.txt');
fileNames = {txtFiles.name};
simData = struct();
for i = 1:length(fileNames)
fileName = fileNames{i};
nameWithoutExt = fileName(1:end-4);
fieldName = ['sim' nameWithoutExt];
data = load(fileName);
simData.(fieldName) = data;
end
%保存
save('PPG-BP.mat', 'simData');
1.2 数据预处理
1.2.1 PPG质量评估
原始数据集共包含 657 段 PPG 波形片段,为剔除存在波形畸变、运动干扰严重、无清晰重搏切迹等问题的低质量无效波形,本实验先对全部原始信号开展质量筛选,仅保留满足质量标准的有效波形用于后续预处理。实验采用基于偏度的信号质量指数(skewness-based SQI,SSQI)作为波形优劣的量化评判指标6,其计算公式为:

式中:为 PPG 信号第i个采样点幅值;
为单段 PPG 信号的幅值均值;
为该段信号幅值的标准差;N 为单段波形总采样点数。本实验对每条 PPG 信号记录进行质量评估处理,具体流程如下:以 1s 为窗口长度、0.5s 为滑动步长,逐窗口计算SQI;取单条记录全部窗口中的最大 SQI 值,作为该段 2.1s PPG 信号的最终质量评分。设置 SQI 阈值 0.7,仅保留评分高于阈值的样本,共筛选得到 392 段有效 2.1s PPG 信号;数据集原始样本来源于 219 名受试者,经上述质量过滤后,将 392 段有效信号作为模型预处理输入数据。其MATLAB代码如下:
Matlab
clc; clear; close all;
load("PPG-BP.mat");
fns = fieldnames(simData);
N = length(fns);
Fs = 1000;
winLen_s = 1;
step_s = 0.5;
winN = round(winLen_s * Fs);
stepN = round(step_s * Fs);
SSQI_all = zeros(N,1);
for i = 1:N
x = simData.(fns{i});
L = length(x);
sqi_win = [];
startIdx = 1;
while startIdx + winN - 1 <= L
seg = x(startIdx : startIdx + winN - 1);
mu = mean(seg);
sigma = std(seg, 1);
nSeg = length(seg);
sqi = 1/nSeg * sum( ((seg - mu)/sigma).^3 );
sqi_win = [sqi_win, sqi];
startIdx = startIdx + stepN;
end
SSQI_all(i) = max(sqi_win);
end
[SSQI_sorted, idx_sort] = sort(SSQI_all, 'descend');
fns_sorted = fns(idx_sort);
result_table = table(fns, SSQI_all, fns_sorted, SSQI_sorted, ...
'VariableNames', {'原始信号名','对应最大SSQI','排序后信号名','排序后SSQI'});
writetable(result_table, "SSQI_Result1.xlsx");
1.2.2 滤波
完成信号质量筛选后,对全部有效 2.1 s PPG 信号开展低通滤波预处理:首先构建 7 阶巴特沃斯低通滤波器,截止频率为 12 Hz;归一化截止频率wn,调用butter函数求解滤波器分子、分母系数。遍历所有受试者 PPG 数据字段,将各段一维 PPG 序列通过零相位滤波函数filtfilt完成滤波,最终输出滤波后信号simData_07.xlsx用于后续分析。下图3为单段典型 PPG 信号的滤波和未滤波波形示例。此外,为定量分析低通滤波对 PPG 信号质量的提升效果,分别计算滤波前后各段 2.1 s PPG 信号的窗口最大 SSQI,对比结果如图4所示。所有样本经 7 阶 12 Hz 巴特沃斯零相位低通滤波处理后,最大 SQI 指标均出现不同程度上升,说明滤波操作能够有效抑制高频噪声干扰、改善 PPG 波形信噪比。
图3 单段典型 PPG 信号波形的滤波和未滤波的结果
图4 滤波前后SSQI对比柱状图
其MATLAB代码如下:
Matlab
clc;
clear;
load("PPG-BP.mat");
Fs = 1000; %
cutoff_freq = 12;
order =7;
wn = cutoff_freq / (Fs/2);
[b, a] = butter(order, wn, 'low');
field_names = fieldnames(simData);
for i = 1:length(field_names)
field_name = field_names{i};
data = simData.(field_name);
data = data(:);
filtered_data = filtfilt(b, a, data);
simData_filtered.(field_name) = filtered_data;
end
save('PPG-BP-filtered.mat', 'simData_filtered');
load("PPG-BP-filtered.mat");
thr1 = 0.7;
idx_07 = SSQI_all > thr1;
fns_07 = fns(idx_07);
SSQI_07 = SSQI_all(idx_07);
simData_07 = struct();
for k = 1:length(fns_07)
name = fns_07{k};
simData_07.(name) = simData_filtered.(name);
end
save("PPG-BP-filtered_0.7.mat", "simData_07");
fns = fieldnames(simData_07);
nCh = length(fns);
maxN = 0;
for i = 1:nCh
L = length(simData_07.(fns{i}));
if L > maxN
maxN = L;
end
end
outCell = cell(nCh, maxN + 1);
for i = 1:nCh
name = fns{i};
vec = simData_07.(name);
outCell{i,1} = name;
outCell(i, 2:length(vec)+1) = num2cell(vec(:));
end
header = ["通道名称", "采样"+(1:maxN)];
T = cell2table(outCell, 'VariableNames', header);
writetable(T, 'simData_07.xlsx');
1.2.3 单周期提取
完成 PPG 信号低通滤波降噪后,采用PPG 周期检测与最优单周期筛选算法提取标准 PPG 脉搏周期波形,算法完整流程如图5所示7。该算法以时长 2.1 s 的连续 PPG 信号作为输入,先借助 NumPy 模块的find_peak函数检索波形全部极值点;将一组连续极小值依次定义为周期起点Ps、重搏切迹z与周期终点Pe,一组连续极大值分别定义为收缩峰x和舒张峰y。首先基于极值点分割得到全部候选单周期,再按照三项约束完成有效性校验:候选周期内同时包含完整有效极大值与极小值、收缩峰幅值大于舒张峰幅值、重搏切迹幅值同时高于周期起点与终点极小值。将满足全部约束的合规周期存入候选列表,遍历全部候选周期后选取收缩峰幅值最大的波形作为最优单周期输出,最终得到标准化单周期信号数据集,**命名为ppg_single_cycle_waveforms.xlsx。**图 6为多组 PPG 样本最优单周期可视化结果。蓝色曲线为归一化 PPG 时序波形,黄色填充区域为算法筛选出的最优单脉搏区间;结果表明所提算法可筛选形态完整、收缩峰幅值最优的单周期波形。
此外,为消除不同脉搏周期时长差异带来的长度不一致问题,采用线性插值将全部最优单周期波形统一插值至 1000 个采样点,完成波形时序标准化。随后结合受试者原始血压数据构建模型输入数据集:通过受试者编号匹配 PPG 波形与对应血压标签,剔除无关人体体征变量,简化字段命名并计算平均动脉压 MAP,最终生成仅包含标准化 PPG 波形、SBP、DBP、MAP 标签的纯净数据集,作为血压预测模型的输入数据(ppg_血压_final.xlsx)。
图5 单周期PPG提取算法
注:该图片来自于文献7
图6 单周期提取结果
其Python代码详细如下:
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.signal import find_peaks
FS = 1000
SIGNAL_DUR = 2.1
MIN_CYCLE_LEN = 600
MAX_CYCLE_LEN = 1000
MAX_CYCLE_SAMP = MAX_CYCLE_LEN + 1
def normalize_ppg(signal):
min_val = np.min(signal)
max_val = np.max(signal)
if max_val - min_val == 0:
return signal
return (signal - min_val) / (max_val - min_val)
def extract_opt_single_cycle(ppg_1d: np.ndarray):
ppg_normalized = normalize_ppg(ppg_1d)
C_list = []
min_idx, _ = find_peaks(-ppg_normalized)
max_idx, _ = find_peaks(ppg_normalized)
for i in range(len(min_idx) - 2):
P_i = min_idx[i]
z = min_idx[i + 1]
P_e = min_idx[i + 2]
cycle_length = P_e - P_i
if not (MIN_CYCLE_LEN <= cycle_length <= MAX_CYCLE_LEN):
continue
peak_in_range = max_idx[(max_idx > P_i) & (max_idx < P_e)]
if len(peak_in_range) >= 2:
x = peak_in_range[0]
y = peak_in_range[1]
if ppg_normalized[x] > ppg_normalized[y] and (ppg_normalized[z] > ppg_normalized[P_i] and ppg_normalized[z] > ppg_normalized[P_e]):
cycle_slice = ppg_normalized[P_i:P_e + 1]
C_list.append({
"cycle_sig": cycle_slice,
"sys_amp": ppg_normalized[x],
"pts": {"Ps": P_i, "z": z, "Pe": P_e, "x": x, "y": y}
})
if len(C_list) == 0:
return None, None, ppg_normalized
best = max(C_list, key=lambda item: item["sys_amp"])
return best["cycle_sig"], best["pts"], ppg_normalized
if __name__ == "__main__":
df = pd.read_excel("simData_07.xlsx")
cycle_wave_storage = []
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
axes = axes.flatten()
plot_idx = 0
total_signals = len(df)
valid_signals = 0
for row in df.itertuples():
sig_id = row[1]
ppg_arr = np.array(row[2:2 + int(SIGNAL_DUR * FS)], dtype=np.float64)
best_cycle, pt_map, ppg_normalized = extract_opt_single_cycle(ppg_arr)
if best_cycle is None:
continue
valid_signals += 1
pad_cycle = np.full(MAX_CYCLE_SAMP, np.nan)
pad_cycle[:len(best_cycle)] = best_cycle
row_dict = {"signal_id": sig_id}
for i, val in enumerate(pad_cycle):
row_dict[f"wave_{i}"] = val
cycle_wave_storage.append(row_dict)
if plot_idx < 4:
ax = axes[plot_idx]
time_axis = np.linspace(0, SIGNAL_DUR, len(ppg_normalized))
ax.plot(time_axis, ppg_normalized, c="blue", lw=1.5, alpha=0.7)
t_start, t_end = time_axis[pt_map["Ps"]], time_axis[pt_map["Pe"]]
ax.axvspan(t_start, t_end, alpha=0.3, color="yellow")
feature_points = {"Ps":"orange","z":"purple","Pe":"orange","x":"red","y":"green"}
for k, c in feature_points.items():
ax.scatter(time_axis[pt_map[k]], ppg_normalized[pt_map[k]], s=100, c=c, edgecolors="black")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Normalized Amplitude")
ax.grid(alpha=0.3)
ax.set_title(f"Signal {sig_id} Optimal PPG Cycle")
plot_idx += 1
for idx in range(plot_idx, 4):
axes[idx].set_visible(False)
plt.tight_layout()
plt.savefig("ppg_opt_cycle_plot.png", dpi=150)
plt.show()
df_cycle_wave = pd.DataFrame(cycle_wave_storage)
df_cycle_wave.to_excel("ppg_single_cycle_waveforms.xlsx", index=False)
## 插值到采样频率
import pandas as pd
import numpy as np
from scipy.interpolate import interp1d
df = pd.read_excel("ppg_single_cycle_waveforms.xlsx")
ids = df["signal_id"]
waves = df.filter(regex="wave").values
res = []
for y in waves:
y = y[~np.isnan(y)]
f = interp1d(np.arange(len(y)), y)
new_y = f(np.linspace(0, len(y)-1, 1000))
res.append(new_y)
out = pd.DataFrame(res, columns=[f"wave_{i}" for i in range(1000)])
out.insert(0, "signal_id", ids)
out.to_excel("wave_1000.xlsx", index=False)
#合并数据集
import pandas as pd, re
df_ppg = pd.read_excel(r'wave_1000.xlsx')
df_vital = pd.read_excel(r'血压与体征数据.xlsx')
df_ppg['subject_ID'] = df_ppg.signal_id.apply(lambda x: int(re.match(r'sim(\d+)_',x).group(1)))
df = pd.merge(df_ppg, df_vital, on='subject_ID', how='left')
df = df.drop(columns=['subject_ID','Height(cm)','Weight(kg)','Heart Rate(b/m)','BMI(kg/m^2)','signal_id','Sex(M/F)','Age(year)'])
df = df.rename(columns={
'Systolic Blood Pressure(mmHg)':'SBP',
'Diastolic Blood Pressure(mmHg)':'DBP'
})
df['MAP'] = (df['SBP'] + 2 * df['DBP']) / 3
df.to_excel(r'ppg_血压_final.xlsx', index=False)
2 方法与实验
2.1 一维卷积神经网络
一维卷积神经网络(1D Convolutional Neural Network, 1D CNN)是卷积神经网络的重要分支,面向时序、文本等具备顺序依赖特性的一维序列数据构建。其基础架构与二维卷积神经网络(2D CNN)同源,但卷积运算仅沿单一序列维度开展,更适配时序信号的局部特征挖掘需求,典型网络层级结构由如下核心模块组成8-9:
1)输入层 为模型的数据接入端口,接收一维时序序列(本实验中为标准化 PPG 单周期波形),完成原始数据的导入与维度规整,为后续特征提取提供基础输入。
2)一维卷积层 作为 1D CNN 的核心特征提取单元,采用一维卷积核对输入序列执行滑动窗口卷积运算,可高效捕获序列内部局部关联规律与时域特征。卷积核尺寸控制单次局部信息的感受野范围,卷积核数量决定输出特征通道数,二者是调控模型特征提取能力与计算开销的关键超参数。
3)激活函数层 堆叠于卷积层后方,通过 ReLU、ELU 等非线性激活函数引入非线性映射,打破网络线性表达局限,使模型具备拟合复杂非线性波形模式的能力,提升提取特征的区分度。
4)池化层 紧随激活层完成下采样操作,在保留核心特征的前提下压缩特征序列长度,削减模型参数量与计算量;同时弱化波形小幅偏移、幅值抖动等微小扰动带来的影响,增强模型泛化性能。最大池化、平均池化为两类最常用下采样方式,前者保留局部最优特征,后者融合局部区域整体特征。
5)全连接层 部署于网络末端,属于全局决策模块。该层将卷积、池化阶段提取的局部时域特征拼接融合,通过权重映射完成全局特征建模,最终输出适配下游任务的预测结果;针对本血压回归任务,网络将输出收缩压、舒张压、平均动脉压的连续估测值,实现从 PPG 波形特征到血压数值预测的完整映射。
其结构如图7所示。
图7 1D CNN结构示意图
2.2 实验设置
2.2.1 基于一维卷积神经网络的血压回归模型构建
实验搭建多输出一维卷积神经网络 OneDCNN 实现收缩压(SBP)、舒张压(DBP)、平均动脉压(MAP)同步估测,网络整体分为共享卷积特征提取模块与多输出全连接回归模块。共享卷积堆叠层包含 3 层一维卷积单元,每层依次配置一维卷积层、批归一化层、ReLU 激活函数与一维最大池化层:卷积核尺寸统一设置为 5,步长 2,填充值 2;三层卷积输出通道数依次为 32、64、128;池化核大小为 2、步长 2。卷积提取的一维时序特征展平后送入全连接回归分支,两层线性层中间加入 0.3 概率的 Dropout 层抑制过拟合,最终输出 3 维向量分别对应 SBP、DBP、MAP 预测值。
2.2.2 数据集加载与划分策略
读取预处理完成的**ppg_血压_final.xlsx**数据集,提取 1000 维标准化 PPG 单周期波形作为模型输入特征 X,以 SBP、DBP、MAP 作为多任务回归标签 Y。采用 StandardScaler 对全部 PPG 时序特征做标准化消除量纲影响;数据集采用分层随机三分法划分:先抽取 20% 样本作为独立测试集,全程不参与训练与超参数调优;剩余 80% 样本再按 4:1 拆分为训练集(总样本 64%)与验证集(总样本 16%)。
2.2.3 模型训练策略
实验超参数设置如下:批次大小8,总迭代轮数300,Adam 优化器初始学习率为0.0001,损失函数选用均方误差。 训练流程分为训练迭代与验证迭代两部分:训练阶段开启网络参数反向传播更新权重;验证阶段关闭梯度计算,仅用于监测模型泛化性能。每轮迭代记录训练集、验证集平均损失,以验证集损失最小值作为模型筛选依据,训练全部结束后加载最优权重模型,在从未参与训练的独立测试集上完成离线评估。
2.3 评价指标
实验中选用平均绝对误差(MAE)和均方根误差(RMSE)10进行测试集的评价。
(1)平均绝对误差是预测值与真实值之间绝对误差的平均值,用于衡量模型预测的平均偏差程度。MAE对所有误差赋予相同的权重,反映预测误差的集中趋势,数值越小表示模型预测精度越高。
其中,是样本总数,
是第 i 个样本的真实血压值,
是第 i 个样本的模型预测血压值。
(2)均方根误差 是预测值与真实值之间误差平方均值的平方根,用于衡量模型预测的离散程度。RMSE对较大误差赋予更高的权重(平方运算),因此对异常值更为敏感,能够反映预测误差的波动性。数值越小表示模型预测精度越高。
其中,是样本总数,
是第 i 个样本的真实血压值,
是第 i 个样本的模型预测血压值。
模型代码如下:
python
import pandas as pd
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import matplotlib.pyplot as plt
class PPGDataset(Dataset):
def __init__(self, x_data, y_data):
self.x = torch.from_numpy(x_data).float()
self.y = torch.from_numpy(y_data).float()
def __len__(self):
return len(self.x)
def __getitem__(self, idx):
return self.x[idx].unsqueeze(0), self.y[idx]
class OneDCNN(nn.Module):
def __init__(self, seq_len=1000, out_dim=3):
super().__init__()
self.conv_stack = nn.Sequential(
nn.Conv1d(in_channels=1, out_channels=32, kernel_size=5, stride=2, padding=2),
nn.BatchNorm1d(32),
nn.ReLU(),
nn.MaxPool1d(kernel_size=2, stride=2),
nn.Conv1d(32, 64, kernel_size=5, stride=2, padding=2),
nn.BatchNorm1d(64),
nn.ReLU(),
nn.MaxPool1d(kernel_size=2, stride=2),
nn.Conv1d(64, 128, kernel_size=5, stride=2, padding=2),
nn.BatchNorm1d(128),
nn.ReLU(),
nn.MaxPool1d(kernel_size=2, stride=2),
)
dummy = torch.randn(1, 1, seq_len)
conv_out_dim = self.conv_stack(dummy).flatten(1).shape[1]
self.fc = nn.Sequential(
nn.Linear(conv_out_dim, 256),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, 64),
nn.ReLU(),
nn.Linear(64, out_dim)
)
def forward(self, x):
feat = self.conv_stack(x)
feat = feat.flatten(1)
return self.fc(feat)
def load_pp_data(file_path="ppg_血压_final.xlsx"):
df = pd.read_excel(file_path)
wave_cols = [f"wave_{i}" for i in range(1000)]
X = df[wave_cols].values
y = df[["SBP", "DBP", "MAP"]].values
scaler_x = StandardScaler()
X_scaled = scaler_x.fit_transform(X)
X_temp, X_test, y_temp, y_test = train_test_split(
X_scaled, y, test_size=0.2, random_state=42, shuffle=True
)
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp, test_size=0.2, random_state=42, shuffle=True
)
return X_train, X_val, X_test, y_train, y_val, y_test
def train_epoch(model, loader, loss_fn, optimizer, device):
model.train()
total_loss = 0.0
for x, y in loader:
x, y = x.to(device), y.to(device)
pred = model(x)
loss = loss_fn(pred, y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_loss += loss.item()
return total_loss / len(loader)
def val_epoch(model, loader, loss_fn, device):
model.eval()
total_loss = 0.0
with torch.no_grad():
for x, y in loader:
x, y = x.to(device), y.to(device)
pred = model(x)
loss = loss_fn(pred, y)
total_loss += loss.item()
return total_loss / len(loader)
def calculate_metrics(y_true, y_pred):
mae_all = mean_absolute_error(y_true, y_pred)
mse_all = mean_squared_error(y_true, y_pred)
rmse_all = np.sqrt(mse_all)
# 分指标 SBP DBP MAP
mae_sbp = mean_absolute_error(y_true[:, 0], y_pred[:, 0])
mae_dbp = mean_absolute_error(y_true[:, 1], y_pred[:, 1])
mae_map = mean_absolute_error(y_true[:, 2], y_pred[:, 2])
mse_s = mean_squared_error(y_true[:, 0], y_pred[:, 0])
mse_d = mean_squared_error(y_true[:, 1], y_pred[:, 1])
mse_m = mean_squared_error(y_true[:, 2], y_pred[:, 2])
rmse_s = np.sqrt(mse_s)
rmse_d = np.sqrt(mse_d)
rmse_m = np.sqrt(mse_m)
return metrics
if __name__ == "__main__":
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"训练设备: {device}")
BATCH_SIZE = 8
EPOCHS = 300
LR = 1e-4
SEQ_LEN = 1000
OUT_DIM = 3
X_train, X_val, X_test, y_train, y_val, y_test = load_pp_data()
train_ds = PPGDataset(X_train, y_train)
val_ds = PPGDataset(X_val, y_val)
test_ds = PPGDataset(X_test, y_test)
train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)
val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False)
test_loader = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=False)
model = OneDCNN(seq_len=SEQ_LEN, out_dim=OUT_DIM).to(device)
loss_func = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=LR)
train_loss_list = []
val_loss_list = []
best_val_loss = float("inf")
best_epoch = 0
best_model_path = "ppg_1dcnn_best_bp.pth"
for epoch in range(EPOCHS):
tr_loss = train_epoch(model, train_loader, loss_func, optimizer, device)
va_loss = val_epoch(model, val_loader, loss_func, device)
train_loss_list.append(tr_loss)
val_loss_list.append(va_loss)
if va_loss < best_val_loss:
best_val_loss = va_loss
best_epoch = epoch + 1
torch.save(model.state_dict(), best_model_path)
print(f"【更新最优模型】Epoch {best_epoch} | 最优验证损失: {best_val_loss:.4f}")
if (epoch + 1) % 5 == 0:
print(f"Epoch [{epoch+1:03d}/{EPOCHS}] | Train Loss:{tr_loss:.4f} | Val Loss:{va_loss:.4f}")
best_model = OneDCNN(seq_len=SEQ_LEN, out_dim=OUT_DIM).to(device)
best_model.load_state_dict(torch.load(best_model_path, map_location=device))
best_model.eval()
y_true_all = []
y_pred_all = []
with torch.no_grad():
for x, y in test_loader:
x = x.to(device)
pred = best_model(x).cpu().numpy()
y_t = y.numpy()
y_true_all.extend(y_t)
y_pred_all.extend(pred)
y_true_all = np.array(y_true_all)
y_pred_all = np.array(y_pred_all)
3 结果分析
图 8为1D CNN 模型训练集与验证集 MSE 损失变化曲线。红蓝曲线分别对应训练、验证损失,红色虚线标记验证损失最小值对应的最优迭代轮次 Epoch 37。曲线表明模型前期快速收敛,37 轮后出现轻度过拟合,正则化机制有效控制了泛化误差,基于验证损失的最优模型保存策略可选取泛化性能最佳的网络权重。
图8 训练和验证损失迭代图
最优模型在独立测试集上的预测结果如图9显示,三类血压估测精度存在明显差异:DBP 预测效果最优,MAE 仅 8.60 mmHg;SBP 误差最高,MAE 达 15.05 mmHg;MAP 误差介于两者之间。该差异源于收缩压受多种生理干扰因素影响,仅依靠单周期 PPG 时域特征难以充分表征其变化规律;舒张压与 PPG 舒张段波形相关性更强,模型更容易完成拟合。整体模型存在收缩压预测精度短板,后续可通过多特征融合、数据集扩充等方式优化。
图9 测试集结果
参考
1 国家心血管病中心, 中国心血管健康与疾病报告编写组. 中国心 血管健康与疾病报告2023概要J. 中国循环杂志, 2024, 39(7): 625-660.
2 国家基层高血压防治管理指南2025版J.中国循环杂志,2025,40(09):836-850.
3姜祁翎,许晓萍,魏增江,等.穿戴式PPG连续血压测量设备准确性研究现状与展望J/OL.中国医疗设备,2026.
4 谢炜芳.CNN-BiLSTM:一种基于光电容积脉搏波信号的房颤检测模型J.计算机时代,2025,(8)8-10+15.
5 Liang, Y., Chen, Z., Liu, G. et al. A new, short-recorded photoplethysmogram dataset for blood pressure monitoring in China. Sci Data 5, 180020 (2018).
6 Liang Y, Elgendi M, Chen Z, et al. Analysis: an optimal filter for short photoplethysmogram signals. Sci Data 5: 1--12EB/OL.(2018)
7 Nishan A, Raju S M T U, Hossain M I, et al. A continuous cuffless blood pressure measurement from optimal PPG characteristic features using machine learning algorithmsJ. Heliyon, 2024, 10(6).
9 基于一维卷积神经网络的PPG信号心肌梗死预测研究_CSDN博客
10 基于领域自适应神经网络(DANN)的无创血压估计方法_CSDN博客
注:若有侵权部分,请留言将会删除。
个人观点,仅供参考