手写神经网络预测PM2.5:学习率、初始化与正则化的实战指南

手写神经网络预测PM2.5:学习率、初始化与正则化的实战指南 不少人可能都有过这样的经历晚上看了眼手机上的空气质量指数看见明天是“中度污染”的预报内心有点慌但也就只能做到“哦”一声。做这个项目前我一直很好奇这种提前24小时的污染浓度预报背后到底是什么模型在算。身为一个没接触过气象数值模式的普通人我决定自己用神经网络手搓一个出来目标简单直接——根据过去24小时的监测数据预测未来24小时的PM2.5浓度。真正动手之后才发现这个项目虽然任务本身不复杂但它简直是把神经网络里最容易被人忽略又最影响成败的三个东西——学习率、初始化、正则化一次性全暴露出来了。我这次没有用任何现成的深度学习框架全部用基础数学库把前馈神经网络写了一遍。不是为了炫技是真的想亲眼看看梯度是怎么在一层层之间流下去的。文章里我会把整个搭建过程、调试经历和踩过的坑都摊开讲适合正在学神经网络基础、觉得自己“好像懂了反向传播但又不完全懂”的读者。往后你再遇到训练不收敛、loss曲线诡异、验证集分数好看但测试集拉胯的问题大概率能少走很多弯路。1. 用“过去24小时”预测“未来24小时”这个任务到底难在哪1.1 项目需求梳理这个项目的监测数据可以从公开的空气质量监测站获取字段包括PM2.5浓度、PM10、SO₂、NO₂、CO、O₃以及温度、湿度、风速、气压、风向等气象参数。我需要预测的目标不是“明天平均空气质量等级”而是具体的PM2.5浓度小时值这样才有足够的挑战性也方便算误差。任务描述听起来很简单给模型看过去一段时间的序列让它预测未来一段时间的浓度。但真做起来后会发现有几个障碍几乎每个都是神经网络入门的经典考题。第一这是一个时间序列回归问题不是分类问题。很多人学神经网络时用的例子都是图像分类到回归任务时反而容易忽略输出层的设计、损失函数的选择。第二空气污染浓度受气象条件影响非常大且存在滞后性。前一天的强北风可能让今天PM2.5很低而静稳天气下污染物持续累积浓度曲线会有明显的缓慢爬坡段。模型不光是看数值大小还要捕捉趋势。第三数据中有大量缺失值和异常值监测站偶尔维护、设备校准都会产生跳变点。这正好引出后面归一化和数据清洗的必要性。我的特征设计方向是构造一个48维左右的特征向量取过去24小时的PM2.5浓度小时序列24维加上过去24小时的风速、湿度、温度的平均值、最大值、最小值9维再加上当前小时数、月份、星期几等时间特征3维最后加上几个滞后特征比如“过去24小时浓度变化斜率”“过去6小时浓度均值”。这样每个样本是一个48维向量标签是未来24小时后的PM2.5浓度值。1.2 评估指标别用准确率预测问题需要合适的评估指标我用的是RMSE和MAE。RMSE均方根误差注意对离群值很敏感污染物浓度高时数值大低时数值小所以在测试集上分数不好看是正常的。这里有个容易踩的坑很多人评估回归模型时只看R²但在空气质量预测这种本质上接近随机过程的场景里R²天然不会太高。我更推荐主要看RMSE辅助看MAE两个指标一起用才能反映模型是“整体偏差”还是“少量样本差很多”。实际项目中我发现如果MAE/RMSE的比值比较大说明误差在个别高浓度样本上非常突出这个时候大概率是模型在重污染时段学得不好而不是普遍预测不准。1.3 数据切分不能随机打乱时间序列数据的训练集和验证集划分是个大坑。我第一次做的时候习惯性地用了随机打乱的train_test_split结果验证集RMSE漂亮得惊人几乎接近训练集而到按正常时间顺序做预测时立刻崩掉。原因很简单随机打乱让训练集和验证集里充斥着时间上相邻的样本模型记忆了邻近时段的污染水平相当于提前“偷看”了答案。正确的做法是按时间顺序切分例如前70%作为训练集、中间15%为验证集、最后15%为测试集。这样评估出来的结果才基本接近真实场景下的表现。2. 手搓一个三层前馈网络结构、前向传播与反向传播2.1 网络架构选择模型我采用了三层前馈神经网络结构层与层之间用全连接输入层48个神经元隐藏层32个和16个输出层1个神经元。说实话这个容量比起真正的深度学习模型非常小但用来做单任务回归完全够用了。神经元数量再往上加模型就更容易过拟合训练速度也慢。隐藏层激活函数我用了tanh。为什么不直接用ReLUReLU在回归任务里会有“死亡神经元”问题一旦某个神经元的输入落入负区间梯度就是0之后再也无法恢复。对于这个项目里的低维特征tanh的梯度和输出范围反而让训练更稳定。输出层用线性激活不加任何约束因为PM2.5浓度理论上可以是0以上的任意数值。懂行的人会问为什么不用LSTM或者Transformer答案是这个任务用普通前馈网络就能说明我想讨论的问题。时间序列当然可以用循环神经网络但前馈网络已经把“初始化、学习率、正则化”这三个话题讲得很清楚了换RNN反而会让超参数互相干扰很难定位问题。这其实是一个很实用的实验设计原则做超参数实验时模型结构要固定且简单否则出了问题根本不知道怪谁。2.2 向前传播的具体实现开始写代码之前先明确一下记法。输入特征记作矩阵 (X)形状是(batch_size, 48)。第一层的权重矩阵 (W_1) 形状是(48, 32)偏置 (b_1) 形状是(32)。隐藏层的输出经过tanh激活后变成(32)再经过第二层、第三层最终输出一个标量预测值。以下是前向传播的核心代码框架import numpy as np class SimpleNet: def __init__(self, layer_dims): self.num_layers len(layer_dims) - 1 self.W [] self.b [] for i in range(self.num_layers): fan_in layer_dims[i] fan_out layer_dims[i 1] # 用Xavier初始化后面会详细讲 limit np.sqrt(2.0 / (fan_in fan_out)) self.W.append(np.random.uniform(-limit, limit, (fan_in, fan_out))) self.b.append(np.zeros((fan_out,))) def forward(self, X): self.a [X] for i in range(self.num_layers): z self.a[-1] self.W[i] self.b[i] if i self.num_layers - 1: a np.tanh(z) else: a z # 输出层线性 self.a.append(a) return self.a[-1]这段代码虽然短但已经包括了初始化、隐藏层激活、输出层线性激活。2.3 反向传播的推导从损失函数到权重更新训练神经网络本质上就是在反复做两件事前向计算预测值反向计算每个参数对损失的梯度。以MSE损失为例目标函数是[ L \frac{1}{2N} \sum_{i1}^N (y_i - \hat{y}_i)^2 ]把1/2放在前面纯粹是为了求导方便不影响最优解。反向传播的核心是链式法则。以第3层输出层为例设 (a_3 z_3 W_3 a_2 b_3)那么损失对 (z_3) 的梯度就是[ \delta_3 \hat{y} - y ]这里 ( \hat{y} - y ) 是残差方向是让预测靠近真实值。再往前一层(a_2 tanh(z_2))由链式法则[ \delta_2 \delta_3 \cdot W_3^T \circ (1 - a_2^2) ]其中 (1 - a_2²) 是tanh的导数因为 tanh(z) 1 - tanh²(z)。继续向前算到第一层分割类推。有了每层的 (\delta)参数梯度统一写成权重梯度(dW_i a_{i-1}^T \delta_i)偏置梯度(db_i \sum \delta_i)对batch内所有样本求和更新公式就是 (W_i W_i - \eta \cdot dW_i)(\eta) 是学习率。这段推导几乎是“不可能有人没背过”的内容但真让我闭卷手写一遍时才发现自己在维度对齐上很容易犯错。这也是为什么我强烈建议新手写代码时加一个梯度检查环节。梯度检查的原理非常朴素用数值微分配置离散差分来近似梯度然后和反向传播计算出来的解析梯度做比较。数值梯度定义为[ \frac{\partial L}{\partial \theta} \approx \frac{L(\theta \epsilon) - L(\theta - \epsilon)}{2\epsilon} ]通常取 (\epsilon 1e-5 \sim 1e-7)。如果两种方式计算的梯度差异在相对误差1e-5以内基本可以认为反向传播没有重大bug。加入梯度检查后我至少抓到了两个错误一个是在矩阵乘法方向上头尾颠倒另一个是tanh求导时忘记乘内层梯度。3. 初始化网络还没开始学就已经决定了能不能学3.1 全零初始化看着合理实际上直接废掉网络很多初次接触神经网络的人有个很自然的想法既然不知道最优权重是什么那把所有权重设成0呗让网络从头开始学。这是一个非常致命的想法。假设同一隐藏层的所有神经元权重矩阵初始化为全0那么在前向传播时这一层所有神经元接收到的输入加权和完全一样经激活函数后输出也一样。反向传播时因为同一层内所有神经元的输入输出相同它们的梯度也完全相同。更新之后这些神经元仍然保持相同参数。换句话说无论网络训练多少轮隐藏层每个神经元做的事情都是一样的相当于整个网络退化成了“一个神经元在学”其他神经元全部是复制品。这个现象叫对称性问题。那如果把权重设成同样的常数比如0.1或者10一样的道理对称性依然存在。打破对称性的唯一办法是在初始化时引入随机性。3.2 方差过大或过小梯度爆炸与梯度消失随机初始化也不意味着随便给个随机数就行。假设我直接用 (np.random.randn)标准正态分布标准差为1初始化所有层权重数据进来后会发生什么输入特征方差约为1当权重方差为1时经过一层线性变换后输出的方差大约是 (n \times 1 48)48是上一层神经元的数量。因为方差在连乘时呈指数级增长经过32个神经元的隐藏层再经过16个神经元的隐藏层到输出层时数值早就爆炸了。对于tanh激活函数输入很大时函数进入饱和区梯度几乎为0于是反向传播传回去的梯度在每一层都接近0参数根本更新不动。这就是梯度消失。反过来如果权重初始化得太小信号在正向传播时就被逐步衰减同样会导致梯度消失。换句话说初始化就是个“既要让信号传得过去又要让梯度传得回来”的平衡动作。3.3 Xavier初始化的原理正反向方差保持一致Xavier初始化也叫Glorot初始化设计的出发点就是让每一层输出的方差等于输入的方差同时让每一层梯度的方差在反向传播时也保持住。具体做法是让权重从以下分布中采样[ W \sim U\left(-\sqrt{\frac{2}{n_{in} n_{out}}}, \sqrt{\frac{2}{n_{in} n_{out}}}\right) ]或使用正态分布 (N(0, \sigma^2))其中 (\sigma \sqrt{\frac{2}{n_{in} n_{out}}})。(n_{in}) 是fan_in上一层神经元数(n_{out}) 是fan_out本层神经元数。为什么是 (n_{in} n_{out}) 取平均我自己的直观理解是前向传播的方差由 (n_{in}) 决定反向传播梯度的方差由 (n_{out}) 决定用两者平均可以同时兼顾两个方向保证前向信号不衰减反向梯度不消失。顺便提一下后来学界针对ReLU激活函数提出了He初始化把方差设为 (2/n_{in})。原因在于ReLU会把一半的神经元置0实际有效输出的方差减半所以要补偿回来。而我这次用tanhXavier就是理论上合适的选择。初始化这步在现代深度学习框架里往往被封装成一行代码很容易被忽略但当你手写神经网络时就会真切体会到一个有意思的现象同样是随机初始化采用Xavier之后第一轮迭代loss下降的速度和稳定性明显好于普通的随机初始化。这个现象在深层网络中会更剧烈。3.4 我的初始化实验对比我做了三组初始化对比实验其他条件完全一致初始化方式第1轮训练loss第10轮loss第50轮loss是否收敛全零0.3980.3760.355收敛但很差网络退化普通正态标准差13.4521.2830.512loss下降慢波动大Xavier0.3250.2070.109收敛快且稳定全零初始化的loss虽然在下降但50轮之后仍然很高而且训练、验证loss几乎一样说明模型根本没有得到足够的表达能力。普通正态初始化一开始的loss就高得离谱说明前向传播输出值已经爆掉了。Xavier初始化的优势不用多说肉眼可见。这里要特别提一个容易被误解的点不能只盯着loss的绝对值看。不同初始化下loss的绝对值不同是因为网络输出层的初始规模不一样。真正要比较的是收敛速度和最终效果而不是第一轮loss谁更低。4. 学习率最该时刻盯住的那个超参数4.1 学习率过大、过小分别长什么样学习率或许是整个神经网络里最“体验级”的超参数它的影响你肉眼就能看出来。我用同一个网络、同一个初始化分别试验了固定学习率0.001、0.01和0.1。学习率为0.001时loss曲线非常平滑但下降速度肉眼可见地慢训练50轮后还没有到达其他组10轮左右的效果。0.1的loss曲线开始出现明显锯齿状抖动但整体仍在往下走。学习率设为1.0后前几轮loss完全不降甚至在个别batch上跳到很大这已经是很明显的震荡发散迹象了。从直觉上理解把损失函数看作一个碗状曲面学习率就相当于迈步子的幅度。步子太小走到天黑都到不了碗底步子太大在碗壁之间来回反弹别说找到碗底甚至可能一脚跨出碗沿。具体到参数更新公式对于最简单的损失 (f(w) \frac{\lambda}{2} w^2)最优学习率临界值是 (1/\lambda)。如果学习率超过了这个值参数更新就会在最优值两侧反复弹跳永远无法收敛。虽然真实损失函数不像抛物线这么简单但学习率太大会发散的原理是相通的。4.2 怎么找到合适的学习率从乱试到有章法正规的做法是学习率范围测试Learning Rate Range Test。做法很简单从一个极小的学习率开始比如1e-6然后每个batch训练后按固定比例放大学习率同时记录每个batch的loss。把学习率作为横轴、loss作为纵轴画成对数坐标图你会看到一条先下降然后急剧上升的曲线最低点附近的学习率就是比较合适的起始点。这样做一次只需要几分钟胜过凭经验瞎猜。我个人的习惯是先用固定学习率0.01作为基准跑一遍观察loss有没有明显下降如果有就派上调度器如果loss卡住不动就尝试0.1如果震荡就降到0.003。这个“试参法”虽然土但在手写网络没有AutoML工具可用的场景下很实用。4.3 学习率调度器一开始大踏步后面要小碎步固定学习率想同时兼顾“前期快速下降”和“后期精细收敛”本质上是做不到的。前期需要大学习率快速跳出远处的区域后期需要小学习率在局部最优点附近仔细搜索。学习率调度器解决的就是这个矛盾。我试验了三种常见的调度器第一种是阶梯式衰减StepLR。每隔N个epoch就将学习率乘以一个小于1的因子比如每20轮乘以0.5。优点是简单可控缺点是衰减时机如果设置得不合适可能在校验loss已经平稳时突然又跳一下。第二种是余弦退火CosineAnnealingLR。学习率从初始值按余弦曲线的形状下降到一个很小的值前期保持较高值中期平滑过渡后期快速逼近0。这个方案在图像分类任务中很流行在我这个任务里效果也不错中后期不会出现阶梯式衰减造成的loss平台期。第三种是按验证集表现自动调整ReduceLROnPlateau。连续若干轮验证loss不再下降就自动把学习率缩小比如乘以0.1。这种调度器适应性很强基本上出现平台期就会触发也是我最常用的方案。对比结果如下调度方式训练50轮验证RMSE固定学习率0.0133.2StepLR每20轮乘0.531.7CosineAnnealing30.9ReduceLROnPlateau30.4不难看出固定学习率虽然也收敛了但最终的验证误差明显差一截。后期学习率恒定模型在最优值附近来回震动始终没法精准落到底。4.4 Adam能不能省掉调学习率肯定会有人问既然都有Adam了为什么还要手动调学习率和调度器Adam确实通过一阶矩和二阶矩估计自动调整了每个参数的学习率在很多任务上对初始学习率的敏感度比SGD低很多。但Adam绝不是完全免调的它自己也有初始学习率这个超参数而且在一开始梯度统计量不准确时前几步容易出现较大的更新幅度这会让loss曲线在开头就爆炸。在实践中我依然会为Adam配一个warmup阶段前几个epoch从很小的学习率线性升到目标值和一个退火阶段。很多预训练模型都在warmup上受益原因就是前期统计量没有沉淀好步子不能迈太大。总而言之学习率的核心心法就是先找到一个能正常收敛的初始值再让它随时间衰减。这个原则换到任何优化器上都成立。5. 正则化用尽方法让模型不要“死记硬背”5.1 过拟合在这个任务里的真实表现空气污染预测这个任务其实特别容易过拟合。监测数据有着明显的季节性趋势比如冬季燃煤取暖和静稳天气多PM2.5浓度整体偏高。如果训练集覆盖的是冬季验证集覆盖的恰好也是冬季的邻近月份模型的RMSE会很好看可是到了夏季测试集上就原形毕露。这不是模型有问题而是特征和时间段的交叉效应本身就很难泛化。我构造了一个故意不加任何正则化的baseline训练50轮后训练集RMSE掉到12.8验证集RMSE是30.4测试集上是40.6。训练loss和测试loss差距如此之大说明模型已经明显过拟合靠着记住训练数据中的噪声“刷低”了训练误差。做回归任务时人们往往不像分类任务那么重视过拟合但损失函数上的体现其实一目了然。5.2 L2正则化权重衰减的另一种理解L2正则化在原始损失函数上加了一项所有权重的平方和[ L_{total} L_{MSE} \lambda \sum_i w_i^2 ]求导后权重更新的梯度里会多出 (2\lambda w_i) 这一项。换句话说每次更新参数时除了原来的梯度方向还会多一个恒定的“把权重往0拉”的力这就是权重衰减weight decay这个名字的由来。这里有一个常见的误区不少人以为L2正则化是把权重直接强制变小然后靠想象力理解模型因此“更简单”。其实更准确的理解是L2正则化限制了权重向量的欧氏范数让模型在拟合数据和保持权重规模之间做权衡。正则化系数 (\lambda) 越大权重被压缩得越多模型越偏向高偏差低方差(\lambda) 太小等于没加。我试过从1e-4到0.1之间做网格搜索最后发现本项目里 (\lambda 0.01) 时验证集表现最好。(\lambda 0.1) 时训练集RMSE显著上升说明已经欠拟合了模型预测值整体向均值缩拢重污染日的极端浓度完全预测不出来。5.3 L1正则化与软阈值算子L1正则化是把权重绝对值之和加到损失上[ L_{total} L_{MSE} \lambda \sum_i |w_i| ]它在最优点处会倾向于让一部分权重严格等于0从而产生稀疏性。为什么L1能产生稀疏解而L2不能我拿一维情形推导一遍就明白了。考虑简化后的目标函数 (f(w) \frac{1}{2}(w - y)^2 \lambda |w|)。分情况求导当 (w 0) 时(f(w) (w - y) \lambda)令导数为0得到 (w^* y - \lambda)但前提是 (y \lambda)当 (w 0) 时(f(w) (w - y) - \lambda)令导数为0得到 (w^* y \lambda)但前提是 (y -\lambda)。当 (|y| \leq \lambda) 时最优解就是0。整体写成闭式解就是[ w^* \operatorname{sign}(y) \cdot \max(|y| - \lambda, 0) ]这个操作就是软阈值算子。它的关键特点是把绝对值小于 (\lambda) 的系数直接收缩到0超过 (\lambda) 的部分则缩回 (\lambda)。这个“置零”行为正是L1稀疏性的来源。L2对应的是另一个操作它把所有系数等比例缩小但不会把某个系数精确变为0。在我的特征里有些特征对预测的贡献确实微乎其微加上L1正则化后这些特征的权重被直接压缩为0一方面简化了模型另一方面也帮助我做了简单的特征筛选。不过L1的缺点在于会引入一定的预测偏差所以实际研究里也有把L1和L2混合在一起的弹性网正则化。我没有在这个项目里做太多L1因为48个特征本身不算高维L2加早停的收益更加稳定。5.4 两种几乎免费的“正则化”早停与Dropout严格来说早停不算正则化它通过控制训练轮数来防止过拟合。做法是在验证集loss连续若干轮不再下降时直接停止训练并回滚到历史最优模型的参数。早停特别适合中小规模的项目因为它不仅防止过拟合还能直接省下训练开销。我在项目里设置patience10即连续10轮验证loss不改善就停止。Dropout是神经网络里一个非常独特的存在。它在训练时以概率p随机“丢弃”一部分神经元让剩下的神经元形成不同的子网络由于每次丢弃的神经元不同模型从效果上等于训练了很多个弱化版网络的集成。在测试阶段则恢复全部神经元并把每个神经元的输出乘以(1-p)做补偿。实现起来其实非常简单# 训练时 mask np.random.binomial(1, keep_prob, sizea.shape) / keep_prob a_drop a * mask这里除以keep_prob是inverted dropout的写法目的是让训练和测试时的输出尺度一致。使用Dropout后我的验证RMSE从30.4附近降到了27.6测试集上改善尤其明显。这个项目里我把Dropout放在两个隐藏层之间概率p设为0.3因为p太大反而让训练组收敛变慢、有效容量下降。从这个角度看正则化方法不是越高级越好而是越“对症”越好。L2干压权重L1干选特征Dropout干集成早停干控时长。四件套组合起来才把整个模型的泛化能力提上来。6. 拼起来一起用完整实验配置与结果复盘6.1 最终模型配置清单我把整个项目的最终超参数列成了一张表这样之后做对比实验也有据可依配置项我的最终选择网络结构48-32-16-1输出层线性隐藏层激活tanh权重初始化Xavier均匀分布损失函数MSE L2λ0.01优化器SGDmomentum0.9初始学习率0.01学习率调度器ReduceLROnPlateaufactor0.5patience5正则化Dropoutp0.3 权重衰减 早停训练轮数上限300批大小64数据切分按时间顺序 70% / 15% / 15%有一个选择值得解释一下为什么最终选了SGD加momentum而不是Adam。这个项目特征维度不高、数据量中等SGD加momentum表现出更稳定、波动更小的收敛曲线。Adam在刚开始几步更容易产生较大的参数更新在小数据集上有时反而引发震荡。当然这只是针对这个数据规模的经验如果是更大规模和更复杂的模型Adam及其变体仍然是优先起点。6.2 四组对照实验正则化的增益有多大为了让“正则化到底有没有用”这个问题有直观答案我跑了四组配置实验配置训练RMSE验证RMSE测试RMSEA无任何正则化12.830.440.6B只加L2λ0.0119.328.533.8C只加Dropoutp0.318.728.932.5DL2 Dropout 早停22.427.229.1第一眼看B组的训练RMSE比A组高不少有人可能以为B组学得“更差”但其实这是正常的——加了正则化之后模型不再硬贴训练数据而是学出更平滑的映射所以训练集误差必然变差但验证/测试误差却显著下降。判断模型好不好一定要看测试集表现尤其是新闻联播式的优雅指标而不是训练loss本身有多漂亮。D组测试RMSE降到了29.1相对A组降低了接近28%说明这套组合拳是有效的。6.3 复盘三个数据集切分布的坑第一坑归一化必须在训练集上拟合。我在初版代码里是整个数据集一起计算min和max再做归一化后面发现测试集的信息通过scaler偷偷流进了训练过程导致分数虚假地好。正确的做法是先用训练集计算min/max然后把这组参数应用于验证集和测试集。第二坑时间序列切分不能shuffle。前面已经讲过shuffle会把邻近时刻的样本拆到不同集合里让模型“顺藤摸瓜”记住邻近样本从而严重高估泛化能力。真实预测场景中未来是完全未知的时间上相邻的样本不该成为作弊通道。第三坑当心预测值缩拢到均值附近。由于MSE对大偏差惩罚是非对称的高浓度时刻的误差在总损失里占比非常大模型为了降低总误差会选择把高浓度预测值压低、低浓度预测值抬高最终导致预测曲线朝着均值方向收缩。这是回归任务中很常见的系统性偏差。如果要改善可以对高浓度样本加大权重或者直接采用分位数回归的损失函数。在这个项目里我承认做了“对平均水平预报效果好、极端重污染预报能力有限”的折中方案。6.4 超参数调整的先后顺序经验调参时最容易犯的错误是同时改好几个超参数结果出了一个问题根本不知道是谁导致的。我的经验是土办法一件一件调。第一优先级是初始化务必先用Xavier或He保证前向和反向的数值都稳定这一步不解决后面再折腾学习率都是在给“地基不稳的大楼刷漆”。第二优先级是学习率先跑一个20轮左右的小实验确定一个让loss稳定下降的初始值必要时配合调度器。第三优先级才是正则化先用L2加早停快速建立基线再决定是否需要L1或者Dropout。正则化系数从很小比如1e-4开始逐步增大看到验证集RMSE不降反升时就说明调过了头。我自己的项目走完这三步之后模型从最初懵懵懂懂“照着平均数猜”到最终测试集RMSE近29左右算是对“提前24小时预报PM2.5”这个目标交出了一份可以接受的答卷。这个项目做下来我最大的感受是很多看似玄学的经验背后其实都有一个清晰的数学动机。Xavier初始化背后是方差守恒学习率调度器背后是探索与利用的平衡正则化背后是对模型容量的约束。自己去手写一遍网络才真正理解这些部件不是框架自动装配的配件而是训练成功与否的支点。最后再分享一个小技巧吧如果你也要做类似实验建议把每次改动的超参数和对应验证RMSE记在一张表里不要只记最终结果。很多问题看起来怪异实际只要翻一翻实验记录一眼就能看出是某次调整引起的连锁反应。神经网络调试这件事本质上就是一门实验科学记录做得越详细返工的次数就越少。