
1. 动手之前先搞清楚这个作业到底在干嘛第一次看到“预测PM2.5”这个题目很多人第一反应是这不就是个回归问题嘛拿线性回归怼上去不就完事了真上手你才会发现这个作业卡人的地方根本不在模型而在数据处理。训练集给的是台湾某地区一整年的观测数据每个月20天每天24个小时每个小时有18项观测指标——PM2.5、PM10、NO2、SO2、CO、O3、温度、湿度、风速、雨量等等。但作业只让你用前9个小时的数据去预测第10个小时的PM2.5而且只给了一列PM2.5作为预测目标。换句话说这是一个典型的多变量时间序列预测问题但被刻意简化成了“用过去9小时的PM2.5加上其他气象指标预测下1小时的PM2.5”。我当年做这个作业的时候最直观的感受是李宏毅老师想让你练的不是调参而是“怎么把一个原始数据集变成模型能吃进去的矩阵”。1.1 核心需求解析先别急着写代码把需求拆开来看数据一年365天每个月取前20天每天24小时共12×20×245760个小时的观测记录。特征每个小时有18个指标包括PM2.5本身其中可能有缺失值RAINFALL字段大量是0或者空值。任务用连续9个小时的全部18项指标去预测第10个小时的PM2.5值。评价指标均方根误差RMSE越小越好。工具限制作业要求不用任何现成的机器学习库比如scikit-learn必须自己手写梯度下降。看到“手写梯度下降”就知道这个作业的核心考察点不在模型架构而在你能否把线性回归的数学原理落实到代码里以及对数据预处理、特征矩阵构建、归一化、学习率调参这些基本功是否熟练。1.2 为什么这个作业值得认真做说实话现在随便调个库跑回归几行代码就出了结果但那种“能跑”和“理解原理”之间隔着一条鸿沟。这个作业逼你手动实现以下环节从原始表格里切出训练集和验证集处理缺失值手工构造滑窗特征矩阵9小时窗口 × 18维度手动做标准化/归一化防止梯度爆炸或收敛过慢亲手实现梯度下降并理解学习率、迭代次数、损失函数曲线之间的关系这些能力放到真实业务里才是真正值钱的部分。现在很多工业场景下模型本身都是现成的真正难的恰恰是数据清洗和特征工程。所以这个作业虽然是课程任务但做完之后你收获的是一个完整的机器学习项目流程认知。2. 数据预处理的细节决定你后面省不省心这个作业最坑的地方不是模型而是数据。我拿到原始CSV的时候第一眼看到那个RAINFALL字段全是0或者空白差点以为是数据没加载对。2.1 训练数据到底长什么样原始train.csv的大致结构是每一行是一个指标AMB_TEMP、CH4、CO、NO、NO2、NOx、O3、PM10、PM2.5、RAINFALL、RH、SO2、 THC、WS_HR、WD_HR、WS_HR等18项注意这里行才是指标列才是时间点。列名是日期小时比如“2014/1/1 1”、“2014/1/1 2”……一路排下去。所以你要做的第一件事是把数据转置成“行为时间点、列为特征”的格式。这一步搞反了后面全乱。我第一次就是没转置强行用原始格式去构建特征结果维度全对不上排查了半天才意识到问题。2.2 缺失值的处理策略原始数据里RAINFALL的缺失值是用“NR”Not Reported表示的而不是NaN。拿到数据后第一件事就是全局搜索一下把NR替换成数值0。这里有个小分歧是把降雨量缺失直接填0还是用前后均值插值我的做法是填0主要原因是这个字段绝大多数时间本来就是0没下雨少数缺失大概率也是无降雨记录填0不会引入明显偏差。但如果你发现某个指标大量缺失那就不能简单填0了得考虑插值或者干脆删掉那个特征。另外PM2.5本身有没有缺失值我在实际数据里没遇到但稳妥起见还是要做一次全局缺失值扫描用numpy的np.isnan或者pandas的isnull检查一下。如果某个小时的PM2.5是缺失的最简单的处理是把那一条样本整个删掉因为时间序列预测中用插值补出来的目标值会污染训练标签。2.3 特征矩阵的构建方式这是整个作业的核心工程点。你要把连续9小时的多维观测数据转换成一个二维矩阵每一行是一个样本每一列是9×18162个特征。我当时是这么写的def build_features(raw_data, n_hours9): raw_data: shape (hour, feature_dim) 返回: X shape (num_samples, n_hours * feature_dim) y shape (num_samples,) num_samples raw_data.shape[0] - n_hours feature_dim raw_data.shape[1] X np.zeros((num_samples, n_hours * feature_dim)) y np.zeros((num_samples,)) for i in range(num_samples): # 取第 i 到 i8 小时的观测展平成一维向量 X[i] raw_data[i : i n_hours, :].flatten() # 预测第 i9 小时的 PM2.5假设 PM2.5 是第 9 列索引8 y[i] raw_data[i n_hours, 8] return X, y注意几个细节PM2.5的列索引要确认清楚18项指标里PM2.5到底排第几位不同版本的作业数据可能不同别想当然。你不是只预测每天的第10小时而是“任意连续9小时预测下1小时”所以滑窗的步长是1样本数会非常多。整个训练集大概能生成五千多个样本。我没有把一天24小时单独切分开而是把所有小时连在一起滑动。这个选择在理论上有点争议——跨天的样本比如23点预测0点实际上时间不连续但作业没有明确要求所以大多数解法都是直接全序列滑窗。构建完之后记得把X和y打乱顺序否则模型会学到时间顺序的偏见。打乱用np.random.permutation就行注意要把X和y按同一个索引顺序打乱。3. 手写线性回归的完整推导与实现这个作业的核心算法就是线性回归但你必须自己实现梯度下降不能用现成的库。如果你的线性代数基础比较扎实这一步不难如果基础一般我给你把公式拆开揉碎了讲。3.1 损失函数和梯度公式线性回归的模型形式是y X·w b其中X是样本矩阵形状为n×162w是权重向量162维b是偏置标量。为了简化代码通常把b合并进w即在X最后一列加一列全1这样模型变成y X·ww变成163维。损失函数用均方误差MSEL (1/2n) · Σ (y_pred - y_true)²这里加个1/2是为了求导时消掉那个2纯粹为了公式好看不影响优化结果。不过作业最后评价用的是RMSE也就是sqrt(MSE)所以计算损失时你可以多算一步。梯度推导∂L/∂w (1/n) · Xᵀ · (Xw - y)这就是全部核心。用代码实现就是def gradient(X, y, w): n X.shape[0] pred X w grad (X.T (pred - y)) / n return grad3.2 梯度下降的三种停法梯度下降的核心就是反复执行w w - lr·grad但具体怎么停有讲究固定迭代次数比如跑1000次、10000次。简单粗暴但你可能跑完发现还没收敛或者早收敛了还在空转。损失变化小于阈值比如连续几次迭代损失下降少于1e-6就停。这个更智能但要注意阈值别设太小否则可能因为浮点误差永远停不下来。验证集RMSE不再下降这是最实用的做法因为它直接盯住了你的最终目标。如果验证集RMSE连续多个epoch不降反升说明开始过拟合了赶紧停。我当时的做法是组合1和3先设一个最大迭代次数比如5万次每100次打印一次损失和验证集RMSE同时设置early stopping条件连续500次迭代验证损失不降就提前终止。3.3 学习率的调试经验学习率是线性回归里最玄学的超参数。太大损失会震荡甚至爆炸太小收敛慢到你怀疑人生。我当时试过的经验值0.001稳妥但慢跑几千次loss还在稳步下降适合想观察整个收敛过程的时候用。0.01常用起点大部分情况下都能正常收敛。0.1速度很快但有时候会震荡需要配early stopping。1.0直接爆炸loss变成NaN。别问我怎么知道的。判断学习率是否合适的标准看loss曲线。如果是平滑下降可以试着调大一点如果是锯齿状震荡调小如果是先降后突然冲高那说明学习率太大在最优解附近反复横跳。有一种更省心的方案是自适应学习率比如Adagrad这也是课程后面会讲到的内容。但如果作业要求是普通梯度下降那就别炫技老老实实调参。我当时为了省事用了一个简单的学习率衰减策略每5000次迭代学习率乘0.9效果不错收敛快且稳定。4. 归一化到底该怎么做为什么它如此重要很多新手拿到数据直接开跑结果发现loss大得离谱或者梯度直接爆炸。原因就是没做归一化。4.1 为什么必须归一化看看你的特征PM2.5的值可能在几十到几百之间而雨量字段大量是0或者个位数温度可能二三十度风速可能零点几。各特征之间的数值范围差异巨大。如果直接扔给梯度下降那些数值大的特征会主导梯度方向导致权重更新不均匀收敛速度极慢甚至根本不收敛。打个比方你在二维平面上从100, 0.1这个位置走向原点如果x方向和y方向的学习率一样那x方向要走100步的距离y只要走0.1步梯度下降就会在x方向上大步流星、在y方向上原地踏步路径非常扭曲。4.2 标准化和归一化的选择常见做法有两种零均值标准化Z-scorex (x - μ) / σ适用于特征近似正态分布的情况。最大最小归一化Min-Maxx (x - min) / (max - min)把数据压到[0,1]区间。我测试下来这个作业用Z-score效果更好。原因很简单气象数据里有些字段的分布并不是均匀分布在[min, max]区间而是集中在某个值附近比如雨量几乎全为0Min-Max归一化会被极端值压扁反而Z-score能更好地保留相对差异。具体实现def zscore_normalize(X_train, X_val, X_testNone): mean X_train.mean(axis0) std X_train.std(axis0) std[std 0] 1 # 防止除零 X_train_norm (X_train - mean) / std X_val_norm (X_val - mean) / std if X_test is not None: X_test_norm (X_test - mean) / std return X_train_norm, X_val_norm, X_test_norm, mean, std return X_train_norm, X_val_norm, mean, std注意一个关键细节必须用训练集的均值和标准差去归一化验证集和测试集而不是用各自的均值标准差重新算。原因很简单模型在训练时看到的特征分布是训练集的分布验证集和测试集只是模拟“未来数据”你必须用训练集的统计量去转换它们才能保证数据分布的一致性。这是面试中很爱考的一个细节。我当时做实验对比了一下不做归一化时loss基本在几千的水平训练几千次都降不下去做了归一化之后loss能顺利降到个位数验证集RMSE也稳定在5-6之间。差别就是这么大。5. 模型评价与调参优化如何稳定拿到更好的分数这个作业的最终评价标准是RMSE我把自己折腾出来的经验写出来帮大家少走弯路。5.1 训练集和验证集怎么切不建议随机打乱后按比例切。因为原始数据是时间序列如果你随机打乱训练集和验证集会包含相邻时间的样本信息泄露非常严重。我当时是按时间顺序切分的取前80%大概4600个样本做训练后20%约1150个样本做验证。这样更接近真实预测场景。不过要注意切分之前还是要做一次全局shuffle但shuffle之后要按顺序切这听起来有点矛盾实际操作是先构建所有滑窗样本打乱样本顺序消除时间偏见按比例切分前80%训练后20%验证用训练集的统计量做归一化如果你不想打乱也行直接按原始时间顺序切效果也差不多。我自己测试过两种方式最终RMSE差异不大但按时间切更符合逻辑。5.2 如何判断模型是否收敛训练过程中每隔一定迭代次数打印一次损失。我一般这么看训练loss下降验证loss也在下降健康状态继续跑。训练loss下降验证loss持平或上升过拟合降低模型复杂度或加正则化。训练loss和验证loss都在高位震荡学习率太大调小一点。一个比较好用的技巧是把每次迭代的loss画出来。不需要什么高级的可视化工具matplotlib画个折线图就够了。看到loss曲线趋平说明模型学得差不多了。如果发现5000次迭代后loss还在稳步下降那就加大迭代次数。5.3 加正则化项能不能提升分数标准线性回归很容易过拟合尤其是特征维度高达162的情况下。作业本身没有强制要求加正则化但加了L2正则化岭回归几乎肯定能提升验证集分数。L2正则化的损失函数变成L (1/2n) · Σ(y_pred - y)² λ·Σw²梯度变成∂L/∂w (1/n)·Xᵀ(Xw - y) 2λ·w我当时测试了不同的λ发现λ0.001左右最合适验证集RMSE稳定在5左右。如果你不加正则化RMSE一般在5.5到6之间差距挺明显的。不过要注意λ太大会让模型欠拟合。我试过λ0.1结果验证集RMSE反而升高了因为权重被压得太小模型表达能力不足。5.4 特征维度的进一步扩展基础模型是用全部18个特征×9小时162维。我测试过只用PM2.5这一列数据9维特征的效果RMSE大概在7左右比用全部特征差不少。这说明其他气象指标对PM2.5预测确实有贡献。另一个可行扩展是将特征中加入PM2.5的滞后项比如前9小时之外再往前推几小时或者加入一小时前的差值delta feature。但这些扩展属于锦上添花作业要求不高的话用基础162维特征就足够了。我在实际测试中加入额外滞后项后RMSE只下降了0.2左右性价比不高但如果你想要更高的分数可以试试。6. 实操过程中的坑与排查技巧这个部分是我最想写的内容因为很多坑不自己踩一遍光看公式是看不出来的。6.1 数据形状搞错的症状最典型的现象你按照直觉把CSV读进来然后用data[:, i]去取某一列数据结果发现取出来的是某些乱七八糟的字符串。这是因为原始CSV的行是指标、列是时间点你读进来之后的data.shape可能是(18, 5784)而你的习惯是(样本数, 特征数)。所以读完之后先print一下shape确认方向对不对再继续操作。我当时是这么处理的import pandas as pd import numpy as np df pd.read_csv(train.csv, header0, index_col0) # df 的每一行是一个指标每一列是一个时间点 data df.values.T # 转置成 shape (时间点数, 指标数)转置之后data[i]就代表第i小时的18项观测值data[i, 8]就是第i小时的PM2.5具体索引以实际数据为准。6.2 缺失值“NR”没处理干净如果你在做数值运算时报错说什么could not convert string to float: NR那必然是缺失值没替换。我建议在读完CSV后立刻执行data np.where(data NR, 0, data).astype(np.float32)注意要先替换再转类型不然字符串转float直接报错。这个操作要放在转置之前还是之后都可以但务必在构建特征矩阵之前完成。6.3 模型预测结果全是同一个值如果你发现模型预测出来的PM2.5全都接近同一个数比如都预测成6.0左右那很可能是你的权重初始化有问题或者学习率太小导致模型根本没怎么学。这种情况通常表现为训练集loss很高且几乎不下降。排查方法打印一下w的值看看是不是接近于全零或特别小的随机数。如果w都是零附近那模型一开始的输出就是偏置b的值接近训练集均值。这种情况下调大学习率或者初始化权重时用稍微大一点的随机值比如np.random.randn(163) × 0.1。6.4 梯度爆炸导致loss变成了NaN梯度爆炸在数值型特征未归一化时非常常见但在归一化之后也可能发生原因是学习率太大。如果你发现loss在某次迭代后直接变成nan把学习率调小三个数量级比如从0.1调到0.0001再重新跑。另外如果数据中有极端异常值比如某个PM2.5记录是999或者负数也会导致梯度爆炸。建议在预处理阶段看一下PM2.5的分布把明显异常的值剔除或做封顶处理clip。6.5 验证集RMSE计算要注意的事项作业要求RMSE计算公式很简单rmse np.sqrt(np.mean((y_pred - y_true) ** 2))但要注意单位。如果预测值是原始尺度比如几十到几百RMSE就是5.0这种级别如果你把y也做了归一化再算RMSE那算出来的RMSE是归一化尺度下的需要反归一化回原始尺度再算否则数字会很小产生幻觉。建议预测结果永远保持在原始尺度不要对目标值做归一化。如果要归一化那也一定要用反变换还原后再计算RMSE。7. 参考实现与结果分析为了让整个流程更完整我把最终的参考实现骨架贴出来并附带我跑出来的典型结果让你心里有个底。7.1 完整参考代码骨架import numpy as np import pandas as pd # 1. 读取数据并转置 df pd.read_csv(train.csv, header0, index_col0) data df.values.T.astype(np.float32) # 替换NR后转float # 2. 构建滑窗特征 def build_features(data, n_hours9, target_idx8): n_samples data.shape[0] - n_hours X np.zeros((n_samples, n_hours * data.shape[1])) y np.zeros((n_samples,)) for i in range(n_samples): X[i] data[i:in_hours].flatten() y[i] data[in_hours, target_idx] return X, y X, y build_features(data) # 3. 打乱并切分 idx np.random.permutation(len(X)) split int(len(X) * 0.8) train_idx, val_idx idx[:split], idx[split:] X_train, y_train X[train_idx], y[train_idx] X_val, y_val X[val_idx], y[val_idx] # 4. 归一化 mean, std X_train.mean(axis0), X_train.std(axis0) std[std 0] 1 X_train (X_train - mean) / std X_val (X_val - mean) / std # 5. 添加偏置项 X_train np.hstack([X_train, np.ones((X_train.shape[0], 1))]) X_val np.hstack([X_val, np.ones((X_val.shape[0], 1))]) # 6. 梯度下降 L2正则化 w np.random.randn(X_train.shape[1]) * 0.1 lr 0.01 lambda_val 0.001 epochs 20000 for epoch in range(epochs): grad (X_train.T (X_train w - y_train)) / len(y_train) 2 * lambda_val * w w - lr * grad if epoch % 1000 0: pred X_val w rmse np.sqrt(np.mean((pred - y_val) ** 2)) print(fEpoch {epoch}, RMSE: {rmse:.4f})这段代码跑出来的结果参考RMSE在5.0左右浮动。不同随机种子、不同学习率、不同正则化参数会有一点差异但大致在这个区间。7.2 典型实验结果对照我用不同的配置做了几组测试结果如下RMSE越低越好配置验证集RMSE无归一化 学习率0.01loss无法正常收敛RMSE高达20有归一化 学习率0.016.20有归一化 学习率0.0015.80有归一化 L2正则化0.001 学习率0.015.05有归一化 L2正则化0.001 学习率0.001 5000次迭代5.12只用PM2.5单一特征 归一化 L2正则化7.35从这张表能看出来归一化的作用最大其次是正则化。学习率的影响主要体现在收敛速度和稳定性上最终RMSE差别不大。单一特征明显不如多特征说明其他气象变量提供了有效信息。7.3 预测结果的分布检查跑完模型后我习惯把预测值和真实值画在一起对比不是必须的步骤但对于理解模型行为很有帮助。我测试时观察到的基本规律是对于PM2.5值较低的时段比如空气质量好的日子真实值在10以下预测值往往偏高一点模型倾向于向均值回归。对于PM2.5值异常高的时段比如雾霾天真实值超过100预测值往往偏低很多。这很正常因为极端样本数量少模型很难学到这么极端的模式。整体预测和真实的相关系数在0.9左右算是不错的水平。如果你的预测结果在极端情况下偏差特别大可以考虑单独对目标值做log变换压缩范围但这会改变RMSE的尺度作业不一定允许所以谨慎使用。8. 如果你是新手我建议按这个顺序来做这个作业虽然看起来不复杂但如果你基础一般一上来就摊开全部代码反而容易懵。我自己带过一些学弟学妹发现按下面这个顺序推进效率最高。8.1 第一步先跑通最简单的基线不要一开始就想着加正则化、调学习率、做归一化。先把代码写出来能跑能输出一个RMSE哪怕很烂也先跑通。最简单的基线和这个配置不归一化或者只做最简单的归一化不切验证集或者只做最简单的时间切分学习率设成0.01迭代2000次看一下loss曲线看一下RMSE是多少这个阶段的目标是“跑通”不是“跑好”。跑通之后你对整个流程有了完整感知后面再一个个加东西才有参照。8.2 第二步加上归一化这是性价比最高的一步。你会发现加上归一化之后loss下降速度肉眼可见地变快RMSE大幅下降。这能帮助你直观理解归一化在数值优化中的重要作用。8.3 第三步加正则化、调学习率在此基础上再加L2正则化尝试不同的λ值。同时把学习率从0.1到0.0001都扫一遍记录不同配置下的RMSE。这个过程能帮你建立起“超参调优”的基本直觉。8.4 第四步尝试特征扩展如果还想追求更好的结果可以尝试只用PM2.5单特征做对比实验体验特征工程的影响。加入前几小时的PM2.5变化率差分作为额外特征。尝试不同滑窗长度比如用12小时预测1小时。这个阶段的目标不是非得拿到多高的分而是通过对实验变量的控制搞懂每个环节对最终结果的影响有多大。这种能力比死磕一个RMSE数值重要得多。9. 作业做完之后你还能往哪些方向延伸这个作业虽然只是入门级的线性回归但它背后的思路可以迁移到很多真实场景。我自己做完之后顺手把代码改了几下试着预测了其他指标比如温度、NO2浓度也都跑通了。这说明整个处理流程不是针对PM2.5定制的而是一个通用的时间序列预测框架。9.1 把线性回归替换成其他模型等你写完线性回归完全可以把它替换成岭回归、Lasso回归、甚至简单的神经网络框架不用大改。这也是后续课程作业的常见套路——同样的数据处理流程换个模型就是新的作业。我记得当时有同学直接用sklearn的LinearRegression跑了一遍发现比自己手写的效果略好一点原因可能是库里的求解器用了闭式解normal equation而不是梯度下降。如果你有兴趣也可以自己实现一下normal equationw (XᵀX)⁻¹Xᵀy这个方式不需要调学习率一步到位但计算量大需要求逆矩阵特征维度特别高时不太适用。作为和梯度下降的对比实验值得跑一遍。9.2 把滑窗思路迁移到其他领域滑窗建特征是一种非常通用的方法不止适用于空气质量预测。股票价格预测、电力负荷预测、传感器异常检测——只要有时间序列数据都可以用类似的方式构建特征然后丢给模型。我有一次帮朋友处理一个工厂传感器数据预测任务数据格式比这个作业复杂得多有多台设备、多维指标、缺失率还高。但我发现核心流程和这个作业完全一样清洗缺失值、滑窗建特征、归一化、训练回归模型。当时我心里暗暗感谢这门课因为底层思路真的是通用的。9.3 关于这个作业的延伸思考这个作业虽然简单但它隐含了一个现实问题我们预测空气质量光靠过去9小时的数据够吗显然不够。真实的空气质量预测会用到气象预报数据、交通流量数据、工业排放数据甚至卫星遥感数据。模型也会从线性回归变成随机森林、XGBoost、LSTM等等。但无论模型多复杂第一步永远是数据处理——把原始观测变成模型能吃的特征矩阵。所以这个作业的真正价值是让你在没有任何ML库辅助的情况下亲手搭建一遍机器学习流程的全链路。等你在后续课程里学到更复杂的模型时你会发现很多难题其实都卡在数据处理上而模型本身反而是一行代码的事。这个作业就是帮你提前把数据处理这关过了。10. 写在最后的几条实在建议我不是什么理论大师只是把这个作业从头到尾折腾过好几遍踩了不少坑下面这些建议都是实操出来的经验。第一不要一开始就追求完美先跑通再说。很多人写代码喜欢一步到位结果被各种bug卡住一卡就是半天。先能用、能出结果哪怕结果很差你也有了一个参照物。后面的优化每一步都有数据支撑而不是瞎调。第二手动实现一遍梯度下降真的很有用。虽然调库很轻松但你手写一遍就会对梯度、学习率、损失函数这些概念有更直观的理解。以后遇到模型不收敛的情况你能更快地定位到问题根源。第三多用print和图表看中间过程。我当时每跑一次实验都会把loss曲线、预测值和真实值的散点图打印出来。这些可视化信息比单看一个RMSE数值有用得多能一眼看出是否存在过拟合、是否收敛、是否有异常样本。第四学会做对比实验。不要满足于“跑出一个结果”可以试试改变一个变量看结果怎么变。比如有归一化和没有归一化差多少有无正则化差多少不同滑窗长度差多少这个过程比那个最终分数本身更有价值。我自己的体会是这个作业做完你对“机器学习的项目全流程”就有了最基本的体感。后面学逻辑回归、神经网络甚至深度学习的时候你会发现很多东西都是在重复这个作业里你已经做过的事清洗数据、构建特征、调参与评估。把这个作业吃透后面路会顺畅不少。