
李宏毅老师的机器学习课作业一做PM2.5预测应该是很多人入门时接触的第一个“完整”机器学习项目。我说的完整不是指调个库把结果跑出来而是从读数据、清洗、特征构造、手写梯度下降到最终提交结果的全流程。这个作业虽然模型简单但五脏俱全数据解析、标准化、训练验证、调参避坑一个都绕不开。这篇文章就把我做完整个项目的复盘过程写下来包含数据集怎么解析、特征怎么构造、为什么作业要求手写线性回归、以及我实际踩过的各种坑。无论你是准备交作业还是想通过一个最小闭环真正理解回归模型这篇都能给你一条可以直接对照复现的路径。1. 项目背景与任务拆解1.1 作业一究竟要我们做什么先把这个任务说清楚。李宏毅机器学习作业一的目标是根据台湾丰原观测站过去9个小时的空气监测数据预测下一个小时的PM2.5浓度。评价指标是RMSE也就是均方根误差公式是RMSE sqrt( (1/N) * sum( (y_pred - y_true)^2 ) )这个指标的特点是大误差会被放大因为先平方再开方所以如果你在某个污染尖峰时刻预测严重偏低整体分数会很难看。这也是为什么后续做误差分析时要特别关注极端值样本。为什么这个作业适合入门因为它数据量小、特征维度低、模型简单但把完整流程走了一遍数据清洗、特征工程、模型训练、评估、预测。很多人学机器学习理论时觉得梯度下降很难但这个作业把梯度下降用在了一个非常直观的连续值预测问题上你能够亲眼看到loss随着迭代下降、RMSE逐步变小这种正反馈对建立信心非常重要。需要注意这里是回归任务不是分类任务。我们在预测一个连续的数值而不是判断“空气质量是好是坏”。很多人把回归和逻辑回归搞混逻辑回归虽然名字里带回归但实际做的是分类。这个作业要用的是经典的线性回归模型任务就是拟合一条或一个超平面来最小化预测值与真实值之间的误差。1.2 数据集的真实结构与坑点很多初学者拿到train.csv后直接当成普通表格丢进模型结果一跑就出问题。原因在于这个数据集的存储格式不是常规的“一行一个样本”而是一种非常容易踩坑的排布方式。训练集train.csv总共4320行。表面上看每一行是某个时刻的18项指标观测值但真实的组织逻辑是每18行才组成一天的数据。这18行的顺序是固定的依次对应AMB_TEMP温度、CH4、CO、NMHC、NO、NO2、NOx、O3、PM10、PM2.5、RAINFALL降雨量、RH相对湿度、SO2、THC、WD_HR风向小时值、WIND_DIREC风向、WIND_SPEED风速、WS_HR风速小时值。每行有24列对应一天当中24个小时的连续观测值。换算一下12个月 × 每月取前20天 × 每天18行 4320行数据量对得上。月份信息并没有单独的一列是通过行号间接编码的每360行切换一个月。第一次处理时如果没意识到这种结构直接把行号当成样本序号就会导致特征全部错位训练出来的模型形同虚设。数据里还有一堆比较隐蔽的格式问题。比如RAINFALL这一列在很多天里是字符串“NR”表示无降雨No Rain如果直接调用pd.to_numeric会报错。再比如每行第一列是一个编号字段实际建模时没什么用需要去掉。这些细节看着小但每一项都会卡住新手相当长的时间。2. 数据预处理与特征工程2.1 读数据与清洗的第一个大坑读取和清洗数据是整个项目最容易翻车的地方。我的建议是分三步走先读入再替换异常值最后重构数据结构。先用pandas把csv读进来。注意默认情况下“NR”会被识别成字符串列RAINFALL那列就变成object类型了这一步不会报错但后面计算会出问题。所以读取之后的第一件事是把所有非数值的内容统一替换掉我这里直接用0填充“NR”代表降雨量为0import pandas as pd import numpy as np df pd.read_csv(train.csv) df df.replace(NR, 0) df df.astype(float)这里有个小细节替换“NR”之后必须把整张表转成float类型否则后续切片计算时pandas会因为你混合了字符串和数值列而给出各种奇奇怪怪的警告。转float之后第一列ID其实已经失去了意义可以直接删掉df df.drop(columns[id])然后就是重头戏把表格重构为“天”级别的三维数组。我的做法是先把DataFrame转成numpy矩阵然后按18行一组切分。因为每个月的第n天对应18行且这18行的顺序固定所以可以直接用reshapedata df.to_numpy() # 4320行, 去掉ID后变成4320行24列 # 12个月 * 20天 240天, 每天18行, 每行24列 days data.reshape(240, 18, 24)这样days的形状就是(240, 18, 24)含义是第i天、第j种指标、第h个时刻的数值。之后所有特征构造都基于这个三维数组进行会方便很多。2.2 特征构造两种思路一个结果导向特征构造是这个作业的灵魂所在。原始作业里有两版常见说法一版是“使用前9个小时的PM2.5数据预测第10个小时”另一版是“使用前9个小时所有18项指标预测第10个小时的PM2.5”。我自己的建议是两个版本都做一遍既能加深理解也能直观体会特征信息量对模型效果的影响。先看最简单版只用PM2.5这一条序列做预测。对于一天内某个时刻t取t-9到t-1共9个小时的PM2.5浓度作为特征预测第t小时的值。滑窗生成样本的代码如下pm25 days[:, 9, :] # 第10个指标是PM2.5, 索引为9 X_list, y_list [], [] for day in range(pm25.shape[0]): for hour in range(9, 24): X_list.append(pm25[day, hour-9:hour]) y_list.append(pm25[day, hour]) X_simple np.array(X_list) y_simple np.array(y_list)这样生成的每个样本都是9维的样本总数为240天乘以每天15个可用时刻一共3600个。逻辑很简单但效果完全够用毕竟PM2.5变化有很强的自相关性前几个小时的值已经能说明很多问题。再来看升级版把前9小时的全部18项指标都拿来作为特征拼接成一个162维的向量。做法是对每一天、每个可用时刻把三维数组中该时刻之前9小时的所有指标全部取出来展平X_full_list, y_full_list [], [] for day in range(days.shape[0]): for hour in range(9, 24): X_full_list.append(days[day, :, hour-9:hour].reshape(-1)) y_full_list.append(days[day, 9, hour]) X_full np.array(X_full_list) y_full np.array(y_full_list)这样每个样本是162维。理论上信息量更大因为像温度、风速、风向、湿度这些变量都可能影响污染物累积和扩散所以升级版通常会取得更低的RMSE。但代价是模型参数更多更容易过拟合训练时对标准化的要求也更高。2.3 标准化梯度下降能不能收敛就看这一步很多第一次做这个作业的人会陷入一个尴尬境地梯度下降跑着跑着loss变成NaN或者干脆不收敛。问题大概率出在没有做标准化。特征标准化最常用的是z-score标准化公式很简单x (x - mean) / std标准化之后每个特征都变成均值为0、方差为1的分布。有了这个前提梯度下降的收敛速度会大幅提升也不会出现某些特征尺度过大、导致更新方向被“带偏”的情况。这里有一个特别容易出错的点标准化时只能使用训练集的均值和标准差验证集和测试集必须复用训练集计算出的同一组统计量。很多人图省事把训练集和验证集合在一起算mean和std这会造成信息泄漏导致验证集的效果评估虚高等真正上线预测新数据时效果立刻打折。正确做法是def normalize(X_train, X_valid): mean X_train.mean(axis0) std X_train.std(axis0) X_train_norm (X_train - mean) / (std 1e-10) X_valid_norm (X_valid - mean) / (std 1e-10) return X_train_norm, X_valid_norm, mean, std加这个1e-10是为了防止某个特征在训练集里取值恒定、标准差为0时导致除零错误属于工程上的防守型写法。3. 线性回归原理与手写实现3.1 模型本质一次线性组合搞定预测线性回归的原理一句话就能讲明白假设输出是输入的线性组合。用数学表示就是f(x) w1x1 w2x2 ... wn*xn b为了写法统一通常把偏置b也吸收进权重向量w里做法是在特征矩阵X最左边加一列全1。这样模型就变成f(X) X * w整个任务就是要找一组权重w让预测值f(X)和真实值y之间的误差尽可能小。误差用什么衡量常用均方误差MSEL(w) (1/2N) * sum( (y - Xw)^2 )有些教材前面乘1/2而不是1/N目的是求导之后消掉平方项的2让公式更简洁。这个常数不会影响最优解的位置只会让loss数值整体缩放所以不用太纠结。那为什么回归任务常用均方误差而不是平均绝对误差一个关键原因是MSE处处可导方便用梯度下降优化另一个原因是它对大误差更敏感模型会更努力去拟合那些偏差大的样本。这个特性在PM2.5预测里其实是双刃剑优点是不会对污染高峰完全忽视缺点是模型会被少数极端值牵制后面我会详细讲。3.2 梯度下降推导与代码实现梯度下降的思路特别像下山你在山腰上不知道该往哪个方向走就看一下当前位置哪个方向坡度最陡沿着最陡的方向迈一步反复迭代最终走到山谷。数学上这个“坡度”就是损失函数对权重w的梯度。对上面那个损失函数求梯度过程很简单∂L/∂w (1/N) * X^T (Xw - y)这个公式推导不难但我想强调一点这里的X^T乘误差向量得到的结果是一个和w维度相同的向量它表示每个权重方向上误差的增加速率。有了梯度更新规则就是w_new w_old - learning_rate * gradient学习率learning rate决定了你每一步迈多大。步子太大可能直接跨过山谷甚至跳飞步子太小则可能半天走不到底。手动实现线性回归训练过程如下def train_linear_regression(X, y, lr0.1, epochs1000): n_samples, n_features X.shape w np.zeros(n_features) loss_history [] for epoch in range(epochs): pred X.dot(w) error pred - y gradient X.T.dot(error) / n_samples w w - lr * gradient mse np.mean(error ** 2) loss_history.append(mse) return w, loss_history这段代码虽然短但包含了线性回归梯度下降的全部核心。值得注意的一点是权重w的初始值可以设置为零向量因为线性回归的损失函数是凸函数不存在局部最优问题从任何位置出发都能收敛到同一个全局最优。这个性质和神经网络完全不同初学者刚开始接触时容易混淆。使用的时候注意输入X需要包含一列1以吸收偏置项。可以这样准备X_train_b np.hstack([np.ones((X_train_norm.shape[0], 1)), X_train_norm])3.3 闭式解与正则化对比除了梯度下降线性回归还有解析解也就是正规方程w (X^T X)^(-1) X^T y这个解法的思路更加直接直接令梯度等于零解出最优权重。为什么还要学梯度下降因为当特征维度很高、样本量很大时直接求矩阵的逆运算代价很大而且X^T X可能不可逆。梯度下降则可以在有限时间内逼近最优解也更容易扩展到复杂的神经网络中。实际做作业时我强烈建议两种方法都实现一遍然后对比结果。闭式解代码很短w_closed np.linalg.inv(X_train_b.T.dot(X_train_b)).dot(X_train_b.T).dot(y_train)实测下来在同样的数据上梯度下降收敛后得到的权重和闭式解的权重几乎一致RMSE差距在0.01以内。这个对比练习能帮你验证自己的梯度下降实现是否正确属于自我检查的黄金标准。再说正则化。线性回归很容易过拟合特别是用162维全特征版本时参数多、样本量也只有3000多模型有足够能力去记忆训练数据。这时可以在损失函数后面加一项L2正则也就是岭回归L(w) (1/2N) * sum( (y - Xw)^2 ) λ * sum(w^2)L2正则的本质是让权重不要长得太大因为过拟合的模型往往某些权重会非常大、对特定特征过度敏感。加入这一项之后梯度更新公式变成w w - lr * (gradient λ * w)λ是正则化系数通常取0.01到1之间的值。加了正则之后闭式解也变成w (X^T X λI)^(-1) X^T y这里的I是单位矩阵注意偏置项对应的权重一般不做正则化不过在实际操作中由于我们构造的X第一列全是1对偏置项做正则影响也不大很多实现会省去这一步。4. 训练、验证与结果分析4.1 验证集怎么划分才不是自欺欺人训练集内部怎么划分验证集是这个作业里最容易被忽略、却最能体现工程意识的地方。很多人图方便直接把样本随机打乱后按比例切分但PM2.5数据是时间序列相邻小时的数据高度相关如果随机切分训练集和验证集里会混入彼此非常相近的样本验证分数会虚高但真实预测能力并没有那么好。更严谨的做法是按时序划分。比如用前11个月共220天做训练留下最后1个月共20天做验证。这样模拟了真实场景模型过去的数据训练预测未来的数据。实现上可以把X_full按240天切回天维度再取前220天、后20天# 以三小时滑窗为例, 假设X_full已经按天展开 X_by_day X_full.reshape(240, -1, X_full.shape[1]) y_by_day y_full.reshape(240, -1) X_train_seq X_by_day[:220].reshape(-1, X_full.shape[1]) y_train_seq y_by_day[:220].reshape(-1) X_valid_seq X_by_day[220:].reshape(-1, X_full.shape[1]) y_valid_seq y_by_day[220:].reshape(-1)这样做的好处是验证集的分布和真实测试场景更接近结果更有参考价值。当然代价是验证集RMSE会比随机切分要高一些因为模型确实没有见过“未来”的数据。4.2 学习率与迭代次数的调参心法学习率是我在这个作业里调得最多的超参数。用标准化之后的数据学习率可以从0.1开始试如果loss曲线在震荡说明学习率偏大减到0.01如果loss下降非常缓慢说明学习率偏小可以加大到0.5甚至1。判断收敛的方法不是看训练loss是否趋向于0而是要看训练损失是否进入平台期同时验证集损失是否还保持合理。一个经典误区是训练loss持续下降验证loss反而上升这就是过拟合的典型信号。遇见这种情况要么减少训练轮数要么加正则化要么减少特征维度。迭代次数我习惯设定为1000同时画出loss曲线来观察。如果500轮就已经平了那1000轮完全够用如果你发现3000轮后loss还在稳定下降那说明学习率可能偏小。切忌盲目设置一个极大epoch数然后发现验证集效果变差因为模型已经开始“死记硬背”训练数据了。下面这段代码可以用于绘制训练loss曲线import matplotlib.pyplot as plt plt.plot(loss_history) plt.xlabel(Epoch) plt.ylabel(MSE Loss) plt.title(Training Loss Curve) plt.grid(True) plt.show()从曲线上你能直观看到梯度下降的行为前期快速下降后期逐渐平稳。如果曲线出现断崖或者NaN基本可以断定学习率设大了。4.3 结果评估、误差分析与进阶方向我在验证集上做过一组对比实验直接把结论放在这里方法特征维度验证集RMSE仅PM2.5历史9小时 线性回归96.8左右全18项指标历史9小时 线性回归1625.5左右全特征 岭回归 (λ1)1625.3左右全特征 多项式扩展 岭回归约8004.5左右这个数字会因为随机种子、验证集切分方式不同而有波动但趋势是一致的特征越丰富模型表达力越强RMSE越低但过拟合风险也越高。加了L2正则之后验证集分数通常会稳定一些。误差分析上我发现RMSE高的样本大多集中在PM2.5浓度骤变的时段。比如清晨和傍晚由于交通排放叠加气象条件浓度会在短时间内快速拉升线性模型对这种突变反应迟钝预测值往往偏保守。这也解释了为什么单纯增加特征维度很快会碰到瓶颈因为PM2.5变化本质上是非线性过程线性模型的上限就摆在那里。如果想在这个作业上继续进阶可以考虑三个方向一是加入多项式特征让模型获得一定的非线性表达能力二是在滑窗构造样本时引入更长的历史窗口比如前12小时甚至前24小时三是换用更复杂的模型比如用LSTM或GRU捕捉长期时序依赖。不过作为第一个作业先把线性回归吃透比盲目上深度学习更重要。5. 常见问题与排坑实录5.1 高频坑点速查我做这个作业期间以及帮同学调试时整理出一张高频坑点对照表现象根本原因解决方法pd.read_csv后加总报错RAINFALL列存在字符串“NR”先df.replace(NR, 0)再转floatreshape报错维度对不上没有删掉ID列或18行一天理解错误确认shape为(240, 18, 24)梯度下降loss变成NaN学习率过大或特征未标准化先标准化再把lr降到0.01以下验证集RMSE特别低但测试集很差随机划分样本造成时间序列信息泄漏改为按时间顺序划分验证集训练loss一直下不去特征尺度差异过大对特征做z-score标准化闭式解报np.linalg.inv错误X^T X不可逆多半有全零或冗余特征加正则项或使用np.linalg.pinv表格里的前两条是我见过最多的问题基本都属于数据格式理解不到位。第三条则是最让初学者头疼的看到loss变成NaN第一反应往往是代码写错了其实很多时候只是学习率设置不合理或者标准化没做。5.2 我的实测经验与心得做完这个作业之后我有几点体会特别深。第一标准化的重要性怎么强调都不为过。我在不标准化的条件下尝试过学习率要压到1e-5量级才能勉强收敛而且收敛速度极慢标准化之后学习率直接调到0.1都没问题几轮迭代就能看到损失明显下降。这个体验会让你直观理解特征缩放对梯度下降的加速作用。第二手写一遍模型带来的认知收益远远超过直接调用sklearn的LinearRegression。虽然调库代码只要一行但你无法理解梯度计算过程更无法处理训练异常。我建议几行核心代码至少自己写一遍哪怕用的是和推导一样的过程亲手敲出来的模型会真正变成你的工具。第三调参要讲究策略而不是碰运气。先画loss曲线观察趋势再动手改学习率、迭代次数、正则化系数。每个改动只动一个变量跑完对比验证集RMSE再决定下一步方向。这种“单点变量法”看似缓慢实际才是最快的方式能够帮你建立可靠的参数直觉。最后再分享一个小技巧训练完成后把验证集里预测偏差最大的几个样本打印出来看看分析一下这些样本的共同点。我这么做了以后发现偏差大的样本几乎都对应着当天某些小时PM2.5突变的情况这让我对模型的能力边界有了更清晰的认识也确定了后续改造方向。这种从错误中提炼信息的能力才是做项目真正积累下来的财富。