
1. 线性回归在解决什么问题一张表、一条线、一个预测1.1 回归问题的本质与场景凡是预测连续数值的问题基本都可以归到回归问题这一类。比如预测明天的气温、预测二手车的挂牌价、预测电商一波促销带来的订单量、预测坐车去机场的用时底子都是同一个套路拿一堆影响因素作为特征去寻找它们和目标值之间的数量关系。线性回归是这套套路里最朴素、也最常用的一个模型。它假设目标值和特征之间能用一条直线二维空间或者一个超平面高维空间来描述。这个假设当然不是每条数据都满足但它有两个非常大的好处第一模型足够简单计算量小出活快第二参数有明确含义能解释成“某个特征变化一单位目标平均变化多少”。也正是因为这两点不管后面你学的是决策树、随机森林还是神经网络线性回归都值得作为第一个被认真吃透的模型。它虽然不是最花哨的算法却是建立“数据、特征、目标、误差”这个基本思考框架的起点。1.2 模型形式与截距的“小把戏”用数学写出来就是[ y w_1 x_1 w_2 x_2 \cdots w_d x_d b ]其中 (b) 是截距也就是当所有特征都是 0 时的基准预测值。你可以把 (b) 看成另一个可训练参数 (w_0)对应一列永远等于 1 的特征这样模型就能写成一个更紧凑的矩阵形式[ \hat{y} Xw ](X) 是 (n) 行 (d1) 列的矩阵左侧第一列是 1后面是各个特征(w) 是 (d1) 维向量。为什么要这么写因为一旦转成矩阵形式参数求解就能用线性代数工具一步到位。后面讲正规方程时你会看到这个写法的威力。我第一次学的时候没想通这一步单纯觉得是为了装酷后来才发现几乎所有机器学习推导都是建立在矩阵基础上的绕开它反而吃亏。1.3 “回归”这个名称的来历以及与分类的区分很多时候你会听到“回归”和“分类”并列出现初学者容易搞混。一个直观的区分方法连续的是回归离散的是分类。预测房价是回归识别图片里是猫还是狗是分类。预测明天降水概率其实是回归因为它输出 0 到 1 之间的连续数值把概率再划成“下”或“不下”才变成分类。至于“回归”这个词本身它来自早期统计学。有人发现高个子父母的后代身高平均不减但很少像父母那样高得极端矮个子父母的后代也类似会向人群均值靠拢。这种现象被称为“均值回归”。后来这个词被延伸为用一组变量去估计另一组变量的建模过程和最早那个具体现象其实已经没有太大关系了。知道了这段来由你再听人说“线性回归”时就不会把它当成一个玄学名词而是可以自然理解成我要找一条线让它在预测某个连续值这件事上尽量靠谱。2. 最小二乘的前因后果为什么是平方误差以及怎么求参数2.1 用平方误差的合理性定义一个模型好不好必须先定义误差。最简单的是直接误差真实值减预测值。但正负误差会互相抵消所以不能用平均水平来代表“总误差”。于是自然会想到误差的绝对值或者误差的平方。为什么不选绝对值绝对值函数在 0 点不可导损失函数带绝对值导不流畅数学优化上很别扭。更关键的是平方误差的导数与误差大小成正比意味着模型对预测得离谱的样本会施加更大的惩罚这样训练时更不敢轻易放飞自我。这里还有一个统计层面的解释如果假设误差服从均值为 0 的正态分布那么在训练样本上做最大似然估计最终目标函数会变成最小化误差平方和。也就是说选择平方误差并不只是一个数学技巧它背后隐含着“误差大致呈正态分布”这个假设。一旦这个假设崩溃平方误差的表现就会变差后面我们会用残差图来检查这一点。2.2 正规方程从求导到闭式解在矩阵形式下损失函数是[ L(w) |y - Xw|^2 ]展开后对 (w) 求梯度并令梯度为 0就能得到[ \hat{w} (X^T X)^{-1}X^T y ]这个结果就是正规方程。理解它只需要三步先展开损失函数再对 (w) 求导最后化简移项。我见过很多教程直接甩公式导致初学者背得很痛苦。实际上如果你自己手动推一遍就会发现它和一元二次函数找最小值的方法一模一样。但这个公式有个使用前提(X^T X) 必须可逆。什么时候不可逆最典型的情况是有两个特征完全线性相关比如“面积”和“面积100”这时候矩阵秩不够方程组无唯一解。另外一种情况是样本数比特征数还少。遇到这种情况正统做法是换成 SVD 分解求解或者加一个很小的惩罚项——这个惩罚项其实就是后面要讲的岭回归。正规方程在数据量小的时候非常香一次矩阵运算即可得到精确解。但数据量一大计算 (X^T X) 的复杂度是 (O(nd^2))求逆还要再花 (O(d^3))如果 (d) 有几千甚至几万等它算完你都能在旁边喝两杯咖啡了。所以在大规模场景里我们更常听到的是梯度下降。2.3 梯度下降另一种迭代路线梯度下降的基本想法是从一个初始参数出发沿着损失下降最快的方向反复更新。更新式是[ w_{\text{new}} w_{\text{old}} - \text{lr} \cdot \text{gradient} ]这里的 gradient 是损失函数对参数的梯度lr 是学习率。学习率太大参数会在最优值附近震荡太小则半天走不动。在理想情况下梯度等于 0 时训练就收敛了。在实践中还有三个变体批量梯度下降用全量数据算梯度最稳定但慢随机梯度下降每次只用一条样本快但有噪音小批量梯度下降一次用一小批样本是现实中的折中。线性回归那边的梯度刚好有闭式表达[ \text{gradient} X^T(y - Xw) ]写起来比神经网络简单很多非常适合用来理解迭代优化的框架。我第一次写梯度下降时还犯了个低级错误忘记把梯度除以样本数。结果训练曲线看起来非常合理但系数怎么都不对后来才发现是学习率被样本数放大了。这种经验不在书里写只有自己动手踩过才有记忆。3. 从零实现到调包对比一次完整的线性回归试做3.1 构造一份可以复现的模拟数据为了不让理论悬在空里我先用 numpy 造了一份数据让每个读者都能在自己电脑上复现零依赖跑通。设置随机种子、300 个样本、两个特征真实参数是 (w[2.0, -0.5])截距 (b3.0)并加上标准差 0.5 的噪声import numpy as np from sklearn.model_selection import train_test_split np.random.seed(42) X np.random.randn(300, 2) true_w np.array([2.0, -0.5]) true_b 3.0 y X true_w true_b 0.5 * np.random.randn(300) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state0 )这样生成的 (y) 和 (X) 之间有一个确定的线性关系你心里知道真值正好用来检验不同算法能不能找回这个真值。3.2 用 numpy 手写最小二乘和梯度下降先写一个正规方程类代码很短class NormalEquation: def fit(self, X, y): Xb np.hstack([np.ones((X.shape[0], 1)), X]) self.w np.linalg.inv(Xb.T Xb) Xb.T y def predict(self, X): Xb np.hstack([np.ones((X.shape[0], 1)), X]) return Xb self.w再看一个纯朴的梯度下降class GDRegressor: def __init__(self, lr0.1, epochs100): self.lr lr self.epochs epochs def fit(self, X, y): Xb np.hstack([np.ones((X.shape[0], 1)), X]) n, d Xb.shape self.w np.zeros(d) for _ in range(self.epochs): grad (Xb.T (Xb self.w - y)) / n self.w - self.lr * grad def predict(self, X): Xb np.hstack([np.ones((X.shape[0], 1)), X]) return Xb self.w注意梯度里我除以了 (n)这样才能保证学习率不受样本量影响。然后用一份测试集计算 (R^2)定义很直观def r2_score(y_true, y_pred): ss_res np.sum((y_true - y_pred) ** 2) ss_tot np.sum((y_true - np.mean(y_true)) ** 2) return 1 - ss_res / ss_tot在这份数据上正规方程跑出来的系数大概是截距 3.01、(w_12.02)、(w_2-0.49)测试 (R^2) 约 0.94梯度下降达到相近值时主要受学习率和收敛轮数影响。两者都在找同一个最小二乘解得到相似结果其实符合预期。3.3 和成熟库的结果对比把同一个数据集丢给 sklearn 的线性回归系数和 (R^2) 几乎完全一致。不过成熟库内部不是硬套正规方程而是用了 SVD 分解。SVD 的好处是哪怕 (X^T X) 不可逆也不会直接报错而是给出一个最小二乘意义下的解数值上更稳定。我的建议是如果你在做项目直接调库就行省时省力但如果你在学原理手写一遍正规方程和梯度下降是不可替代的。跑通这两段代码的收获可能比刷十道练习题更有用。3.4 特征归一化对两种求解方式完全不同的影响这里有个很多书里没讲清楚的点正规方程对特征尺度不敏感。因为它是闭式解算的就是最优参数无论特征是米还是毫米模型都能从数学上给出相应的系数。但梯度下降对尺度非常敏感。想象一个二维损失函数(x_1) 范围在 0 到 1 之间(x_2) 范围在 0 到 10000 之间损失函数的等高线会被压成细长的椭圆。梯度在这些椭圆之间来回横跳就像在走 Z 字形归一化后等高线接近圆形每一步都能直接指向圆心。因此使用梯度下降前最好把每个特征标准化为均值 0、方差 1。代码上就是先做 StandardScaler注意测试集的 scaler 要在训练集上 fit 后再 transform防止信息泄漏。很多人一开始在整体数据上 fit scaler后面模型评估虚高这是特别容易踩的坑。4. 模型诊断比训练更重要R²、残差图与特征变换4.1 R²与调整R²怎么读(R^2) 的定义是 1 减残差平方和除以总平方和可以理解为模型解释掉的方差比例。(R^2) 等于 0.9 代表 90% 的波动被模型解释了。但必须记住(R^2) 是一个相对指标不是绝对指标。对同一份数据(R^2) 越高通常模型越好对不同数据(R^2) 没法直接比较因为数据的噪声水平完全不同。还有一个魔鬼细节往模型里加任何特征(R^2) 都不可能下降它只会持平或上升因为最小二乘总能找到至少不更差的解。这就催生了调整 (R^2)它把特征数量作为惩罚因子每多一个特征都会抵消一部分 (R^2) 收益。所以在你评估“加这个特征到底有没有用”时看调整 (R^2) 比看 (R^2) 更可信。我在项目里更常使用的指标是 RMSE因为它和原始目标量纲一致可以直接告诉业务方“平均预测偏差是多少”。(R^2) 展示解释力RMSE 展示误差大小两个配合起来用才完整。4.2 残差图的三把尺子(R^2) 只是“一口价”残差图才是真正能看清模型毛病的工具。残差等于真实值减预测值。画散点图纵轴是残差横轴是预测值理想情况是所有点随机散布在 0 线上下像一个均匀的云团。如果看到喇叭形也就是预测值越大、残差离散程度越大这是异方差。解决办法可以先对 (y) 取 log把大值范围的差距压缩一下。如果看到残差呈现出明显的弯曲比如先正后负再正说明模型缺了非线性结构常见对策是加平方项、交互项或者换更灵活的模型。如果看到残差点几乎都在 0 线上方或下方那是模型存在系统性偏差先检查是否有截距再检查数据刻度单位是否弄错了。有一次我在某次练习中残差图总是一条斜线折腾半天发现“目标值”那一列包含了拼接错误部分行把面积当成房价写进去了。图这种东西你盯久了真的能看出数据本身的怪味。4.3 特征变换与多项式特征的一次补救线性回归的“线性”指的是参数线性不是特征线性。你可以放心地往特征列表里塞平方项、开方项、对数项只要参数还是线性的它依然是广义的线性回归。我做了个实验构造一份明显含交互效应的数据 (y 1.2x_1 - 0.5x_2 0.8x_1x_2 \text{noise})。先用纯线性模型拟合残差图出现对称的弯曲怎么看都不顺眼加上 (x_1x_2) 这一列再拟合弯曲消失(R^2) 直接从 0.6 左右跳到了 0.9 以上。这个对比非常直观地说明了特征工程在模型边界内的威力。在实际项目中如果你业务上能判断出两个变量会共同影响目标比如广告投入和渠道评级会互相放大效果就应该主动生成交互特征而不是等模型自己发现——线性模型本来就没这个能力。5. 一份模拟房价数据的完整试做案例5.1 数据探索与清洗这次我构造了一份 2000 条左右的模拟二手房数据含五个特征面积、卧室数、房龄、周边评分、是否精装。目标价格由这些特征线性组合加噪声生成并且我故意在其中埋了几个脏值面积等于 0 的记录、房龄为负的记录、少数价格异常大的离群点。第一步一定是看数据摘要describe、isnull、duplicated。看到面积最小值为 0 时不要直接全部删除先想想 0 是缺测还是录入错误用中位数填充还是剔除。我在这里选择把面积小于 5 的样本删掉因为“房龄负数”更像年份字段被写错用该列中位数替换更合理。这类清洗动作的每一步都要记录下来写清楚为什么删、为什么填。项目评审时别人问你“为什么样本数从 2000 变到 1973”你能给出可解释的答案这会让整个模型的可信度上一个台阶。5.2 特征编码与建立模型“是否精装”是 0/1 标签直接映射。像“朝向”这种多分类变量则需要独热编码。独热编码很容易制造多个相关列必须设置 drop_first 删除其中一个否则会引入共线性。关于这一点不做的时候只是知道做了之后看到系数矩阵变成奇异才真正理解“虚拟变量陷阱”。然后照例划分训练集和测试集比例 8:2。StandardScaler 要先用训练集 fit再到测试集 transform注意千万别在切分前对整个数据做归一化。我用 LinearRegression 拟合后测试集上的 (R^2) 大约是 0.71。对一个带不少噪声的模拟数据来说这个表现说明特征选择的方向是对的。5.3 系数解释与验证得到的系数中面积系数约 2.13房龄系数约 -0.42。在原始尺度下解释时需要保证口径一致。假设面积单位为平方米价格单位为万元那结论就是其他条件不变时面积每增加一平方米价格平均增加 2.13 万元房龄每增加一年价格平均减少 0.42 万元。这种口头解释很容易被业务方接受也是线性回归的主要卖点。不过要真诚地加一句警示当特征相关时单个系数的符号和大小可能不稳。面积和卧室数高度相关模型中卧室数系数甚至有可能变成负的这不是说卧室多会让房子贬值而是“面积”把相关信息拿走以后剩余的边际信息本来就有限很容易被噪声淹没。5.4 试做过程中的几个实际槽点我在这次试做里至少踩了三个坑值得写出来。第一价格单位混用输入数据里有的行是“万元”有的行是“元”我不仅没统一还直接跑了模型导致系数和残差图全都乱了套。第二测试集归一化泄漏在全体数据上先做 StandardScaler 再切分相当于模型“提前偷看”了测试集的统计量预测效果虚高。正确顺序一定是先 split 再 fit scaler。第三训练时忘填截距如果完全用标准化的特征训练模型会强制过原点很多场景下截距其实是残差最后的兜底项。建议永远让模型学截距或把截距列加进去然后观察截距是否显著非零从而判断是否需要调整特征配方。这些坑都不高级但每一条都真实存在。把过程记录成笔记价值不亚于模型本身。6. 线性回归的边界预警多重共线性、离群点与什么时候换模型6.1 多重共线性为什么会让系数失控如果两个特征强相关比如面积和卧室数(X^T X) 会变得接近奇异求逆时会放大微小数值波动最终系数的方差变得巨大。换句话说模型可以“记得”训练集但换一批数据系数可能完全变样。检测多重共线性常用方差膨胀因子 VIF一般认为 VIF 大于 10 就有问题。处理办法有几种直接删除冗余特征、用 PCA 把相关特征压缩成不相关的成分、或者使用岭回归。我建议先从业务角度删特征因为 PCA 之后模型可解释性会下降如果没有业务约束再用正则化兜底。6.2 离群点与稳健回归线性回归用平方误差所以对离群点异常敏感。一个典型的极端案例有一组大致沿对角线分布的点只要横坐标较大的位置冒出一个偏离很远的点拟合线就会被硬拽过去。这就是为什么先要画散点图或箱线图看数据。如果离群点数量不多且确认是录入错误可以删除但如果离群点来自真实分布比如一些极端高价房屋简单删除会丢失信息更好的思路是使用 Huber 回归这样对离群值不敏感的稳健方法。它的损失函数在误差较小时是平方超过阈值后变成线性既保留效率又限制离群点的影响。6.3 正则化第一次救火前面提到的岭回归就是在最小二乘目标函数里加一个 L2 惩罚项 (\lambda|w|^2)让参数不能太大。这能有效对抗多重共线性带来的系数方差膨胀。更进一步的 Lasso 使用 L1 惩罚 (\lambda|w|_1)它会让部分系数被压缩到 0等于在训练过程中自动做特征选择。正则化系数的选取通常交给交叉验证完成。道理并不复杂(\lambda) 越大惩罚越强模型越偏向简单(\lambda) 太小惩罚只是挠痒。试着画出 (R^2) 随 (\lambda) 的变化曲线你会清楚看到过拟合到欠拟合的过渡过程。6.4 基线模型思维与后续扩展我个人工作习惯是接到任何数值预测任务第一步永远是跑一个最朴素的线性回归 baseline记录 (R^2) 和 RMSE。不是因为我相信线性关系而是我想知道数据里到底有多少可学习的信号。如果 baseline 的 (R^2) 已经很高复杂模型提升空间有限不值得增加复杂度如果 baseline 很差也该先看看是特征工程有问题还是数据本身噪声太大而不是盲目上一棵巨树。后续扩展也有清晰路径特征不够用加多项式特征误差分布不合适换广义线性模型非线性太强再看支持向量回归、随机森林或梯度提升树。但不管走到哪一步线性回归这块地基打得牢不牢决定了你能不能在需要时回头解释“模型为什么这么预测”。这次把线性回归从里到外试了一遍我最大的收获不是会调参而是终于理解了一个朴素的公式背后有那么多前提和细节。如果你也想把基础知识吃透建议从手写代码和残差图开始少看几个指标多看几张图。