简介:一套面向计算机相关专业毕业设计的时间序列预测完整项目,覆盖数据预处理、多种模型构建、效果评估与系统部署全流程,适合用作毕业设计、课程设计或深度学习入门实战。资源共二十二个文件,以Python源码为主,配合CSV时序数据、Markdown部署说明和数据集归档,整体仅一百七十四KB,虽小但目录清晰、模块完整。源码包含LSTM、ARIMA、SVM、ARMA等预测模型实现,并附带美股SP500与太阳黑子等真实数据集,从数据加载、模型训练到评价指标计算均有对应脚本,可对比不同算法的预测精度与建模思路;太阳黑子数据还提供差分序列,便于观察平稳化处理对预测的影响。部署资料同时提供Django与Flask两种方式,能快速搭建演示站点;代码均经测试通过,且作为导师认可的毕业设计成果,适合在此基础上二次扩展。已有103人浏览学习,对需要参考完整毕设方案的同学具有实用价值。
1. 毕业设计做时间序列预测:为什么模型总在测试集上好看,一部署就翻车
如果你正在做基于 Python 深度学习的时间序列预测毕业设计,大概率逃不过这个场景:训练集上 loss 一路下降,验证集曲线也贴合得像模像样,论文里的误差指标一个比一个漂亮。可等模型真正跑起来,用前 72 小时数据去预测后 24 小时,输出要么是一条平移的直线,要么把上一时刻的值原封不动地搬过来。这不是模型不够深,而是整个流程里某个环节出了问题,而毕设恰恰最容易在这里踩坑。
这篇文章围绕“基于 Python 深度学习的时间序列预测”这个方向,把数据构造、模型选型、训练验证到部署文档的全链路拆开讲清楚。你用 LSTM、Transformer 还是 Temporal Fusion Transformer 不是第一位的,第一位是把数据窗口、特征泄漏、还原尺度、服务化部署这几个环节想明白。下面是我的落地做法和踩过的坑,可以直接照着复现。
2. 时间序列预测项目怎么开场:任务类型、数据形态与基线模型
2.1 先定任务:单变量预测还是多变量预测
毕设里最常见的两种设定是单变量预测和多变量预测。单变量就是只有一条序列,比如某站点过去 720 天的日最高气温,预测未来 7 天。多变量则是多个特征同时输入,典型例子是空气质量预测,输入 PM2.5、PM10、SO₂、NO₂、风速、气压六列数据,输出下一小时的 PM2.5。
这两个任务的数据构造逻辑完全不同。单变量预测的输入张量通常是(样本数, 时间步长, 特征数),其中特征数就是 1;多变量预测的特征数会变成 5 到 20 不等。毕设论文里如果需要做对比实验,单变量更适合放 ARIMA、SVR 这些经典模型做对照,多变量则更适合展示深度模型的特征提取能力。我一般建议毕设选多变量加单变量混合:用单变量跑通基线,用多变量展示深度模型的优势,这样对比实验结果会比较饱满。
另一个要提前定的是预测步长。预测未来 1 步是简单模式,预测未来 24 步才是真正有挑战的模式。毕设答辩时老师大概率会问“你的模型能预测多远”,所以直接把预测步长定为 24,哪怕只预测一个关键指标,也比只做单步预测更有说服力。
2.2 数据从哪来:真实数据集、公开数据集与自造数据的优先级
时间序列预测毕设不推荐自己造数据,除非题目本身就是算法研究。答辩时评委一眼就能看出数据是正弦波拼接的还是真实采集的,数据的可信度直接影响论文的评审印象。公开数据集首选 UCI 和 GitHub 上的气象、电力、交通流量数据,这些数据有明确的物理含义。以电力数据集为例,包含每小时电力消耗、温度、湿度等字段,训练集和测试集是官方划分好的,你不需要纠结数据泄漏问题。
如果找不到合适的公开数据,也可以自己用爬虫抓天气数据。需要注意抓到的数据往往有缺失值和时间戳不对齐的问题,这反而是做数据清洗的素材,答辩时可以说“本文重点解决了真实场景下的数据缺失问题”。
2.3 环境准备与依赖清单
开始写代码前先把环境准备好。我统一用 Python 3.9 加 PyTorch 2.0 的组合,因为 PyTorch 在时间序列任务里的 Dataset 和 DataLoader 机制比 TensorFlow 更直观。
python -m venv ts_env source ts_env/bin/activate # Windows 用 ts_env\Scripts\activate pip install numpy pandas matplotlib pip install torch --index-url https://download.pytorch.org/whl/cu118 # GPU 版 pip install scikit-learn # 用于计算 MAE、RMSE 等指标 pip install tqdm # 训练进度条Windows 用户如果没装 CUDA,直接执行pip install torch会默认装 CPU 版,训练速度慢 10 倍以上,建议先用nvidia-smi确认显卡驱动支持的 CUDA 版本,再选择对应的 torch 安装轮子。这块出错最多的地方是版本不匹配导致 CUDA 不可用,启动训练时报错显示AssertionError: Torch not compiled with CUDA enabled,处理办法就是重新安装对应 CUDA 版本的 PyTorch。
3. 数据预处理与滑窗构造:时间序列预测里最容易被忽视的黑匣子
3.1 缺失值处理:不能用均值填充了事
时间序列的缺失值处理和普通表格数据完全不同。普通表格里某个字段缺失可以直接填均值,但时间序列里的相邻样本有时间相关性,简单的均值填充会把序列的短期波动信息抹掉。常见的做法是线性插值和前向填充。线性插值适合短时间缺失,例如某个小时的数据丢了,用前后两小时做线性插值;前向填充适合长时间缺失,例如连续一整天的数据没有采集,用前一天的同一时刻的值补上,因为气象数据通常有日周期特性。
import pandas as pd df = pd.read_csv('weather.csv', parse_dates=['date'], index_col='date') # 线性插值,限制连续缺失不超过 12 个点 df = df.interpolate(method='linear', limit=12, limit_direction='both') # 仍有缺失的用前一天同一时刻的值填充 df = df.fillna(method='ffill')这段代码的关键是limit=12,它限制连续插值只填补不超过 12 个缺失点,避免整段序列被插成一条直线。fillna(method='ffill')用在插值之后,补上长缺失的尾部。处理完缺失值后要做一个可视化的数据检查,把填充前后的曲线画在一起,确认插值没有在峰值区域产生明显变形。这一步很多人直接跳过,结果模型训练后预测值总比真实值低一截,找半天才发现是插值把尖峰削平了。
3.2 滑窗构造:时间步长和预测步长怎么选
深度学习时间序列预测的标准做法是把原始序列切成固定长度的滑动窗口。输入窗口长度input_len决定模型能看到多长的历史,输出长度pred_len决定模型要预测多远的未来。毕设里输入窗口一般取 72 或 168,对应过去三天或一周;预测步长取 24,对应未来一天。这两个参数的设定要结合数据的周期特性,如果数据有明显周规律,输入窗口至少包含一个完整周期。
def create_sequences(data, input_len=72, pred_len=24): X, y = [], [] for i in range(len(data) - input_len - pred_len + 1): X.append(data[i : i + input_len]) y.append(data[i + input_len : i + input_len + pred_len]) return np.array(X), np.array(y)滑窗构造代码里有一个训练集和测试集划分的隐患:直接用train_test_split(random_state=42)会打乱数据顺序,导致测试集里混入训练集之前的时间点,模型在测试集上使用了未来的数据。正确做法是按时间顺序切分,前 80% 作为训练集,后 20% 作为测试集。这两个数据集的边界之间要留出一段空白,避免滑窗把边界两边的时间点拼在一起产生重叠。
3.3 归一化的隐藏陷阱:预测结果还原尺度
时间序列预测的归一化和普通分类任务不一样。分类任务只需要归一化特征,预测结果不需要还原;时间序列任务则要把预测的目标值也归一化,评估指标时再反归一化回原始尺度。常见的做法是用训练集的均值和标准差来做归一化,测试集直接用训练集的 mean 和 std,而不是用测试集自己的统计量,否则测试集的分布信息会偷偷进入模型。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() train_data = scaler.fit_transform(train_df.values) # 只在训练集上 fit test_data = scaler.transform(test_df.values) # 测试集只 transform # 预测后还原 pred_original = scaler.inverse_transform(pred_normalized)这个代码块里最需要注意的就是fit_transform和transform的使用位置。如果把scaler.fit(test_df.values)也做了,测试集的均值和标准差就成了模型的特征信息,这在论文评审时属于数据泄漏,误差指标会被严重低估。还原尺度这个操作要放到评估和可视化之前,画预测曲线时一定要画原始尺度的曲线,归一化尺度的曲线答辩时没人看得懂。
4. 模型实验设计:LSTM 和 Transformer 怎么选,最少跑多少个模型才够
4.1 LSTM 基线:比想象中容易过拟合
毕设模型选型有一个不成文的规矩:先跑一个简单模型做基线,再跑你的核心模型。基线的意义是给核心模型一个对比的下限,如果核心模型连 LSTM 都比不过,那设计就不成立。单变量序列用 LSTM 做基线时网络结构不需要太复杂,两层 LSTM 加一个全连接输出层即可。
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_dim, hidden_dim=64, num_layers=2, pred_len=24): super().__init__() self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True, dropout=0.2) self.fc = nn.Linear(hidden_dim, pred_len) def forward(self, x): out, _ = self.lstm(x) # out: [batch, input_len, hidden_dim] out = out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.fc(out)这段代码里有几个关键参数需要调。hidden_dim=64是隐藏单元数量,数值太小模型拟合能力不足,太大容易过拟合;num_layers=2层数增加能捕捉更长时间依赖,但训练时间也会翻倍;dropout=0.2只对多层 LSTM 生效,单层设置 dropout 在 PyTorch 里会报警告。模型输出的pred_len=24是直接输出未来 24 个点的预测序列,这种方式比迭代预测更快,也是 Informer 等模型的标准做法。
跑完基线后观察 Loss 曲线,如果训练集 loss 远小于验证集 loss,说明过拟合了,优先加大 dropout,其次是减小hidden_dim。LSTM 的时序依赖靠隐藏状态传递,输入序列过长时后面时间步的信息会覆盖前面时间步的信息,这个问题在长序列预测里几乎无解,SU 是 Transformer 被引入时间序列预测的核心原因。
4.2 Transformer 变体:Informer 是毕设的稳妥选择
如果想在毕设里使用 Transformer 架构,不需要从零实现原版 Transformer,直接用 Informer 或 Autoformer 的公开实现更现实。Informer 的贡献是 ProbSparse 自注意力机制和蒸馏操作,它在长序列预测上的计算复杂度比标准 Transformer 低一个量级,而且能处理更长输入。
毕设使用 Transformer 时有一个明显的坑:公开实现里通常包含大量辅助代码,例如时间特征编码、嵌入层、蒸馏模块,新手很难分清哪些代码在做什么,修改参数时容易改错。我的建议是先把 Informer 官方代码在公开数据集上跑通,再逐步改输入输出。不要一上来就改代码结构,先看训练日志的 loss 和输出形状是否合理。
Transformer 类模型在时间序列预测中最大的特点是训练集表现极好,验证集表现平平,这是注意力机制记住了训练样本的模式。解决方法是减少模型维度或增加 early stopping,而不是盲目加大训练轮数。
4.3 实验矩阵怎么设计才够答辩用
毕设论文里至少要有四组实验:经典统计基线(ARIMA)、机器学习基线(SVR 或 XGBoost)、深度学习基线(LSTM)、核心模型(Informer/Transformer)。每组模型在相同的数据划分和相同的评估指标下对比,才具有说服力。实验记录表包含模型名、MAE、RMSE、MAPE 和训练时长。
| 模型 | MAE | RMSE | MAPE(%) | 训练时长 |
|---|---|---|---|---|
| ARIMA | 3.82 | 5.17 | 8.6 | 3 min |
| SVR | 3.45 | 4.92 | 7.8 | 5 min |
| LSTM | 2.91 | 4.03 | 6.2 | 12 min |
| Informer | 2.46 | 3.51 | 5.1 | 28 min |
毕设答辩时老师不只是看核心模型的结果,还会关注你是否理解了为什么核心模型比其他模型好。Informer 的优势在于自注意力机制能直接建模长距离依赖,而 LSTM 需要把信息逐步传递。这个解释要写在论文的实验分析部分,而不是只贴一张对比表。
4.4 提前停止和随机种子:两个影响公平对比的细节
对比实验必须在相同的随机种子下运行。PyTorch 里设置随机种子的代码要放在 import 之后第一时间执行。
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False如果不设置随机种子,LSTM 每跑一次结果都会不同,有时误差差距大到让结论反转。cudnn.deterministic = True让 CUDA 的卷积和循环操作使用确定性算法,代价是训练速度下降约 5% 到 10%。论文里的所有实验结果应该是同一个随机种子下的结果,并在论文中注明。
另一个影响公平对比的细节是早停轮的设置。我一般用验证集 loss 连续 10 个 epoch 不下降就停止训练。深度学习模型普遍有收敛慢的特点,很多模型训练到第 50 个 epoch 才开始明显下降,提前停止设太早反而会停在一个不优的点上。
5. 模型部署与推理接口:把训练好的模型封装成可调用服务
5.1 模型保存与加载:不要只存权重,要连带归一化参数一起存
很多人在部署环节踩的第一个坑是:模型在训练脚本里预测正常,换成部署脚本加载后预测结果完全不对。原因是部署时只加载了模型权重,没有加载归一化器的 mean 和 std,输入数据没有经过相同的标准化处理。因此保存模型时要一并保存归一化参数。
# 训练结束后保存 torch.save({ 'model_state_dict': model.state_dict(), 'input_dim': input_dim, 'hidden_dim': hidden_dim, 'num_layers': num_layers, 'pred_len': pred_len, 'scaler_mean': scaler.mean_, 'scaler_scale': scaler.scale_, 'input_len': input_len }, 'checkpoint/best_model.pth') # 部署时加载 ckpt = torch.load('checkpoint/best_model.pth', map_location='cpu') model = LSTMPredictor(ckpt['input_dim'], ckpt['hidden_dim'], ckpt['num_layers'], ckpt['pred_len']) model.load_state_dict(ckpt['model_state_dict']) model.eval()保存格式用字典而不是直接torch.save(model.state_dict()),好处是一次把所有配置和参数都记录下来。部署时需要把模型切到eval()模式,这个操作会关闭 dropout 和 batch normalization 的训练行为。模型推理结果要经过scaler.inverse_transform还原成原始数据的单位后再返回给前端。
5.2 用 FastAPI 封装预测接口
最常见的部署方式是封装成 REST API,输入一段历史序列返回未来预测值。FastAPI 相比 Flask 在性能和数据校验上更适合这个场景。
from fastapi import FastAPI, HTTPException from pydantic import BaseModel import numpy as np import torch app = FastAPI() ckpt = torch.load('checkpoint/best_model.pth', map_location='cpu') model = LSTMPredictor(ckpt['input_dim'], ckpt['hidden_dim'], ckpt['num_layers'], ckpt['pred_len']) model.load_state_dict(ckpt['model_state_dict']) model.eval() class InputData(BaseModel): history: list[float] # 输入长度必须等于 input_len @app.post('/predict') def predict(data: InputData): if len(data.history) != ckpt['input_len']: raise HTTPException(400, f'需要 {ckpt["input_len"]} 个历史时间点') x = np.array(data.history, dtype=np.float32) x = (x - ckpt['scaler_mean']) / ckpt['scaler_scale'] x = torch.tensor(x).unsqueeze(0).unsqueeze(-1) with torch.no_grad(): pred = model(x).numpy().flatten() pred = pred * ckpt['scaler_scale'] + ckpt['scaler_mean'] return {'prediction': pred.tolist()}接口里做了三项校验:输入长度必须等于训练时的input_len,归一化使用保存的 mean 和 scale,推理过程包在torch.no_grad()里。这三项缺一个都会在部署后出问题。启动服务后要立即用curl验证接口,确认输出不是 NaN。
curl -X POST http://127.0.0.1:8000/predict \ -H "Content-Type: application/json" \ -d '{"history": [20.1, 20.5, 21.0, 21.3, 20.8, 20.2, 19.9, 20.4]}'curl 验证成功后再接入前端。推荐用uvicorn app:app --host 0.0.0.0 --port 8000启动服务,本地测试时不要加--reload,这个参数会监听文件变化并重启服务,生产环境中会造成不必要的开销。
6. 毕设避坑与常见问题排查:数据泄漏、形状错误和训练不收敛
6.1 形状错误:torch Tensor 的维度对不上
现象:训练时报错RuntimeError: Expected 3D tensor, got 2D或size mismatch for LSTM。
原因:LSTM 输入要求三维张量(batch_size, sequence_len, input_dim),新手经常把二维的(batch_size, sequence_len)直接传进去,或者漏了unsqueeze(-1)增加特征维度。
解决:检查 DataLoader 输出的批次张量形状,加上x = x.unsqueeze(-1)将特征维度补上;模型前向传播时也可以用print(x.shape)打印形状辅助调试。这类错误不会让程序卡住,但会一直报错让人崩溃。
6.2 特征泄漏:模型指标好得离谱
现象:测试集 MAE 只有 0.05,但画出的预测曲线和真实值几乎重合。如果用这种模型去预测未来数据,效果立即变差。
原因:归一化时用了全部数据的均值和标准差,或者滑窗构造时训练集和测试集发生了重叠,测试集的信息已经泄露进训练过程。
解决:归一化只 fit 训练集,测试集只 transform;时间序列切分时用train = data[:int(len(data)*0.8)],test = data[int(len(data)*0.8):],两段之间留出input_len大小的空白区域。这个坑是毕设里最隐蔽的,因为模型在验证集上表现越好,越可能是泄漏而不是模型能力越强。
6.3 训练不收敛:Loss 初始就是 NaN
现象:第一个 epoch 的 loss 直接输出nan。
原因:最常见的是学习率设得太大或数据里有 NaN 值。时间序列数据里的 NaN 没清理干净,进入模型后梯度计算产生 NaN。
解决:先检查数据,df.isna().sum()看缺失值数量;再降低学习率到 0.0001 重新训练。如果数据没问题但 loss 仍然是 NaN,考虑是否使用了torch.autograd.set_detect_anomaly(True)定位具体是哪一层产生的 NaN。经验法则是用 Adam 优化器时学习率从 0.001 开始,如果第一个 epoch 出现nan,直接乘 0.1 再试。
7. 进阶技巧:把模型做到“能答辩还能用”的最后一步
7.1 用模型预测结果的业务合理性来验证
毕设模型部署后,很多人不知道下一步该做什么。我习惯用两个指标验证模型的实际可用性:预测曲线是否产生了“滞后”现象和极端值是否被拉平。如果模型预测的峰值比真实峰值低 20% 以上,说明模型偏向预测均值而不是捕捉变化趋势。这时候可以尝试把输入窗口改大,让模型看到更多历史信息。另外把预测结果按业务场景分析,例如电力预测中,预测误差在高峰时段是否远大于低谷时段,如果是,说明模型没有真正学到时间相关的特征。
7.2 多步预测的两种模式:直接预测和迭代预测的取舍
直接预测是模型一次性输出未来 24 个时间点,计算快,但长预测步的误差会更大。迭代预测是模型每次只预测未来 1 步,把预测结果拼到输入序列末尾再预测下一步,每一步的误差会累积但短期趋势保持得更好。对于毕业设计,我建议用直接预测展示模型能力,然后在讨论部分补充说明迭代预测的优缺点。答辩时提到这两种模式的取舍,老师会觉得你真的想过这个问题。
7.3 跨序列的泛化能力:用两份不同数据验证模型
如果精心准备的模型只在单一数据上表现好,换一份数据就失效,这不是一个成功的深度学习方案。我在最后通常会把训练好的模型直接拿到另一份不同来源的数据上跑一次,不做任何调参,记录这个跨数据的结果。跨数据结果通常会差一些,这是正常的,通过分析差距原因,例如数据分布差异或周期不同,能加深对模型容错性的理解。这也直接对应对答时“你的模型换一个场景还能用吗”这类问题。
以上这些做完,你的毕设就不只是“跑通了一份代码”,而是一套从数据到部署、从训练到验证的完整方案。做时间序列预测最大的玄学就是模型表现好不一定是模型好,数据处理的每一个环节都会影响最终结果,按这条链路走下来,翻车的概率会小很多。希望这篇实战笔记能帮到你。
本文还有配套的精品资源,点击获取