ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

BP神经网络与tanh函数在天气质量预测中的Python实战

BP神经网络与tanh函数在天气质量预测中的Python实战 简介基于PythonBP神经网络的天气质量预测模型面向算法入门者及毕设、课程设计、工程实训等场景解决利用历史气象数据对未来AQI值进行预测的问题。模型采用tanh函数作为激活函数并配合梯度下降法训练完整呈现BP神经网络的构建与优化流程。压缩包共3个文件、总大小约25KB内含xlsx格式的益阳2021年AQI历史数据集、md格式的项目说明文档以及m格式的核心预测脚本文件精炼、结构清晰。读者可获得预测脚本与配套数据参照说明文档理解tanh激活函数、梯度下降更新、前向与反向传播等关键环节便于快速上手与二次开发。已有141人学习下载适合机器学习入门者用作练手项目也可作为课程设计或相关课题的参考模板。1. 天气质量预测为什么先选BP神经网络和tanh函数从单站小样本说起手里有一份气象站的历史观测数据想预测明天的空气质量指数。这类天气质量预测任务样本量往往只有几百到几千条特征也就是十几个气象要素和污染物浓度这时候直接上LSTM、Transformer是杀鸡用牛刀训练慢不说还会因为数据不足而学偏。基于Python的BP神经网络搭配tanh函数是这个规模下最容易在一晚上跑通、并且效果能看得见的方案。BP神经网络对非线性回归问题的拟合能力足够tanh函数又比sigmoid收敛更顺配合一组合理的滞后特征就能把气象条件映射到AQI或PM2.5浓度上。适合正在做环境数据分析、量化预报或者毕设预警系统的开发者不要求你有GPU一台普通笔记本就能跑完整个流程。2. 把气象数据清洗成BP神经网络能吃的样子滞后窗口与监督学习格式2.1 先把天气质量预测定义成回归任务输出AQI还是PM2.5天气质量预测在建模上是一个回归问题输出是一个连续数值不是类别。有人在网上问“能不能把AQI等级当分类做”可以但代价很大AQI等级边界是人为划分的50和51在数值上只差1在等级上却跨了一档分类模型会在这类边界样本上反复出错。更常见也更稳妥的做法是直接预测PM2.5浓度或者AQI数值最后再按国标映射到等级。我一般会把目标变量定成PM2.5而不是AQI。原因有两个一是PM2.5是AQI的主要贡献污染物大部分时间AQI的等级就是被PM2.5拉高的二是PM2.5的数值分布比AQI更平滑极端值少BP神经网络拟合起来更容易收敛。如果你想做的是“明天会不会污染”这种二分类问题那属于另一个任务不在本次的BP回归框架里。2.2 从原始表里挑特征气象要素和污染物浓度怎么取舍原始数据表通常长这样一个日期列若干污染物浓度列若干气象要素列。常见的气象站数据里污染物有SO2、NO2、PM10、PM2.5、CO、O3气象要素有温度、气压、湿度、风速、风向、降水量。真正值得进模型的我建议是下面这张表里的列类别字段说明污染物PM2.5、PM10、SO2、NO2、CO、O3当前及滞后时刻的浓度气象温度、气压、湿度、风速影响污染物扩散和二次生成周期小时、星期、月份交通排放和气象的周期性站点编号、数据来源ID、人工备注这类列不能喂进去。它们是类别变量直接当数值用会污染梯度。有些列看起来是数值实际上是一段一段的人工编码比如站点类型这类也要剔除。取舍标准很简单这一列去掉之后模型误差变化小于5%就说明它没提供有效信息留着纯属增加过拟合风险。2.3 构造滞后特征与滚动窗口时间序列怎么变成监督学习样本BP神经网络本身不感知时间顺序它只认“一行特征对应一个目标值”。所以要把时间序列转成监督学习格式核心手段就是滞后特征和滚动窗口。滞后特征的意思是用过去6小时、12小时、24小时的PM2.5浓度来预测当前或未来的PM2.5浓度。滚动窗口则是过去一段时间的均值或最大值用来描述累积污染水平。import pandas as pd import numpy as np df pd.read_csv(weather_air.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 滞后特征过去6、12、24小时的PM2.5浓度 for lag in [6, 12, 24]: df[fpm25_lag{lag}] df[PM2.5].shift(lag) # 滚动窗口过去72小时3天PM2.5均值描述累积污染 df[pm25_roll3] df[PM2.5].rolling(72).mean() # 周期特征小时、星期、月份作为周期性先验 df[hour] df[date].dt.hour df[weekday] df[date].dt.weekday df[month] df[date].dt.month # 移除构造滞后特征时产生的空值行 df df.dropna().reset_index(dropTrue)这段代码里shift(lag)是滞后特征的关键shift(6)表示把PM2.5整列往下移6行这样当前行的pm25_lag6就是6小时前的浓度。rolling(72).mean()是滚动均值72对应3天的小时数用于捕捉污染物的累积效应。这里要注意dropna()必须放在所有特征构造完之后因为前72行会因为shift和rolling产生NaN直接丢掉是合理的。周期特征容易被新手忽略。小时、星期、月份这三个特征能让神经网络区分早晚高峰和周末效应在天气质量预测里相当有用。构造完特征之后再把特征列与目标列分开feature_cols [ PM10, SO2, NO2, CO, O3, TEMP, PRES, HUMI, WIND_SPD, pm25_lag24, pm25_roll3, hour, weekday, month ] X df[feature_cols].astype(float) y df[PM2.5].astype(float)为什么只用了pm25_lag24而不是全部三个滞后特征这是为了控制输入维度。在小样本场景下每多一个特征就多一分过拟合风险pm25_lag24和pm25_roll3基本能覆盖短期和累积信息lag6和lag12的信息被lag24和roll3部分包含留着反而增加冗余。特征不是越多越好BP神经网络尤其如此。3. tanh函数在BP网络里强在哪前向计算、反向传播与梯度饱和3.1 激活函数不是玄学sigmoid和tanh的零中心化差异激活函数的选择直接影响BP神经网络的收敛行为。sigmoid函数输出范围是(0,1)所有输出都是正数这会导致后一层神经元的输入永远带同一个正偏置梯度更新时呈现锯齿状路径收敛变慢。tanh函数输出范围是(-1,1)零中心均值为零梯度更新方向更对称损失面走起来更顺。import numpy as np def sigmoid(x): return 1.0 / (1.0 np.exp(-x)) def tanh(x): return np.tanh(x) def sigmoid_derivative(x): s sigmoid(x) return s * (1 - s) def tanh_derivative(x): t tanh(x) return 1.0 - t * t # 对比同一输入下的输出与梯度 x np.array([-2.0, -1.0, 0.0, 1.0, 2.0]) print(sigmoid:, sigmoid(x)) print(tanh :, tanh(x)) print(sigmoid导数:, sigmoid_derivative(x)) print(tanh导数 :, tanh_derivative(x))运行这段对比代码你能直接看到tanh在零附近输出的变化幅度比sigmoid大梯度也更大。什么意思在BP神经网络反向传播时梯度要大一些参数更新才走得动。sigmoid在输入绝对值较大时梯度趋近于0信息传不到浅层这是经典梯度消失问题的源头之一。tanh也有梯度饱和区但它的饱和区比sigmoid窄零中心特性让它在前几轮迭代里表现明显好于sigmoid。在天气质量预测这个场景里输入特征经过归一化后落在[-1,1]附近正好是tanh梯度最灵敏的区域。这是tanh函数和这类小样本回归任务最匹配的地方。ReLU虽然解决了正区间的梯度消失但它的输出非零中心而且一旦某个神经元落在负区间梯度就是0后续永远不会再激活也就是所谓的“死亡ReLU”问题。在小规模BP网络里ReLU的死亡神经元问题比大网络更明显所以我更愿意用tanh而不是ReLU作为隐含层激活函数。3.2 tanh的梯度饱和什么情况下反向传播会停摆tanh的导数是1 - tanh²(x)当x趋近正无穷或负无穷时导数趋近0。也就是说如果某层输入绝对值过大tanh被推到饱和区反向传播到这里梯度就断掉了更浅的层几乎学不到东西。这正是为什么输入必须做归一化。如果不归一化比如把温度原始值30°C直接喂进去和滞后特征里0到300的PM2.5浓度混在一起线性组合的结果很容易超出tanh的有效范围网络进入饱和区损失曲线变成一条平线。这一点在天气质量预测里尤其容易碰见因为不同特征的量纲差异太大了CO浓度是0到几PM2.5是0到300气压是900到1000混在一起不做缩放tanh很快饱和。解决办法就是把所有特征缩放到[-1,1]或[0,1]区间让线性组合后的值大概率落在tanh的灵敏区间。这也是为什么第4章里我会用MinMaxScaler(feature_range(-1,1))而不是用z-score标准化。z-score处理后的数据虽然均值是0方差是1但会有相当比例的值超出[-1,1]对tanh来说就是一部分样本被推到饱和区白白浪费梯度。3.3 BP神经网络结构图里最常用的拓扑输入层、隐含层、输出层网上搜“BP神经网络结构图”看到的拓扑几乎都是同一个形态最左边输入层中间一到两个隐含层最右边输出层层与层之间全连接。天气质量预测模型我一般用一层或两层隐含层神经元数量从输入特征数的一倍到两倍之间取比如特征数14隐含层神经元就从16开始在32以内调。更深的结构在这个数据规模下没有收益只会拉长训练时间。层神经元数激活函数说明输入层14无特征列数决定隐含层116到32tanh主要逼近非线性关系隐含层28到16tanh可选样本量大时加输出层1线性回归任务用线性激活输出层不能用tanh因为tanh输出范围是[-1,1]而PM2.5的预测值在反归一化之前需要保持连续线性激活才允许网络输出任意范围的数值。这是很多新手在结构图上翻车的地方看到大家都在用tanh就把输出层也设成tanh结果预测值永远被压在[-1,1]里。BP反向传播的核心逻辑用一段最小代码可以讲清楚。下面这段是单隐层网络的单步更新展示前向传播和反向传播的骨架实际调参时我仍然用sklearn但理解这段代码会让你知道模型在内部到底做了什么。def train_one_step(X, y, W1, b1, W2, b2, lr0.01): # 前向传播输入 - 隐含层(tanh) - 输出层(线性) Z1 X W1 b1 A1 np.tanh(Z1) Z2 A1 W2 b2 y_hat Z2 # 输出层梯度均方误差的导数 dZ2 y_hat - y.reshape(-1, 1) dW2 A1.T dZ2 db2 np.sum(dZ2, axis0, keepdimsTrue) # 反向传播到隐含层关键在这一步的tanh导数 dA1 dZ2 W2.T dZ1 dA1 * tanh_derivative(Z1) dW1 X.T dZ1 db1 np.sum(dZ1, axis0, keepdimsTrue) # 梯度下降更新参数 W1 - lr * dW1 b1 - lr * db1 W2 - lr * dW2 b2 - lr * db2 return W1, b1, W2, b2这里逐行说Z1是输入经过权重和偏置后的线性组合A1 np.tanh(Z1)是tanh激活y_hat是输出层因为没有接激活函数所以就是Z2。反向传播时dZ1 dA1 * tanh_derivative(Z1)就是tanh的导数在反向传播链里起的作用它把上层传回来的梯度按tanh的斜率缩放。如果Z1绝对值很大tanh_derivative(Z1)接近0梯度在这里断掉网络就学不动了。这是理解tanh函数和BP神经网络关系最关键的一行。4. 用Python把天气预测模型训练跑通MLPRegressor归一化、训练与特征灵敏度4.1 归一化必须匹配tanh的输出范围X和y都要缩放上一章说了输入X要缩放到[-1,1]这里展开讲原因。MLPRegressor的activationtanh作用于隐含层但网络的第一层输入是先经过权重线性组合再做tanh的。如果输入的尺度不统一部分特征绝对值过大线性组合结果很容易把tanh推到饱和区模型就失去了学习能力。目标变量y也要缩放这取决于输出层是否用了线性激活。输出层用identity线性y理论上可以不缩放但残差过大时损失函数值会很大梯度更新步长在Adam的默认设置下可能震荡。我一般把y也缩到[0,1]这样损失数值和梯度的量级更匹配训练曲线更平顺。注意一个关键细节归一化只能对训练集做fit_transform验证集或测试集只能transform。这个要求不是洁癖而是防止数据泄漏。如果对全量数据做fit均值、最小值、最大值里就包含了未来信息验证集的表现会虚高。4.2 最小可复现流程加载数据、训练、反归一化、评估假设你已经用第2章的代码准备好了X和y下面这整段就是完整的训练流程。如果你还没装scikit-learn先执行pip install scikit-learn这是Python生态里实现BP神经网络最省事的库不需要手写反向传播。from sklearn.preprocessing import MinMaxScaler from sklearn.neural_network import MLPRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error # 严格按时间顺序切分前80%训练后20%验证 split_idx int(len(X) * 0.8) X_train, X_test X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test y.iloc[:split_idx], y.iloc[split_idx:] # 输入缩放到[-1,1]匹配tanh灵敏区目标缩放到[0,1] scaler_x MinMaxScaler(feature_range(-1, 1)) scaler_y MinMaxScaler(feature_range(0, 1)) X_train_scaled scaler_x.fit_transform(X_train) X_test_scaled scaler_x.transform(X_test) y_train_scaled scaler_y.fit_transform(y_train.values.reshape(-1, 1)).ravel() y_test_scaled scaler_y.transform(y_test.values.reshape(-1, 1)).ravel() # 构建BP神经网络 model MLPRegressor( hidden_layer_sizes(32, 16), # 两个隐含层神经元数32和16 activationtanh, # 隐含层激活函数tanh solveradam, # Adam自适应学习率 learning_rate_init0.001, # 初始学习率 max_iter600, # 最多迭代600轮 early_stoppingTrue, # 验证集loss不降则提前停 n_iter_no_change20, # 连续20轮无改善就停 validation_fraction0.1, # 从训练集抽出10%做早停验证 random_state42 # 固定随机种子结果可复现 ) model.fit(X_train_scaled, y_train_scaled) # 预测并反归一化回原始量纲 y_pred_scaled model.predict(X_test_scaled) y_pred scaler_y.inverse_transform(y_pred_scaled.reshape(-1, 1)).ravel() print(MAE :, mean_absolute_error(y_test, y_pred)) print(RMSE:, mean_squared_error(y_test, y_pred) ** 0.5)参数说明按顺序讲。hidden_layer_sizes(32, 16)是网络拓扑两个隐含层第一层32个神经元第二层16个如果样本量少先降成(16,)单层。activationtanh是这次的主题隐含层激活函数。solveradam在中小数据集上比sgd收敛更省心不用手动调学习率衰减策略。learning_rate_init0.001是Adam的初始学习率太大损失曲线震荡太小要等很久才收敛。early_stoppingTrue配合validation_fraction0.1意思是网络会自动从训练集里抽出最后10%当早停验证集连续n_iter_no_change20轮loss没下降就停止训练。对于小样本天气数据这个机制能有效避免过拟合。random_state42必须固定不然BP网络权重初始化是随机的你跑两次结果不一样对排查问题很不利。训练完成后做评估有两个指标要同时看。MAE是平均绝对误差告诉你平均偏离多少μg/m³RMSE是均方根误差对极端大误差更敏感。如果RMSE远大于MAE说明模型在少数重污染天严重漏报这是后续调优需要关注的方向。4.3 训练曲线和迭代参数判断模型是不是真的在学训练完不要急着收工先看一眼训练曲线。这是判断模型学到东西还是网上常见的那种“loss直接变NaN”或“loss一条直线”的快速手段。import matplotlib.pyplot as plt train_loss model.loss_curve_ plt.plot(train_loss) plt.yscale(log) plt.xlabel(iteration) plt.ylabel(log loss) plt.title(BP loss curve with tanh) plt.show()model.loss_curve_是BP神经网络迭代每轮的训练损失记录plt.yscale(log)用对数尺度因为损失下降最快阶段在头几十轮线性坐标下后面的变化看不清。如果曲线是单调下降并且后来趋于平坦说明训练过程健康配合early_stopping可以放心使用。如果曲线在前几轮直接掉到接近0不用高兴大概率是泄漏。如果曲线从头到尾都是平的甚至还在上升检查两点一是学习率learning_rate_init是不是太大导致震荡二是输入数据里有没有NaN或者类别变量混进来。4.4 特征灵敏度分析打乱每个输入特征看误差增量BP神经网络训练完是一个黑匣子但我们可以通过特征灵敏度分析来打开一点点。做法是每次随机打乱一个特征的值然后重新预测看误差增加了多少。误差增量越大的特征对模型输出的影响越大。baseline_rmse mean_squared_error(y_test, y_pred) ** 0.5 importance {} rng np.random.default_rng(42) for col in feature_cols: X_test_permuted X_test.copy() # 打乱当前特征破坏它与目标的关系 X_test_permuted[col] rng.permutation(X_test_permuted[col].values) X_permuted_scaled scaler_x.transform(X_test_permuted) pred_permuted scaler_y.inverse_transform( model.predict(X_permuted_scaled).reshape(-1, 1) ).ravel() importance[col] mean_squared_error(y_test, pred_permuted) ** 0.5 - baseline_rmse # 按误差增量从大到小排序 importance sorted(importance.items(), keylambda x: x[1], reverseTrue) for col, imp in importance: print(f{col}: RMSE增量 {imp:.2f} μg/m³)这段代码的核心逻辑是某个特征如果是有效预测因子打乱它的值会让模型输入失真预测误差明显变大如果是噪声特征打乱后误差变化很小。输出的排序可以直接指导特征筛选误差增量排在最后几位、增量小于0.1的下次训练可以从feature_cols里删掉。要注意的是这只能反映模型对特征的依赖不严格等于因果重要性。如果两个特征高度相关比如PM10和PM2.5打乱其中一个时模型还可以借另一个来弥补误差增量会被低估。所以这个分析只用于排查明显没用的特征不要用它去解释“PM2.5为什么受某个气象因子影响”这类科学结论。5. 天气质量预测的避坑记录5个常见翻车点与对应修法5.1 时间泄漏模型偷偷用到了未来数据现象训练集和验证集的RMSE都低到离谱比如训练集误差2μg/m³验证集误差也接近0。但把模型接到实时数据上做预测时误差立刻涨到七八十甚至永远比真实值滞后一格。原因这是天气质量预测里最容易踩的坑。常见的有两种泄漏路径。第一构造滞后特征时shift用错了方向用了未来的值去预测当前。第二归一化时对全量数据做了fit_transform均值、最小值、最大值里包含了未来数据的信息模型在验证阶段相当于偷看了答案。解决滞后特征只允许shift(lag)取过去的值归一化必须先切分数据只用训练段做scaler.fit_transform测试段只做transform构造滚动窗口时rolling窗口内只能包含当前时刻之前的数据不能包含当前时刻之后的数据。5.2 归一化范围与激活函数不匹配tanh输出不了100以上的AQI现象训练结束后预测值全部集中在一个很小的区间比如0到10之间或者全部压在某个上限上不动。反归一化之后真实值300的重污染天预测出来只有50。原因输出层用了tanh激活函数。tanh的输出范围是[-1,1]这是数学上写死的边界。如果输出层不接线性激活网络无论怎么更新权重最终输出的范围都被锁在[-1,1]在PM2.5浓度动辄200、300的数据上表达上限就锁死了自然预测不到高值。另一种常见翻车是y没有做归一化目标值范围是0到300而tanh输出范围只有[-1,1]损失不管怎么降都不可能收敛。解决输出层用线性激活sklearn里MLPRegressor默认就是线性输出y在训练前用MinMaxScaler(feature_range(0, 1))缩到[0,1]预测后再inverse_transform回原始量纲。隐含层激活继续用tanh输出层和隐含层的激活函数是两回事输出层永远为回归任务保持线性。5.3 类别特征直接当数值喂进去梯度出现NaN现象训练的损失函数值显示为NaN或者前几轮还是正常数值后面突然变成NaN。检查数据每一列都没有空值也都没有缺失。原因气象站数据里风向、天气现象、站点类型这些字段经常以“晴、多云、阴、小雨”或者“东北风、西南风”这样的文本形式存在。如果直接把文本标签替换成1、2、3、4这样的整数等于强行给类别排序“晴1”和“阴2”在数值上就像在说“阴比晴高1等”这会把完全无意义的差距塞进权重计算里。极端情况下如果某个类别被替换成很大的整数线性组合的输出直接溢出梯度变成NaN。解决类别人数很少的列直接删掉。风向这类可保留的用one-hot编码拆成若干0/1列。注意one-hot之后要把这些列也纳入归一化0/1和连续值混在同一模型里不能让连续特征自己独占量纲优势。sklearn里用pd.get_dummies或者OneHotEncoder都能处理之后把这些列拼接回特征矩阵。5.4 数据量小还强行加深网络训练集和验证集两重天现象训练集上的MAE只有5μg/m³验证集上的MAE却有50μg/m³。把max_iter调小之后两边差距缩小了但误差又都变大了。原因样本量本身只有几百条网络结构却设成了(128, 64, 32)三层参数数量远超样本量BP网络开始逐条背诵训练集而不是学习规律。天气数据本身噪声不小同一组气象条件下PM2.5浓度也有自然波动过度自信地拟合训练样本等于把噪声也当成规律学进来。解决把网络规模降下来从小结构开始试。先跑(16,)单隐含层再对比(32, 16)两层哪组验证误差小就用哪组。同时开启early_stopping让网络在验证loss不再改善时及时停下来。不要一上来就追求网络深度小数据配小网络才是正解。5.5 随机种子不固定同一个脚本每次结果都不一样现象同一个脚本同一个数据集今天跑和明天跑的结果不一样MAE差10μg/m³。你以为代码有bug其实改一行就复现出来了。原因BP神经网络的权重初始化是随机的sklearn里如果没有指定random_state每次fit都会用一个新的随机种子初始化权重收敛到哪个局部最优就全看运气。数据量越小初始化随机性对结果的影响越大。天气质量预测正好是小样本任务受影响明显。解决在MLPRegressor里固定random_state42。如果还往train_test_split里用了随机切分同样固定它的random_state。这样不同次运行的结果就完全一致排除了偶然性之后你再调参才有意义。另外feature_cols的顺序也要固定因为MinMaxScaler的输出顺序依赖输入顺序换一列位置结果也可能变。6. 让模型上线前算得准TimeSeriesSplit与误差分解的技巧普通K折交叉验证不适合时间序列数据。K折随机打乱样本会让模型用未来的样本去预测过去评估结果虚高。天气质量预测的交叉验证应该用TimeSeriesSplit它严格按时间顺序切分训练集永远在验证集之前from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(X): X_train_cv, X_val_cv X.iloc[train_idx], X.iloc[val_idx] y_train_cv, y_val_cv y.iloc[train_idx], y.iloc[val_idx] # 每个fold里重新fit scaler只用train_idx的数据这里最容易被忽略的是scaler也要在每个fold里重新fit而不是在循环外一次性fit全量数据。泄漏问题会在每一个fold重演只要有一次用了全量数据做归一化整组交叉验证的结果都不可信。评估时把MAE和RMSE一起看。MAE反映平均偏离多少μg/m³RMSE对重污染天单独放大惩罚。我现在的习惯是同时记录“非污染天RMSE”和“污染天RMSE”把误差按浓度分两段拆开就能定位模型到底是日常预测不准还是极端事件漏报。若误差集中在污染时段就去检查滞后特征窗口是不是设置太短或者重污染天的数据量是不是太少。模型保存用joblib.dump(model, bp_aqi_model.joblib)加载时用joblib.load这是BP神经网络上线最直接的持久化方式。重训节奏建议按周走每周拉取最新数据全量重训一次不追求每日增量训练。BP网络每次训练从头迭代到收敛增量更新在这个框架里收益有限不如固定节奏重建来得干净。我刚起步时也试过每天增量结果样本越堆越多网络越训越慢效果还没有每周重训好。这个教训我一直留着现在每做一版天气预测模型都先把时间切分和误差分解做对再谈加特征和调网络。希望帮到你。本文还有配套的精品资源点击获取
返回列表