Python实现LSTM-GNN时间序列预测:从数据组织到模型搭建的完整指南
简介这份资源面向计算机、电子信息工程、数学等专业的学生与研究人员提供一套Python实现的LSTM-GNN时间序列预测完整代码可用于课程设计、期末大作业或毕业设计也适合希望深入理解时序建模与图神经网络结合思路的开发者。压缩包共82个文件约159KB以43个py脚本为核心覆盖模型定义、训练流程、图构建与数据预处理等模块另有26个csv结果文件、5个sql建表与标签脚本、4个md说明文档及txt、json等配置目录按src、graph_construction、eICU_preprocessing、results等分层组织结构清晰便于按模块查阅。资源附带案例数据可直接运行代码采用参数化编程关键参数集中管理注释较为详细便于修改与二次开发。目前已有228人学习下载读者可借此掌握LSTM与GNN融合建模的完整实现路径并对照结果文件验证不同模型在时序预测任务上的表现。1. 从一份 .rar 说起LSTM-GNN 做时间序列预测到底在解决什么很多人第一次看到「Python实现LSTM-GNN时间序列预测.rar」这类压缩包第一反应是找源码跑起来但真正卡住的地方往往不是代码而是不清楚 LSTM 和 GNN 为什么要拼在一起。时间序列预测的经典做法是 LSTM、GRU、TCN 这类序列模型它们擅长捕捉单条序列的时间依赖比如某一路传感器过去 24 小时的走势。但现实里的序列很少是孤立的一个城市的交通流量受相邻路段影响一个电站的负荷受同一电网其他节点牵动一只股票的波动和同板块其他标的联动。这些「变量之间的空间关系」用纯 LSTM 很难显式建模于是把图神经网络GNN引进来让每个时间步的节点特征先经过图卷积聚合邻居信息再喂给 LSTM 处理时间维度这就是 LSTM-GNN 的核心动机。这份压缩包对应的技术方向适合三类人做多变量、多节点预测的算法工程师手上有传感器网络、路网、电网、金融面板数据想比单变量 LSTM 再进一步的人以及想找一个能同时练序列建模和图建模的完整项目练手的 Python 学习者。它不解决「单条序列预测」这种简单问题用 LSTM 就够了硬上 GNN 只会增加调参负担。下面从数据组织、模型搭建、训练排错到进阶技巧把这条路走一遍。2. 数据怎么组织把时间序列变成「图 序列」双结构2.1 为什么普通滑窗喂不进 GNN标准 LSTM 预测的数据形状是(样本数, 时间步, 特征数)每个样本是一条独立序列。但 GNN 需要额外的邻接矩阵描述节点之间怎么连。如果你的数据是 N 个节点、每个节点 T 个时间步、每个节点 F 个特征那么一次前向传播要同时用到两块信息形状为(N, T, F)的节点特征张量和形状为(N, N)的邻接矩阵。很多人翻车就翻在这里——直接把(样本, 时间步, 特征)塞进图卷积维度对不上或者把节点维度和时间维度搞混。常见做法是固定图结构邻接矩阵在整个训练过程中不变节点数 N 固定。如果节点之间的关系随时间变化那属于动态图复杂度会高一个量级入门阶段不建议碰。我一般会先把邻接矩阵归一化避免高度节点在聚合时数值爆炸。2.2 构造邻接矩阵的三种可靠方式邻接矩阵的质量直接决定 GNN 部分有没有用。如果图是瞎连的GNN 反而引入噪声。三种常用构造方式方式适用场景关键参数注意点距离阈值路网、传感器阈值 rr 太小图断裂太大全连接相关性金融、负荷相关系数阈值需用训练集算防止未来信息泄漏先验知识电网、管网拓扑表最可靠但需要领域数据用相关性建图时有个血泪经验相关系数一定要在训练集上计算再应用到验证和测试集。如果拿全量数据算相关性等于把未来信息泄漏进图结构验证指标会虚高上线就崩。2.3 一个可复现的数据准备脚本下面这段代码把原始多节点时间序列切成滑窗样本并生成归一化邻接矩阵。假设原始数据是(T, N, F)即先时间、再节点、再特征。import numpy as np import pandas as pd def build_adjacency(data, threshold0.5): # data: (T, N, F)用训练段计算节点间相关性 T, N, F data.shape flat data.reshape(T, N * F) corr np.corrcoef(flat.T) # (N*F, N*F) corr corr[:N, :N] # 取节点块简化处理 adj (np.abs(corr) threshold).astype(np.float32) np.fill_diagonal(adj, 1.0) # 自环保留自身信息 deg adj.sum(axis1, keepdimsTrue) adj_norm adj / np.maximum(deg, 1e-6) # 行归一化 return adj_norm def make_windows(data, adj, window24, horizon1): # data: (T, N, F) - X:(S, window, N, F) y:(S, N, horizon) T, N, F data.shape X, y [], [] for t in range(T - window - horizon 1): X.append(data[t:twindow]) # (window, N, F) y.append(data[twindow:twindowhorizon, :, 0]) # 预测第0个特征 X np.transpose(np.array(X), (0, 2, 1, 3)) # (S, N, window, F) return X, np.array(y), adj逻辑说明build_adjacency用相关系数阈值建图并做行归一化行归一化让每个节点聚合邻居时权重和为 1训练更稳。make_windows把时间维切窗并把节点维提到第二维得到(样本, 节点, 时间步, 特征)这是后面模型输入的标准形状。参数上window是回看步数交通流量常用 12 到 24horizon是预测步数先做 1 步预测跑通再扩多步。threshold建议从 0.3 到 0.7 之间试观察验证集损失。提示节点数 N 很大时上千邻接矩阵会占大量内存考虑稀疏矩阵或只保留 top-k 邻居。3. 模型怎么搭图卷积和 LSTM 的拼接顺序有讲究3.1 先图后序列还是先序列后图这是 LSTM-GNN 最容易被问的问题。两种主流结构第一种是「先图后序列」每个时间步先用 GCN 对(N, F)做空间聚合得到(N, F)把所有时间步拼起来得到(N, T, F)再按节点维度送进 LSTM最后接全连接输出预测。这种结构空间信息在每个时间步都被刷新适合节点关系稳定的场景。第二种是「先序列后图」先用 LSTM 对每个节点独立编码时间维得到(N, H)再用 GCN 在节点间聚合最后输出。这种计算量小但空间聚合只做一次时间上的空间演化被丢掉了。我一般选第一种因为时间序列预测里空间关系往往随时间动态变化每个时间步都聚合更合理。代价是计算量随 T 线性增长window 别设太大。3.2 用 PyTorch 搭一个最小可跑模型下面是一个「先图后序列」的实现GCN 层手写避免依赖额外图库方便你直接跑。import torch import torch.nn as nn class GraphConv(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.linear nn.Linear(in_dim, out_dim) def forward(self, x, adj): # x: (B, N, F) adj: (N, N) support self.linear(x) # (B, N, out_dim) out torch.einsum(nn,bnf-bnf, adj, support) # 邻居聚合 return torch.relu(out) class LSTMSpatialTemporal(nn.Module): def __init__(self, feat_dim, gcn_hidden, lstm_hidden, horizon1): super().__init__() self.gcn GraphConv(feat_dim, gcn_hidden) self.lstm nn.LSTM(gcn_hidden, lstm_hidden, batch_firstTrue) self.head nn.Linear(lstm_hidden, horizon) def forward(self, x, adj): # x: (B, N, T, F) B, N, T, F x.shape x x.permute(0, 2, 1, 3).reshape(B * T, N, F) # 合并B,T x self.gcn(x, adj) # (B*T, N, H) x x.reshape(B, T, N, -1).permute(0, 2, 1, 3) # (B,N,T,H) x x.reshape(B * N, T, -1) # 每节点一条序列 out, _ self.lstm(x) # (B*N, T, hidden) out out[:, -1, :] # 取最后时间步 out self.head(out) # (B*N, horizon) return out.reshape(B, N, -1)逻辑说明GraphConv用einsum做邻接矩阵和节点特征的乘法等价于对每个节点加权求和邻居特征adj已归一化所以不用再除度。LSTMSpatialTemporal先把批次和时间维合并做图卷积再拆开把节点维合并进批次做 LSTM这样每个节点的时间序列独立过 LSTM最后取最后时间步预测。参数上gcn_hidden控制空间聚合后的维度一般 32 到 64lstm_hidden控制时间建模容量64 到 128 起步horizon是预测步数。3.3 训练循环和损失选择多节点预测的损失要对所有节点求平均。如果各节点量纲差异大先做标准化否则大数值节点会主导梯度。def train_one_epoch(model, loader, adj, optimizer, criterion): model.train() total 0.0 for x, y in loader: optimizer.zero_grad() pred model(x, adj) # (B, N, horizon) loss criterion(pred, y) # y: (B, N, horizon) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() total loss.item() * x.size(0) return total / len(loader.dataset)逻辑说明clip_grad_norm_是 LSTM 训练的后悔药梯度爆炸时能救一命阈值 5.0 是常用起点。损失用nn.MSELoss或nn.HuberLoss后者对异常值更稳。标准化建议按节点做 z-score保存训练集的均值和方差推理时复用。注意adj要放在和模型相同的设备上CPU/GPU 不一致会直接报错这是新手最常见的翻车点之一。4. 训练不收敛、指标虚高LSTM-GNN 的避坑清单4.1 损失震荡不下降先查图结构现象训练损失上下跳动验证损失不降。原因多半是邻接矩阵没归一化或者阈值设得太低导致图接近全连接聚合后数值尺度失控。解决确认adj行和为 1把阈值调高让图稀疏一些再检查输入特征是否标准化。4.2 验证指标好得离谱警惕信息泄漏现象验证集 MAE 低到不合常理测试集一塌糊涂。原因是用全量数据算了标准化参数或相关性图未来信息漏进了训练。解决所有统计量均值、方差、相关系数只在训练段计算验证和测试段用训练段的参数变换。4.3 节点数一多就显存爆现象N 到几百就 OOM。原因是(B*T, N, F)的中间张量和邻接矩阵都是稠密的。解决减小 batch 和 window邻接矩阵转稀疏或者用 top-k 邻居只保留每行最大的 k 个权重。k 取 10 到 20 通常够用。4.4 预测结果整体滞后一个时间步现象预测曲线形状对但总是慢半拍。原因是模型学到了「用上一时刻预测当前时刻」的平凡解尤其在强自相关序列上。解决预测目标改成差分或变化率或者在损失里加入对变化量的惩罚逼模型学趋势而非复制。4.5 LSTM 和 GCN 学习率不匹配现象图卷积部分梯度很小几乎不更新。原因是 GCN 和 LSTM 参数量、梯度尺度差异大共用一个学习率时一方被压制。解决给 GCN 和 LSTM 设置不同学习率GCN 用大一点如 1e-3LSTM 用小一点如 5e-4或者分别做 warmup。5. 进阶技巧让 LSTM-GNN 真正跑赢单变量 LSTM5.1 用残差连接稳住深层堆叠单层 GCN 加单层 LSTM 往往不够但直接堆深会梯度消失。我习惯在 GCN 输出和 LSTM 输入之间加残差把原始节点特征投影到相同维度后相加。这样即使 GCN 层数增加信息也能直通。代码上就是在GraphConv里加一条self.skip nn.Linear(in_dim, out_dim)前向时out relu(agg skip(x))。5.2 验证 GNN 到底有没有贡献很多人加了 GNN 但没验证它是否真的有用。一个干净的对比方法把邻接矩阵换成单位矩阵即每个节点只聚合自己其他不变跑一遍。如果指标和用真实邻接矩阵差不多说明图结构没带来增益要么图建错了要么数据本身空间相关性弱。这个对照实验能帮你判断该不该继续投入 GNN 这条线。5.3 多步预测的两种策略直接多步输出层维度设成 horizon一次预测多步误差不累积但难学。滚动多步每次预测一步把预测值拼回输入再预测下一步简单但误差会累积。我一般先用直接多步跑通horizon 不超过 3需要更长预测时再考虑滚动并监控误差随步数的增长曲线。5.4 一个我常犯的错早期我总想把 window 设得很大觉得看得越远越准结果训练慢、显存高、指标还更差。后来发现时间序列的有效记忆长度有限window 超过一定值后纯属浪费。现在我的习惯是从 12 开始每次加 6观察验证损失一旦不再下降就停。这个习惯帮我省了大量调参时间。希望帮到你。本文还有配套的精品资源点击获取