简介:一套用于温度预测的Python期末大作业项目,基于LSTM神经网络实现,面向计算机相关专业正在完成课程设计或期末大作业的学生,也适合需要时间序列预测实战经验的开发者。项目经导师指导并以98分评审通过,完整呈现数据探索、季节性分解、模型训练与结果可视化等环节,内容可靠。资源压缩包约1.51MB,共15个文件,涵盖5个Python脚本(负责模型搭建、训练、预测与保存)、6张PNG可视化图表(展示温度走势、真实值对比、训练与验证损失等)、1份Excel温度数据集、1个预训练h5模型文件以及1个Markdown说明文档,目录结构清晰,便于按流程学习。目前已有154人学习下载,读者可借此掌握LSTM在温度预测中的完整流程,包括数据预处理、序列构造、模型训练与评估,还能直接复用或改造代码,快速产出自己的期末项目。
1. 基于LSTM神经网络的温度预测:这道期末大作业为什么值得做
把LSTM神经网络用在温度预测上,是Python期末大作业选题里的一个常见选择,但它绝不是那种只为了凑代码量的题目。温度是一个典型的连续时间序列,历史温度之间有强相关性,今天的气温会深刻地影响明天和后天的气温,这种结构天然适合用LSTM来建模。相比用普通线性回归或者BP神经网络硬拟合,LSTM的循环结构能把时间维度的依赖关系真正学进去,这让它既能体现深度学习模型的威力,又不会像图像分类那样对训练资源要求过高。对期末这个场景来说,LSTM温度预测具备三个讨好老师的特征:数据公开易获得、代码量适中可控、结果能用曲线和误差指标直观展示,做出来既有技术深度又有交付感。这篇笔记就把项目从数据清洗到模型训练的完整流程和你需要提前规避的坑讲一遍,让新手能照着复现,也能让已经写完代码的同学检查边界和参数是否合理。
2. 数据准备与预处理:温度序列从CSV到Tensor的全部操作
2.1 数据长什么样:温度序列的采样频率与时间跨度
温度预测的第一个坑往往不在模型,而在数据本身。公开历史气象数据里最常见的温度格式是每小时一条或每天一条,记录字段包含时间戳、温度、湿度、风速、气压等。做LSTM温度预测时,你首先要确认的是采样频率和连续时长,因为这两个参数直接决定滑窗长度、序列步数和训练样本数量。
常见做法是选择小时级数据或日级数据。小时级数据的优点是样本量充足,一天有24个连续观测点,序列长度为7天时能切出168步的窗口;缺点是数据噪声偏大,夜间温度波动、冷空气过境都会让曲线出现毛刺。日级数据则平滑得多,但样本量骤减,一年只有365条,序列长度为30天时窗口里的步数并不算多,而且一天之中最高温最低温的差异被抹掉了。做期末项目我一般建议优先选小时级数据,因为滑窗能切出的样本更多,模型训练效果更好讲。
拿到原始数据后要做三件基础设施工作:检查时间戳是否连续、处理缺失值、剔除异常值。时间戳不连续最常见,比如某些气象站夜间不更新或采集设备掉线,导致时间序列出现空洞。对LSTM来说,空洞是致命问题,模型默认相邻两步间隔相同,中间缺一条数据会让邻接关系失真。处理办法一般是按照标准频率重采样,空值填充用前一天同时刻的值或前后线性插值,不要直接删掉那行,否则序列断裂。
2.2 滑窗切分:用过去多少步预测下一步
LSTM本身不会自动产生预测,它收到的是一个长度为seq_len的序列,输出是这个序列之后下一个时刻的值。所以数据预处理的核心操作是构造“输入窗口-目标标签”对。滑窗长度就是seq_len,它决定了模型能往前看多远。这里先给出一个通用的滑窗构造代码,后面再解释参数选择。
import numpy as np import pandas as pd # data_series: pd.Series, 按时间升序排列的温度序列 def create_sequences(data, seq_len=72): """ 将一维温度序列切分为 [输入窗口, 目标值] 的监督学习格式 seq_len: 用过去多少个小时/多少天的数据来预测下一点 """ xs, ys = [], [] for i in range(len(data) - seq_len): x = data[i:i + seq_len] # 输入窗口,连续seq_len个点 y = data[i + seq_len] # 下一个时刻的温度 xs.append(x) ys.append(y) return np.array(xs).reshape(-1, seq_len, 1), np.array(ys).reshape(-1, 1)这段代码的逻辑很直观:对每个位置i,取i到i+seq_len之间的所有温度值作为输入,第i+seq_len个温度值作为要预测的目标。循环遍历完整个序列后,就得到了一组时间窗口样本。注意reshape成二维数组后,第三步的维度1代表特征数量,这里暂时只有一个温度特征。
seq_len的选择直接决定训练样本量和模型能捕捉的周期长度。如果你的数据是小时级的,一天有24个点,seq_len=72就是看三天的温度做预测,这比较合理,既保留了昼夜周期,样本也没有被过多压缩。如果seq_len=720(看整月),对期末项目的计算资源来说训练速度慢,而且序列太长梯度在传播过程中更容易消失,效果反而可能变差。如果数据是日级的,seq_len一般取30或60,对应一个月或两个月的趋势记忆。初始训练我建议从seq_len=72开始,跑通后再尝试24和168对比效果。
2.3 归一化与数据集划分:先fit训练集,再transform测试集
温度序列的数值通常在零下十几度到零上四十度之间,直接喂给LSTM会带来两个副作用:第一,LSTM内部的sigmoid和tanh激活函数对输入范围敏感,输入数值过大会让梯度偏小甚至饱和;第二,不同地区的数据量纲差异大,模型对量纲产生依赖后泛化能力差。常见的处理是使用MinMaxScaler或StandardScaler,把数据压缩到0到1之间或变成均值为0、方差为1的标准正态分布。
from sklearn.preprocessing import MinMaxScaler # 原始温度二维数组 (n_samples, 1) scaler = MinMaxScaler(feature_range=(0, 1)) # 注意: 只对训练集做fit,再用同一个scaler转换测试集 train_len = int(len(temperature_data) * 0.8) scaled_train = scaler.fit_transform(temperature_data[:train_len]) scaled_test = scaler.transform(temperature_data[train_len:])上面这段代码有一点至关重要:fit_transform只在训练集上执行一次,测试集只调用transform。原因很直接,scaler如果看到全量数据的最大值和最小值,等于把未来信息泄漏到了标准化参数里。训练集看到的min和max和测试集不同,测试集还沿用训练集的标准,这才是真实的预测场景。另一点要注意的是,scaler保存后要保留到模型预测阶段,因为模型输出的是0到1之间的值,要还原成真实温度就必需调用scaler.inverse_transform。这个步骤常被漏掉,导致预测图和真实值对不上,后面避坑章节会重点展开。
数据切好窗口后还需要按时间顺序划分训练、验证、测试三份数据集。时间序列和普通分类任务不同,不能随机打乱,否则模型"看到"了未来的数据,测试集就失去了意义。一般用8:1:1的比例切分,严格保持时间顺序。验证集用来做early stopping和调超参数,测试集只在最终评估时用一次。
3. 模型搭建:基于PyTorch定义LSTM网络的完整代码与逐参数说明
3.1 PyTorch里定义LSTM:input_size、hidden_size、num_layers到底怎么设
PyTorch是目前实现LSTM最主流的框架,比手写循环单元可靠,也比较容易在答辩时讲清楚结构。我们来定义一个新类,把LSTM层和输出层封装在一起。
import torch import torch.nn as nn class TemperatureLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, seq_len=72, dropout=0.2): """ 用于温度预测的LSTM回归模型 :param input_size: 每个时间步的特征数量,只用温度特征时为1 :param hidden_size: 隐藏层神经元个数,控制模型容量 :param num_layers: LSTM堆叠层数,2层比较稳妥 :param seq_len: 输入序列长度 :param dropout: 多层LSTM之间的dropout比例,防止过拟合 """ super(TemperatureLSTM, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.seq_len = seq_len # LSTM层 self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0 ) # 输出层:把最后一个时间步的隐藏状态映射为温度值 self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x shape: (batch_size, seq_len, input_size) out, _ = self.lstm(x) # 取最后一个时间步的输出 last_out = out[:, -1, :] # 接全连接层,压缩成单个预测值 result = self.fc(last_out) return result这段代码里最值得解释的是out[:, -1, :]这行。LSTM会为输入序列的每个时间步都输出一个hidden_state,out的形状是(batch_size, seq_len, hidden_size)。我们要预测的是seq_len之后那一个时刻的温度,相当于把输入序列最后一个时间步的输出交给全连接层,所以取索引为-1的那个时间步。很多初学者会把整个out展平后再接全连接层,这是不必要的,还会引入大量冗余参数。
hidden_size取64是一个比较均衡的默认值。太小(比如16)会让模型表达能力不足,温度这种连续的强相关序列学得粗糙;太大(比如256)会让参数量剧增,训练需要更多数据和更多轮数,期末项目的数据量很难喂饱它。num_layers选2层时,模型能学到更高层的组合特征,一层的效果会偏弱。如果电脑跑得动,也可以尝试3层,但注意层数增加的同时参数量快速增长,训练速度和过拟合风险都同步上升。
3.2 训练循环:损失函数、优化器与early stopping的配合方式
模型定义好之后,训练循环是比较模板化的。温度预测是回归任务,损失函数用MSELoss即可。优化器常见的是Adam,不需要手动调整太多参数。训练过程中最需要设计的机制是early stopping,也就是当验证集loss连续多个epoch不下降时提前终止训练,防止模型在训练集上过度拟合。
import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset # model放在GPU或CPU上 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = TemperatureLSTM(input_size=1, hidden_size=64, num_layers=2, seq_len=72).to(device) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.001) # 把训练数据转换成Tensor的Dataset train_dataset = TensorDataset(x_train_tensor, y_train_tensor) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) val_dataset = TensorDataset(x_val_tensor, y_val_tensor) val_loader = DataLoader(val_dataset, batch_size=64) def valid_loss(model, loader): model.eval() total_loss, total_cnt = 0.0, 0 with torch.no_grad(): for xb, yb in loader: xb, yb = xb.to(device), yb.to(device) pred = model(xb) loss = criterion(pred, yb) total_loss += loss.item() * xb.size(0) total_cnt += xb.size(0) return total_loss / total_cnt best_val = float('inf') patience, trigger = 20, 0 epochs = 100 for epoch in range(epochs): model.train() train_epoch_loss = 0.0 train_cnt = 0 for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) optimizer.zero_grad() output = model(xb) loss = criterion(output, yb) loss.backward() optimizer.step() train_epoch_loss += loss.item() * xb.size(0) train_cnt += xb.size(0) val_loss = valid_loss(model, val_loader) train_avg = train_epoch_loss / train_cnt if (epoch + 1) % 10 == 0 or epoch == 0: print(f"Epoch {epoch+1:3d}/{epochs} | train_loss: {train_avg:.5f} | val_loss: {val_loss:.5f}") # early stopping: 连续patience个epoch val_loss不创新低就停 if val_loss < best_val: best_val = val_loss trigger = 0 torch.save(model.state_dict(), "best_model.pt") else: trigger += 1 if trigger >= patience: print(f"Early stopping at epoch {epoch+1}") break训练循环里的核心机制有三个。第一个是DataLoader的shuffle只在训练集打开,验证集不开,保证验证时的数据顺序不干扰loss计算。第二个是每个epoch都做一次验证,用验证集判断模型当前状态,而不是等训练全部结束才评估。第三个是best_model.pt保存的是验证集loss最低的那一版参数,如果训练过程中发生过拟合,最终加载这个文件而非最后一轮的参数,能得到一个更稳定的模型。
关于优化器的细节也值得说两句,learning_rate用0.001起步是行业默认值,LSTM对学习率比卷积网络更敏感,lr大于0.01时loss非常容易震荡。如果训练100轮还没有下降到预期水平,可以试试把lr减半再从头训练,这比盲目调网络结构更直接。
3.3 预测与反归一化:把模型的输出还原成真实温度
模型训练完成后,预测流程包含三个层次:加载最优权重、对测试集做前向推理、把归一化后的预测值还原成真实温度。反归一化这步被漏掉的案例非常多,切了窗口后直接inverse_transform会报形状错误;忘记存scaler甚至无法还原;测试集预测结果和真实值画出来图形差了一个均值偏移——这些现象本质上都是反归一化环节出错。
model.load_state_dict(torch.load("best_model.pt", map_location=device)) model.eval() test_pred_list = [] test_true_list = [] with torch.no_grad(): for xb, yb in test_loader: xb = xb.to(device) pred = model(xb).cpu().numpy() test_pred_list.append(pred) test_true_list.append(yb.numpy()) pred_all = np.concatenate(test_pred_list, axis=0) true_all = np.concatenate(test_true_list, axis=0) # 还原为真实温度值:这里必须用和训练时同一个scaler pred_temp = scaler.inverse_transform(pred_all).flatten() true_temp = scaler.inverse_transform(true_all).flatten()这里用scaler.inverse_transform把预测结果和真实值同时还原回真实温度刻度。有人只还原预测值而不还原真实值,对比图就是一条0到1的线和一条几十度的线叠在一起。虽然最后你可能发现模型没毛病,但图表上无法直观呈现给老师看。数据形状也要留个心眼:scaler接受二维输入,所以pred_all和true_all必须是(n_samples, 1)的形状再传入。
4. 训练与评估:超参数设置、指标计算与源码包文档结构
4.1 必调参数清单:seq_len、hidden_size、batch_size、lr
LSTM温度预测项目的最终效果,80%取决于数据预处理和超参数设置,而不是模型本身的高深改动。这里给一张参数清单,覆盖我跑这个项目时的起点和调整方向。
| 参数 | 常用起点 | 调整方向 | 原因 |
|---|---|---|---|
| seq_len | 72(小时级) | 24 / 168 对比 | 过短学不到昼夜周期,过长梯度消失 |
| hidden_size | 64 | 32 / 128 | 偏小欠拟合,偏大训练慢且过拟合 |
| num_layers | 2 | 1 / 3 | 1层欠拟合,3层训练耗时上涨 |
| batch_size | 64 | 32 / 128 | 影响收敛速度,64是均衡点 |
| learning_rate | 0.001 | 0.005 / 0.0005 | 过大大loss震荡,过小收敛慢 |
| patience | 20 | 10 / 30 | 决定早停快慢,过小容易早停过早 |
| dropout | 0.2 | 0.1 / 0.3 | 防止多隐藏层过拟合 |
调参的推荐顺序是固定seq_len和hidden_size之后先调lr,lr稳定了再调batch_size,最后才去动网络结构。原因很简单,lr问题导致的loss震荡会被误以为是模型结构问题,白花很多时间。训练时观察epoch的train_loss和val_loss两条曲线是最直接的诊断方式:train降而val不降,就是在过拟合;train和loss都不降,可能是lr太小或数据泄漏;train剧烈震荡,是lr太大或者梯度异常。
4.2 回归任务指标:MAE、RMSE、R²分别代表什么
期末答辩时老师一定会问"你的模型效果到底怎么样",不能用一句"loss是0.01"糊弄过去。loss是归一化空间里的误差,老师没有直观感受,你需要转换到真实温度刻度计算回归指标,这组数字才是交付结论。
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score mae = mean_absolute_error(true_temp, pred_temp) rmse = np.sqrt(mean_squared_error(true_temp, pred_temp)) r2 = r2_score(true_temp, pred_temp) print(f"MAE : {mae:.3f} ℃") print(f"RMSE: {rmse:.3f} ℃") print(f"R² : {r2:.3f}")MAE是平均绝对误差,单位是摄氏度,直观含义是"平均每个预测点偏离真实值多少度",2度以内算是能用的模型,超过3度需要检查数据和模型结构。RMSE因为是平方项,对极大误差更敏感,如果RMSE明显大于MAE,说明存在少数预测点误差非常大,这往往是局部天气突变或者数据异常导致的。R²则代表模型解释了多少方差,通常0.95以上说明曲线拟合得很好,但对于温度这种平滑序列,R²高是正常的,不代表模型一定有强泛化能力。这三个指标配合测试集的预测曲线图,是答辩时最有说服力的材料。
4.3 源码包的文档说明怎么写:README、代码结构和答辩思路
标题里的"文档说明"对期末大作业来说不是装饰品,而是评分的一部分。一份能让老师快速理解项目结构的文档,通常包含以下六个部分,用Markdown编写即可:
- 项目简介:用150字说清楚干了什么,地址是什么,用LSTM预测温度。
- 环境依赖:列python版本和所需的torch、numpy、pandas、sklearn、matplotlib依赖,建议附requirements.txt。
- 项目结构:用tree命令输出的文件层级,把data、model、train.py、utils.py、README.md标出来。
- 数据来源与预处理:描述数据集的时间跨度、采样频率、缺失值处理方式、滑窗参数。
- 模型结构:简要说明LSTM层数、hidden_size、全连接层,并附一张手绘网络结构图。
- 训练结果:放train/val loss曲线、预测对比图、MAE/RMSE/R²指标。
源码包的组织方式一般建议拆成四个文件:data_loader.py负责数据读取和滑窗,model.py负责模型定义,train.py负责训练和验证,predict.py负责加载最优模型并出预测图。不要把全部代码堆进一个脚本里,虽然能跑,但答辩时老师追问代码结构会很被动。
5. 避坑指南:LSTM温度预测的五个常见翻车点与解决办法
5.1 现象:预测曲线整体向上或向下偏移,真实趋势对不上
原因:归一化时对整个数据集执行了fit_transform,而不是分训练测试两步,导致测试集合的min/max参与计算,模型输出的归一化数值和真实值之间产生系统性偏移。
解决:严格按照先切分再归一化的顺序,训练集做fit_transform,测试集只做transform,保存scaler到本地文件,预测结束后用同一个scaler做inverse_transform。
5.2 现象:模型训练时loss平滑下降,测试集误差却大得离谱
原因:数据做了随机打乱,或者序列切分时跨了时间段边界。模型的记忆里既有历史信息又有未来信息,训练时看起来收敛正常,一旦面对没见过的真实预测区间,表现就会雪崩。
解决:切分数据集前保持原始时间顺序,先按8:1:1比例分割再切窗口。如果要从不同位置单独构造窗口,也必须在同一个时间段内部进行。
5.3 现象:训练中loss突然跳高,甚至出现NaN
原因:学习率设置过高会导致梯度更新幅度过大,plus温度序列的值经过LSTM内部多层tanh叠加,可能出现梯度爆炸。另一常见原因是输入数据里混入了极大或极小的异常值,归一化后仍然有离群点。
解决:把lr降到0.0005或0.0001;检查数据集中是否有填充失败遗留的负无穷、空值等脏数据;或对loss做梯度裁剪,torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)可以兜底,防止某个batch把参数冲开。
5.4 现象:train_loss一路下降,val_loss在某个epoch后反而连续上升
原因:典型的过拟合,模型把训练集里噪声也学进去了,对于温度这种本身有气候噪声和时间波动的序列,数据量不大的情况下很容易出现过拟合。
解决:早停机制在val_loss连续上升后停止训练;把dropout从0.2调到0.3;增加seq_len可以提升模型视野的同时削弱噪声影响。观察过拟合时刻的epoch数,如果太早说明模型容量过剩,可以减小hidden_size。
5.5 现象:训练结果每次复现都不一样,同一模型跑多轮指标差距很大
原因:torch在没有设置随机种子时,模型参数的初始值、dropout失效模式、batch顺序都是随机的,这些因素叠加会让结果出现波动。这不算bug,但期末答辩时如果两次运行结果差异明显,很容易留下"代码不稳定"的印象。
解决:在训练脚本开头固定随机种子。
import random import numpy as np import torch SEED = 42 random.seed(SEED) np.random.seed(SEED) torch.manual_seed(SEED) torch.cuda.manual_seed_all(SEED)这五行代码加上后,相同的数据和相同环境下的训练结果是可复现的。不过注意在GPU上运行时某些操作仍然有极小概率产生浮点误差,CPU环境复现性更好。答辩前先跑一次,再把结果图保存好,不要现场现跑。
6. 答辩加分技巧:特征扩展、多步预测与验证方法
模型跑通之后,下一步是让这个项目在答辩时有增量亮点。温度预测入门版只用温度本身做输入,但气象学和工程实践里,湿度、气压、风速、以及时间特征(星期几、几点钟、是否节假日)都对温度有影响。给序列加特征是性价比很高的改进方向:把时间戳转成hour_of_day和day_of_year两个特征,风压湿温四个气象变量一起喂给模型,input_size从1变成6,代码改动很少,但指标通常能明显改善。这类改动在答辩时非常容易讲清楚,因为你是在给模型加信息量。
另一个值得尝试的是多步预测,而不是单步递归滚动。单步预测只能看到24小时后的下一个小时,模型无法直接给出未来24个小时的曲线。常见做法是递归预测:把预测值拼到输入序列末尾,删掉最早的那个点,这样滚动构造出未来24小时或168小时的预测序列。这个方案实现不复杂,但误差会随步数累积,预测得越远曲线越平滑、越偏离真值。答辩时如果能把这个误差累积的现象画出来,配上"未来24小时预测-滚动过程导致误差增大"的解释,会对评委的冲击力很强。
验证方法上,除了分测试集,还有一个值得做的实验:切换数据集验证泛化能力。从另一个城市或另一个季节拉一段温度数据,直接用之前保存的模型参数预测,如果MAE依然在2度左右,说明模型学到的是温度变化规律而不是死记了一条曲线。这是期末答辩最容易出彩的部分,因为绝大多数同学的模型只在自己划分的测试集上验证过,换了城市和季节还能保持稳定,已经超出课程要求了。
最后分享一个我自己最深刻的教训:整个流程从数据清洗到最终出图,最耗费时间的永远是第一版参数调通,不要指望一次定死seq_len和hidden_size就能拿到理想结果。先跑通最小规模(seq_len=24,hidden_size=32),确认训练流程没问题后再把参数调大,每一步只动一个变量,记录对应指标变化。保持这样的习惯,最终指标不会太差,你也能在答辩时从容地回答"为什么这么设置"。希望这篇笔记里的代码和参数能帮你在期末阶段少走几条弯路,把时间留给真正值得打磨的结果分析上。
本文还有配套的精品资源,点击获取