尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于LSTM的北京空气质量指数预测:从CSV数据到模型部署的完整实战

基于LSTM的北京空气质量指数预测:从CSV数据到模型部署的完整实战 简介这份资源是面向高校学生与Python初学者的LSTM空气质量指数预测完整项目源码可直接用于期末大作业、课程设计或毕业设计。项目以北京空气质量历史数据为基础通过长短期记忆网络完成AQI时序预测代码注释详尽新手也能读懂并快速部署运行。压缩包共约2000个文件以1989个csv数据文件为核心辅以少量xml配置、4个py源码文件、1个md说明文档及1个pyc缓存文件整体约29.33MB数据与代码分层清晰便于按模块查阅。目前已有222人学习下载属于经过导师认可的高分项目。读者可获得一套可直接运行的完整方案包括数据预处理、LSTM模型搭建与训练、预测结果输出等环节既能作为作业提交也能在此基础上调整网络结构或更换数据集开展进一步实验具有较高的参考与复用价值。1. 从一份北京空气质量 CSV 说起这套 LSTM 预测源码到底能跑出什么如果你手头正好有一份beijing_extra_20140509.csv这样的逐日空气质量数据又需要在两周内交出一个能演示、能讲清原理、还能经得起导师追问的 Python 期末大作业那这套基于 LSTM 的空气质量指数预测源码就是为你准备的。它把数据读取、缺失值处理、滑动窗口构造、LSTM 建模、训练与预测可视化串成了一条完整链路代码里带注释新手照着改路径就能跑。适合谁课程设计赶工的学生、想快速摸清 LSTM 时间序列预测套路的入门者以及需要一个可复现基线来对比自己模型的从业者。它不解决“预测精度吊打论文”的问题但能让你在有限时间里拿到一个结构完整、逻辑自洽、能讲出所以然的结果。2. 数据到张量把北京空气质量 CSV 喂进 LSTM 之前要做的四件事拿到beijing_extra_20140501.csv到beijing_extra_20140907.csv这一批文件很多人第一反应是pd.concat一把梭然后直接model.fit。我见过太多这样跑出来的 loss 曲线像心电图最后预测值贴着一条水平线不动。问题不在 LSTM而在数据进网络之前的那几步。这一章把数据预处理拆成四个必须落地的动作每一步都给出可抄的代码和参数解释。2.1 多文件合并与时间列对齐这批 CSV 是按日期命名的文件名里的20140509就是数据日期。常见做法是用glob批量读取再从文件名或表内时间列解析出统一的时间戳最后按时间排序。注意不同文件的列顺序可能不一致合并前先统一列名。import pandas as pd import glob import os # 按文件名排序保证时间顺序 files sorted(glob.glob(data/beijing_extra_*.csv)) df_list [] for f in files: tmp pd.read_csv(f) # 从文件名提取日期写入新列避免依赖表内可能缺失的时间列 date_str os.path.basename(f).replace(beijing_extra_, ).replace(.csv, ) tmp[date] pd.to_datetime(date_str, format%Y%m%d) df_list.append(tmp) df pd.concat(df_list, ignore_indexTrue) df df.sort_values(date).reset_index(dropTrue) print(df.shape, df[date].min(), df[date].max())逻辑说明sorted(glob.glob(...))保证文件按名称升序读入文件名里的日期天然可排序。从文件名提取日期比信任表内时间列更稳因为这类数据集经常出现时间列格式不统一或缺失的情况。pd.concat之后必须sort_values再reset_index否则后续滑动窗口会跨日期错位。参数上format%Y%m%d对应文件名里的 8 位数字如果你的文件命名不同改这里即可。2.2 缺失值与异常值的处理边界空气质量数据里 PM2.5、PM10、SO2 这些列经常有NaN有时还有-1或999这类占位符。直接dropna会丢掉大量行直接fillna(0)又会引入虚假的“清洁日”。我一般会先把明显异常的占位符替换成NaN再用前向填充加后向填充兜底最后对剩余缺失行做删除。import numpy as np # 常见占位符替换 df df.replace([-1, 999, -999, NaN, ], np.nan) # 只对数值列做填充 num_cols df.select_dtypes(include[np.number]).columns df[num_cols] df[num_cols].ffill().bfill() # 仍然缺失的行比如开头连续缺失直接删 df df.dropna(subsetnum_cols).reset_index(dropTrue) print(处理后缺失值, df[num_cols].isna().sum().sum())逻辑说明replace把多种占位符统一成np.nan这是后续填充的前提。ffill().bfill()的组合意味着优先用前一天的值补如果开头就缺再用后一天的值补。dropna只针对数值列保留日期列。参数上如果你的数据里还有-9999之类的值加进replace列表即可。这一步做完数据行数通常会减少 5% 到 15%属于正常范围。2.3 滑动窗口构造用多少天预测下一天LSTM 时间序列预测的核心是把连续序列切成(样本数, 时间步, 特征数)的三维张量。假设用过去 7 天的多列污染物数据预测第 8 天的 AQI窗口大小look_back7预测目标target_colAQI。这里有个容易翻车的地方特征列里如果包含了 AQI 本身预测目标就泄露了模型会“作弊”。from sklearn.preprocessing import MinMaxScaler feature_cols [PM2.5, PM10, SO2, NO2, CO, O3] # 不含 AQI target_col AQI look_back 7 scaler_X MinMaxScaler() scaler_y MinMaxScaler() X_scaled scaler_X.fit_transform(df[feature_cols].values) y_scaled scaler_y.fit_transform(df[[target_col]].values) X, y [], [] for i in range(len(X_scaled) - look_back): X.append(X_scaled[i:i look_back]) y.append(y_scaled[i look_back]) X np.array(X) y np.array(y) print(X shape:, X.shape, y shape:, y.shape)逻辑说明feature_cols明确排除AQI这是防止目标泄露的关键。MinMaxScaler把每个特征缩放到[0,1]LSTM 对输入尺度敏感不做归一化时梯度容易爆炸。循环里X取i到ilook_back共 7 天y取第ilook_back天即用前 7 天预测第 8 天。参数上look_back可以改成 3、14、30但要注意样本数会相应减少look_back越大可训练样本越少。2.4 训练集与测试集的切分方式时间序列不能随机打乱切分否则未来信息会泄露到训练集。常见做法是按时间顺序取前 80% 做训练后 20% 做测试。如果数据量本身不大比如只有几百条可以按 90/10 切。split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] print(训练集:, X_train.shape, 测试集:, X_test.shape)逻辑说明X[:split]保证训练集在时间上早于测试集。split取 0.8 是经验值数据量少于 500 条时可以调到 0.9。切分之后不要再对全体数据重新fit归一化器否则测试集的统计量会污染训练过程。正确顺序是先切分再分别fit训练集、transform测试集但上面为了代码简洁先做了整体归一化严格场景下建议把scaler的fit放在切分之后。3. 搭一个能收敛的 LSTM层数、隐藏单元与 Dropout 的参数取舍数据准备好之后下一步是搭网络。PyTorch 和 Keras 都能写 LSTM这套源码常见的是 Keras 的Sequential写法因为代码短、注释清楚适合交作业。但“能跑”和“能收敛”是两回事。这一章把网络结构、编译参数和训练回调讲透让你知道每个数字改了会发生什么。3.1 输入维度与 LSTM 层堆叠输入张量的形状是(样本数, look_back, 特征数)对应(None, 7, 6)。第一层 LSTM 必须指定input_shape(look_back, len(feature_cols))。堆叠第二层 LSTM 时第一层要设return_sequencesTrue否则第二层拿不到序列维度。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential() model.add(LSTM(64, return_sequencesTrue, input_shape(look_back, len(feature_cols)))) model.add(Dropout(0.2)) model.add(LSTM(32, return_sequencesFalse)) model.add(Dropout(0.2)) model.add(Dense(1)) model.summary()逻辑说明第一层 64 个隐藏单元第二层 32 个逐层递减是常见做法目的是先提取较丰富的时序特征再压缩成固定长度向量。return_sequencesTrue只在需要接下一层 LSTM 时设置最后一层 LSTM 设False输出二维张量(batch, units)。Dropout(0.2)放在两层 LSTM 之间降低过拟合。参数上隐藏单元不是越多越好数据量几百条时 6432 已经偏大可以降到 3216。Dense(1)输出单值对应预测的 AQI。3.2 损失函数、优化器与评估指标回归任务默认用mse但 AQI 数值范围大MSE 的梯度也大学习率稍高就震荡。常见做法是先用adam默认学习率 0.001配合EarlyStopping防止过拟合。评估指标加mae因为 MAE 对异常值不如 MSE 敏感更能反映平均预测偏差。from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau model.compile(optimizerAdam(learning_rate0.001), lossmse, metrics[mae]) early_stop EarlyStopping(monitorval_loss, patience15, restore_best_weightsTrue) reduce_lr ReduceLROnPlateau(monitorval_loss, factor0.5, patience8, min_lr1e-5) history model.fit(X_train, y_train, validation_split0.1, epochs200, batch_size32, callbacks[early_stop, reduce_lr], verbose1)逻辑说明validation_split0.1从训练集尾部再切 10% 做验证注意这里仍然是按时间顺序切不是随机。EarlyStopping的patience15表示验证损失连续 15 轮不下降就停restore_best_weightsTrue保证拿回最优权重而不是最后一轮。ReduceLROnPlateau在损失停滞时把学习率减半min_lr1e-5防止学习率降到零。参数上batch_size32适合几百到几千条样本样本数少于 200 时改成 16 或 8。epochs200配合早停实际训练轮数通常远小于 200。3.3 训练过程监控与过拟合判断训练时重点看loss和val_loss的走势。如果loss持续下降但val_loss在某个点后开始上升说明过拟合需要加 Dropout 或减少隐藏单元。如果两者都下降但都很慢可能是学习率太小或窗口太短。如果loss剧烈震荡先检查归一化是否做了、batch_size是否太小。import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain_loss) plt.plot(history.history[val_loss], labelval_loss) plt.xlabel(epoch) plt.ylabel(mse) plt.legend() plt.savefig(loss_curve.png, dpi150) plt.show()逻辑说明保存 loss 曲线图是交作业时的加分项导师一眼就能看出你有没有认真调。dpi150保证图片清晰。如果曲线图里val_loss最低点出现在很靠前的位置说明模型容量过大可以砍掉一层 LSTM 或把隐藏单元减半。3.4 预测结果反归一化与可视化模型输出的是归一化后的值必须用scaler_y.inverse_transform还原成真实 AQI 量级否则画出来的图和实际差几个数量级。y_pred_scaled model.predict(X_test) y_pred scaler_y.inverse_transform(y_pred_scaled) y_true scaler_y.inverse_transform(y_test) plt.figure(figsize(12, 5)) plt.plot(y_true, label真实 AQI) plt.plot(y_pred, label预测 AQI) plt.xlabel(测试样本序号) plt.ylabel(AQI) plt.legend() plt.savefig(prediction.png, dpi150) plt.show()逻辑说明inverse_transform的参数必须是二维数组y_test本身就是(n,1)所以直接传入。如果预测曲线整体偏低或偏高先检查scaler_y是不是用训练集fit的如果用了全体数据fit测试集的还原会有偏差。参数上figsize(12,5)适合横向对比dpi150保证清晰度。4. 避坑与排查这套 LSTM 源码最容易翻车的五个地方代码能跑通不代表结果可信。这一章列五个我实际踩过或见别人踩过的坑每条按现象、原因、解决写你对照自己的运行结果排查。4.1 预测曲线是一条水平直线现象prediction.png里预测值几乎不随真实值波动像一条直线。原因最常见的是目标泄露被反向影响或者归一化时scaler_y用了全体数据导致训练和测试的尺度不一致另一个原因是学习率过大模型直接收敛到均值附近。解决确认feature_cols不含AQI把scaler_y的fit严格限制在训练集学习率从 0.001 降到 0.0005 再试。4.2 训练损失为 NaN现象第一个 epoch 的loss就是nan。原因输入数据里还有NaN或inf或者归一化后某些列方差为零导致除零。解决在构造X之前打印np.isnan(X_scaled).sum()和np.isinf(X_scaled).sum()确认都是 0检查是否有常数列如果有就删掉该特征。4.3 验证损失比训练损失低很多现象val_loss明显低于loss。原因validation_split从训练集尾部切如果尾部数据恰好比前段更平稳就会出现这种“反常”现象也可能是 Dropout 在验证时关闭导致验证表现偏好。解决不用太担心只要预测曲线合理即可如果想更严谨改用按时间切分的独立验证集而不是validation_split。4.4 多文件合并后日期重复或乱序现象合并后的df里同一天出现多次或者日期跳跃。原因不同 CSV 文件覆盖的日期范围有重叠或者文件名解析出的日期与表内日期不一致。解决合并后先df[date].duplicated().sum()看重复量再df.drop_duplicates(subset[date], keepfirst)如果表内有时间列以表内为准文件名只做校验。4.5 换一批数据就报形状错误现象用自己下载的其他城市数据跑报ValueError: Input 0 of layer lstm is incompatible。原因特征列数量变了input_shape里的len(feature_cols)没跟着改或者look_back大于数据总长度。解决把feature_cols改成自己数据的列名列表确保len(df) look_back否则减小look_back或补充数据。5. 从能跑到能讲把这份源码改造成你自己的高分项目交作业和做项目最大的区别是前者要能讲清楚每一个选择。这套源码给了你一个可运行的基线但导师追问“为什么用 7 天窗口”“为什么两层 LSTM”“MAE 是多少”时你得有答案。这一章给三个进阶方向和一个验证习惯让你从“跑通”走到“讲透”。5.1 用对比实验支撑参数选择不要只跑一组look_back7。把look_back设成 3、7、14 各跑一次记录测试集 MAE用表格呈现。同样把 LSTM 隐藏单元从 6432 改成 3216 再跑一次。导师看到对比数据比看到单一结果更认可。实验编号look_backLSTM 单元测试集 MAE136432待填276432待填3146432待填473216待填表格里的 MAE 用from sklearn.metrics import mean_absolute_error算填进去就是你的实验记录。5.2 加入基线模型做参照只报 LSTM 的 MAE 没有说服力加一个“用前一天 AQI 直接作为预测值”的朴素基线再算一个线性回归的 MAE。如果 LSTM 的 MAE 比朴素基线还差说明模型没学到东西得回头查数据泄露或归一化。from sklearn.metrics import mean_absolute_error from sklearn.linear_model import LinearRegression # 朴素基线用前一天真实 AQI 预测当天 naive_pred y_true[:-1] naive_true y_true[1:] print(朴素基线 MAE:, mean_absolute_error(naive_true, naive_pred)) # 线性回归基线 lr LinearRegression() lr.fit(X_train.reshape(len(X_train), -1), y_train.ravel()) lr_pred lr.predict(X_test.reshape(len(X_test), -1)) print(线性回归 MAE:, mean_absolute_error(y_true.ravel(), lr_pred))逻辑说明朴素基线把y_true错一位相减模拟“明天和今天一样”的预测。线性回归把三维输入展平成二维虽然丢了时序结构但能作为一个弱基线。如果 LSTM 的 MAE 低于这两个基线你的项目就有话可讲。5.3 保存模型与推理脚本分离交作业时经常需要现场演示。把训练和推理拆成两个脚本训练脚本保存model.h5和两个scaler推理脚本加载后直接对最新 7 天数据预测。这样演示时不用等训练。# 训练完成后保存 model.save(aqi_lstm.h5) import joblib joblib.dump(scaler_X, scaler_X.pkl) joblib.dump(scaler_y, scaler_y.pkl) # 推理脚本中加载 from tensorflow.keras.models import load_model model load_model(aqi_lstm.h5) scaler_X joblib.load(scaler_X.pkl) scaler_y joblib.load(scaler_y.pkl) recent df[feature_cols].values[-look_back:] recent_scaled scaler_X.transform(recent).reshape(1, look_back, -1) pred scaler_y.inverse_transform(model.predict(recent_scaled)) print(下一日 AQI 预测, pred[0, 0])逻辑说明joblib保存scaler比pickle更稳尤其是MinMaxScaler内部有 numpy 数组。推理时reshape(1, look_back, -1)把单条样本变成 batch 为 1 的三维张量。注意推理用的scaler_X必须是训练时保存的那个不能重新fit。5.4 我每次改完数据都强制跑一遍的检查清单从那以后我每次换数据或改特征列都强制走一遍这个检查打印X.shape和y.shape确认维度打印np.isnan(X).sum()确认无缺失跑 3 个 epoch 看loss是否下降画一张预测对比图看趋势是否合理。这四步花不了五分钟但能挡掉八成“跑通但结果离谱”的情况。希望帮到你。本文还有配套的精品资源点击获取
返回列表