基于tanh激活函数的BP神经网络AQI预测与梯度消失优化
2026/9/15 14:12:32 网站建设 项目流程

简介:一套基于Python+BP神经网络的天气质量预测模型资源,使用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(drop=True) df[feature_cols + [target_col]] = df[feature_cols + [target_col]].fillna(method="ffill").fillna(method="bfill") 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]])
  • ffill+bfill填充空值:先拿前一天的值补当天,若前一天也是空,则拿后一天补。这种简单策略在时间序列里比均值填充更合理,因为气象数据本身有连续性。
  • 注意scaler_y也要独立 fit。预测完成后必须用inverse_transform还原成真实AQI,否则输出的是 [0,1] 区间的小数,没法直接解读。
  • 我选的8个特征并不一定最优,你完全可以根据自己的数据增加“季节”“星期几”等派生特征。特征越多,网络需要学习的参数越多,越容易过拟合。

2.3 滑动窗口生成监督学习样本

BP神经网络不能直接吃“序列”,它要求每个样本是固定维度的向量。所以需要把连续的时间序列转换成监督学习格式:用过去lookback天的所有特征,预测未来forecast_horizon天的AQI。这里我设定 lookback=7(预测未来1天),也就是用一周的气象变化推测下一天AQI。

import numpy as np def create_sequences(data, lookback=7, horizon=1): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data[i:i+lookback].flatten()) y.append(data[i+lookback:i+lookback+horizon, -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, lookback=7, horizon=1) print(X_seq.shape, y_seq.shape)
  • data[i:i+lookback].flatten()把7天×9个数值展平成63维向量,作为网络输入。
  • y取的是第i+lookback天的AQI值,也就是预测目标。
  • 这里的关键参数是lookback:窗口越小,模型只看到短期波动;窗口越大,样本数越少,且容易引入过多噪声。益阳2021年数据不足365条,lookback=7 后样本数约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) ** 2
  • tanh的导数是1 - tanh(z)^2,当输入落在0附近时导数接近1,梯度能有效回传;当输入绝对值大于3时,tanh输出接近±1,导数接近0,这就是“饱和区”。
  • 反向传播时需要同时保存前向传播的za,因为计算梯度时要用到前一层的激活值。

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, lr=0.001): m = X.shape[0] delta2 = (Z2 - y) / m dW2 = np.dot(A1.T, delta2) db2 = np.sum(delta2, axis=0, keepdims=True) delta1 = np.dot(delta2, W2.T) * tanh_derivative(Z1) dW1 = np.dot(X.T, delta1) db1 = np.sum(delta1, axis=0, keepdims=True) W1 -= lr * dW1 b1 -= lr * db1 W2 -= lr * dW2 b2 -= lr * db2
  • delta2 = 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.pyplot

Matlab的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的方差会随输入维数增加。输入维度63,Z1的方差大约是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_in=63fan_out=16,所以标准差sqrt(2/(63+16)) ≈ 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_dimout_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(f"epoch {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最低点停止训练,这就是早停。
  • 学习率lr=0.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_reg=0.001是一个可以接受的起点。

5. 滚动预测未来AQI:模型验证与误差技巧

5.1 多步滚动预测实现

训练好的模型只能预测下一天的AQI,如果想知道后三天的情况,一种朴素方法是把预测值当作已知输入,再继续迭代预测。这就是滚动预测。代码实现如下:

def roll_predict(model, X_input, steps=3): 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, axis=1) current_input[0, -1, :] = new_row return predictions
  • np.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(f"MAE={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增加到14,MAE约下降10%左右,但训练时间也会翻倍。我一般会先跑一个简易网格搜索:

for lb in [7, 14, 30]: X_lb, y_lb = create_sequences(data_all, lookback=lb, horizon=1) # 训练并记录val_loss print(f"lookback={lb}, loss={val_loss:.4f}")

最终选择验证损失最小的lookback,而不是一味加大窗口。这个技巧在你做课程设计答辩时非常有用,能直接展示你尝试过不同时间窗口,并基于验证集做出选择,而不是拍脑袋定参数。

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

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

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

立即咨询