☰
ARIMA、LSTM与Transformer在心跳时间序列预测中的实战对比
2026/10/22 4:18:48 网站建设 项目流程

简介:面向毕业设计、期末大作业与课程设计场景,这份项目包融合ARIMA、Transformer、LSTM三类模型,对心跳时间序列数据进行训练与预测,并提供完整源码与文档说明。代码含详细注释,部署简单,适合希望快速复用高分项目的Python学习者,也便于初学者对照源码理解数据处理与预测的完整流程。压缩包共1179个文件、约19.85MB,核心内容涵盖310组心跳记录及对应标注文件(hea/dat/atr),23个Python脚本与9个Notebook用于数据预处理、模型构建与对比实验,另有144张结果图、文档、配置及Docker部署等辅助文件,目录结构清晰,便于按模块查阅。已有225人浏览学习。项目经过调试可直接运行,读者可据此理解三种模型在时序预测中的差异,掌握数据预处理、训练评估与可视化流程,并基于现有代码快速改造出属于自己的毕业设计或课程作业。

1. 先说清楚:心跳序列预测到底在预测什么

拿到“基于ARIMA、Transformer、LSTM对心跳时间序列数据进行预测”这类题目,第一反应往往是把三种模型依次跑一遍、挑误差最低的交上去。但真正做下来你会明白:心跳时间序列数据的预测难点从来不在模型选型,而在你用什么口径定义“心跳序列”。RR间期的提取、滑窗的长度、预测步长,每步都是一道选择题,选错了,再强的模型也只是在错误的数据上自嗨。

本文把三种模型的共同输入锁定为RR间期序列:用ARIMA做传统统计基线,用LSTM做循环网络代表,用Transformer做注意力机制代表,最后统一评估口径、给出可复现的代码和踩坑清单。读者如果是做课程设计或毕业设计,可以把这套流程当项目框架直接套用;已经是工程师的,可以直接跳到第2章的滑窗策略和第5章的避坑记录。目标就一个:让预测结果不只在图上好看,而是能经得起“滞后一拍”这类常见质疑。

2. 数据与任务:先把“预测目标”钉死在代码里

2.1 预测目标二选一:下一拍间隔还是未来一段波形

心跳时间序列的数据源通常有两种:医疗级心电记录仪导出的原始ECG波形,采样率在250Hz到1000Hz;可穿戴设备已经算好的瞬时心率或RR间期序列。做预测之前必须先定清楚落点,这一步决定了后面所有模型输入长什么样。

如果直接预测原始波形,输出的是一个高频连续信号。波形里叠加了基线漂移、工频干扰,而且单拍形态本身信息冗余——你真正想知道的是“下一拍什么时候来”,不是“下一拍波形长什么样”。用神经网络直接预测波形,结果会糊成一团平均形态,既没有临床意义,也不符合“时间序列预测”的常规评价体系。我一般选RR间期:两次相邻心跳之间的毫秒数。它的序列天然非等间隔但有生理节律,ARIMA这类统计模型可以直接建模,LSTM和Transformer需要的输入维度也只有一维,训练成本低得多。

选RR间期还有一个实际好处:指标口径容易对齐。医生习惯看心率变异性指标,全部由RR间期派生;而瞬时心率被换算放大了异常,不如直接建模原始间隔。如果你的数据源只有瞬时心率,也可以按60秒/心率换算成RR间期,但要注意这种换算在异常拍上会被放大误差,尽量别把瞬时心率直接当模型输入。

2.2 R峰提取与序列构建:从原始心电到可训练的时间序列

如果拿到的是原始心电波形,第一步是R峰检测。常见做法是带通滤波加峰值查找:R波是QRS波群中最尖锐的成分,能量集中在5~20Hz,先做带通滤波去掉基线和工频干扰,再用峰值查找算法定位R波位置,最后把相邻R峰间隔换算成毫秒。

import numpy as np from scipy.signal import butter, filtfilt, find_peaks def detect_r_peaks(ecg, fs=250): """提取R峰索引,输入原始心电信号,输出R峰位置(单位:样本点)""" low = 5.0 / (fs / 2) high = 20.0 / (fs / 2) b, a = butter(4, [low, high], btype='band') filtered = filtfilt(b, a, ecg) # 最小距离:按心率上限200bpm计算,相邻R峰至少间隔300ms min_dist = int(0.3 * fs) peaks, _ = find_peaks(filtered, distance=min_dist, height=np.percentile(filtered, 85)) return peaks peaks = detect_r_peaks(ecg_signal, fs=250) rr_intervals = np.diff(peaks) / fs * 1000 # 单位转毫秒

逻辑说明:带通滤波的核心是保留R波所在频段;find_peaks的distance参数按心率上限200bpm反推为300ms,如果设得太小,T波容易被误判成R峰;height用百分位阈值而不是固定值,因为不同设备的信号幅度差异很大,固定阈值在不同人身上会失效。

过滤时先别急着删除异常值。低于300ms或高于2000ms的间隔,先看是连续的还是孤立的:连续多个短间隔多半是误检,单独一个长间隔可能是漏检了中间一拍。按生理范围过滤后,统计可疑片段比例,超过2%就要回头调滤波参数或检查电极接触问题。这一步直接影响后面三个模型的输入质量。

2.3 滑窗切分与归一化:序列长度、预测步长与反归一化

任务可定义为:用最近32个RR间期,预测未来1步或1~8步的RR间期。我一般把seq_len=32、pred_len=1作为主实验组,再做3步和8步的对照。为什么是32:正常人RR序列既有高频的呼吸性窦性心律不齐,也有低频的交感副交感调节,32拍大约覆盖20到40秒,信息量比16拍足,又不会像64拍那样让训练样本急剧减少。

归一化推荐z-score,但有一条铁律:只能用训练集的均值、标准差去转换验证集和测试集,不能用全序列一起fit,否则就是数据泄漏。对ARIMA来说归一化不是必须的,但对LSTM和Transformer几乎必须,否则激活函数很容易饱和、梯度不稳定。

import numpy as np from sklearn.preprocessing import StandardScaler def make_windows(seq, seq_len=32, pred_len=1, step=1): X, y = [], [] for i in range(0, len(seq) - seq_len - pred_len + 1, step): X.append(seq[i:i + seq_len]) y.append(seq[i + seq_len:i + seq_len + pred_len]) return np.array(X), np.array(y) # 只对训练段做归一化 scaler = StandardScaler() train_seq = seq[:n_train] scaler.fit(train_seq.reshape(-1, 1)) train_norm = scaler.transform(train_seq.reshape(-1, 1)).ravel() # 验证集、测试集用同一个 scaler.transform,不再 fit

参数说明:step=1意味着相邻窗口只差一个样本,样本数暴涨但独立样本很少,严格评估时建议按时间顺序先切段再滑窗,或使用step=seq_len让窗口不重叠。pred_len=1时标签是标量;pred_len>1时是向量,LSTM输出层要相应调整。预测完成后做反归一化:preds * scaler.scale_ + scaler.mean_,所有评估都必须在原始毫秒尺度上进行,在归一化空间里比较误差没有意义。

3. 用ARIMA打基线:统计模型不是过时,是省心

3.1 ARIMA(p,d,q)定阶:从ACF/PACF到信息准则

ARIMA把时间序列分解成自回归和移动平均两部分,核心是三个参数:p是自回归阶数,d是差分数,q是移动平均阶数。对RR间期序列,d大概率是0或1,先做ADF检验看平稳性。p、q的初判靠ACF和PACF图的截尾、拖尾规律:PACF在某个阶数后突然截断,p取那个阶数;ACF截断则对应q。但这种肉眼判断容易玄学,更可靠的做法是网格搜索AIC。

import itertools from statsmodels.tsa.arima.model import ARIMA def best_arima(series, max_p=5, max_q=5, d_candidates=(0, 1)): best_aic = float('inf') best_order = None for d in d_candidates: for p, q in itertools.product(range(max_p + 1), range(max_q + 1)): try: model = ARIMA(series, order=(p, d, q)) res = model.fit() if res.aic < best_aic: best_aic = res.aic best_order = (p, d, q) except Exception: continue return best_order

逻辑说明:AIC在拟合优度和参数数量之间做权衡,同数据上越小越好。p、q上限设5就够,RR序列的短期自相关主要来自前几拍,上限拉到10只会让搜索变慢,还容易选中过拟合阶数。d只取0和1,超过1的差分会让序列失去可解释性,预测结果也难以反算回原始尺度。try/except不是偷懒,statsmodels某些阶数组合会因数值问题收敛失败,跳过即可。

训练ARIMA前,把序列按时间顺序分成训练段和测试段,不要随机打乱。时序模型没有“乱序训练”这回事,打乱后的数据对统计模型没有意义。

3.2 跑通ARIMA的最小代码:训练、预测与反归一化

拿到最优阶数后直接建模。statsmodels的预测接口有两个:predict支持指定起点终点,forecast做固定步长外推。滚动回放场景下,更稳妥的做法是每次用当前全部历史数据重新拟合,再预测下一步。

import numpy as np from statsmodels.tsa.arima.model import ARIMA def arima_roll_forecast(train, test, order): history = list(train) preds = [] for t in range(len(test)): model = ARIMA(history, order=order) res = model.fit() pred = res.forecast(steps=1)[0] preds.append(pred) history.append(test[t]) # 滚动:把真实值送回去 return np.array(preds)

逻辑说明:每一步重新拟合,速度慢但贴近实时场景。做离线实验时也可以改为每10步重拟合一次,中间的10步用forecast连续外推,速度提升明显,误差差别不大。forecast返回的是条件均值,ARIMA预测本质是均值回归,所以它的预测曲线天然比真实序列平滑,这个现象不是bug。

参数上还有两个开关值得注意:enforce_stationarity和enforce_invertibility。默认开启会强制模型满足平稳性和可逆性约束,如果报错或拟合失败,优先怀疑d取大了,而不是关掉约束。

3.3 ARIMA的边界:什么场景下它必然翻车

ARIMA是线性模型,对RR序列的节律性波动能学个大概,但遇到三类情况基本翻车。第一类是早搏等突发变化,序列出现单拍骤变,ARIMA的预测会向均值回归,完全抓不住突变;第二类是长时外推,预测步数超过5步后误差迅速放大,因为它没有对生理节律周期性建模;第三类是运动状态下心率持续爬坡这种非线性趋势,差分只能去掉线性趋势,去不掉非线性成分。

我的习惯是把ARIMA当基线,不指望它拿最好成绩,而是用它提供“传统方法的天花板”。后续如果LSTM和Transformer的结果连ARIMA都打不过,问题大概率不在模型,而在数据预处理或评估协议上——先修数据,再谈换模型。这个判断顺序能省下大量无意义的调参时间。

4. LSTM与Transformer:把心跳序列交给神经网络

4.1 为什么是LSTM:梯度、门控与序列建模

RNN处理序列时,反向传播经过多个时间步,梯度容易指数级消失,普通循环单元学不到远距离依赖。LSTM用输入门、遗忘门、输出门把记忆路径单独拉出来,让梯度可以“抄近道”流过,因此对30到60步的序列已经够用,计算量也比Transformer小很多。对心跳这种长度中等、依赖集中在近几拍的序列,LSTM往往比深层模型更稳。

import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, pred_len=1, dropout=0.1): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout) self.head = nn.Linear(hidden_size, pred_len) def forward(self, x): # x: (batch, seq_len, 1) out, _ = self.lstm(x) last = out[:, -1, :] return self.head(last)

逻辑说明:input_size=1是因为输入只有RR间期一维;hidden_size=64、num_layers=2是常见起步值。数据量小时1层加32也够,层数多不一定好——RR序列样本通常只有几万拍,网络太深容易过拟合。取最后一个时间步的隐状态接全连接,等价于“看完整段32拍再给出预测”。

4.2 LSTM训练细节:批次、学习率与状态重置的陷阱

训练循环有三个坑。第一,批次不能像图片分类那样全量shuffle,滑动窗口相互重叠,shuffle后验证集和训练集会悄悄共享大量信息;第二,每个batch必须重置隐状态,否则上一个batch的隐状态会污染当前batch,Loss曲线会出现周期性跳变;第三,Adam默认学习率1e-3对LSTM往往偏大,从5e-4起步更稳。

def train_lstm(model, train_loader, epochs=30, lr=5e-4): opt = torch.optim.Adam(model.parameters(), lr=lr) loss_fn = nn.MSELoss() model.train() for ep in range(epochs): total_loss = 0 for x, y in train_loader: opt.zero_grad() # 每个batch都重置隐状态,防止跨batch时间泄漏 pred = model(x) loss = loss_fn(pred, y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() total_loss += loss.item() print(f"epoch {ep}: {total_loss / len(train_loader):.5f}")

参数说明:clip_grad_norm的max_norm设为1.0防止梯度爆炸,时序模型比普通MLP更需要梯度裁剪。epochs在30以内就够,心跳数据规律性强,训练Loss通常10轮内就收敛,之后主要看验证Loss。验证Loss下降后反弹就是过拟合,优先减小hidden_size或提高dropout,别考虑数据增强,生理时序不搞随机平移那套。

4.3 Transformer适配数值序列:位置编码、因果掩码与d_model

Transformer的强项是并行计算长距离依赖,但直接套NLP的做法容易翻车。三个适配点必须处理:不需要word embedding,直接把一维RR序列线性投影到d_model维;位置编码要能区分“第几拍”,用可学习位置编码或固定三角函数都可以;预测任务必须加因果掩码,让每个位置只能看到自身及之前的位置,否则训练时模型偷看了未来的RR间期,测试时立刻暴露。

import torch import torch.nn as nn class TransformerPredictor(nn.Module): def __init__(self, d_model=64, nhead=8, num_layers=2, pred_len=1): super().__init__() self.proj = nn.Linear(1, d_model) self.pos = nn.Parameter(torch.randn(1, 32, d_model)) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, batch_first=True) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers) self.head = nn.Linear(d_model, pred_len) def forward(self, x): # x: (batch, seq_len, 1) h = self.proj(x) + self.pos h = self.encoder(h) # 注意:实际使用时需传入因果掩码 return self.head(h[:, -1, :])

逻辑说明:这里TransformerEncoder默认不加掩码,实际使用时需要构造一个上三角为-inf的注意力矩阵传入attn_mask,保证预测当前时刻时看不到未来。d_model从32到128足够,nhead用4或8,层数2到3层足够。如果发现Transformer结果反而不如LSTM,先查两件事:因果掩码有没有加,位置编码是否让模型区分了时间先后。这两个问题比调任何超参数都致命。

4.4 三种模型放一起比较时,指标和图表要统一

三种模型各自跑通后,最怕各写各的评估逻辑。统一口径只有一条:在测试集上滚动回放,每步输入真实历史(或上一步真实值),逐步前进,三种模型都预测同一段测试区间。禁止一种模型用滚动回放、另一种用整段外推,那样的对比没有意义。

模型RMSE(ms)MAE(ms)方向准确率现象描述
ARIMA实测值实测值实测值曲线平滑,突发波动抓不住
LSTM实测值实测值实测值趋势跟随较好,短步优势明显
Transformer实测值实测值实测值长窗依赖更强,配置不当易抖动

数值以你的实验输出为准,但报告里至少要补三个说明:RR间期单位是毫秒,误差在临床尺度上意味着什么;方向准确率(预测增量方向与真实增量方向是否一致)不能省略;直接把“上一拍复制值”作为第四种基线模型写进对比表。如果三种模型都打不过复制上一拍,结论应该是预测步长本身超出了可预测范围,而不是继续堆模型。

文档组织也遵循同一逻辑:把数据预处理决策、评估口径、盲基线为什么存在写清楚,比贴大段模型原理更得分。评审看的是你是否理解边界,不是看你会不会调参。

5. 避坑:这几处坑让“高分项目”变成“修复现场”

5.1 数据泄漏:滑窗重叠让验证集名不副实

现象:验证集Loss比训练集还低,测试集上却一塌糊涂,RMSE差出30%以上。

原因:最常见的是把全序列的均值、方差一起fit进scaler,或者滑动切窗后没有按时间区间划分,导致训练窗口和验证窗口重叠了同一段原始数据。窗口重叠意味着模型在训练时已经见过验证样本的“邻居”,评估结果虚高,上线后立刻现原形。

解决:scaler只fit训练段;先按时间顺序划出train/val/test,再在各区间内部滑窗;数据量小时使用按块划分,块与块不重叠。验证集第一次出现异常低Loss时别高兴,先排查这两处再继续往下走。

5.2 预测曲线“复读机”:滞后一拍的假象

现象:模型预测和真实曲线几乎重合,但仔细看预测值就是前一个点平移过来,RMSE很小,方向准确率却接近随机水平。

原因:RR间期相邻值强相关,短期预测的最优解往往就是“复制最近值”。LSTM如果学成了恒等映射,损失函数依然很低,但它没有学到任何增量趋势。

解决:把预测目标改成差分序列,让模型学习“下一次间隔相对于这一次的变化量”,而不是绝对值;同时额外报告方向准确率,看增量的符号预测准不准。更直接的验证方式是画滞后图,把预测序列整体平移1拍再算相关系数;如果平移后相关性不降,说明模型在抄作业。把“复制上一拍”放进对比表,是最有效的破假象手段。

5.3 RR数据噪声与生理边界被忽略

现象:拟合曲线很漂亮,但模型预测出了200ms或3000ms这种生理上不可能的间隔,一眼就能看出是检测环节的怪值混进去了。

原因:R峰检测在噪声段会把T波误判成R峰,或者漏检一拍导致RR间期翻倍形成假长间期。这些异常值如果是真实早搏需要保留,如果是检测错误就必须清洗,而很多项目直接按阈值删掉,反而把真实生理波动也丢了。

解决:先做人眼抽查,把检测出的R峰叠加到原始波形上,看两分钟就能发现误检模式。再按生理范围做边界标记:小于300ms或大于2000ms的先标记,结合相邻差值突变程度判断是病理事件还是检测噪声。保留“事件标记”而不是直接删除,模型的预测结果才具有临床可解释性。

6. 一个让预测结果可用度翻倍的实战技巧:残差二次校正

三种模型都跑通后,最稳的提分手段不是换更大的模型,而是残差二次校正。思路是:先让主模型(我一般用ARIMA或LSTM)做第一版预测,主模型擅长把握整体趋势;把真实值减去预测值得到残差序列,再用一个轻量模型学习残差的模式。心跳序列中的突发波动,主模型学不到,但残差里往往有明显规律——比如早搏前几拍残差的符号和幅度是固定的。

def residual_correction(y_true, y_pred, residual_model=None): residual = y_true - y_pred # 残差模型用同样的x_window去预测residual_window corr = residual_model.predict(x_window) final_pred = y_pred + corr return final_pred

落地方式:把训练集切成两份,第一份训练主模型,第二份用主模型预测得到残差,再训练残差模型。残差模型用单层LSTM或Ridge回归就够,复杂了会过拟合。这样把“节律预测”和“异常波动预测”拆成两个问题,各有分工,最终预测等于趋势加校正量。

另一个习惯是固定加一条盲基线:把“直接复制上一拍”写进对比表。如果三种模型都不能稳定压过它的方向准确率,说明这个问题的可预测窗口本身就短,结论应该是缩短预测步长,而不是硬调模型。我去年接手某个时序评估时把这条写进了文档,评审反而更认可,因为它体现了对数据本质的判断,而不是盲目追求数字好看。

把这两个习惯放进你的项目交付,比十组超参数记录更有说服力。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询