资讯详情

手推线性回归:从公式推导到PM2.5预测实战

📅 2026/10/14 15:56:38 | 华诺云谱 👁 阅读
手推线性回归:从公式推导到PM2.5预测实战
1. 项目概述为什么手推线性回归是机器学习入门的“照妖镜”“李宏毅机器学习2020HW1——手推线性回归公式预测PM2.5”这个标题里藏着三个关键信号李宏毅课程、手推公式、PM2.5预测任务。它不是一道普通的编程作业而是一次对机器学习底层逻辑的“压力测试”。我带过不少刚接触AI的学生发现一个普遍现象能调通sklearn.LinearRegression()的人很多但真正能从零写出损失函数、求导、矩阵推导、手动更新权重的人不到三成。而这恰恰是区分“会用工具”和“理解模型”的分水岭。这个作业的核心价值不在于预测PM2.5的精度有多高实测RMSE在35–42之间已属合理而在于强制你把抽象的数学符号变成一行行可验证的计算步骤。比如当你亲手写下 $\frac{\partial}{\partial w} \left( \frac{1}{2N} \sum_{i1}^{N}(y_i - w^T x_i)^2 \right) -\frac{1}{N} \sum_{i1}^{N}(y_i - w^T x_i) x_i$再把它翻译成grad -1/N * X.T (y - X w)时你才真正“看见”了梯度下降在干什么——它不是黑箱里的魔法而是沿着最陡峭下坡方向一步一脚印地挪动参数。PM2.5数据集本身也极具教学意义它来自某高校环境实验室连续一年的监测记录包含每小时的温度、湿度、风速、SO2、NO等18项特征目标是预测未来一小时的PM2.5浓度。数据天然带有时间相关性、部分缺失、量纲差异大等特点逼着你必须做标准化、处理空值、设计特征组合比如“湿度×温度”可能比单个变量更有解释力。这不是在玩玩具数据集而是在模拟真实场景中“脏数据”的第一道关卡。适合谁来深挖这个项目三类人收益最大一是正在啃李宏毅课程的初学者它能帮你把视频里一闪而过的公式定格、拆解、复现二是准备面试的数据岗求职者手推过程本身就是极佳的算法题应答脚本三是想夯实基础的转行者当你能独立完成从数据加载→特征工程→公式推导→数值实现→结果可视化全流程那种“原来如此”的通透感远胜于刷十道LeetCode。提示别被“手推”二字吓退。它不等于纯纸面演算而是“纸面推导代码验证”双轨并行。我建议你先用笔在草稿纸上走一遍最小二乘闭式解 $w^* (X^T X)^{-1} X^T y$ 的每一步再立刻用NumPy写出来跑通。这种“推-写-验”闭环才是吃透它的唯一路径。2. 核心思路拆解为什么必须放弃sklearn坚持手写矩阵运算2.1 闭式解 vs 梯度下降两种路径的本质差异与取舍逻辑HW1明确要求“手推公式”但没限定必须用哪种求解方式。实践中绝大多数人会自然选择梯度下降GD因为李宏毅视频里演示的就是它。但如果你真去推导闭式解Normal Equation会发现它其实更“干净”——没有学习率、没有迭代次数、一步到位。那为什么课程仍主推GD答案藏在计算复杂度里。闭式解的核心是求逆矩阵 $(X^T X)^{-1}$。假设你的特征维度是 $d18$原始特征样本数 $N4320$2014年全年每小时数据那么 $X^T X$ 是一个 $18 \times 18$ 的小矩阵求逆成本几乎可忽略$O(d^3) \approx 5832$ 次浮点运算。但如果某天你把特征工程做到极致——加入所有两两交互项$C_{18}^2 153$ 个、多项式特征如温度²、湿度³维度 $d$ 涨到200$d^3$ 就飙升至800万次运算且矩阵可能接近奇异求逆失败风险陡增。梯度下降则不同它只依赖矩阵乘法每次迭代计算grad -1/N * X.T (y - X w)。乘法复杂度是 $O(Nd)$对 $N4320, d18$单次迭代仅约15.6万次运算。虽然要迭代1000次总成本约1.56亿次看似更高但它内存友好、可增量更新、天然适配大数据——这才是工业界的真实逻辑。所以HW1让你手写GD不是为了刁难而是提前给你打下“可扩展性”思维的地基。2.2 特征工程设计为什么PM2.5预测必须做“时间窗口”重构原始数据是按小时排列的时序数据形如[t0, t1, t2, ..., t4319]每行18维特征1维标签PM2.5。如果直接用X[i]预测y[i]模型学不到任何动态规律——它看到的只是“此刻的气象条件”而非“过去几小时的变化趋势”。PM2.5的累积与消散有明显滞后性实测发现用前10小时的特征预测当前PM2.5效果远优于单时刻。因此标准做法是构建滑动时间窗口。以窗口长度 $L10$ 为例输入 $X$ 的第 $i$ 行不再是单时刻向量而是拼接 $[x_{i}, x_{i1}, ..., x_{iL-1}]$维度变为 $18 \times 10 180$标签 $y$ 的第 $i$ 行对应 $y_{iL}$即用前10小时预测第11小时的值。这带来两个硬性约束数据截断原始4320小时数据经窗口化后只剩 $4320 - L 4310$ 条有效样本特征爆炸$d$ 从18跃升至180闭式解的 $(X^T X)^{-1}$ 计算量从 $18^3$ 暴涨至 $180^3 \approx 583万$此时GD的 $O(Nd)$ 优势彻底凸显——$N4310, d180$单次迭代约77.6万次运算1000次共7.76亿次虽多但稳定可控。我试过对比不做窗口$L1$时GD训练1000轮后RMSE≈48用 $L10$ 后同样参数下RMSE稳定在36–38。提升12个点全靠这个“时间记忆”设计。它提醒你机器学习不是调参游戏特征构造才是真正的建模艺术。2.3 标准化策略为何必须用Z-score而非Min-Max且要“先拆分后标准化”PM2.5数据各特征量纲天差地别温度范围[-10, 40]℃SO2浓度[0, 50]μg/m³风速[0, 15]m/s而气压高达[990, 1030]hPa。若不做处理梯度下降会陷入“瘸腿奔跑”——气压的微小变化就能引发梯度爆炸而温度变化则像毛毛雨根本推不动权重更新。这里有个致命陷阱必须在划分训练/测试集之后仅用训练集统计量均值、标准差去标准化全部数据。错误做法是先对整个数据集标准化再切分。原因很现实真实部署时你永远无法预知测试数据的全局均值。模型上线后每来一条新数据只能用训练阶段“记住”的均值$\mu_{train}$和标准差$\sigma_{train}$去做变换 $x \frac{x - \mu_{train}}{\sigma_{train}}$。如果训练时用了全局统计量就等于作弊——让模型偷偷看到了测试数据的分布信息。Z-score标准分数比Min-Max更受青睐因为Min-Max将数据压缩到[0,1]但对异常值极度敏感。PM2.5数据中偶有传感器故障导致的离群值如湿度报出150%会拉高max把正常数据全挤到[0, 0.1]区间丧失区分度Z-score以均值为锚点标准差为尺度天然具备鲁棒性。即使有离群值只要不超过3σ影响有限。实测显示用Z-score标准化后GD收敛速度提升约40%且最终损失波动更小。注意标准化必须作用于特征矩阵X而非标签y。预测目标PM2.5本身无需缩放——你最终要的是真实浓度值不是“标准化后的PM2.5”。强行标准化y会导致反变换时引入额外误差得不偿失。3. 实操细节解析从数据加载到公式落地的完整链路3.1 数据预处理如何安全处理缺失值与异常值原始PM2.5数据CSV中缺失值标记为NRNot Reported。直接丢弃含NR的整行不行。一年4320条数据若每列都有1%缺失粗暴删除可能损失超30%样本。更稳妥的做法是按列填充对每个特征用其所在列的中位数median替代NR。为什么选中位数而非均值因为PM2.5数据存在右偏分布多数时间浓度低少数沙尘暴天极高均值会被极端高值拉高导致填充值系统性偏高。中位数则稳如泰山。例如某天温度列有5个NR该列其余4315个值中位数是22.3℃那就全填22.3——既保持分布中心又避免引入偏差。异常值检测采用IQR法则四分位距计算每列Q125%分位数、Q375%分位数IQR Q3 - Q1定义异常范围$[Q1 - 1.5 \times IQR,\ Q3 1.5 \times IQR]$落在此外的值视为异常用中位数替换。对PM2.5标签列单独处理因目标值本身波动剧烈IQR阈值设为2.0倍更宽松避免误杀真实高污染事件。实测下来温度、湿度列异常值占比0.5%而PM2.5标签列达3.2%符合环境常识。代码层面用Pandas可一行搞定# 假设df是原始DataFrame for col in df.columns: if col PM2.5: # 标签列特殊处理 q1, q3 df[col].quantile(0.25), df[col].quantile(0.75) iqr q3 - q1 lower_bound, upper_bound q1 - 2.0*iqr, q3 2.0*iqr median_val df[col].median() df.loc[(df[col] lower_bound) | (df[col] upper_bound), col] median_val else: # 特征列 median_val df[col].median() df[col] df[col].replace(NR, median_val).astype(float) q1, q3 df[col].quantile(0.25), df[col].quantile(0.75) iqr q3 - q1 lower_bound, upper_bound q1 - 1.5*iqr, q3 1.5*iqr df.loc[(df[col] lower_bound) | (df[col] upper_bound), col] median_val3.2 时间窗口构建手写高效向量化实现拒绝Python循环构建 $L10$ 的滑动窗口最直觉的想法是写双重for循环# 千万别这么写效率极低 X_windowed [] y_windowed [] for i in range(len(df) - L): window_features [] for j in range(L): window_features.extend(df.iloc[ij][feature_cols].values) X_windowed.append(window_features) y_windowed.append(df.iloc[iL][PM2.5])对4320行数据内层循环执行10次外层4310次总操作量超4万次且频繁list append导致内存碎片化实测耗时12秒。正确姿势是NumPy向量化切片import numpy as np # df_numeric是清洗后的数值型DataFramefeature_cols是18个特征列名 X_raw df_numeric[feature_cols].values # shape: (4320, 18) y_raw df_numeric[PM2.5].values # shape: (4320,) L 10 N len(X_raw) - L # 窗口化后样本数 # 预分配内存避免动态增长 X_windowed np.empty((N, 18 * L)) y_windowed np.empty(N) for i in range(L): # 取第i个时间偏移的所有行从第0行到第N-1行即X_raw[0:N, :] # 拼接到X_windowed的第i*18到(i1)*18列 X_windowed[:, i*18:(i1)*18] X_raw[i:iN, :] y_windowed y_raw[L:LN] # y[iL] 对应 X_windowed[i]核心思想把时间维度“展开”为特征维度。X_raw[i:iN, :]提取从第i小时开始、连续N个样本的特征正好填满窗口的第i个“时间槽”。此方法全程使用NumPy底层C实现无Python解释器开销4320行数据窗口化仅需0.018秒提速600倍以上。3.3 公式手推与代码映射从数学符号到NumPy张量的逐行对照现在进入最硬核环节把梯度下降公式逐项翻译成可运行代码。我们以带L2正则化的GD为例HW1虽未强制但加正则能显著防过拟合强烈推荐数学推导损失函数$J(w) \frac{1}{2N} \sum_{i1}^{N}(y_i - w^T x_i)^2 \frac{\lambda}{2} |w|^2_2$对 $w$ 求导$\nabla_w J(w) -\frac{1}{N} \sum_{i1}^{N}(y_i - w^T x_i) x_i \lambda w$令梯度为0得迭代更新式$w^{(t1)} w^{(t)} - \eta \left[ -\frac{1}{N} X^T (y - X w^{(t)}) \lambda w^{(t)} \right]$其中 $\eta$ 是学习率$\lambda$ 是正则系数。代码映射关键# 初始化 w np.random.normal(0, 0.01, size(X_train.shape[1],)) # 随机初始化 eta 1e-3 # 学习率需调优 lambd 1e-5 # L2正则系数需调优 loss_history [] for epoch in range(1000): # 步骤1计算当前预测值 y_pred X w y_pred X_train w # 步骤2计算残差 error y - y_pred error y_train - y_pred # 步骤3计算梯度主体 -1/N * X.T error grad_main -1/X_train.shape[0] * X_train.T error # 步骤4加上正则项梯度 lambd * w grad_reg lambd * w # 步骤5合成总梯度 grad_total grad_main grad_reg # 步骤6更新权重 w w - eta * grad_total w w - eta * grad_total # 步骤7计算当前损失用于监控 mse np.mean(error ** 2) loss 0.5 * mse 0.5 * lambd * np.sum(w ** 2) loss_history.append(loss)每一行代码都严格对应一个数学步骤。特别注意X_train.T error这是矩阵形式的 $\sum_{i1}^{N}(y_i - w^T x_i) x_i$NumPy的运算符完美承载了求和与向量内积的双重语义。这种“数学即代码”的映射正是手推的价值所在——你不再把X.T error当黑盒而清楚知道它在物理上代表“所有样本的残差按各自特征向量加权求和”。3.4 学习率与正则系数调优网格搜索背后的直觉经验学习率 $\eta$ 和正则系数 $\lambda$ 是GD的两大命门。调参不是玄学有清晰的物理意义学习率 $\eta$决定每步跨多大。太大如1e-1权重在最优解附近疯狂震荡损失曲线锯齿状上升太小如1e-6收敛慢如蜗牛1000轮后还在平原徘徊。PM2.5数据的特征尺度经Z-score后均在[-3,3]经验法则是 $\eta$ 设为 $10^{-3}$ 到 $10^{-2}$。我实测$\eta5\times10^{-3}$ 时损失在300轮内快速下降600轮后趋稳$\eta10^{-2}$ 则在200轮后开始震荡RMSE反而升高0.8。正则系数 $\lambda$控制模型复杂度。$\lambda0$ 即无正则易过拟合训练RMSE28测试RMSE45$\lambda$ 过大如1e-2模型欠拟合训练/测试RMSE均42。最佳平衡点在 $10^{-5}$ 量级。有趣的是$\lambda$ 与特征维度 $d$ 强相关$d$ 从18升到180$\lambda$ 应同比例放大因 $|w|^2$ 项随维度增加而自然变大。所以窗口化后$\lambda$ 从1e-5调至5e-5效果最佳。网格搜索代码简洁有力etas [1e-4, 5e-4, 1e-3, 5e-3, 1e-2] lambdas [1e-6, 1e-5, 5e-5, 1e-4] best_score float(inf) best_params {} for eta in etas: for lambd in lambdas: w_trained train_gd(X_train, y_train, eta, lambd, epochs1000) pred_test X_test w_trained rmse np.sqrt(np.mean((y_test - pred_test) ** 2)) if rmse best_score: best_score rmse best_params {eta: eta, lambd: lambd}实测最优组合$\eta5\times10^{-3}, \lambda5\times10^{-5}$测试RMSE35.2较基线提升10%以上。4. 实操过程与核心环节实现从零搭建可复现的训练流水线4.1 完整代码框架模块化封装拒绝脚本式杂乱一个健壮的手推项目绝不能是几十行堆砌的脚本。我将其拆为四个核心模块每个模块职责单一便于调试与复用data_loader.py专注数据IO与清洗输出标准化后的(X_train, X_test, y_train, y_test)feature_engineer.py封装窗口化、特征交叉、多项式生成等输入原始特征输出高维特征矩阵optimizer.py实现GD、SGD、Adam等优化器统一接口train(X, y, config)evaluator.py提供RMSE、MAE、R²等评估函数及可视化工具。以optimizer.py为例其GD实现支持早停与学习率衰减def train_gd(X, y, config): config: dict, keys: eta, lambd, epochs, tol, patience w np.random.normal(0, 0.01, size(X.shape[1],)) eta, lambd config[eta], config[lambd] N X.shape[0] loss_history [] best_loss float(inf) patience_counter 0 for epoch in range(config[epochs]): y_pred X w error y - y_pred grad -1/N * X.T error lambd * w w w - eta * grad # 计算当前损失 mse np.mean(error ** 2) loss 0.5 * mse 0.5 * lambd * np.sum(w ** 2) loss_history.append(loss) # 早停机制若连续patience轮损失未改善则停止 if loss best_loss - config[tol]: best_loss loss patience_counter 0 else: patience_counter 1 if patience_counter config[patience]: print(fEarly stopping at epoch {epoch}) break # 学习率衰减每200轮衰减10% if (epoch 1) % 200 0: eta * 0.9 return w, loss_history这种设计让你能轻松切换优化器换一行train_sgd()即可或调整早停参数而无需动核心逻辑。4.2 关键参数配置表一份可直接抄作业的调参清单基于我在5台不同配置机器i5-8250U / Ryzen 7 5800H / M1 Pro上的127次实测整理出PM2.5预测任务的黄金参数组合。所有参数均针对 $L10$ 窗口、Z-score标准化后的数据参数类别参数名推荐值物理意义调参提示优化器learning_rate (η)5e-3每步更新幅度若损失初期下降慢可增至1e-2若震荡降至1e-3regularization (λ)5e-5权重惩罚强度特征维度翻倍时λ需同步翻倍如d180时用1e-4epochs1000最大迭代轮数早停通常在600–800轮触发无需硬设数据window_length (L)10使用前L小时特征L5时捕捉不到趋势L15时边际收益递减且增加过拟合风险train_ratio0.8训练集占比PM2.5数据量充足0.8/0.2划分足够稳健正则化early_stopping_patience50损失不降即停轮数防止过拟合实测节省30%训练时间lr_decay_factor0.9每200轮学习率衰减比例后期微调更精细提升最终精度0.3–0.5 RMSE注意train_ratio0.8并非固定。我曾尝试0.9/0.1测试RMSE波动增大标准差从1.2升至2.8说明小测试集方差过大0.8是精度与稳定性的最佳折中。4.3 训练过程可视化用三张图看懂模型是否健康训练不是黑箱三张图足以诊断一切损失曲线图Loss vs Epoch横轴迭代轮数纵轴训练损失。健康曲线应快速下降前100轮随后平缓收敛600轮后斜率趋近0。若出现“之”字形震荡说明学习率过大若长期水平延伸说明学习率过小或陷入局部极小。权重分布直方图Weight Distribution训练结束后绘制w向量的直方图。理想状态是近似正态分布集中在[-0.5, 0.5]。若大量权重绝对值2说明正则不足或学习率过大若全部权重趋近0说明正则过强或学习率过小。预测vs真实散点图Prediction vs Ground Truth横轴真实PM2.5纵轴预测值。健康模型应呈现紧密的45度直线带离散点均匀分布在直线两侧。若出现“喇叭形”低值区密集、高值区发散说明模型对高污染事件预测能力弱需加强高值样本权重或改进特征。我用Matplotlib生成这三张图的代码模板如下可直接嵌入训练循环末尾import matplotlib.pyplot as plt # 图1损失曲线 plt.figure(figsize(12, 4)) plt.subplot(1, 3, 1) plt.plot(loss_history) plt.title(Training Loss Curve) plt.xlabel(Epoch) plt.ylabel(Loss) # 图2权重分布 plt.subplot(1, 3, 2) plt.hist(w, bins50, alpha0.7) plt.title(Weight Distribution) plt.xlabel(Weight Value) plt.ylabel(Frequency) # 图3预测vs真实 plt.subplot(1, 3, 3) pred_test X_test w plt.scatter(y_test, pred_test, alpha0.6, s1) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--, lw2) plt.title(Prediction vs Ground Truth) plt.xlabel(True PM2.5) plt.ylabel(Predicted PM2.5) plt.tight_layout() plt.show()这三张图就是你的“模型心电图”比任何数字指标都直观。4.4 结果分析与归因为什么某些特征权重接近零训练完成后w向量长180维10小时×18特征。观察各维度权重绝对值会发现一个有趣现象“前1小时”的温度、湿度权重较大|w|0.15而“第10小时”的相同特征权重很小|w|0.02。这完全符合环境科学常识——PM2.5浓度对即时气象条件更敏感历史条件影响随时间衰减。更值得深挖的是那些权重趋近零的特征比如“第5小时的SO2”。这并非模型失效而是数据告诉你的真相在该数据集中SO2浓度与PM2.5的相关性极弱皮尔逊系数仅0.12模型自动将其“忽略”。这比强行保留所有特征更科学。你可以用以下代码快速定位关键特征# 获取特征名列表按窗口顺序hour0_temp, hour0_humid, ..., hour9_temp... feature_names [] for h in range(10): # 10小时 for f in feature_cols: # 18个原始特征 feature_names.append(fhour{h}_{f}) # 按权重绝对值排序取Top 10 import pandas as pd feat_importance pd.DataFrame({ feature: feature_names, weight: w, abs_weight: np.abs(w) }).sort_values(abs_weight, ascendingFalse).head(10) print(feat_importance[[feature, weight]])实测Top 3通常是hour0_TEMP,hour0_PM10,hour1_WIND_SPEED。这验证了模型学到的物理规律当前温度、同源颗粒物PM10、以及稍早的风速影响扩散是预测PM2.5的三大支柱。5. 常见问题与排查技巧实录那些只有踩过坑才知道的事5.1 问题速查表高频Bug与一招解决法问题现象根本原因快速诊断法一招解决法训练损失不下降甚至上升学习率 $\eta$ 过大梯度更新越过最优解绘制损失曲线若呈锯齿状剧烈震荡将 $\eta$ 降低10倍如从1e-2→1e-3测试RMSE远高于训练RMSE10点过拟合正则不足或窗口过长比较训练/测试损失曲线若测试损失持续上升而训练下降增大 $\lambda$如×10或缩短窗口 $L$如10→5权重 $w$ 全为nan或inf数据含未处理的inf或除零错误np.isnan(w).any()或np.isinf(w).any()检查数据清洗确保无log(0)、1/0等操作用np.nan_to_num(X, nan0.0)兜底预测值全部趋近同一常数学习率过小或特征未标准化np.std(pred_test)极小0.1立即检查Z-score标准化步骤确认X_train和X_test使用同一组mu, sigma内存ErrorOOM窗口化后特征维度爆炸X矩阵过大X_windowed.nbytes显示GB级内存占用改用float32存储X_windowed X_windowed.astype(np.float32)内存减半5.2 独家避坑技巧教科书不会写的实战经验技巧1用“单位测试”验证梯度计算GD的核心是梯度但手写grad -1/N * X.T (y - X w)容易出错。最可靠的方法是数值梯度检验对每个权重 $w_j$微扰 $\epsilon1e-5$计算 $(J(w\epsilon e_j) - J(w-\epsilon e_j)) / (2\epsilon)$与解析梯度对比。若相对误差 $1e-4$则梯度正确。我写了个一键检验函数def gradient_check(X, y, w, lambd, eps1e-5): grad_analytic -1/X.shape[0] * X.T (y - X w) lambd * w grad_numeric np.zeros_like(w) for i in range(len(w)): w_plus w.copy(); w_plus[i] eps w_minus w.copy(); w_minus[i] - eps loss_plus 0.5 * np.mean((y - X w_plus)**2) 0.5 * lambd * np.sum(w_plus**2) loss_minus 0.5 * np.mean((y - X w_minus)**2) 0.5 * lambd * np.sum(w_minus**2) grad_numeric[i] (loss_plus - loss_minus) / (2 * eps) diff np.linalg.norm(grad_analytic - grad_numeric) / np.linalg.norm(grad_analytic grad_numeric) print(fGradient check diff: {diff:.2e}) return diff 1e-4每次改完梯度代码必跑此函数。它曾帮我揪出3次索引越界和1次正则项漏乘的bug。技巧2学习率热身Learning Rate WarmupGD初期权重随机损失曲面崎岖。若一开始就用大$\eta$极易冲出盆地。我的做法是前50轮$\eta$ 从0线性增至目标值。代码只需两行if epoch 50: eta_current config[eta] * epoch / 50 else: eta_current config[eta] w w - eta_current * grad_total实测使收敛轮数减少15%且最终RMSE稳定提升0.4。技巧3特征重要性可视化进阶——SHAP值单纯看权重 $w_j$ 有局限它假设特征间独立而实际中温度与湿度高度相关。更科学的是用SHAPSHapley Additive exPlanations计算每个特征对单个预测的贡献。安装shap库后import shap explainer shap.Explainer(lambda x: X_test w, X_train[:1
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑