☰
基于LSTM的气温预测及可视化:一套可复用的时间序列预测模板
2026/10/3 3:28:21 网站建设 项目流程

简介:面向Python课程设计与期末大作业场景,一份基于LSTM的气温预测与可视化完整项目,源码与文档说明均包含在内。项目覆盖数据采集、预处理、模型构建、训练评估、预测及可视化全流程,适合希望参考完整实现思路、快速搭建同类任务的学习者。压缩包共16个文件,以8个Python脚本为主,分别承担爬虫采集、数据处理、模型定义、训练、预测和演示等职责,另含2个Excel数据表、2个Markdown说明文档及少量pyc缓存,整体仅723KB,目录结构清晰。目前已有179人学习下载,是典型的高分期末大作业方案。代码注释完善,新手也能读懂关键逻辑;配合说明文档和演示脚本,下载后简单部署即可运行,也可作为课程设计、答辩展示或二次开发的参考基础。

1. 基于LSTM的气温预测及可视化:从期末大作业到能直接改的时间序列模板

基于LSTM的气温预测及可视化 python 源码,本质是一条完整的数据采集—序列构建—模型训练—预测回测—可视化管线。说它适合当期末大作业,不只是因为注释齐全,更因为它把 LSTM 时序预测最容易翻车的环节摆在了明面上:数据怎么滑窗、训练集怎么切、预测怎么画才不骗自己。我拆这份资源最直观的感受:它不是让你跑通 demo 就完事,而是给你一个能换数据集、能改参数、能加特征的最小框架。适合正在做课程设计的学生,以及第一次接触 LSTM 时间序列、想拿温度数据练手的开发者。如果你想要一个黑匣子预测脚本,它反而显得啰嗦;想借这个项目理解时序模型,它的结构值得照着拆一遍。

2. 项目结构与数据链路:五个 .py 模块分别干了什么

2.1 文件清单与模块职责:把压缩包里的 .py 对号入座

打开压缩包,第一眼看到的是一堆散落的 .py 文件,没有 src 目录、没有包结构,典型的期末大作业风格。解压后你会看到 t-main 和 ClimateAnalysis 两个目录,README.md 放在最外层。但这套源码的命名其实很有规律:weather_spider.py 管采集,dataset.py 和 data.py 管数据加工,model.py 管网络定义,train.py 管训练,predict.py 和 demo.py 管预测与展示,config.py 管全局参数。我先给一张模块对照表,后面逐段讲。

文件职责输入 → 输出
config.py全局参数所有模块读取的默认配置
weather_spider.py天气数据爬取URL → 原始天气表
data.py数据预处理原始 CSV → 清洗后的气温序列
dataset.py滑窗数据集气温序列 → (x, y) 样本对
model.pyLSTM 网络定义样本维度 → 模型实例
train.py训练主循环模型 + 数据 → 权重文件
predict.py预测与评价权重 + 测试数据 → 预测值
demo.py一键演示直接运行 → 出图

提示:pycache是 Python 运行时生成的缓存目录,可以不管它;真正要动的文件是 config.py、model.py、train.py 三个。

模块划分的合理性在于数据、模型、训练、预测四层互相解耦。我在实际跑的时候,改的最多的是 config.py 和 dataset.py,因为换数据集、调滑窗长度基本只碰这两个文件,不需要动 model 和 train 的逻辑。这比把爬虫、训练、画图全塞进一个 main.py 的做法好维护得多。另外 README.md 里有环境依赖清单,建议先跟一遍再动代码,常见坑是缺 numpy、pandas、torch、matplotlib、requests、beautifulsoup4 这几个包,缺哪个 pip install 哪个就行。

2.2 weather_spider.py:爬虫的请求方式与字段清洗

weather_spider.py 负责把历史天气数据抓下来。常见做法是请求天气网站的月视图页面,解析 HTML 表格里的日期、最高温、最低温字段,最后落成一个 CSV。这个项目里的关键函数一般是 fetch_weather 和 parse_weather_table 这样的结构,requests 负责拿页面,BeautifulSoup 负责解析,我用一个简化版本说明它做的事:

import requests from bs4 import BeautifulSoup import pandas as pd def fetch_weather(city_code, year, month): # 构造天气网站的月视图 URL url = f"https://example-weather.com/{city_code}/{year}{month:02d}.html" headers = {"User-Agent": "Mozilla/5.0"} resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "utf-8" return resp.text def parse_weather_table(html): soup = BeautifulSoup(html, "html.parser") rows = [] # 定位天气数据表格行,提取日期、最高温、最低温 for tr in soup.select("table tr"): cells = [td.get_text(strip=True) for td in tr.select("td")] if len(cells) >= 3: rows.append({"date": cells[0], "tmax": cells[1], "tmin": cells[2]}) return pd.DataFrame(rows) if __name__ == "__main__": df = parse_weather_table(fetch_weather("101010100", 2023, 6)) df.to_csv("weather_raw.csv", index=False, encoding="utf-8-sig")

逻辑说明:fetch_weather 用 requests 拿页面,parse_weather_table 用 BeautifulSoup 解析表格,最后存成 CSV。编码用 utf-8-sig 是关键,Excel 打开不会乱码;requests 的 timeout 参数一定要设,否则某个请求卡住整个循环就断了。参数说明:city_code 是气象站城市编码,timeout=10 表示超过 10 秒放弃这次请求,encoding 指定了解析编码,不同天气站的页面编码可能是 gbk,这个值要根据实际页面调整。

爬虫这块是整条链路里最脆的环节,因为天气网站的页面结构说改就改,选择器一变,parse_weather_table 里那条 select 规则就失效。我一般会先打印一页 HTML 确认选择器再跑全量,别一上来就循环抓三年数据,抓错格式再回头排查很浪费时间。如果你不想依赖爬虫,也可以把别人整理好的历史气温 CSV 直接丢进 data/ 目录,只要列名和 data.py 里读的字段一致就行。

2.3 dataset.py 与 data.py:滑窗切分是 LSTM 数据的核心

data.py 负责把 CSV 里的温度和日期字段整理成干净的序列,比如把"最高温"列转成 float、把日期解析成 datetime、处理缺失值。这里有一个顺序问题:清洗必须在滑窗之前,因为如果有缺失值,滑窗切出来的样本里就会有 NaN,LSTM 拿 NaN 训练,loss 大概率会变成 nan。我见过不少人跳过 data.py 直接手动读 CSV,结果训练出来模型权重全是 NaN,回头排查才发现温度列里混了空字符串。dataset.py 和 data.py 的分工是:data.py 输出原始序列,dataset.py 输出可以直接丢给 DataLoader 的样本对,这样换数据集只改 data.py,换滑窗策略只改 dataset.py。

import numpy as np import torch from torch.utils.data import Dataset class WeatherDataset(Dataset): def __init__(self, series, seq_len=7): # series: 一维 numpy 数组,seq_len: 用过去多少天预测下一天 self.seq_len = seq_len self.x, self.y = self._build_samples(series) def _build_samples(self, series): x, y = [], [] for i in range(len(series) - self.seq_len): x.append(series[i:i + self.seq_len]) y.append(series[i + self.seq_len]) return np.array(x).reshape(-1, self.seq_len, 1), np.array(y) def __len__(self): return len(self.y) def __getitem__(self, idx): return torch.tensor(self.x[idx], dtype=torch.float32), \ torch.tensor(self.y[idx], dtype=torch.float32)

逻辑说明:_build_samples 用滑窗把一条长序列切成长度为 seq_len 的输入和单个目标值,完成从"原始时序"到"监督学习样本"的转换。这里的 feature_dim 是 1,因为只用了气温一个特征;如果你后续加入湿度、气压,feature_dim 会变成 3 甚至更多,滑窗切出来的每个样本就是 (seq_len, feature_dim) 的二维矩阵。参数说明:seq_len=7 表示用一周的历史预测明天,这个值不是越大越好——气温序列有较强的周期性和自相关性,seq_len 太小模型学不到趋势,太大反而引入噪声,常见取值是 7、14、30,我一般会用 7 和 14 做对比实验。

这里有个隐蔽问题:滑窗样本之间高度重叠,第 i 个样本和第 i+1 个样本只差一天的数据,如果不做随机打乱,训练时梯度会非常平滑,模型容易过拟合到最近几天的模式。所以 train.py 里 DataLoader 的 shuffle=True 不是可选项,是必须项。训练集和测试集的划分也要在切片之前完成,最好的顺序是:清洗 → 划分 → 滑窗,先切分再滑窗,才能保证测试集的样本不泄漏训练集信息。如果你先滑窗再切分,滑窗产生的边缘样本会在两个集合之间"串门",评估结果就是虚高的。

3. 模型设计与训练:hidden_size、seq_len 和早停怎么配合

3.1 model.py:网络结构、hidden_size 与层数的选择

model.py 里的 LSTM 结构很标准:输入是连续数值序列,所以不需要 Embedding 层,直接从全连接映射接 LSTM 再接输出层。核心是下面这段:

import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, seq_len=7): super().__init__() self.lstm = nn.LSTM(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x 形状: (batch, seq_len, input_size) out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) last = out[:, -1, :] # 取最后一个时间步的隐状态 return self.fc(last) # (batch, 1)

逻辑说明:输入 (batch, seq_len, 1) 经过 LSTM 后,输出每个时间步的隐状态,我们只取最后一个时间步的隐状态,再过一个全连接层得到预测值。这里 batch_first=True 让张量形状更直观,避免在 batch 和 seq_len 两个维度上绕晕。参数说明:hidden_size=64 是隐状态维度,决定模型容量;num_layers=2 是堆叠的 LSTM 层数,两层就能捕捉一定的层次特征,单层往往欠拟合,三层以上在小数据集上基本只会过拟合。input_size=1 对应单变量温度。

这里我要说一个常被新手忽略的点:LSTM 的初始隐状态 h0 和 c0 默认是零,PyTorch 的 nn.LSTM 会在内部自动处理,所以 forward 里没有手动传 (h0, c0)。但如果你要做多步预测(比如一次预测未来 7 天),就必须自己维护隐状态并在每个预测步之间传递,否则等于每次都从零开始,预测会严重失真。项目里的 predict.py 走的是单步回测路线,所以没暴露这个问题,后面第 4 章会展开。

补充一下:model.py 里通常还会写一个独立的推理分支,但核心结构不变。如果你在答辩时被问到"为什么用两层不用三层",可以直接回答:数据集只有几百天的日温记录,样本量小,层数加深带来的参数增量没有足够数据支撑,两层是容量和泛化之间的平衡点。这个回答比"我试出来的"显得专业得多。

3.2 train.py:训练循环、学习率与早停机制

train.py 是整条管线的中枢。标准流程是:读配置 → 加载数据 → 建模型 → 定损失函数和优化器 → 循环训练 → 每轮记录 loss → 保存最好权重。我用项目里常见的写法做一个可运行的训练主循环:

import torch import torch.nn as nn from torch.utils.data import DataLoader def train(model, train_loader, epochs, lr=0.001, device="cpu"): criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=lr) model.to(device) best_loss = float("inf") for epoch in range(epochs): model.train() total_loss = 0.0 for x, y in train_loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() pred = model(x).squeeze(-1) loss = criterion(pred, y) loss.backward() optimizer.step() total_loss += loss.item() * x.size(0) avg_loss = total_loss / len(train_loader.dataset) print(f"Epoch {epoch+1:03d} | Loss {avg_loss:.6f}") # 保存训练过程中 loss 最低的权重 if avg_loss < best_loss: best_loss = avg_loss torch.save(model.state_dict(), "best_model.pth") return model

逻辑说明:每一轮把数据按 batch 喂给模型,计算 MSE 损失,反向传播更新权重,最后用平均 loss 判断要不要覆盖保存 best_model.pth。MSELoss 是回归任务标配,温度预测的误差平方和损失能直接反映预测值和真实值的偏离程度。参数说明:lr=0.001 是 Adam 的常见起点,epochs 一般设 50 到 100,batch_size 在 DataLoader 里通常设 32 或 64。注意这里没有做学习率衰减,项目里的原版同样简单,所以如果你的 loss 在后期震荡不降,可以在 Adam 外面包一层 torch.optim.lr_scheduler.StepLR,每 20 轮把学习率乘 0.5。

早停是另一个值得补的点。温度预测这种小数据集,训练到后面 loss 曲线通常是"快速下降 → 平台期 → 微微过拟合",靠 epoch 上限硬撑并不明智。我在复现时习惯在训练循环里加一个 patience 计数器:连续 10 轮验证 loss 不降就提前终止,最优权重已经保存在 best_model.pth 里。加早停的代码很短,就是维护一个 best_loss 和一个 counter:

best_loss = float("inf") patience = 10 counter = 0 for epoch in range(epochs): # ... 正常训练代码,得到 valid_loss ... if valid_loss < best_loss: best_loss = valid_loss counter = 0 torch.save(model.state_dict(), "best_model.pth") else: counter += 1 if counter >= patience: print(f"Early stop at epoch {epoch + 1}") break

逻辑说明:valid_loss 来自验证集(训练集最后 20% 那一段),当它连续 patience 轮没有刷新最优值时,触发 break;每轮结束都要把 best_loss 和 counter 更新一遍。参数说明:patience=10 表示容忍 10 轮不进步,一般取 5 到 15,数据越小声越小。这个机制配合 best_model.pth 的保存逻辑,能保证你最终加载的是历史上验证 loss 最低的那个权重,而不是最后一个 epoch 的权重。

3.3 config.py:把超参数集中到一处,调参不靠改代码

这个项目做得比较聪明的一点是 config.py 把散落各处的超参数集中到了一起。我建议你不要忽视这个文件,期末大作业答辩时老师大概率会问"你的参数为什么这么设",config.py 就是你回答的依据。

# config.py DATA_PATH = "data/weather.csv" # 原始气温数据路径 SEQ_LEN = 7 # 滑窗长度:用 7 天预测下一天 BATCH_SIZE = 32 # 每批样本数 EPOCHS = 80 # 最大训练轮数 LR = 0.001 # 学习率 HIDDEN_SIZE = 64 # LSTM 隐状态维度 NUM_LAYERS = 2 # LSTM 堆叠层数 TRAIN_RATIO = 0.8 # 训练集占比,前 80% 做训练 DEVICE = "cpu" # 训练设备,有 GPU 改成 cuda

逻辑说明:config.py 本身不参与计算,它只是把 train.py、dataset.py、predict.py 都要读的公共参数集中到一处,import config 之后各个模块拿到的 SEQ_LEN、HIDDEN_SIZE 就是同一个值,避免改一个参数要翻三个文件的尴尬。参数说明:TRAIN_RATIO=0.8 意味着用前 80% 的时间段训练、后 20% 做验证,这个切法对时序数据是必须的——不能随机抽样,否则未来信息会泄漏进训练集。SEQ_LEN=7 和 HIDDEN_SIZE=64 是最值得调的两个旋钮:SEQ_LEN 影响模型看到的历史窗口,HIDDEN_SIZE 影响模型容量。如果你的数据是小时级的,SEQ_LEN 可能要 24 或者 48;如果是日温,7 到 14 就够。DEVICE 字段在无 GPU 环境下保持 "cpu",有 CUDA 显卡时改成 "cuda" 能快一个数量级,前提是你的 torch 是带 CUDA 的版本。

调参的顺序我一般是:先固定其他参数只改 SEQ_LEN,对比 7 和 14 两组;再固定它改 HIDDEN_SIZE,对比 32、64、128;最后看 loss 曲线决定 EPOCHS。一次只动一个变量,别同时改三个,不然 loss 变好了你都不知道是谁的功劳。把每次实验的 MAE、RMSE、训练时间记录在一个表格里,答辩时拿出来非常加分。

4. 预测与可视化:回测策略、出图规范与两个指标

4.1 predict.py:单步回测与多步预测的区别

predict.py 做的事可以分两步:加载训练好的权重,对测试集做滑窗回测,然后把预测值和真实值一起返回。回测的含义是:用第 1 到 7 天的温度预测第 8 天,再用第 2 到 8 天预测第 9 天……每一步的输入里都包含真实值,所以叫"teacher forcing"的单步回测。

import numpy as np def predict(model, test_loader, device="cpu"): model.eval() preds, trues = [], [] with torch.no_grad(): for x, y in test_loader: x = x.to(device) pred = model(x).squeeze(-1) preds.append(pred.cpu().numpy()) trues.append(y.numpy()) return np.concatenate(preds), np.concatenate(trues)

逻辑说明:model.eval() 关闭 dropout 和 batch normalization 的训练行为,no_grad() 关闭梯度计算,这两行是推理的必要设置,少了任何一行预测结果都会不稳或浪费显存。参数说明:test_loader 的数据不能 shuffle,因为预测结果要和真实值按时间顺序对齐画图,一旦打乱,你画出来的真实值和预测值就对不上号了。这里的 test_loader 是用 WeatherDataset 包住测试集序列后创建的,和 train_loader 同源。

这里要提醒一个很多人栽过的坑:单步回测的误差是"每步都用真实值纠正"的误差,它天然偏乐观。真正要检验模型能不能做多步预测,得另外实现一个循环:预测出第 8 天,把它当作已知输入去预测第 9 天,这样往后推 7 步。项目里的 predict.py 是单步回测,我建议你至少手动跑一遍多步预测,看看误差是怎么随着步数放大的,那才是 LSTM 在气温上的真实表现。多步预测的核心代码是把预测值动态拼回输入序列,每次只滑动一步:

def multi_step_predict(model, seed, steps=7, device="cpu"): # seed: 长度为 seq_len 的一维数组,作为预测起点 model.eval() model.to(device) seq = seed.copy() preds = [] with torch.no_grad(): for _ in range(steps): x = torch.tensor(seq).reshape(1, -1, 1).float().to(device) y = model(x).squeeze(-1).item() preds.append(y) # 把预测值追加到序列末尾,丢掉最前面一天 seq = np.append(seq[1:], y) return np.array(preds)

逻辑说明:先把测试集最后 seq_len 天取出来作为种子序列,每次用模型预测下一天,再把预测值追加到序列末尾、丢掉最前面一天,如此循环 steps 次,得到连续的多步预测。参数说明:steps 是你要预测的天数,设 7 就预测未来一周;seed 序列越长,初始上下文越充分,但多步预测的误差还是会随着步数累积——预测第 1 天误差可能 1 度,第 7 天可能就 3 度了,这个放大趋势本身就值得写进报告。

4.2 demo.py:把 loss 曲线和预测对比图画出来

demo.py 是演示入口,运行后应该画出两类图:训练 loss 曲线和测试集上真实温度 vs 预测温度的对比曲线。绘图用 matplotlib,这是最常用的方案。我给出项目的核心绘图逻辑:

import matplotlib.pyplot as plt def plot_result(trues, preds, save_path="result.png"): plt.figure(figsize=(12, 5)) plt.plot(trues, label="True", color="black", linewidth=1) plt.plot(preds, label="Pred", color="red", linewidth=1, linestyle="--") plt.legend() plt.xlabel("Day") plt.ylabel("Temperature (°C)") plt.title("LSTM Temperature Prediction") plt.grid(alpha=0.3) plt.savefig(save_path, dpi=150, bbox_inches="tight") def plot_loss(history, save_path="loss.png"): plt.figure(figsize=(8, 5)) plt.plot(history, color="blue") plt.xlabel("Epoch") plt.ylabel("MSE Loss") plt.title("Training Loss") plt.grid(alpha=0.3) plt.savefig(save_path, dpi=150, bbox_inches="tight")

逻辑说明:真实值用黑色实线、预测值用红色虚线,温度单位是摄氏度,横轴是天数。loss 曲线看收敛性,对比曲线看拟合度,这两张图是期末大作业报告里必须出现的核心证据。参数说明:dpi=150 保证论文插图清晰,bbox_inches="tight" 自动裁剪空白边距。保存文件而不是用 plt.show(),是因为跑批量实验时不可能每轮都弹窗盯着看。

如果你想把图做得更专业,可以把训练集和测试集的预测分别画在两个子图里,训练集用浅色、测试集用深色,一眼就能看出模型在没见过的数据上掉不掉链子。这个细节在答辩时很讨喜。另外,demo.py 里通常还会把真实温度和预测温度的温度差单独画一个误差子图,超出某个阈值(比如 2 度)的部分就是模型的"高风险日",这类日子多出现在气温骤变的换季期。

4.3 评价指标:MAE 与 RMSE 怎么看才算懂

光画图不够,报告里得给数字。气温预测最常用的两个回归指标是 MAE(平均绝对误差)和 RMSE(均方根误差),它们都能在 predict.py 里顺手算出来。

def evaluate(preds, trues): mae = np.mean(np.abs(preds - trues)) rmse = np.sqrt(np.mean((preds - trues) ** 2)) print(f"MAE: {mae:.3f} °C | RMSE: {rmse:.3f} °C") return mae, rmse

逻辑说明:MAE 告诉你平均每天预测偏了多少度,RMSE 因为对误差取了平方,会放大那些偏离大的样本,所以 RMSE 通常大于等于 MAE。两个指标结合看:如果 RMSE 比 MAE 大很多,说明模型在个别极端温度(突然降温、升温)上预测得很差,这是时序模型的常见病。参数说明:温度的 MAE 在 1 到 2 摄氏度之间算不错,超过 3 度说明模型基本在"猜";这个数值没有绝对标准,要看数据本身的波动幅度,冬天的日温波动 10 度以上,MAE 2 度就算可接受。

我一般会把 mae、rmse 和两张图画进同一个对比文件里,作为训练日志的一部分。这样每次实验结束,打开 outputs 目录就能横向比较不同参数组合的预测质量,不用重新跑一遍。如果你把多步预测的误差也按步数画成一条误差增长曲线,那这份日志的说服力会比单看两个数字强很多。

5. 避坑与常见问题:五个坑的现场还原与修复

5.1 loss 变成 NaN:先查数据再查学习率

现象:训练跑到某个 epoch,loss 打印出来是 nan,模型权重全废,后续所有 epoch 都在 nan 里打转。

原因:大概率是数据里有缺失值或非数值字符,NaN 一旦进入 loss 反向传播,梯度也会变成 NaN,之后每一步训练都在污染权重;少数情况下是学习率过大导致梯度爆炸,也会在某个 step 直接冲破数值上限。

解决:进模型之前做一次数据清洗:用 pandas 的 dropna 删掉缺失行,或者用前后两天的均值填充;再把温度序列做归一化(Min-Max 或 StandardScaler),让输入落在 0 到 1 附近,LSTM 的激活函数才不会饱和。数据清洗永远在滑窗之前做,这个顺序不能反。如果清洗完还是 nan,把 lr 从 0.001 降到 0.0001 再试。

5.2 预测曲线整体滞后一拍:模型在抄昨天的答案

现象:画出来的预测线几乎就是真实线往右平移了一天,真实值涨它跟着涨,真实值跌它跟着跌,但永远慢半拍。

原因:这是时序预测最典型的现象。模型学到的最优策略是"复制上一天的温度",因为气温是强自相关的,昨天的温度对今天有很强的预测力,模型走捷径比真的学习温度变化规律更划算。单步回测评估方式还会让这个问题被指标掩盖——滞后一拍的 RMSE 可能并不难看。

解决:引入更多特征让模型无法偷懒:加入月份、季节编码、前一天的温度差(一阶差分),或者直接改用多步预测损失来训练。我在复现气温项目时,把"温度差"作为额外特征加进去之后,滞后现象明显缓解,因为模型不能再靠复制上一天来蒙混过关了。

5.3 训练集完美、测试集崩掉:泄漏和过拟合在捣鬼

现象:训练集上 loss 低到 0.1,测试集上 MAE 飙到 4 度,两张曲线肉眼可见地分家。

原因:通常有三个。一是切分前没有按时序切,而是随机抽样了,导致测试集里混进了和训练集重叠的信息,你看到的"训练集完美"其实是模型见过测试集的假象;二是模型容量太大,hidden_size 设到 128 以上,数据量只有几百天,必然过拟合;三是滑窗样本重叠太多,训练集内部高度的自相似让模型记住了样本而不是规律。

解决:确认 train_test_split 是按时序切而不是 shuffle 切,检查顺序是"清洗 → 划分 → 滑窗"还是反了;降低 hidden_size 到 32 或 64;训练时打开 shuffle=True 并把 dropout 加到 0.2 左右。改完这三处,测试集指标通常能拉回来一大截。

5.4 CPU 训练慢到怀疑人生:先做烟雾测试再谈精度

现象:笔记本没有独立显卡,DEVICE="cpu",几十个 epoch 跑下来要等十几分钟甚至更久。

原因:CPU 上的矩阵运算本来就不擅长 LSTM 这种串行结构,seq_len 长、hidden_size 大、epoch 多三者叠加,时间成本直线上升。很多人为了省时间直接跳到小 epoch,结果模型没收敛就交上去了。

解决:先减小 EPOCHS 到 10、SEQ_LEN 到 3 做烟雾测试,确认整条链路能跑通、loss 在正常下降,再恢复正式参数。batch_size 适当调大到 64,能提高 CPU 的向量化利用率。如果实在没耐心,把 seq_len、hidden_size 都减半跑一轮当基线,后面再逐步加回来,好过一直在等一个大参数集的完整训练。

5.5 matplotlib 中文和负号变方块:字体配置两行搞定

现象:图例写成"温度"直接变方块,坐标轴的负号也变成奇怪的框,图拿不出手。

原因:matplotlib 默认字体里没有中文字形,黑体、宋体这类系统字体需要手动指定;负号问题则是默认的 Unicode 负号渲染在某些字体下会失败。

解决:绘图脚本开头强制指定字体:

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # Windows 黑体 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题

逻辑说明:第一行指定黑体作为默认无衬线字体,第二行关掉 Unicode 负号渲染,改用 ASCII 负号。如果你在 Linux 服务器上跑,SimHei 不存在,改成 "WenQuanYi Zen Hei" 或者干脆把图例写成英文,别在字体上耗时间。这两个参数是 matplotlib 中文图表的老坑,项目里如果没写,你接手时一定要补上。补完之后,重新运行 demo.py,中文和负号就都正常了。

6. 进阶改造:从单变量到多变量,再加一个多步预测自检

6.1 加特征比换模型更划算

既然爬虫已经能拿到原始天气页,顺手把湿度、气压、风速一起解析出来,model.py 的 input_size 改成 4,滑窗样本最后一维变成 (seq_len, 4),其余代码几乎不用动。答辩时"我做了多变量温度预测"比"我跑通了 LSTM"好讲得多。注意先各自归一化再拼接,不然气压 1000 上下的数值范围会把 -10 到 40 的温度特征淹没。

6.2 换成 GRU 做对比:改两行就能出实验数据

PyTorch 里把 nn.LSTM 换成 nn.GRU 只需改 model.py 的构造和 forward 各一行,train.py 完全共用。GRU 参数更少、收敛更快,在小数据集上常和 LSTM 相当。用第 4 章同一套 evaluate 流程算两组 MAE、RMSE,画一张对比柱状图,"实验对比"章节就有了。

import torch.nn as nn class GRUPredictor(nn.Module): def __init__(self, input_size=4, hidden_size=64, num_layers=2): super().__init__() self.gru = nn.GRU(input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.gru(x) return self.fc(out[:, -1, :])

逻辑说明:GRU 只有隐状态 h,没有 cell state 的 c,所以 forward 里只解包一个状态值,其余结构和 LSTM 完全一致。参数说明:input_size=4 对应湿度、气压、风速加温度四个特征;epochs 可以适当缩短,因为 GRU 收敛更快。

6.3 多步预测自检:数字可以骗人,曲线不会

我每改一次参数都会跑一个自检:用训练好的模型做 7 步滚动预测,把预测曲线画出来看趋势是否合理。如果第 3 天起预测开始平坦化甚至震荡,回头检查特征和滑窗长度,别急着调层数。从那次被"滞后一拍"坑过之后,我养成了习惯:任何时序模型先画多步预测曲线,再报指标数字。数字可以骗人,曲线不会。这个习惯帮我避掉了不少评估上的雷,希望也能帮到你。

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

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

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

立即咨询