☰
LSTM空气质量预测实战:数据清洗、模型训练与评估全解析
2026/10/8 13:53:35 网站建设 项目流程

简介:这是一份面向期末大作业、毕设与课程设计的 Python 空气质量指数预测项目源码,基于 LSTM 实现,包含完整可运行的模型、数据预处理和训练预测流程,代码注释较多,新手也能看懂,适合想快速搭建预测系统或参考高分作品写法完成课程任务的学生。资源包为 zip 压缩格式,共两千个文件、二十九点三三 MB,主体是北京地区空气质量 CSV 历史数据,另有 PY 预测脚本、XML 配置和说明文档。目前已有 222 人学习/下载。项目界面完善、操作简单,经过严格调试可直接部署使用;包含数据读取、模型构建、训练、评估与可视化等环节,CSV 数据集覆盖多个时间点,可替换其他城市数据或扩展特征后重新训练。整体代码结构、注释风格和 LSTM 建模调参思路,对期末大作业、毕业设计或课程设计都有较高参考价值。

1. 一篇LSTM空气质量预测源码,凭什么能当高分期末大作业

期末大作业选“基于LSTM的空气质量指数预测”,十个人里有八个是在复现别人的教程,最后提交的代码却往往卡在同一个地方:训练完画个图,loss曲线还行,但预测值和真实值对不上。真正能拿高分的那种源码,不是模型花哨,而是把数据清洗、归一化、滑动窗口构造、模型训练、误差评估、可视化这一整条链路做扎实了。这篇笔记会顺着这套Python+LSTM预测源码的关键步骤拆开讲,每一步都给出能直接跑的代码和参数说明,也会把常见翻车点提前标出来。适合正在做期末大作业、想交出一份结构完整可复现项目的Python学习者,也适合开始用PyTorch做时序回归、但还没形成工程化习惯的人。

2. 把AQI数据喂进LSTM之前:清洗、归一化与滑动窗口构造

2.1 用pandas读入空气质量CSV:最容易被扣分的一步

LSTM本身不认CSV,只认数值张量。期末项目里最常见的翻车,不是模型写错,而是数据读进来之后,日期列没解析、空值没处理,直接把NaN喂给了神经网络。这里的前提是你已经装好了Python 3.8或更高版本,并且装全了pandas、numpy、torch、matplotlib和scikit-learn这些常用库——装numpy库的时候注意用pip install numpy,不要用系统自带的旧版Python。

我的做法是先用pandas读原始文件,确认列名和缺失值情况,再统一转成float32。空气质量监测数据的常见CSV格式会包含日期时间、各项污染物浓度(PM2.5、PM10、SO2、NO2、CO、O3)和计算出的AQI。代码按这个结构来写,你的字段名不一样就改一下列名列表。

import pandas as pd import numpy as np # 读取原始监测数据,假设CSV包含:datetime, PM2.5, PM10, SO2, NO2, CO, O3, AQI df = pd.read_csv('air_quality.csv', parse_dates=['datetime']) print(df.info()) # 检查列类型和缺失值 print(df.isnull().sum()) # 统计每列缺失数量 # 删除全空行,对单列缺失用前向填充 df = df.dropna(how='all') df = df.ffill().bfill() # 选取建模用特征列,按时间排序 feature_cols = ['PM2.5', 'PM10', 'SO2', 'NO2', 'CO', 'O3'] data = df[feature_cols].astype(np.float32).values print(data.shape) # 例如 (43800, 6)

这里用parse_dates把时间列变成datetime类型,后续排序和画图都依赖它。ffill()是多数空气质量监测数据的常规处理方式——污染物浓度在短时间内的变化是连续的,用前向填充比直接删除更能保留时间序列结构。如果某一列开头就是空值,再补一个bfill()兜底。astype(np.float32)是为了防止Windows上double和float混用导致的训练速度下降。

注意不要用df.dropna()暴力删行,那会打断时间连续性。如果你的数据里空值超过10%,就需要回到数据源重新检查,常见原因是传感器离线,这类情况在答辩时可以直接作为数据局限性讲出来,反而显得你认真做过数据探索。

2.2 MinMax归一化:LSTM对输入尺度有多敏感,参数怎么设

LSTM内部用的是tanh和sigmoid激活函数,输入动辄几百的PM2.5浓度值会直接把激活函数推到饱和区,梯度更新变得非常慢。这就是很多人发现LSTM神经网络预测结果像一条直线的原因之一。常见的做法是用sklearn的MinMaxScaler把每个特征缩放到[0,1],而不是用StandardScaler做标准化——LSTM对输出的有界性更友好,且逆变换时不容易出现负值。

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) scaled_data = scaler.fit_transform(data) # data形状: (n_samples, 6) # 保存scaler供预测结束后反归一化 import joblib joblib.dump(scaler, 'scaler.save') print('归一化后最小值:', scaled_data.min(), '最大值:', scaled_data.max())

feature_range=(0,1)是LSTM预测任务的默认配置。千万别用StandardScaler,它会把数据变成均值0方差1的分布,虽然也能训练,但逆变换后得到的预测值可能超出合理范围,比如PM2.5变成负数。另外,fit_transform只能用在训练集上,验证集和测试集要用已经拟合好的scaler.transform,否则会引入未来数据信息,这在答辩时很容易被老师问出漏洞。

保存scaler是一个特别容易被忽略的工程细节。很多期末项目只保存模型权重,忘了保存归一化参数,导致评估阶段无法把预测值还原成真实浓度单位。joblib.dump保存的是整个scaler对象,加载时joblib.load('scaler.save')就能直接inverse_transform。我在构建模型时也习惯直接把它写进checkpoint,第3章会演示。

2.3 构造滑动窗口样本:look_back参数的选择逻辑

LSTM一次看一段序列,而不是单个时间点。所以要把一维时间序列切成“用过去24小时预测下一小时”的样本。这个窗口长度就是look_back。取值没有标准答案,常见做法是24(一天)、48(两天)或168(一周)。期末数据通常只有几个月,窗口太大样本量会急剧减少,所以24最稳妥。如果你的数据呈明显周期性,可以观察自相关曲线再决定,但期末项目里24通常不会错。

def create_sequences(data, look_back=24): X, y = [], [] for i in range(len(data) - look_back): X.append(data[i:i+look_back, :]) # 过去look_back小时的所有特征 y.append(data[i+look_back, 0]) # 预测下一小时的PM2.5(第一个特征) return np.array(X), np.array(y) look_back = 24 X, y = create_sequences(scaled_data, look_back) print('X shape:', X.shape, 'y shape:', y.shape) # 按时间顺序切分,前80%训练,后20%验证 split_idx = int(len(X) * 0.8) X_train, X_valid = X[:split_idx], X[split_idx:] y_train, y_valid = y[:split_idx], y[split_idx:]

这里X的形状是(样本数, look_back, 特征数),正是PyTorch中LSTM需要的(seq_len, batch, input_size)的转置版本,后面训练时要做维度调整。y取的是data[i+look_back, 0],也就是预测PM2.5这一列。如果你想把AQI作为预测目标,就把特征列里的AQI放到第一个,或者改成y.append(data[i+look_back, 特征列索引])。用多个特征预测单一PM2.5,是空气质量预测的常规设定——PM10、SO2等与PM2.5有物理相关性,LSTM可以从中学到协同变化,效果通常比单变量自回归好。

时间序列切分不能使用随机打乱。如果像普通分类任务那样用train_test_split(X, y, test_size=0.2, shuffle=True),验证集里会混入训练集时段的信息,模型相当于提前看到了未来,验证指标会虚高。这个点如果能在文档里写明,答辩会非常加分。

提示:create_sequences生成的是重叠窗口,相邻两个样本共享look_back-1个时间点,所以有效样本数约等于总长度减look_back,不是总长度。

3. 用PyTorch手写LSTM预测模型:网络结构、训练循环与模型保存

3.1 定义LSTM回归网络:输入维度、隐藏层数与dropout的取舍

很多教程会引导你用现成的LSTM封装库,但期末项目里我建议直接用PyTorch的nn.LSTM手写,几十行代码就能说清原理,答辩时也能讲明白每个张量是怎么流转的。LSTM模型不需要很复杂,一个单层LSTM加一个全连接层足以拟合空气质量数据。隐藏层大小一般取32~128之间,数据量不大时取64比较稳妥。我见过不少人一上来就写两层LSTM加attention,结果训练时间翻倍,精度却没提高,答辩还得解释复杂结构里每个张量的shape,纯属给自己挖坑。

import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=6, hidden_size=64, num_layers=1, dropout=0.0): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.regressor = nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) = self.lstm(x) # 取最后一个时间步的输出,也可以直接使用h_n[-1] last_output = out[:, -1, :] # (batch, hidden_size) return self.regressor(last_output) model = LSTMPredictor(input_size=6, hidden_size=64, num_layers=1) print(model)

batch_first=True让输入张量的形状是(batch, seq_len, input_size),写数据处理和调试都更直观。取out[:, -1, :]是LSTM序列建模的常规操作——含义是“看完整个窗口后,最后时刻的隐状态携带了整个序列的压缩信息”。如果你用h_n[-1]效果类似,但out更灵活,后期想做多步预测时可以直接取中间时刻。num_layers=1在多数情况下足够,除非你的数据有明显多小时尺度的周期叠加,再考虑加到2层,并给中间的LSTM层加上dropout=0.2防止过拟合。

为什么是6个输入特征?因为data里有6列污染物浓度,没有把AQI放进去。AQI是六项污染物浓度的综合指数,把它作为输入特征会造成信息冗余,而且会导致目标变量与输入高度共线,模型学到的只是“用AQI预测PM2.5”这种伪关系。期末项目里想体现对特征的理解,可以在报告里写一句“未使用AQI作为特征,避免目标泄漏”,这是很加分的。

3.2 训练循环:损失函数、优化器、epoch和batch_size的典型配置

损失函数选MSE,因为预测目标是连续浓度值,MSE对大的偏差惩罚更重,符合空气污染预测“宁可偏高,不能低估”的场景直觉。优化器用Adam,学习率设1e-3比较安全;如果数据量少,想稳定收敛,可以用1e-4。epoch在50~200之间,期末项目不需要跑到上千轮,跑太多反而过拟合。

from torch.utils.data import DataLoader, TensorDataset X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.float32).view(-1, 1) X_valid_t = torch.tensor(X_valid, dtype=torch.float32) y_valid_t = torch.tensor(y_valid, dtype=torch.float32).view(-1, 1) train_dataset = TensorDataset(X_train_t, y_train_t) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=False) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) epochs = 100 for epoch in range(epochs): model.train() total_loss = 0.0 for batch_X, batch_y in train_loader: optimizer.zero_grad() pred = model(batch_X) loss = criterion(pred, batch_y) loss.backward() optimizer.step() total_loss += loss.item() * batch_X.size(0) if (epoch + 1) % 10 == 0: avg_loss = total_loss / len(X_train_t) print(f"epoch {epoch+1:3d}, train MSE: {avg_loss:.6f}")

注意这里的shuffle=False。时间序列样本有重叠(相邻窗口来自相邻时刻),打乱虽然不影响训练收敛,但会让梯度更新在局部时间上反复摆动,也容易让人误以为模型学到了全局模式。如果显存有限,batch_size=64合适;如果你的数据总共只有几千条,batch_size可以降到16或32,效果往往更好,因为小batch的梯度噪声有助于逃离局部极小值。

每个epoch打印一次训练MSE是为了观察收敛速度。如果10个epoch后loss还在原地踏步,先不要加模型复杂度,去查学习率和归一化。Loss曲线要是完全不动,十有八九是把原始数据直接喂进去了。还有一个血泪经验:PyTorch版本不同,默认初始化略有差异,如果你复现别人的代码发现loss表现不一样,先固定torch.manual_seed(42)再谈对比。

3.3 保存与加载模型:答辩现场可复现的关键

期末项目答辩最尴尬的时刻,是老师让你现场再预测一段数据,而你只保存了model.state_dict(),却忘了保存scaler,导致预测结果根本没法还原成真实浓度。正确做法是把模型权重、scaler和配置参数一起打包。

# 保存 torch.save({ 'model_state_dict': model.state_dict(), 'input_size': 6, 'hidden_size': 64, 'num_layers': 1, 'look_back': 24, 'scaler': scaler, # 可以直接存对象,或单独joblib }, 'lstm_aqi_model.pt') # 加载 checkpoint = torch.load('lstm_aqi_model.pt', map_location='cpu') model = LSTMPredictor( input_size=checkpoint['input_size'], hidden_size=checkpoint['hidden_size'], num_layers=checkpoint['num_layers'] ) model.load_state_dict(checkpoint['model_state_dict']) model.eval()

把scaler直接放进同一个字典,是后期写预测脚本时最省事的方案,不用再管两个文件之间的路径依赖。map_location='cpu'是为了防止在GPU上训练、CPU上推理时出现设备不匹配的报错。model.eval()一定要在预测前调用,它会关闭dropout和batch norm的随机行为,避免同一条数据每次预测结果不一样。这一点是很多人忽略却在答辩时被发现的雷:老师多跑两次预测,输出完全不一样,你就说不清了。

如果你在GPU上训练,记得把训练数据也to('cuda'),但期末项目数据量通常不大,CPU跑完全够。真用GPU时,注意保存checkpoint之前把模型model.cpu(),否则加载时会因为键名带cuda而报错。用torch.save保存整个模型对象虽然省事,但会携带类的代码路径,换机器或改项目目录后容易出问题,所以这里只推荐保存state_dict加配置。

4. 预测效果怎么量化:误差指标、对比图与24小时滚动预测

4.1 计算RMSE、MAE和R²:三个指标代码与学术解读

模型训练完,不能只说“loss很低”,期末报告里必须给出测试集上的回归指标。RMSE和MAE的单位都是µg/m³,R²是无量纲的拟合优度。RMSE对大误差更敏感,MAE反映平均偏差,R²则用来向答辩老师说明模型解释了多大气象变化。这三个指标在scikit-learn里都有现成函数,不自己造轮子。

from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score model.eval() with torch.no_grad(): pred_valid = model(X_valid_t).numpy().flatten() # 反归一化,还原为真实PM2.5浓度 other_features = X_valid[:, -1, 1:6] # 每个样本窗口最后时刻的其余特征 pred_real = scaler.inverse_transform( np.hstack([pred_valid.reshape(-1, 1), other_features]) )[:, 0] y_valid_real = scaler.inverse_transform( np.hstack([y_valid.reshape(-1, 1), other_features]) )[:, 0] rmse = np.sqrt(mean_squared_error(y_valid_real, pred_real)) mae = mean_absolute_error(y_valid_real, pred_real) r2 = r2_score(y_valid_real, pred_real) print(f"RMSE: {rmse:.2f} µg/m³, MAE: {mae:.2f} µg/m³, R²: {r2:.3f}")

这里反归一化的写法容易把人绕晕。MinMaxScaler是对6列特征一起做的,逆变换时需要把预测的PM2.5和其他5列的真实值拼回一个6维向量。X_valid[:, -1, 1:6]取的是每个样本最后一个时刻的除PM2.5外其余特征,因为预测目标对应窗口最后时刻之后的那个值,所以用最后时刻的特征来辅助还原维度。如果你嫌麻烦,更优雅的做法是单独用一个scaler只对PM2.5目标列做归一化,其余特征用另一个scaler,这样预测值直接scaler_y.inverse_transform(pred_valid)即可。期末项目强烈建议这个分离式方案,避免拼接维度错误。

指标怎么解读?如果R²大于0.8,说明模型解释了大部分波动;0.6~0.8属于可用,但预测曲线可能滞后或振幅偏小;低于0.5就要回到数据或模型层找问题。空气质量预测本质上受气象因素影响,数据里没有气温、风速、湿度这些外部变量时,R²不高不代表模型失败。在报告里写明“由于缺少气象特征,R²在0.6附近是可以接受的”,比藏着掖着更专业。

4.2 画真实值与预测值曲线:matplotlib可视化代码

可视化是高分项目的门面。只给loss曲线会让老师觉得你只是把代码跑通了,而不是真正理解预测结果。要画这样一张图:横轴是时间步,纵轴是PM2.5浓度,蓝色真实值,橙色预测值,窗口取最近200个点就够了,全部画上去会糊成一片。

import matplotlib.pyplot as plt plt.figure(figsize=(12, 5)) plt.plot(y_valid_real[-200:], label='True PM2.5', color='blue', alpha=0.7) plt.plot(pred_real[-200:], label='Predicted PM2.5', color='orange', alpha=0.7) plt.xlabel('Time step (hour)') plt.ylabel('PM2.5 (µg/m³)') plt.title('LSTM Air Quality Prediction on Validation Set') plt.legend() plt.grid(alpha=0.3) plt.tight_layout() plt.savefig('prediction_curve.png', dpi=150) plt.show()

记住把图保存成文件再放进报告。dpi=150足够清晰,图例和坐标轴标签是基本要素。答辩时老师一眼能看到预测曲线是否贴合真实曲线的相位,如果你的预测曲线明显滞后(整体向右平移),说明模型把上一个时刻的真实值“背”下来了,这个问题在第5章会重点讲。这里一个小技巧:把预测误差的阴影带也画出来,例如用plt.fill_between填充预测值±MAE的区间,能直观展示不确定性,这是超出一般学生作业的加分项。

4.3 往后预测24小时:滚动预测实现与需要注意的误差累积

期末项目要拿“优秀”,光做历史验证不够。很多现成源码只停留在“预测验证集”,但题目要求“预测未来”,所以需要一个滚动预测函数:用最后一个已知窗口作为输入,预测出下一小时,然后把预测值当作新的已知值拼进窗口,再预测下下一小时,循环24次。

def predict_future(model, last_window, scaler, steps=24): model.eval() future = [] current_window = last_window.copy() # shape: (1, look_back, input_size) with torch.no_grad(): for _ in range(steps): input_tensor = torch.tensor(current_window, dtype=torch.float32) pred = model(input_tensor).item() # 预测归一化后的值 future.append(pred) # 用预测值替换窗口最后一个时间步的PM2.5位置 # 其他特征在滚动时用真实最新值近似(或保持最后状态) new_step = current_window[0, -1, :].copy() new_step[0] = pred current_window = np.roll(current_window, shift=-1, axis=1) current_window[0, -1, :] = new_step # 反归一化PM2.5列 future = np.array(future).reshape(-1, 1) future_real = scaler.inverse_transform( np.hstack([future, np.repeat(current_window[0, -1, 1:6].reshape(1, -1), steps, axis=0)]) )[:, 0] return future_real # 取验证集最后一个样本的窗口 last_window = X_valid[-1:].copy() future_24 = predict_future(model, last_window, scaler, steps=24) print('未来24小时PM2.5预测:', future_24)

滚动预测的误差会累积:第2小时的预测基于第1小时的预测,第24小时的预测误差通常比第1小时大一截。这是LSTM预测的固有局限,不是bug。写报告时主动提出来,反而显得你理解时序预测的本质。比如你可以画一张“不同提前量下RMSE变化”的小图,展示误差随预测步长增长,这是很有说服力的分析。另外,这里滚动时只更新PM2.5列,其他5个特征在短期预测中通常变化不大,用最近时刻的真实值近似是可接受的工程简化。如果你有更完整的天气数据,把天气预测值也接进来会更好,但期末项目里这个简化完全够用。

5. LSTM空气质量预测的常见问题排查:5个翻车现场与修复方法

下面这五个问题,是我从好几个学生的期末项目里反复看到的。每个都按现象、原因、解决的顺序讲,你可以直接对照自己的打印日志来排查。这些问题不解决,改再多的LSTM层数都是白费。

5.1 loss不下降或下降极慢:先查学习率和归一化

现象:训练10个epoch后MSE仍然在0.2以上不动,或者一直在某个值震荡。

原因:最常见的有两个。一是把原始浓度(成百上千)直接送进LSTM,tanh和sigmoid饱和,梯度消失;二是学习率设置过大(比如0.1),让loss在一个大值附近打转。还有一种玄学情况:数据和标签都归一化到了[0,1],但是batch_first设置错误,导致输入shape变成(seq_len, batch, features),模型根本没有读到完整序列,loss也降不下去。

解决:确认数据已经用MinMaxScaler缩放到[0,1];把学习率降到1e-3以下;检查batch_first=True后输入是否形如(batch, seq, feature)。再用torch.manual_seed(42)固定随机种子,排除初始化随机性。如果还不行,把batch_size从64降到16试试,小batch带来的梯度噪声有时能帮模型跳出局部平坦区。我一般会先跑3个epoch,把loss打出来,如果前3个epoch的loss没有任何下降,那问题大概率不在模型,而在数据或优化器。

5.2 预测曲线永远比真实值“慢半拍”

现象:验证集曲线整体向右平移了1~2个小时,RMSE看着不高,但相位明显滞后,真实值上升时预测值还在低位。

原因:这几乎都是因为输入窗口里包含了与目标同刻的当前时刻真实值。比如构造样本时用了data[i: i+look_back]作为特征,y=data[i+look_back],理论上是没问题的;但如果你在预测阶段输入的最后一个时间点恰好是当前时刻的真实浓度,模型学到的最强信号就是“把上一个值拷贝成下一个值”,因为污染物浓度短时间自相关很强,拷贝的loss比任何非线性拟合都低。模型因此退化成一个带延迟的恒等映射,看起来就是“慢半拍”。

解决:检查预测阶段的输入是否包含目标时刻之后的信息。正确做法是,预测下一小时时,输入窗口必须截止到上一小时,目标是从该时刻之后的那一个点。训练阶段没有泄漏,但预测阶段不能把当前真实浓度作为已知值再去预测当前浓度。更彻底的做法是使用多步滚动预测,并报告不同提前量的误差。如果条件允许,把窗口去掉最近一小时重训一次,延迟会明显改善。如果你想在报告里量化这个滞后现象,可以计算预测序列与真实序列的互相关,把最大相关对应的平移量写出来,这比画一条线更有说服力。

5.3 预测出负的PM2.5浓度

现象:反归一化后,预测值出现负值,或者超过600 µg/m³这种离谱区间。

原因:网络输出层用的是线性激活,输出本身是无界的。如果训练数据里浓度刚好分布在低值区,模型在边界外外推时就可能给出负值。另一个原因是逆归一化时用错了scaler,比如验证集样本对应的时间点没有正确对齐,导致用了某个特征列的最大最小值去还原另一列的预测值。

解决:最简单的是在逆变换之后做一次np.clip(pred, 0, None),把低于0的值截断为0。根本做法是改用对数变换,先对浓度取log1p再归一化,预测后再expm1还原。这种方法能抑制负值,且在污染浓度跨越几个数量级的数据集上效果更好。如果模型在低值区仍给出负数,就说明训练数据里低浓度样本占比太高,可以稍微调整训练集的时间范围,把浓度较高、规律较强的时段纳入训练。你也可以统计一下负数在预测结果中的占比,如果超过5%,那基本可以确定是模型或归一化的问题,而不是随机噪声。

5.4 训练集R²=0.98、验证集却崩了

现象:训练集上RMSE很小,R²接近1,验证集上R²只有0.2甚至为负。

原因:除了过拟合,更隐蔽的原因是数据划分泄漏——时间序列数据被随机打乱了。如果使用train_test_split(X, y, test_size=0.2, shuffle=True),验证集里包含训练集期间的数据,模型见过未来片段,验证指标毫无参考价值。另一种泄漏是把scaler在全量数据上fit_transform,再划分数据集,归一化时已经偷看了验证集的统计量,严格来说也算泄漏。

解决:严格按时间切分,训练集在前、验证集在后。scaler只对训练集调用fit,然后transform验证集。如果确认划分没问题但验证集仍然崩,那就是过拟合,适当增加LSTM的dropout,或把num_layers从2降回1,减少参数规模。还可以在训练循环中每10个epoch计算一次验证集loss,一旦验证loss连续上升就提前停止,这个早停技巧在期末报告里可以写成一个带patience参数的简单实现。最简单的自查方法:打印验证集的时间范围,确认它晚于训练集,如果重叠,立刻改划分方式。

5.5 内存暴涨或训练龟速

现象:小数据量训练要等十几分钟一个epoch,或者跑着跑着内存占用翻倍,最后程序被杀。

原因:最常见的是在循环里重复调用torch.tensor(X)创建临时张量,没有做with torch.no_grad():,计算图不断累积。另一个原因是look_back太大,比如168,且特征有6列,生成的样本数接近(总长度-168),每个样本是(168,6)的浮点数组,几万样本会轻松超过1GB内存。

解决:用DataLoader分batch迭代,避免一次性把所有样本都加载成tensor;训练循环里确保每次迭代都optimizer.zero_grad(),否则梯度会累加到下一次,计算图不断堆积。如果look_back=168后内存超限,把batch_size降到16,或把特征列减少到PM2.5、PM10、CO、O3四个主要组分。在Windows上注意别给DataLoader设置num_workers>0,否则容易触发内存泄漏和假死,这是PyTorch在Windows上的老问题,降低num_workers=0就能解决。想定位内存问题,可以用psutil.Process().memory_info().rss在每个epoch后打印显存或内存占用,观察是否存在持续上涨。

6. 给源码加一个命令行入口:让期末作业从“能跑”变成“能用”

6.1 用argparse封装predict.py,支持--input、--hours和--checkpoint

训练和评估做完后,再写一个predict.py,让老师和同学能从命令行直接调用,这是把项目从“我的实验脚本”提升成“可复现工程”的关键一步。期末报告里写“项目支持命令行预测”是实打实的亮点,而且实现起来只需要十几行。

import argparse import numpy as np import torch import pandas as pd from sklearn.preprocessing import MinMaxScaler parser = argparse.ArgumentParser(description='LSTM AQI Forecast Tool') parser.add_argument('--input', type=str, default='air_quality.csv', help='path to input CSV') parser.add_argument('--hours', type=int, default=24, help='how many hours to forecast') parser.add_argument('--checkpoint', type=str, default='lstm_aqi_model.pt', help='path to model checkpoint') args = parser.parse_args() # 加载checkpoint、构造最近窗口、调用predict_future,输出结果到CSV

然后按第3章的加载方式重建模型,读入args.input的最后一段数据,构造窗口调用predict_future,把结果保存成forecast_result.csv。注意兼容检查:这个脚本要能处理checkpoint里没有scaler的旧模型,如果'scaler' not in checkpoint,就用某个默认路径加载scaler.save并给出警告。这体现了防御性编程,老师抽查时不会翻车。

我自己的教训是:第一版预测脚本只保存了模型权重,答辩现场演示时输出全是0到1之间的归一化值,老师在旁边看着,场面很尴尬。后来我养成了习惯——凡是做时序预测项目,一定把scaler和模型绑在一起保存,并且写一个能从命令行独立运行的小工具。这个习惯后来在好几个项目里都派上用场。如果你正卡在LSTM空气质量预测这个方向,别急着堆模型结构,先把数据、评估、封装这三件事做扎实,项目自然就有高分的样子。希望帮到你。

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

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

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

立即咨询