
简介一套基于PythonBP神经网络的天气质量预测模型资源使用tanh激活函数与梯度下降法面向希望入门神经网络或完成课程设计、毕业设计的学习者。资源以天气历史数据为输入对未来的AQI空气质量指数进行预测适合作为人工智能相关项目的初期模板。压缩包共3个文件包含xlsx格式的AQI数据集、md格式的说明文档以及m格式的预测代码整体仅25KB结构精简便于快速上手。目前已有141人学习使用。通过该资源读者可获得完整的数据文件与可运行模型代码理解BP神经网络在时序预测中的搭建思路掌握tanh函数与梯度下降法的实际应用方式并能够在此基础上扩展特征或调整网络结构开展进一步的实验与二次开发。1. 从AQI预测理解BP神经网络为什么tanh是默认选择空气质量指数AQI的预测本质上是一个时序回归问题用过去几十天的气象与污染物浓度推断未来几天的空气质量级别。传统的统计模型如ARIMA需要平稳性与大量历史数据面对PM2.5、PM10、SO₂、NO₂、O₃、CO、风速、湿度等多维特征时特征交互关系很难用线性方程表达。BP神经网络的优势在于在隐藏层激活函数不饱和的前提下它能通过梯度下降自动逼近任意连续非线性函数这正是AQI这类多因子耦合场景所需要的。项目给定了一份益阳市2021年的日度气象与污染物数据yiyang2021aqi.xlsx和一份Python/Matlab混合实现WeatherForecost.m核心思路是以历史AQI及相关气象因子作为输入利用BP网络隐藏层激活采用tanh函数输出层采用线性激活用梯度下降法迭代更新权重得到未来的AQI预测值。与ReLU相比tanh输出以0为中心梯度更新更均衡训练更稳定与sigmoid相比tanh的梯度最大值为1梯度消失的临界点更远因此在中等深度网络中往往收敛更快。这篇文章从一个可直接运行的工程视角出发先把数据整理成监督学习样本再手写一个紧凑的BP网络最后给出滚动预测和误差评估的完整流程适合正在做课程设计、毕业设计或刚接触深度学习的开发者。2. 构造天气AQI数据集从Excel到可训练的样本矩阵2.1 原始数据字段与预测目标定义先看数据文件。yiyang2021aqi.xlsx里包含逐日记录字段通常是日期、AQI、PM2.5、PM10、SO₂、NO₂、O₃、CO以及温度、湿度、气压等。这里的预测目标不是空气质量等级而是具体的AQI数值——回归输出。如果用分类模型去预测“良/轻度污染”会丢失数值粒度也无法计算RMSE这类连续误差。我做的第一件事是先读出来看结构和缺失情况import pandas as pd df pd.read_excel(yiyang2021aqi.xlsx, parse_dates[date]) print(df.dtypes) print(df.isna().sum()) print(df.head())parse_dates把日期列转成时间索引方便后续按天排序和滑动窗口切分。查看isna().sum()是为了定位哪些特征存在空值。气象站数据经常因为设备维护导致个别天缺失常见做法是用前后两天均值填充而不是直接删行。输出前几行是为了确认列名的大小写和单位。例如CO的单位可能是 mg/m³而PM2.5是 μg/m³单位不同会导致归一化后数值分布差异很大。2.2 缺失值处理与特征归一化AQI预测中特征尺度差异极大O₃常年在几十到两百CO则在 0.x 到 2.x 之间。若不做归一化梯度下降在“大尺度特征”上权重更新过快在“小尺度特征”上几乎不动网络很难收敛。常见做法是使用 min-max 归一化把特征压缩到 [0,1]但这里有一个细节由于后续要用 tanh 激活tanh 的输出范围是 [-1,1]输入层归一化到 [0,1] 没有问题但隐藏层的输出会被自动映射到 [-1,1]所以不需要刻意把输入也调整到 [-1,1]。from sklearn.preprocessing import MinMaxScaler feature_cols [pm2_5, pm10, so2, no2, o3, co, humidity, wind_speed] target_col aqi df df.sort_values(date).reset_index(dropTrue) df[feature_cols [target_col]] df[feature_cols [target_col]].fillna(methodffill).fillna(methodbfill) scaler_x MinMaxScaler(feature_range(0, 1)) scaler_y MinMaxScaler(feature_range(0, 1)) X_scaled scaler_x.fit_transform(df[feature_cols]) y_scaled scaler_y.fit_transform(df[[target_col]])用ffillbfill填充空值先拿前一天的值补当天若前一天也是空则拿后一天补。这种简单策略在时间序列里比均值填充更合理因为气象数据本身有连续性。注意scaler_y也要独立 fit。预测完成后必须用inverse_transform还原成真实AQI否则输出的是 [0,1] 区间的小数没法直接解读。我选的8个特征并不一定最优你完全可以根据自己的数据增加“季节”“星期几”等派生特征。特征越多网络需要学习的参数越多越容易过拟合。2.3 滑动窗口生成监督学习样本BP神经网络不能直接吃“序列”它要求每个样本是固定维度的向量。所以需要把连续的时间序列转换成监督学习格式用过去lookback天的所有特征预测未来forecast_horizon天的AQI。这里我设定 lookback7预测未来1天也就是用一周的气象变化推测下一天AQI。import numpy as np def create_sequences(data, lookback7, horizon1): X, y [], [] for i in range(len(data) - lookback - horizon 1): X.append(data[i:ilookback].flatten()) y.append(data[ilookback:ilookbackhorizon, -1]) # 最后一列是AQI return np.array(X), np.array(y) data_all np.hstack([X_scaled, y_scaled]) X_seq, y_seq create_sequences(data_all, lookback7, horizon1) print(X_seq.shape, y_seq.shape)data[i:ilookback].flatten()把7天×9个数值展平成63维向量作为网络输入。y取的是第ilookback天的AQI值也就是预测目标。这里的关键参数是lookback窗口越小模型只看到短期波动窗口越大样本数越少且容易引入过多噪声。益阳2021年数据不足365条lookback7 后样本数约350个左右足够训练一个隐藏层几十个神经元的网络。3. tanh激活与反向传播手写BP网络的Python实现3.1 BP网络结构与tanh导数计算很多课程设计直接调用sklearn.neural_network.MLPRegressor但要想讲清楚“tanh为什么能让梯度下降正常工作”最好还是手写一次反向传播。网络结构设为输入层63个神经元7天×9个特征隐藏层16个神经元输出层1个神经元。隐藏层使用tanh输出层不接激活函数这样输出可以是一个连续值再经过inverse_transform还原成AQI。def tanh(z): return np.tanh(z) def tanh_derivative(z): return 1 - np.tanh(z) ** 2tanh的导数是1 - tanh(z)^2当输入落在0附近时导数接近1梯度能有效回传当输入绝对值大于3时tanh输出接近±1导数接近0这就是“饱和区”。反向传播时需要同时保存前向传播的z和a因为计算梯度时要用到前一层的激活值。3.2 前向传播与损失函数定义权重矩阵W1输入→隐藏、b1以及W2隐藏→输出、b2。损失函数用均方误差MSE因为AQI预测是连续值回归问题。def forward(X): Z1 np.dot(X, W1) b1 A1 tanh(Z1) Z2 np.dot(A1, W2) b2 return Z1, A1, Z2 def compute_loss(y, y_hat): return np.mean((y - y_hat) ** 2)np.dot(X, W1)使用矩阵乘法一次处理整个batch而不是单个样本循环这样能利用numpy的向量化加速。损失取平均是为了让loss大小不受batch数量影响便于在不同batch size之间比较。3.3 梯度下降参数更新与代码实现反向传播的推导是BP网络的核心。对输出层误差项delta2 y_hat - y对隐藏层误差项delta1 delta2 * W2 * tanh(Z1)。权重梯度就是误差项乘以上一层激活值。def backward(X, y, Z1, A1, Z2, lr0.001): m X.shape[0] delta2 (Z2 - y) / m dW2 np.dot(A1.T, delta2) db2 np.sum(delta2, axis0, keepdimsTrue) delta1 np.dot(delta2, W2.T) * tanh_derivative(Z1) dW1 np.dot(X.T, delta1) db1 np.sum(delta1, axis0, keepdimsTrue) W1 - lr * dW1 b1 - lr * db1 W2 - lr * dW2 b2 - lr * db2delta2 Z2 - y是因为输出层是线性激活MSE的偏导恰好就是误差值。如果输出层加了sigmoid这里还要再乘sigmoid的导数。dW2是当前batch的梯度严格来说应该除以batch大小这里delta2已经除以m所以dW2也就归一化了。学习率lr是最敏感的超参数。tanh在初始权重较大时很容易落入饱和区此时梯度极小学习率再大也没用。我一开始把权重初始化为np.random.randn * 1.0发现loss完全不动后来改用Xavier初始化才解决。3.4 与Matlab版本WeatherForecost.m的差异对照项目里附带的WeatherForecost.m是Matlab实现其核心逻辑与上面的Python版本一致但有几个工程差异值得注意实现环节Matlab常见做法Python建议做法数据读入readtable/xlsreadpandas.read_excel归一化mapminmaxsklearn.MinMaxScaler网络迭代循环epoch常用双层fornumpy矩阵化一个epoch用一次矩阵乘法绘图plotmatplotlib.pyplotMatlab的mapminmax默认把数据缩放到 [-1,1]而Python的MinMaxScaler默认到 [0,1]。这两者本身都不影响预测精度但要特别注意如果你先用Matlab跑出了标准结果再用Python复现必须保证输入尺度一致否则同一组超参数下收敛速度会完全不同。我通常会在Python里直接把feature_range(-1,1)配给scaler_x这样就和Matlab的mapminmax对齐。4. tanh饱和区的收敛陷阱梯度消失与学习率调整4.1 为什么tanh在初始化不当时会卡死tanh函数是零中心对称的理论上比sigmoid好但它仍存在饱和区。假设输入特征归一化到 [0,1]权重矩阵如果直接用标准正态分布初始化隐藏层的线性输出Z1 X·W1的方差会随输入维数增加。输入维度63Z1的方差大约是63于是很多Z1落到 -5 或 5 附近tanh的导数接近0。反向传播时delta1乘以近0的tanh_derivative梯度被压成几乎消失loss一开始就卡住。4.2 权重初始化Xavier与He的选择针对tanh激活标准做法是Xavier初始化权重方差取2 / (fan_in fan_out)其中fan_in是输入节点数fan_out是输出节点数。这里fan_in63fan_out16所以标准差sqrt(2/(6316)) ≈ 0.159。def init_weights(in_dim, out_dim): limit np.sqrt(2 / (in_dim out_dim)) return np.random.uniform(-limit, limit, (in_dim, out_dim))使用均匀分布还是正态分布都可以关键是方差受in_dim和out_dim约束。Xavier假设激活函数在0附近线性tanh在0附近确实接近线性所以适用。如果是ReLU激活通常用He初始化方差为2/fan_in。ReLU在正区间导数为1不需要考虑输出端对称性。初始化失误时loss曲线表现为前几十个epoch保持在同一个数值无法下降。这时先别调学习率先检查np.std(Z1)。如果std(Z1)大于2说明初始权重过重立即改用Xavier。4.3 训练过程的监控指标与早停训练时不能只看最终loss要看训练集和验证集的loss曲线是否同步下降。我用训练集前80%做训练后20%做验证每10个epoch打印一次loss。epochs 3000 batch_size 32 lr 0.01 for epoch in range(epochs): idx np.random.permutation(len(X_train)) for start in range(0, len(X_train), batch_size): end start batch_size batch_idx idx[start:end] X_batch X_train[batch_idx] y_batch y_train[batch_idx] Z1, A1, Z2 forward(X_batch) backward(X_batch, y_batch, Z1, A1, Z2, lr) if epoch % 100 0: _, _, pred_train forward(X_train) _, _, pred_val forward(X_val) print(fepoch {epoch}: train_loss{compute_loss(y_train, pred_train):.4f}, val_loss{compute_loss(y_val, pred_val):.4f})np.random.permutation让每个epoch内样本顺序打乱避免模型学到样本间的顺序依赖。验证集不参与反向传播只用于计算监控loss。验证loss先降后升是过拟合信号应在验证loss最低点停止训练这就是早停。学习率lr0.01在Xavier初始化下是tanh网络的合理起点。如果训练loss震荡剧烈说明lr偏大如果下降太慢可以按0.001→0.01→0.1三档试但不要一上来就用0.1。4.4 正则化与dropout的边界因为样本量只有300多条网络很容易在几十个epoch后记住训练集。常见做法是L2正则化在损失函数中加λ * (W1^2 W2^2)梯度更新时额外减去λ * W。另一种是dropout但在手写BP里实现稍微麻烦。lambda_reg 0.001 def compute_loss_with_reg(y, y_hat): mse np.mean((y - y_hat) ** 2) reg lambda_reg * (np.sum(W1 ** 2) np.sum(W2 ** 2)) return mse reg在反向传播中dW1 np.dot(X.T, delta1) lambda_reg * W1dW2同理。正则系数太小起不到抑制过拟合的作用太大则会使权重过于接近0模型退化成线性回归。对于AQI这种特征非线性较强的场景lambda_reg0.001是一个可以接受的起点。5. 滚动预测未来AQI模型验证与误差技巧5.1 多步滚动预测实现训练好的模型只能预测下一天的AQI如果想知道后三天的情况一种朴素方法是把预测值当作已知输入再继续迭代预测。这就是滚动预测。代码实现如下def roll_predict(model, X_input, steps3): predictions [] current_input X_input.copy() # shape (1, lookback, n_features) for _ in range(steps): flat_input current_input.flatten().reshape(1, -1) _, _, pred forward(flat_input) pred_value scaler_y.inverse_transform(pred)[0, 0] predictions.append(pred_value) # 用预测的AQI替换最旧一天并滚动序列 new_row current_input[0, -1, :].copy() new_row[-1] pred[0, 0] current_input np.roll(current_input, -1, axis1) current_input[0, -1, :] new_row return predictionsnp.roll把整个7天窗口向左移一天最旧的数据被丢弃最新的预测值填充到序列末尾。注意实际预测时其他气象特征如PM2.5、SO₂也是未知的但简化处理时可以把它们保持为最后一天的观测值或者另用气象预报值替代。这个滚动策略的误差会随steps增加而累积所以多步预测的误差通常高于单步。5.2 评价指标MAE、RMSE与R²模型不能只看loss需要用原始尺度评估。我分别计算MAE平均绝对误差、RMSE均方根误差和R²决定系数。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score y_pred_inv scaler_y.inverse_transform(pred_val) y_true_inv scaler_y.inverse_transform(y_val) mae mean_absolute_error(y_true_inv, y_pred_inv) rmse np.sqrt(mean_squared_error(y_true_inv, y_pred_inv)) r2 r2_score(y_true_inv, y_pred_inv) print(fMAE{mae:.2f}, RMSE{rmse:.2f}, R2{r2:.3f})MAE越接近0说明平均偏差越小但会被个别极端AQI值拉高。RMSE对大误差更敏感如果RMSE远大于MAE说明预测在某些重污染天严重失准。R²负数说明模型比直接预测均值还差通常是因为训练/验证切分时没有打乱时间序列导致验证集的AQI分布与训练集差异很大。这里我把数据按时间顺序排序后再切分实际上把前80%天数的数据训练后20%验证。若跨季节验证集可能全是夏季的低AQI值R²自然不好。更严谨的做法是使用时间序列交叉验证。5.3 一个容易踩的坑预测值滞后现象用过去7天预测未来1天时常见问题是预测曲线总是比真实AQI曲线“慢一拍”也就是所谓滞后效应。原因是AQI有强自相关性模型发现最简单的降低损失方式就是直接复制最近一天的AQI。要检查这一点可以计算预测值和真实值的前后相关性如果预测值序列与真实值错位一天后的相关系数更高说明模型确实在拖延。缓解滞后有三个办法一是增加lookback到14或30天让模型看到更长周期二是把预测目标从“未来1天”改为“未来3天”迫使模型学习趋势三是加入日期特征如月份、星期作为输入打破纯自相关的捷径。实测中把lookback从7增加到14MAE约下降10%左右但训练时间也会翻倍。我一般会先跑一个简易网格搜索for lb in [7, 14, 30]: X_lb, y_lb create_sequences(data_all, lookbacklb, horizon1) # 训练并记录val_loss print(flookback{lb}, loss{val_loss:.4f})最终选择验证损失最小的lookback而不是一味加大窗口。这个技巧在你做课程设计答辩时非常有用能直接展示你尝试过不同时间窗口并基于验证集做出选择而不是拍脑袋定参数。本文还有配套的精品资源点击获取