
简介这份资源面向Python开发初学者、深度学习入门者及气象数据方向的学生与开发者提供一套基于LSTM的气温预测与可视化完整项目可用于毕业设计、课程设计或算法实践。项目从中国天气网爬取北京、上海、广州、郑州四城多年气温数据经预处理后构建时间序列样本以过去5天特征预测次日温度并借助Keras搭建LSTM网络完成训练、评估与结果可视化。压缩包共16个文件约721KB包含8个py源码文件、4个pyc编译文件、2个xlsx数据表与2个md说明文档源码覆盖爬虫、数据处理、模型定义、训练与演示等模块文档则交代项目背景与运行方式。目前已有281人学习下载。读者可据此掌握数据爬取、时序建模、超参数调优与可视化分析的完整链路并借助注释与说明快速理解设计思路与结果解读。1. 气温预测这件事LSTM 到底比传统方法强在哪拿历史气温数据预测未来几天的温度听起来像个老掉牙的回归问题。我一开始也这么想随手用 ARIMA 跑了一版结果在换季那几周误差直接飙到 4℃ 以上——模型完全没抓住温度变化的非线性拐点。后来换成 LSTM 做时间序列预测同样的数据MAE 从 3.2 降到 1.4 左右这才意识到序列建模里「记住多久之前的信息」是个关键变量。这个方案要解决的问题很具体给你一份逐日或逐小时的气温 CSV用 Python 搭一个 LSTM 网络训练完之后能预测未来 N 天的温度并且把历史曲线和预测曲线画在一张图上看趋势。适合有 Python 基础、想入门深度学习时间序列的开发者也适合做气象、农业、能耗分析方向、需要快速搭一个预测原型的从业者。整条链路不复杂数据清洗 → 构造滑动窗口 → 搭 LSTM 模型 → 训练 → 预测 → 可视化。但每一步都有几个参数能让你翻车后面逐章拆。2. 数据准备与滑动窗口构造LSTM 的输入到底长什么样2.1 气温数据清洗的三个必做步骤气温数据最常见的来源是气象站导出的 CSV字段一般包含日期、最高温、最低温、平均温、湿度、风速等。原始数据几乎不可能直接喂给模型我一般按这个顺序处理第一步处理缺失值。气温序列的缺失不能简单填 0那会制造出虚假的骤降。常见做法是用线性插值pandas.DataFrame.interpolate(methodlinear)如果连续缺失超过 3 天考虑直接删掉那段或者用同期历史均值填充。第二步处理异常值。比如传感器故障导致某天温度记录为 99℃这种值要识别出来替换掉。我一般用 IQR 方法做粗筛import pandas as pd import numpy as np df pd.read_csv(temperature.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 线性插值补缺失 df[avg_temp] df[avg_temp].interpolate(methodlinear) # IQR 异常值检测 Q1 df[avg_temp].quantile(0.25) Q3 df[avg_temp].quantile(0.75) IQR Q3 - Q1 lower Q1 - 1.5 * IQR upper Q3 1.5 * IQR # 把异常值替换为 NaN 再插值 df.loc[(df[avg_temp] lower) | (df[avg_temp] upper), avg_temp] np.nan df[avg_temp] df[avg_temp].interpolate(methodlinear) print(f处理后数据范围: {df[avg_temp].min():.1f} ~ {df[avg_temp].max():.1f}) print(f剩余缺失值: {df[avg_temp].isna().sum()})这段代码的逻辑是先用线性插值补掉零散缺失再用 IQR 圈出合理范围超出范围的标记为异常后二次插值。参数上1.5 倍 IQR 是标准做法如果你的数据本身波动就大比如高原地区昼夜温差极端可以放宽到 2.0 甚至 2.5但别超过 3.0否则异常值就筛不出来了。第三步归一化。LSTM 对输入尺度敏感气温一般在 -30 到 45 之间用 MinMaxScaler 压到 [0,1] 是最稳妥的from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) data_scaled scaler.fit_transform(df[[avg_temp]].values)注意scaler 必须只在训练集上 fit然后 transform 验证集和测试集。如果全量数据一起 fit就造成了数据泄露验证集上的误差会虚低上线后直接翻车。2.2 滑动窗口用多少天预测下一天LSTM 的输入是序列不是单点。假设你用过去 30 天的气温预测第 31 天那就要把数据切成一个个长度为 30 的窗口。这个窗口长度time_step是 LSTM 气温预测里最重要的超参数之一。def create_sequences(data, time_step30): X, y [], [] for i in range(len(data) - time_step): X.append(data[i:i time_step, 0]) y.append(data[i time_step, 0]) return np.array(X), np.array(y) time_step 30 X, y create_sequences(data_scaled, time_step) # 重塑为 LSTM 需要的 3D 输入: [样本数, 时间步, 特征数] X X.reshape(X.shape[0], X.shape[1], 1) # 按 7:2:1 划分训练/验证/测试 train_size int(len(X) * 0.7) val_size int(len(X) * 0.2) X_train, y_train X[:train_size], y[:train_size] X_val, y_val X[train_size:train_size val_size], y[train_size:train_size val_size] X_test, y_test X[train_size val_size:], y[train_size val_size:] print(f训练集: {X_train.shape}, 验证集: {X_val.shape}, 测试集: {X_test.shape})time_step 怎么选我的经验是日数据用 14~30小时数据用 48~72。太小了模型看不到周期性气温有明显的周波动和季节波动太大了训练慢且容易过拟合。如果你不确定可以跑一组对比实验分别试 7、14、30、60看验证集 MAE 哪个最低。别用测试集来选超参数那是自欺欺人。另外注意划分方式时间序列不能随机打乱再划分必须按时间顺序切。随机划分会让未来数据泄露到训练集里模型在测试集上表现好得离谱实际部署时完全不能用。3. 搭 LSTM 模型层数、隐藏单元和 Dropout 怎么定3.1 PyTorch 版 LSTM 网络结构用 PyTorch 搭一个两层 LSTM 加全连接输出的结构这是气温预测里最常用的配置import torch import torch.nn as nn class TempLSTM(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.2): super(TempLSTM, self).__init__() self.hidden_size hidden_size self.num_layers num_layers self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # h0, c0 初始化为全零 h0 torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) c0 torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) out, _ self.lstm(x, (h0, c0)) # 只取最后一个时间步的输出 out self.fc(out[:, -1, :]) return out逐参数说明。input_size1是因为我们只用气温一个特征如果你加了湿度、风速就改成对应的特征数。hidden_size64是隐藏层维度控制模型记忆容量气温预测一般 32~128 够用数据量小于 1000 条时建议 32大于 5000 条可以上 128。num_layers2是堆两层 LSTM第一层输出序列给第二层继续提取特征层数超过 3 层在气温这种低维数据上几乎不会带来提升反而增加过拟合风险。dropout0.2只在多层 LSTM 的层间生效单层 LSTM 设了也没用。forward里out[:, -1, :]取的是最后一个时间步的隐藏状态因为我们要预测的就是窗口末尾之后那一个值。如果你要预测未来 7 天可以把输出改成nn.Linear(hidden_size, 7)一次输出 7 个值。3.2 训练循环与学习率调度训练部分用 Adam 优化器加 MSE 损失配合 ReduceLROnPlateau 做学习率衰减from torch.utils.data import DataLoader, TensorDataset from torch.optim.lr_scheduler import ReduceLROnPlateau device torch.device(cuda if torch.cuda.is_available() else cpu) # 转成 Tensor train_ds TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) val_ds TensorDataset(torch.FloatTensor(X_val), torch.FloatTensor(y_val)) train_loader DataLoader(train_ds, batch_size32, shuffleTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse) model TempLSTM(input_size1, hidden_size64, num_layers2, dropout0.2).to(device) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience10) best_val_loss float(inf) patience_counter 0 early_stop_patience 20 for epoch in range(200): model.train() train_loss 0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() pred model(xb) loss criterion(pred, yb.unsqueeze(1)) loss.backward() # 梯度裁剪防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() # 验证 model.eval() val_loss 0 with torch.no_grad(): for xb, yb in val_loader: xb, yb xb.to(device), yb.to(device) pred model(xb) val_loss criterion(pred, yb.unsqueeze(1)).item() avg_val_loss val_loss / len(val_loader) scheduler.step(avg_val_loss) # 早停 if avg_val_loss best_val_loss: best_val_loss avg_val_loss patience_counter 0 torch.save(model.state_dict(), best_lstm.pth) else: patience_counter 1 if patience_counter early_stop_patience: print(fEarly stop at epoch {epoch}) break if epoch % 20 0: print(fEpoch {epoch}: train_loss{train_loss/len(train_loader):.6f}, val_loss{avg_val_loss:.6f})几个关键点。batch_size32是常用起点数据量小可以降到 16数据量大可以升到 64 或 128。clip_grad_norm_的 max_norm1.0 是 LSTM 训练的标配不加这个遇到梯度爆炸 loss 会直接变 NaN。ReduceLROnPlateau的 patience10 意思是验证损失连续 10 个 epoch 不降就砍半学习率factor0.5 是砍半系数。早停 patience 设 20意思是验证损失连续 20 个 epoch 没刷新最优就停这个值别设太小LSTM 有时候会在第 30 个 epoch 才突然下降。注意如果你发现训练 loss 一直在降但验证 loss 从第 5 个 epoch 就开始涨说明模型容量太大或者数据太少先把 hidden_size 降到 32、num_layers 降到 1 试试。4. 预测结果反归一化与可视化把曲线画对才有说服力4.1 反归一化与评估指标计算模型输出的是 [0,1] 之间的值必须用之前 fit 好的 scaler 反变换回真实温度model.load_state_dict(torch.load(best_lstm.pth)) model.eval() with torch.no_grad(): X_test_tensor torch.FloatTensor(X_test).to(device) pred_scaled model(X_test_tensor).cpu().numpy() # 反归一化 pred_real scaler.inverse_transform(pred_scaled) y_test_real scaler.inverse_transform(y_test.reshape(-1, 1)) # 计算指标 from sklearn.metrics import mean_absolute_error, mean_squared_error mae mean_absolute_error(y_test_real, pred_real) rmse np.sqrt(mean_squared_error(y_test_real, pred_real)) mape np.mean(np.abs((y_test_real - pred_real) / y_test_real)) * 100 print(fMAE: {mae:.2f}℃) print(fRMSE: {rmse:.2f}℃) print(fMAPE: {mape:.2f}%)反归一化这一步经常有人忘导致画出来的预测曲线在 0~1 之间跟真实温度完全对不上。MAE 和 RMSE 的单位都是摄氏度直接看数值就知道误差多大。MAPE 是百分比但气温有可能接近 0℃这时候 MAPE 会爆炸所以气温预测里我更看 MAE。4.2 用 Matplotlib 画历史 vs 预测对比图可视化是这类项目的门面图画不好别人根本不会看你的指标import matplotlib.pyplot as plt import matplotlib.dates as mdates plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(14, 5)) # 取测试集对应的时间索引 test_dates df[date].iloc[-len(y_test_real):].values ax.plot(test_dates, y_test_real, label真实气温, color#2c7fb8, linewidth1.5) ax.plot(test_dates, pred_real, labelLSTM 预测, color#e6550d, linewidth1.5, linestyle--) ax.set_xlabel(日期) ax.set_ylabel(气温 (℃)) ax.set_title(LSTM 气温预测 vs 真实值) ax.legend() ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) ax.xaxis.set_major_locator(mdates.MonthLocator(interval2)) plt.xticks(rotation45) plt.tight_layout() plt.savefig(prediction_result.png, dpi150) plt.show()画图有三个容易踩的坑。第一中文字体不设置会显示方块SimHei在 Windows 上一般没问题Linux 上可能需要换成WenQuanYi Micro Hei。第二x 轴日期要跟测试集对齐别把训练集的日期也画进去否则曲线长度对不上。第三如果预测曲线整体比真实曲线低一截说明模型有系统性偏差检查一下训练集和测试集的温度分布是不是差太多。如果你要做更花哨的可视化大屏可以把预测结果导出成 JSON前端用 ECharts 渲染。但就项目本身而言Matplotlib 出的图已经足够说明问题。5. 避坑指南LSTM 气温预测里最容易翻车的 5 个地方5.1 现象训练 loss 正常下降验证 loss 从第 3 个 epoch 就开始飙升原因模型过拟合。气温数据如果只有一两年样本量可能就几百条两层 LSTM 64 隐藏单元对这点数据来说太复杂了。解决先把 num_layers 降到 1hidden_size 降到 32dropout 提到 0.3。如果还不行加 L2 正则化weight_decay1e-4。再不行就说明数据量根本不够得去补数据至少 3 年以上的日数据才比较稳。5.2 现象预测曲线是一条几乎水平的直线原因模型没学到东西输出收敛到了均值附近。常见于学习率太大导致训练初期就跳到了局部最优或者输入数据没归一化。解决检查归一化是否做了检查学习率是不是 1e-3 以上试试降到 1e-4。另外确认 time_step 是不是太小比如设了 3模型看不到任何周期规律自然只能输出均值。5.3 现象反归一化后预测值全部偏移了同一个固定量原因scaler 在训练集上 fit 之后测试集 transform 时用的还是训练集的 min/max。如果测试集的温度范围超出了训练集反变换就会产生系统偏差。解决这是正常现象不是 bug。但如果偏差超过 2℃说明训练集和测试集分布差异太大应该重新划分数据或者用全体数据的 min/max 做归一化但要注意数据泄露风险。折中做法是用训练集的 min/max但在划分数据时确保测试集不超出训练集范围太多。5.4 现象GPU 上训练 loss 变成 NaN原因梯度爆炸。LSTM 的循环结构在反向传播时梯度容易累积放大尤其是序列较长时。解决加梯度裁剪clip_grad_norm_(model.parameters(), max_norm1.0)这个基本能解决 90% 的 NaN 问题。如果还不行把学习率降到 1e-4或者把 time_step 从 60 降到 30。5.5 现象同样的代码换个数据集 MAE 从 1.2 变成 5.8原因不同地区的气温波动幅度差异很大。沿海城市全年温差可能就 20℃内陆城市能到 40℃。模型在波动大的数据上绝对误差自然更大。解决不要跨数据集比较 MAE 绝对值要看 MAPE 或者把 MAE 除以温度标准差做归一化。另外波动大的数据需要更大的 hidden_size 和更长的 time_step因为模型需要记住更复杂的模式。6. 多步预测与滚动预测从「预测明天」到「预测一周」单步预测只能输出下一天的温度实际用的时候往往需要未来 3~7 天的预报。有两种做法直接多输出和滚动预测。直接多输出就是把nn.Linear(hidden_size, 1)改成nn.Linear(hidden_size, 7)标签也改成未来 7 天的温度向量。这样做简单但 7 个输出之间没有依赖关系可能出现第 3 天比第 2 天突然跳 10℃ 的不合理情况。滚动预测是每次只预测下一天然后把预测值追加到输入序列末尾再预测下一天。代码大概长这样def rolling_predict(model, last_sequence, steps7): last_sequence: 最后 time_step 天的归一化数据, shape(1, time_step, 1) model.eval() predictions [] current_seq last_sequence.copy() with torch.no_grad(): for _ in range(steps): x torch.FloatTensor(current_seq).to(device) pred model(x).cpu().numpy()[0, 0] predictions.append(pred) # 把预测值追加到序列末尾去掉最早的一天 current_seq np.append(current_seq[:, 1:, :], [[[pred]]], axis1) return np.array(predictions) # 取测试集最后 30 天作为种子 last_seq X_test[-1:] # shape: (1, 30, 1) future_scaled rolling_predict(model, last_seq, steps7) future_real scaler.inverse_transform(future_scaled.reshape(-1, 1)) print(未来 7 天预测温度:, future_real.flatten().round(1))滚动预测的优点是预测值之间有连续性不会出现跳变。缺点是误差会累积——第 1 天预测偏了 0.5℃第 2 天基于偏了的输入再预测误差可能变成 0.8℃到第 7 天可能就偏了 2℃ 以上。我的经验是滚动预测超过 5 天就不太可靠了如果非要预测 7 天以上建议用直接多输出 滚动预测结合先用直接多输出得到粗预测再用滚动预测做微调。验证多步预测效果的时候别只看最后一天的误差要看每一天的 MAE 变化曲线。如果第 1 天 MAE 是 1.0℃第 3 天变成 1.5℃第 7 天变成 3.0℃这个衰减速度是可以接受的。如果第 2 天就跳到 3℃ 以上说明模型的单步预测本身就不够准得先回去把单步预测做好。还有一个实用技巧如果你的数据有明确的季节性比如夏季和冬季的温度模式完全不同可以按季节分别训练模型。我试过把一年的数据按季度切成四份每份单独训一个 LSTM夏季和冬季的 MAE 比全年统一模型低了 0.3~0.5℃。代价是每个模型的训练数据变少了所以这个方法适合有 5 年以上数据的场景。最后说个我自己的习惯每次训完模型我都会把预测结果和真实值按月份分组算 MAE看看模型在哪些月份表现差。十有八九是换季那两个月误差最大因为温度变化最剧烈。如果你的项目对换季预测要求高可以考虑在损失函数里给换季样本更高的权重或者单独为换季训练一个模型。这个方案从数据到可视化整条链路都跑通大概需要半天到一天值得动手试一遍。希望帮到你。本文还有配套的精品资源点击获取