十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

PM2.5预测实战:从数据预处理到线性回归调优全解析

PM2.5预测实战:从数据预处理到线性回归调优全解析 1. 从经典作业聊聊为什么几乎所有初学者都要写一次PM2.5预测聊到机器学习入门台湾大学李宏毅老师的课程几乎是华语圈的“现象级”资源。而这门课的作业一——用回归模型预测PM2.5我敢说每一个认真跟完课程的人都在这道题上熬过几个晚上。它看起来特别简单给你过去9个小时的观测数据预测第10个小时的PM2.5浓度。但就是这么一道“入门题”里面埋着数据预处理、特征工程、梯度下降、学习率调整、正则化甚至还有对过拟合的初步感知。我当年做完这道题之后最大的感受是原来学校里教的数学公式真正落到数据上每一步都可能是坑。这篇文章就以这个经典作业为蓝本完整复盘一遍PM2.5预测实例从数据处理到模型训练、再到调优验证的全过程。不绕弯子直接讲每一步怎么做、为什么这么做、踩过的坑长什么样。如果你正在跟李宏毅老师的课或者想找一个最小可运行的回归实战项目练手这篇文章基本能让你少走一半弯路。1.1 核心需求解析这到底是个什么问题先把这个题目翻译成人话。作业给的数据集是一年365天、每天24小时的空气质量观测记录每小时会记录18项指标包括PM2.5、PM10、SO2、NO2、CO、O3还有温度、湿度、风速、降雨量等等。数据本身是CSV格式每行代表某个观测站某一天某个时刻的完整指标快照。作业的核心任务是用前9个小时的所有观测数据预测第10个小时的PM2.5浓度值。这里有个关键点要拎清楚——这是一个典型的回归问题不是分类。我们要预测的是一个连续数值而不是“空气好/空气差”这种离散标签。所以输出层的激活函数用不上sigmoid那一套损失函数也不能用交叉熵而是用均方误差MSE这类衡量“预测值与真实值距离”的指标。李宏毅老师的课里会用这个例子反复强调回归和分类在数学框架上同源但因为输出的性质不同整个训练目标都会跟着变。另一个容易忽略的点是作业虽然在课程早期出现但它其实要求你手写梯度下降而不是直接调sklearn或者PyTorch一把梭。目的很简单——逼你在代码层面理解“参数是怎么被更新”的。如果你直接用框架一行搞定那么学习率、梯度消失、特征缩放这些问题你永远只是“听说过”而不是“真正见过”。1.2 数据观察训练集与测试集划分的潜台词作业提供的原始数据里前一部分是完整的训练数据后一部分是测试数据。但测试数据的标签是被隐藏的——也就是说你只能看到同样格式的18项指标但最后一列PM2.5的真实浓度没有提供。你需要把模型的预测结果提交到课程平台上由平台给你打分。这种设定非常接近真实世界的机器学习流程你手上的永远只有部分有标签的数据模型要面对的是未知的、没有标签的新数据。所以你在训练时不能自己“偷看”测试集标签来调参否则就是在自欺欺人。正确的做法是把训练数据再切出一部分作为验证集用验证集的误差来指导调参最后才让模型去预测测试集生成提交文件。我当时是这样做的把训练数据按“天”为单位打乱用前80%的天作为训练集后20%的天作为验证集。注意这里是按天切分而不是按行随机切分。因为同一小时内18项指标是有内在相关性的如果把同一时刻的不同指标拆到训练集和验证集会造成信息泄漏验证结果会虚高。这个细节虽然不深但能帮你早早建立“数据切分不是随便切切”的意识。2. 手把手拆解数据预处理与特征工程才是这个项目的重头戏很多人做这个作业一上来就急着搭模型结果代码跑完发现loss居高不下或者收敛得慢得离谱。我自己的经验是这个项目里数据预处理和特征工程的优先级远高于模型结构。因为线性回归模型的表达能力本来就有限你喂给它的特征质量基本上决定了性能天花板。李宏毅老师在课上把这个例子作为“回归入门”来讲解但他其实默认你已经能把数据处理好。2.1 原始数据的坑缺测值与无效值处理原始数据里有一个很烦人的设定——缺测值用-1或者NR来表示NR一般出现在降雨量那一列意思是No Rain即无降雨。虽然NR在语义上是“无降雨”但如果直接把它当作数值参与运算模型就会认为降雨量是-1这完全不符合物理含义。所以第一步必须做数据清洗把所有NR字符串替换成数值0把所有-1之类的缺测标记处理成NaN再决定是补零、取均值还是直接丢弃。这里有个所有教程都不会细说、但你在真实项目中一定会碰到的教训对缺测值最简单的处理不一定是“最正确”的处理。比如PM2.5浓度如果出现-1补零显然不合理因为浓度不可能是负数取前后时刻的平均值则相对合理因为污染物浓度的变化是连续的。对于这个作业我的做法比较保守先统计每个特征维度的缺测比例。如果缺测比例很小小于1%直接丢弃对应行如果缺测集中在某一列就对该列做线性插值。实操下来这个策略对最终分数的影响不大因为数据本身质量比较高但面对真实数据集时这一步往往会直接决定成败。2.2 特征窗口设计为什么用9个小时而不是更长或更短这个作业最核心的建模决策是用过去9个小时的观测数据预测下一个小时。为什么是9因为题目就是这么出的但这个设定在数学上并非唯一选择。在我自己复现时我尝试过用6小时、12小时、24小时的窗口长度做对比实验结果还挺有意思窗口越长模型的表现反而没有显著提升甚至略有下降。原因很直观——PM2.5的浓度在短时间内受气象条件、排放源等因素影响存在一定的“记忆效应”但是这种记忆并不是无限延长的。过去24小时的平均浓度确实有影响但如果你把它拆成24个单独的输入特征线性模型反而很难从高维稀疏的输入中学到有效规律因为特征维度增加了样本量没变过拟合的风险就上来了。所以这里有一个通用的特征工程思路与其盲目堆更多小时的数据不如把已有数据加工成更有信息量的特征。比如原始作业只给每个时刻的18项指标但你可以自己构造一些衍生特征比如“过去9小时PM2.5的均值”“过去9小时的最大值”“过去3小时的趋势斜率”。我在训练中发现加上这几列衍生特征之后验证集上的均方误差大约降低了5%到8%。这在Kaggle竞赛里可能不算什么但作为作业已经能明显感觉到模型的“聪明”程度不一样了。2.3 特征范围差异太大先用标准化把一切拉回同一起跑线原始数据里不同指标的量纲差异非常大——PM2.5浓度可能只有几十而CO浓度可能是几百甚至上千温度和湿度又是另一种数量级。如果你直接把这样原始的数据喂给线性回归模型梯度下降会非常痛苦因为不同维度上的参数更新步长会被特征的尺度差异扭曲。有些维度已经收敛了有些维度还在原地踏步。解决办法就是特征标准化最常用的是Z-score标准化也就是对每个特征减去均值、除以标准差让每个特征的分布大致落在零附近、标准差为1。这样处理后梯度下降的收敛速度会明显加快而且能减少某些大数值特征对损失函数的主导。注意这里的均值和标准差只能从训练数据中计算然后用于训练集和测试集的转换绝不能用测试集的数据来计算均值和标准差否则就是信息泄漏这几乎是所有机器学习比赛里最不能犯的错之一。3. 模型选型与训练细节线性回归背后的数学和工程数据处理好之后就进入模型搭建环节了。李宏毅老师的作业版本里模型就是一个再简单不过的线性回归y wx b。这里w是权重向量x是输入特征向量b是偏置项。在实现上你可以把b合并到w里做法是在x向量的末尾拼接一个常量1这样整个模型就写成y w^T x代码更整齐计算也更方便。3.1 手写梯度下降从公式推导到代码落地线性回归的损失函数是均方误差模型要做的就是找到一组w让所有训练样本上的平均平方误差最小。对损失函数求梯度之后你会发现一个非常漂亮的结果梯度只和输入特征、预测误差有关。这意味着你不需要任何复杂的反向传播库只需要用numpy就能手写完整的训练循环。代码如下import numpy as np import pandas as pd # 假设 X_train 是已经做过标准化的特征矩阵形状为 (N, D) # y_train 是标签向量形状为 (N, ) def train_linear_regression(X_train, y_train, learning_rate0.01, epochs1000): N, D X_train.shape # 在最后一列拼接1对应偏置项 X_train np.hstack([X_train, np.ones((N, 1))]) w np.zeros(D 1) for epoch in range(epochs): y_pred np.dot(X_train, w) loss np.mean((y_pred - y_train) ** 2) grad np.dot(X_train.T, (y_pred - y_train)) / N w w - learning_rate * grad if epoch % 100 0: print(fEpoch {epoch}, Loss: {loss:.4f}) return w这段代码就是核心了。别小看它我见过不少同学在这个地方卡住原因不是公式不会推而是矩阵形状对不上导致numpy报错。我的建议是每一步都打印出X_train的形状、w的形状、grad的形状确保它们在矩阵乘法时能对上。一旦跑通理解“梯度下降就是在梯度的反方向更新参数”这句话就不再是空话了。3.2 学习率怎么定太大loss爆炸太小收敛到天亮手写梯度下降之后你马上会面临一个非常现实的问题学习率到底设多少学习率太大loss会剧烈震荡甚至发散到NaN学习率太小模型要跑几千轮才能收敛浪费时间。李宏毅老师的课里专门提到这个问题并且介绍了一个很实用的策略——自适应学习率Adagrad。简单说Adagrad会根据每个参数历史梯度的平方和来动态调整该参数的学习率梯度大的方向步长变小梯度小的方向步长变大。这比手动调一个全局学习率要省心得多。我当时用普通梯度下降配合固定学习率跑到5000轮loss还在缓慢下降切换成Adagrad之后几百轮就收敛了。Adagrad的实现也不复杂核心思想是维护一个梯度平方的累积变量然后每次更新时除以累积量的平方根。不过要注意的是累积量会不断增大导致学习率越来越小所以通常会在分母上加一个很小的平滑项防止除以零。代码如下def train_adagrad(X_train, y_train, learning_rate1.0, epochs1000): N, D X_train.shape X_train np.hstack([X_train, np.ones((N, 1))]) w np.zeros(D 1) grad_square_sum np.zeros(D 1) eps 1e-8 for epoch in range(epochs): y_pred np.dot(X_train, w) grad np.dot(X_train.T, (y_pred - y_train)) / N grad_square_sum grad ** 2 w w - learning_rate / np.sqrt(grad_square_sum eps) * grad if epoch % 100 0: loss np.mean((y_pred - y_train) ** 2) print(fEpoch {epoch}, Loss: {loss:.4f}) return w如果你使用PyTorch或者TensorFlow不需要自己实现Adagrad直接用内置的优化器就行。但通过手写一次你能深刻理解那些优化器内部到底发生了什么。3.3 正则化绕不开的话题也是拿分关键纯线性回归在这个作业上训练集loss会很低但验证集上的表现往往一般。为什么因为特征维度并不低18个指标乘以9个小时再算上衍生特征轻松超过100个维度而有效的训练样本量并没有想象中那么多。这种情况下模型很容易过拟合——它记住了训练数据里的噪声而不是捕捉普遍的规律。解决过拟合的标准手段是加正则化项。最常见的是L2正则化岭回归/L1正则化Lasso李宏毅老师在这个作业中推荐的版本会在损失函数里增加权重的平方和项。加了正则化之后模型会倾向于把权重压缩到较小的数值从而降低对单个特征的敏感度。实际操作时你需要额外设置一个正则化系数lambdalambda太小起不到约束作用lambda太大则会欠拟合。我自己的经验是先给lambda设置为0.001观察验证集loss的变化再逐步调大或调小。很多同学在这一步容易犯的毛病是把正则化系数和梯度下降的学习率搞混。这两个是完全不同的超参数——学习率控制的是更新步长正则化系数控制的是权重的大小约束。我在调试时习惯把学习率和正则化系数分开调整先固定正则化系数为0找到一个合适的学习率区间然后再开启正则化搜索合适的lambda值。4. 训练验证与调优在分数之外的收获训练过程跑通之后很多人就急着提交了。但如果你愿意多花半小时做点验证和调优你会发现这个项目的收益会远大于“跑出一个预测结果”本身。因为真正的机器学习项目几乎永远是“训练-验证-分析-再训练”的循环而不是一次到底的流水线。4.1 验证集上的误差分析不只是看一个数字只盯着验证集上的平均loss是看不出模型哪里做得不好的。更有效的做法是把预测值和真实值画在一张散点图里查看误差的分布情况。我当时的做法是计算每个样本的绝对误差按误差大小排序然后看看误差最大的那些样本有什么共性。结果很有意思误差最大的样本基本集中在PM2.5浓度剧烈变化的时刻比如清晨的峰值、沙尘暴事件的突发时段。这说明线性回归模型擅长描绘趋势性的变化但很难捕捉突变性的事件。这个发现有什么用它能指导你做两个方向的改进一是为模型添加“突变提示”特征比如过去1小时和过去3小时PM2.5浓度的一阶差分这个特征在线性模型里可以变相模拟出“对趋势变化的响应”二是将训练数据按时间段分组建模采用分段回归的思路比如高峰期和非高峰期分别训练一个子模型再融合结果。我当时做了第一个方向的尝试验证集loss又下降了一些。这也算是从“完成作业”迈向“理解模型能力边界”的第一步。4.2 使用现成框架重写一次对比彻底搞懂框架帮你做了什么手动实现一遍之后强烈建议再用PyTorch或者sklearn把同一个模型重写一遍。这不是无用功而是帮你建立“数学公式/代码实现/框架API”三者之间的映射关系。用PyTorch写这个任务核心代码量也就二三十行但你可以清晰看到optimizer.zero_grad()、loss.backward()、optimizer.step()这三个步骤恰好对应着手写版本里的前向计算、梯度计算、参数更新。我记得第一次用PyTorch重写时最不适应的是它默认的权重初始化方式。手写版本我用的全是零初始化而PyTorch默认用均匀分布或正态分布初始化。理论上只要数据标准化做得好两者都能收敛到差不多的结果但在某些随机种子下线性回归的训练曲线会有一点差异。这个经验让我意识到在深度学习框架中随机初始化本身就是一个值得关注的超参数来源只是在这个简单作业中它影响不大。4.3 常见问题速查表这里整理了我遇到过的所有坑以下表格整理了我自己以及周边同学在这个作业中最常遇到的问题、可能的原因和解决方法。建议先收藏遇到问题再回来对照查表。问题现象可能原因解决方法训练几轮后loss变成NaN学习率过大梯度更新步长导致数值溢出降低学习率或将输入特征做标准化训练loss很低验证loss很高模型过拟合特征维度过多、样本不足增加L2正则化或减少特征维度训练loss始终不下降特征未标准化梯度更新方向被大数值特征带偏对特征做Z-score标准化或用Adagrad验证集loss在训练中不断波动学习率偏大导致参数在最优值附近震荡降低学习率或者使用学习率衰减策略预测结果全部接近某个固定值输出特征尺度过小模型倾向于预测平均值附近检查标签是否标准化调整损失函数权重用验证集调参后测试集分数仍不高验证集切分方式不当或验证集与测试集分布不同按时间切片切分验证集避免随机打乱带来的信息泄漏程序报维度不匹配错误拼接偏置项时维度没对齐逐行打印X_train.shape、w.shape检查np.hstack这里面最隐蔽的是“验证集切分方式不当”这一条。如果验证集是随机从所有天里抽出来的那么训练集和验证集里会同时出现同一天的相邻时刻数据造成信息重叠验证集loss会比真实测试集表现乐观。对于时间序列类数据切分时一定不能随机打乱直接按时间先后切分才是正确的做法这一点在工程实践中几乎天天遇到。5. 关于这个作业的延展思考线性回归是终点也是起点做完这个PM2.5预测作业回头看它的设计价值我觉得它最大的贡献不是教你线性回归公式而是逼你先成为一个“数据工程师”再成为一个“模型工程师”。你在预处理阶段处理的缺测值、构造的特征、标准化方式在真实业务中占比可能超过70%的工作量。而模型那一小块——无论是线性回归还是后来的深度学习模型反而是最容易被替代的部分。这也是为什么我建议你即使已经用PyTorch能轻松调出各种模型也值得抽时间把这个作业手写一遍。因为只有当你亲手实现过一次梯度下降看到loss从几百一路降到几十、再降到十几的过程你才会真正理解深度学习框架里那一个个抽象的API到底在做什么。之后再去看Adam、RMSProp这些优化器的论文你会有一种“原来如此”的顿悟感而不是被一堆数学符号劝退。我自己在实际操作中还有一个体会不要只停留在复现作业的层面可以试试给这个项目加一点自己的改动比如把线性模型换成决策树或者试试支持向量回归然后对比哪种模型在这个任务上表现更好。当你开始做对比实验的那一刻你就已经从“跟着教程跑代码”进阶到“带着问题做研究”了。这个转变才是这道作业真正想教给你的东西。
返回列表