news 2026/9/20 3:55:45

手搓BP神经网络预测雾霾:学习率、初始化与正则化实战复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手搓BP神经网络预测雾霾:学习率、初始化与正则化实战复盘

写这个项目的时候,我手边正好有一批去年冬天的空气质量数据。每天看着“轻度污染”“中度污染”的预报,我一直在想,能不能自己动手做一个能提前一天预测雾霾的小模型。不需要多复杂,只要能把明天的PM2.5浓度估个八九不离十就行。思来想去,干脆不用现成的深度学习框架,用纯Python加NumPy手搓一个神经网络。这个过程走下来,最大的收获不是预测精度有多高,而是原本那些对我来说模模糊糊的概念——学习率、参数初始化、正则化,全都在调试loss曲线的过程中彻底搞明白了。这篇文章就当是一次完整复盘,从数据准备讲到网络实现,再到三个核心算法的拆解,把我踩过的坑和调参的经验一并写出来。

1. 项目背景与问题建模

1.1 为什么选择手搓前馈神经网络

雾霾预报本质上是一个回归问题。输入的是当天的气象条件和空气数据,输出的是第二天的PM2.5浓度。神经网络在这里扮演的是一个“万能函数拟合器”的角色,它能自动从历史数据里学到“什么样的天气组合容易导致污染累积”这类映射关系。

选择前馈神经网络而不是LSTM这类循环网络,是因为做“提前一天”的预测其实不需要特别强的时序建模能力。我们可以把过去N天的数据折叠成一个特征向量,一次性喂给网络。这种“时间窗口折叠”的做法在气象、金融这类场景里非常常见,它本质上是在用特征工程弥补模型结构的简单性。提到BP神经网络,很多人可能觉得它是“老古董”,但在这种中小规模数据集上,一个结构清晰、能完全掌控的BP网络,往往比直接套用复杂模型更实用。

手搓而不是用PyTorch,主要目的是为了看清楚每一个参数在训练过程中到底发生了什么变化。框架封装得太好,梯度、权重、学习率这些概念反而容易变成黑盒。自己实现一遍前向传播、反向传播、参数更新之后,再回去看任何深度学习框架的文档,都会有“原来如此”的感觉。

1.2 数据准备与特征选择

我用的数据是某市环保监测站的历史记录,包含以下字段:

  • PM2.5浓度(当天的,也是明天预测的基础)
  • PM10浓度
  • SO₂、NO₂、CO、O₃浓度
  • 温度、湿度、气压、风速、风向

原始数据是逐小时的,我先按天做了平均,得到每天一条记录。这里有一个很关键的细节:预测明天,必须只用“今天及以前”的数据,否则就构成了数据泄露。也就是说,输入向量的所有特征都是t时刻或者t-1、t-2时刻的值,标签才是t+1时刻的PM2.5浓度。

我使用的特征窗口是3天,具体格式如下:

特征编号含义
1-6t日六项污染物浓度
7-10t日四项气象参数
11-16t-1日六项污染物浓度
17-20t-1日四项气象参数
21-26t-2日六项污染物浓度
27-30t-2日四项气象参数

这样每个样本的输入维度是30,输出维度是1。数据集总共收集了两年约700天的数据,按照8:2划分成训练集和测试集。在把数据喂给网络之前,必须做标准化处理。我用的方法是Z-score标准化,也就是对每个特征减去均值、除以标准差。这一步极其重要——如果不做标准化,像CO这种数值在个位数级别的特征和PM2.5这种数值在几十到几百之间波动的特征混在一起,神经网络的训练会变得异常缓慢,甚至不收敛。

1.3 网络结构设计

网络结构采用经典的三层结构:

  • 输入层:30个神经元
  • 隐藏层:12个神经元,激活函数用tanh
  • 输出层:1个神经元,无激活函数(回归任务)

隐藏层神经元数量为什么选12?这是根据经验公式balance出来的。大致可以参考输入层和输出层神经元数量之和的平方根附近,再往大了调。12这个数字在我的数据集上训练速度和精度比较均衡。也可以试试16、20,但隐藏层过多在小数据集上容易过拟合,后面正则化部分会重点聊这个问题。

2. 核心细节:从零实现BP网络

2.1 手写前向传播与反向传播

既然要“手搓”,代码里就不能出现任何框架的自动求导。前向传播很好理解,数据从输入层进来,经过权重矩阵和激活函数,一层层往外传。反向传播则要把输出端的误差一层层传回来,用链式法则计算出每个权重的梯度。

下面给出我用NumPy实现的核心部分,这几乎是BP网络最精简的模板:

import numpy as np def tanh(x): return np.tanh(x) def tanh_deriv(x): return 1.0 - np.tanh(x) ** 2 class BPNet: def __init__(self, n_input, n_hidden, n_output): # 初始化权重和偏置 self.W1 = np.random.randn(n_input, n_hidden) * np.sqrt(2.0 / n_input) self.b1 = np.zeros(n_hidden) self.W2 = np.random.randn(n_hidden, n_output) * np.sqrt(2.0 / n_hidden) self.b2 = np.zeros(n_output) def forward(self, X): self.z1 = np.dot(X, self.W1) + self.b1 self.a1 = tanh(self.z1) self.z2 = np.dot(self.a1, self.W2) + self.b2 return self.z2 def backward(self, X, y, lr): m = X.shape[0] y = y.reshape(-1, 1) # 输出层误差 dz2 = self.z2 - y dW2 = np.dot(self.a1.T, dz2) / m db2 = np.sum(dz2, axis=0) / m # 隐藏层误差 da1 = np.dot(dz2, self.W2.T) dz1 = da1 * tanh_deriv(self.z1) dW1 = np.dot(X.T, dz1) / m db1 = np.sum(dz1, axis=0) / m # 参数更新 self.W2 -= lr * dW2 self.b2 -= lr * db2 self.W1 -= lr * dW1 self.b1 -= lr * db1

注意最后两步,参数更新的写法就是神经网络的学习过程。每一次迭代,都在朝“让损失函数下降最快的方向”挪一小步,这一小步的尺度就是学习率。

2.2 损失函数与评估指标

回归任务最常用的损失函数是均方误差MSE。对雾霾预测这个场景来说,MSE对异常值比较敏感,这意味着如果某天出现极端污染事件,模型会被“拉着”去拟合那个尖峰,反而影响了普通天的预测效果。但从实际效果来看,MSE依然是最好训练、最稳定的选择,所以我还是先用它。

评估指标除了MSE之外,我还关注平均绝对误差MAE,因为MAE的单位和PM2.5浓度一致,更容易向别人解释。“我们模型的平均误差是每立方米15微克”,听起来就比“均方误差是380”直观得多。

2.3 训练过程的全貌

完整训练循环一般长这样:

  1. 把训练数据按小批量(batch)打乱,每个batch喂给网络
  2. 前向传播计算预测值
  3. 计算损失
  4. 反向传播求梯度
  5. 用学习率更新权重
  6. 跑完所有batch记为一个epoch
  7. 每个epoch结束之后,在验证集上评估一次

刚开始我图省事,用全量梯度下降,也就是一次性把所有数据都喂进去。训练速度太慢,而且容易陷入局部最优。后来改成小批量,batch大小设为32,收敛速度明显提高,而且loss曲线更平滑。训练过程大概跑了2000个epoch才完全收敛,这个数字听起来很多,但因为网络结构很小,实际运行时间只有几百毫秒。

3. 学习率:那个让人又爱又恨的超参数

3.1 学习率过大或过小的表现

学习率决定了神经网络每一步参数更新的跨度。我用同一个模型、同一种初始化方式,只修改学习率,得到的结果差异非常大:

学习率训练集MSE测试集MAE表现
0.001未充分收敛34.2训练极慢,loss几乎不下降
0.0121018.6收敛正常,精度尚可
0.115615.3收敛很快,测试集表现不错
0.5出现震荡29.8loss上下剧烈波动,难以稳定
1.0发散到NaN无法预测权重爆炸,彻底失败

当学习率太小的时候,loss下降得跟蜗牛爬一样,训练几千个epoch仍然欠拟合。这种情况下模型有一种“永远学不会”的错觉,但实际上只是步子迈得太小。当学习率太大的时候,参数更新的步长会跨过最优点,甚至一步一步跳到损失函数的“悬崖”外面,梯度不断累积,最终导致权重变成NaN,整个模型报废。

我在调试时最喜欢用的一个技巧是:先用一个较大的学习率(比如0.1)去试探,如果loss在第一个epoch内就发散,那就依次除以10往下缩。如果loss能正常下降但速度偏慢,再适当放大。这个过程就像拧水龙头,先开到最大,再慢慢往回调。

3.2 学习率调度器的必要性

固定学习率有一个天生缺陷:训练前期权重距离最优解很远,需要大步伐快速接近;训练后期权重已经很接近最优解了,这时候再用大步伐就容易在最优解附近来回震荡,永远无法精确定位到最低点。

解决办法就是学习率调度器。最简单的策略是阶梯式下降,比如每500个epoch把学习率缩小到原来的0.5倍。我自己试过一种更顺滑的方式——余弦退火调度,公式如下:

def cosine_annealing(epoch, total_epochs, lr_max, lr_min=1e-5): return lr_min + 0.5 * (lr_max - lr_min) * (1 + np.cos(epoch / total_epochs * np.pi))

用这个调度器之后,我观察到训练前期的收敛速度和后期loss曲线在最低点附近的平稳度都有了明显改善。最终我的选择是初始学习率0.1配合余弦退火,在2000个epoch里从0.1平滑降到接近0。这比固定学习率的效果好了大约5%的测试集MAE。

3.3 动量与自适应方法的经验

除了调度器,在优化算法层面也有让学习率更高效的改进办法。动量法就是典型代表——它让参数的更新方向不仅依赖于当前梯度,还部分参考了上一次更新的方向。类比来说,学习率只管每次迈多大步,而动量是在小球下山的时候给小球加一个“惯性”,让它滚过小的坑洼,更快冲向谷底。

我测试了带动量的SGD和Adam优化器。观察到亚当优化器在雾霾预测这个问题上基本不需要精细调学习率,0.001就能跑得很好。但我的目的本来就包含教学,所以我还是坚持把SGD和余弦退火作为主推方案——因为它的行为更可预测,更容易解释每一次更新背后发生的事情。

4. 初始化:一个好的起点决定了终点

4.1 全零初始化为什么不行

如果所有权重都初始化为0,会发生什么?前向传播时,每一层所有神经元接收到的输入是一样的,输出的值也一样。反向传播时,同一层内所有神经元的梯度完全相等,更新之后权重依然相同。无论训练多少个epoch,隐藏层的12个神经元都在做一模一样的事情——整个网络退化成只有一个神经元的线性模型,表达力全部丧失。

这就是为什么随机初始化是必须的。但它不是随便随机就可以的,初始化方差的设置直接影响训练的成败。

4.2 Xavier与He初始化的选择逻辑

Xavier初始化(也叫Glorot初始化)和He初始化是目前最常用的两种方案。它们的核心逻辑都是:让信号在层间传递时保持方差稳定,不要越传越放大到发散,也不要越传越缩小到消失。

具体来说:

  • Xavier初始化适合tanh和sigmoid这类对称激活函数,权重从均值为0、方差为2/(n_input + n_output)的分布中采样
  • He初始化适合ReLU及其变体,方差为2/n_input

我的隐藏层用的是tanh激活函数,所以我最初用Xavier初始化。后来我试着把隐藏层换成ReLU,发现必须搭配He初始化才能让训练稳定。这背后的原因是ReLU会把负半轴的梯度全部置零,如果不把正向的方差适当放大,信息经过几层传递之后就会淹没在噪声里。

实际代码里我直接用了NumPy的randn乘以缩放系数:

# Xavier self.W1 = np.random.randn(n_input, n_hidden) * np.sqrt(2.0 / (n_input + n_hidden)) # He self.W1 = np.random.randn(n_input, n_hidden) * np.sqrt(2.0 / n_input)

这里想特别提醒的是:偏置bias的初始化。很多人容易忽略它,我自己的习惯是全部初始化为0,这样配合随机初始化的权重是没问题的。但如果你把偏置也设成很大的随机值,那就相当于在每一层都施加了一个直流偏置信号,会严重阻碍前期的收敛。

4.3 我的一次踩坑经历

有次我贪省事,直接用np.random.randn初始化权重,标准差是1。当时没觉得有什么问题,结果loss曲线在一个很小的值附近徘徊了特别久。我把学习率从0.01一路调到0.5还是没多大反应。后来打印出来的权重分布一看,初始权重太大,经过tanh之后输出全部饱和在±1附近,梯度几乎完全消失。换成Xavier初始化之后,loss在同一个学习率下立刻开始正常下降。

这件事给我的教训非常直接:遇到模型“不学习”的情况,先检查初始化,再看数据标准化,最后才调学习率。顺序错了,花再多时间也是白费。

5. 正则化:对付过拟合的关键武器

5.1 过拟合在雾霾预测里的具体表现

雾霾数据本身就带有很强的季节性特征,冬天的污染水平天然高于夏天。如果模型把“冬天”这个标签记住而不是学会“污染物逐渐累积”这个规律,就会在测试集上表现很差。具体表现是:训练集loss降得很低,但测试集loss连续多个epoch不降反升。

为了进一步放大过拟合现象,我还专门做了一组对比实验:把隐藏层从12个神经元增加到64个神经元,去掉所有正则化手段,只训练300个epoch。训练集MSE降到了30左右,但测试集MAE反而比12个神经元的模型还要差。这就是典型的“记住了训练数据,却没学会规律”。

5.2 L1正则化与L2正则化的本质区别

正则化的思路是在损失函数后面加上一个惩罚项,约束权重不要变得太大。L1正则化和L2正则化的惩罚方式不同,产生的效果也不同。

L2正则化(也叫权重衰减)的惩罚项是所有权重的平方和,对应公式为:

loss = mse_loss + lambda * np.sum(W1 ** 2) + lambda * np.sum(W2 ** 2)

它对大权重的惩罚是二次的,所以权重越大,被压制得越狠。结果就是模型里每个特征的贡献都被“平均化”了,权重整体变小且分布比较平滑。L2正则化解决的是“模型过于自信”的问题,让预测不那么容易被某个单特征左右。在水质、空气质量这类多种因素共同作用的问题上,L2正则化效果非常理想。

L1正则化的惩罚项是所有权重绝对值之和,对应的特点是会把一部分权重直接压到0。它天然具备特征选择的作用——某些无关紧要的输入特征会被网络主动“遗忘”。在雾霾预测这个场景里,如果我加入了很多毫无意义的特征(比如当天的股市指数),L1正则化可能就会让这些输入对应的权重变成0。

有个经典结论是“软阈值算子是L1正则化的解”,翻译成白话就是:L1正则化会让权重在做梯度更新的时候经历一个“先缩到0、一部分彻底清零”的过程。如果数据集特征很多,很多特征是噪声的话,L1会比L2更好用。但考虑到我的特征都是经过挑选的、和空气质量强相关的指标,L2正则化更多时候是我的首选。

下面是同样网络、同样数据下加正则化和不加正则化的对比:

模型训练集MSE测试集MAE
隐藏层64节点,无正则化3821.7
隐藏层64节点,L2(λ=0.001)14015.9
隐藏层64节点,L2(λ=0.01)20514.8
隐藏层12节点,L2(λ=0.01)19015.1

可以看到,加了L2正则化之后,虽然训练集上的拟合效果变差了,但测试集MAE明显下降。这个“用训练精度换泛化能力”的权衡,正是正则化存在的意义。

5.3 Dropout与早停的实战对比

对全连接神经网络来说,Dropout也是个非常有效的正则化手段。Dropout的原理是在训练过程中随机丢弃一部分神经元,强迫网络学会“分布式编码”,不能把全部希望寄托在某个单一神经元上。

我在手写代码里实现Dropout只加了两行:

# 训练时,以keep_prob概率保留神经元 mask = np.random.rand(*self.a1.shape) < keep_prob self.a1 *= mask self.a1 /= keep_prob # 保证期望值不变

注意后面那步要除以keep_prob,这叫“反向缩放”。如果不做这一步,网络在训练时和推断时输出的数值范围会不一致,导致测试效果大幅缩水。

早停法算是零成本的正则化。做法是在每个epoch结束之后检查验证集loss,如果连续20个epoch都没有下降,就提前终止训练并恢复最优权重。这个策略非常适合雾霾预报这种场景——数据量本身不大,过度训练的风险很高,早停能有效控制训练时长。

有意思的是,如果把早停和L2正则化叠加在一起,反而没有各自单独使用效果好。我猜测原因是两种正则化机制的作用存在部分重叠,叠加之后会过度压制模型的表达能力。所以我的建议是:小规模BP网络优先用早停,如果测试集泛化误差还是不满意,再加入较小的L2正则项,不要一开始就一顿操作全怼上去。

5.4 正则化系数怎么定

正则化系数λ的取值是个很头疼的问题。太大,模型变得过于简单,欠拟合;太小,正则化不起作用。我采用的是“对数网格搜索”策略:

  1. 先把λ按数量级试:0.001、0.003、0.01、0.03、0.1
  2. 观察训练集和验证集loss的差距
  3. 如果训练集和验证集loss都高,说明λ太大了
  4. 如果训练集低但验证集高,说明λ太小了
  5. 在表现最好的两个数量级之间继续二分

最终在这个项目里,L2系数定为0.01效果最好。不过也要提醒一句,这个系数高度依赖数据规模和特征标准化方式,直接抄作业不一定有效,务必重新调试。

6. 实验过程与结果分析

6.1 完整训练配置速查表

这里把最终成功跑通的配置罗列出来,方便复现:

配置项最终选择
网络结构30-12-1
激活函数隐藏层tanh,输出层linear
损失函数MSE
优化器SGD with Momentum(momentum=0.9)
初始学习率0.1
学习率调度余弦退火,2000个epoch降到1e-5
初始化Xavier
正则化L2(λ=0.01)
Batch大小32
早停验证集连续40个epoch不改善则停止

这个组合在测试集上得到的MAE约为14.6μg/m³。对一个只用了一公里之外气象站数据、没有加入任何卫星遥感或污染源排放清单的简易模型来说,这个精度已经达到了我预定的目标。

6.2 预测效果解读

从结果看,模型对重度污染天气的预测能力弱于轻度污染。后来我分析了误差最大的几个样本,全部是PM2.5浓度超过200μg/m³的重度霾日。深层原因是训练集中这样的极端样本占比非常小,模型根本没有足够的样本去学习“极端污染是如何形成的”。

解决这种问题有三个方向:一是收集更长时间跨度的数据,尤其是包含更多极端污染事件的年份;二是采用加权损失函数,给极端样本更高的权重;三是把回归问题转化为多分类问题,比如把污染等级分为优、良、轻度、中度、重度五档,分类模型对极端类别的召回率通常会比回归模型高一些。

6.3 特征重要性初探

虽然BP神经网络的权重解释性不强,但我可以通过一个简单粗暴的方法观察哪些特征更重要:每次把某个特征的所有值都替换为该列均值,重新预测,计算MSE的变化量。MSE上升幅度越大,说明这个特征对预测的贡献越大。

实测结果中最重要的是当天的PM2.5浓度和气压。这个结论非常合理——污染物浓度本身具有惯性,今天的空气质量对明天有很强的指示作用;而气压高低决定了大气扩散条件的好坏。温度的作用相对较弱,风速和湿度则介于中间。

7. 常见问题排查与避坑指南

7.1 问题速查表

现象最可能原因排查顺序
loss为NaN学习率过大从0.001开始重调,检查数据是否有无穷值
loss完全不下降数据未标准化检查特征均值方差
训练好但测试极差过拟合加正则化、减小网络规模、早停
收敛极慢初始化不当换成Xavier或He初始化
loss先下降后猛涨学习率调度失效检查是否有梯度爆炸,考虑梯度裁剪
预测总是接近一个固定值输出层无激活函数但标签范围过大检查标签是否标准化

7.2 数据泄露是最大的隐性坑

在整理时间序列数据时最隐蔽的错误是误把未来数据当特征。比如用当天全天的平均PM2.5去预测“当晚零点之后”的PM2.5,这个信息事实上属于同一个污染过程,模型自然“预测”得很准,但一上线就完蛋。

我的判断原则是:训练集里出现的所有信息,在真实推理时必须是“当时已经可以获得的”。天气也一样,如果你用的是“明天实测的天气数据”去预测“明天的空气质量”,这在部署时是不可行的,因为在提前一天做预测的时候,明天实测数据还没有产生。正确的做法是用天气预报数值,或者干脆只用历史序列特征。

7.3 手搓网络时的几个额外建议

最后分享一个我反复踩过坑的点:每写一层新的实现,都要先用一个极小的数据集(比如10个样本)做一次“过拟合测试”。如果模型连10个样本都学不会,说明代码里大概率有bug。等这个测试通过了,再逐步扩大数据量。这个习惯帮我节省了大量排查问题的时间。

另外一个建议是,把训练过程中的每种配置都记录下来。我用一个字典保存了学习率、初始化方式、正则化系数、batch大小,以及对应的训练和测试指标。模型调试本质上是在高维空间里搜索最优解,没有日志回溯,前面试过的所有配置都等于白试。

整个项目做下来,最大的体会是神经网络的三个核心超参数——学习率、初始化、正则化——并不是孤立发挥作用的。好的初始化让模型有了一个好的起点,合适的学习率让模型每一步都走得稳,正则化防止模型在终点附近走火入魔。三者缺一不可。后续如果有时间,我打算把模型改成多输出,同时预测明天和后天的PM2.5浓度,或者在输入端加入未来24小时的模式预报结果,看看预测精度还能往上走多少。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 3:51:09

BoxMOT:从零到可用的多目标跟踪方案,一篇就够

BoxMOT&#xff1a;从零到可用的多目标跟踪方案&#xff0c;一篇就够 【免费下载链接】boxmot BoxMOT: Pluggable Python and C SOTA multi-object tracking modules with support for axis-aligned and oriented bounding boxes 项目地址: https://gitcode.com/GitHub_Trend…

作者头像 李华
网站建设 2026/9/20 3:49:56

ESP32蓝牙开发实战:BLE连接、GATT通信与WiFi共存避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 3:49:32

VS2022 AI编程工具选型指南:横向对比与实战推荐

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 3:49:25

信创平台运维高频故障排查实战指南

干信创平台运维这行&#xff0c;酸甜苦辣基本都尝遍了。刚开始接手的时候&#xff0c;我天真的以为信创平台就是“换了张桌面的 Linux”&#xff0c;结果被一个又一个的故障按在地上摩擦。直到后来我把这些坑一个个填平&#xff0c;才真正意识到&#xff1a;信创环境复杂的地方…

作者头像 李华
网站建设 2026/9/20 3:48:08

MATLAB优化函数实战排错指南:fmincon/linprog/fminbnd工程调参手册

简介&#xff1a;本资源是一份面向MATLAB初学者与数学建模实践者的系统性学习资料&#xff0c;聚焦最优化方法在MATLAB中的工程化实现。内容覆盖线性规划、非线性规划、多目标优化等核心分支&#xff0c;详解优化工具箱中fmincon、fminunc、fsolve、lsqnonlin等20余个关键函数的…

作者头像 李华
网站建设 2026/9/20 3:47:47

SpringBoot+Vue3+MyBatis搭建企业客户管理系统全流程实战

做了两三个后台管理系统之后&#xff0c;我越来越觉得“客户管理”这类业务是最适合拿来练手也最适合拿来落地的项目。它不像电商那样牵扯复杂的交易链路&#xff0c;也不像审批流那样被一堆状态机追着跑&#xff0c;但该有的东西全都有&#xff1a;用户登录鉴权、数据列表分页…

作者头像 李华