前两天有个朋友把“linear代码线性回归”这个搜索词丢给我说想学机器学习线性回归结果搜出来的东西杂得离谱——有讲linear decoders的有讲hex start linear address record这类十六进制记录格式的还有直接甩三行sklearn代码让你跑的。我太理解这种感受了理论公式看不懂代码跑通了又不知道它到底在算什么。这篇我打算按自己带项目时的思路走一遍先用手写numpy把线性回归算法跑通再切到sklearn和statsmodels做正规建模然后补上特征工程和正则化最后聊聊模型上线后最容易踩的四个坑。适合已经会写Python但没系统做过机器学习线性回归实验的人也适合那些只会调用LinearRegression.fit却解释不了系数含义的人。1. 线性回归到底在拟合什么从直觉到矩阵表达1.1 一条直线能解释多少业务问题线性回归的直觉非常朴素找一条直线让所有样本点到它的垂直距离总和尽量小。比如预估房租把面积作为特征x房租作为目标y我们假设每平方米单价基本稳定那模型就是 y w * x b。这里的w就是面积每增加一平米租金平均涨多少b可以理解为固定费用的底价。这个例子听起来过于简单但很多复杂业务的起点都长这样。我之前做过一次门店销量预测第一版模型就是用“客流量 平均客单价 当天是否为周末”这三个特征做线性回归效果已经能超过店长的拍脑袋估算。业务方要的不是炫技而是一个能说清楚解释逻辑的模型。线性回归的优势就在这里系数可以直接反推业务含义。需要注意“线性”不代表数据必须真的在一条直线上而是说特征和目标之间的关系可以用加权求和表达。真实世界里完全线性的关系很少但在局部范围内线性近似往往够用。这也是为什么像语音识别、推荐系统里的深度学习模型第一层很多也是线性变换——神经网络本质上就是一堆线性回归结构叠加非线性激活函数。1.2 误差最小化的数学选择为什么偏偏是MSE既然是找直线就要定义什么叫“拟合得好”。最常用的做法是最小化均方误差MSE也就是把所有样本的误差平方加起来再取平均。公式写成loss (1 / n) * Σ (w * x_i b - y_i)²为什么用平方而不是直接用绝对误差原因有三个。第一平方对大误差的惩罚更重模型会被迫更关注那些偏离很远的点第二平方项是光滑可导的后面做梯度下降非常方便第三在误差满足独立同分布的正态分布假设下最小化这个平方误差等价于最大似然估计有统计学上的理论依托。有些人会问那用MAE平均绝对误差不行吗也可以用MAE对异常值的敏感度更低但它在零点不可导优化起来要绕弯子。实际业务里如果异常值本身是噪声用MAE会更稳如果异常值也是真实业务信号用MSE反而能逼着模型去解释它。这个选择没有标准答案但默认从MSE入门一定不会错。把loss分别对w和b求偏导可以得到两种参数更新方式。一种是梯度下降每轮迭代都沿着梯度的反方向调整参数另一种是令偏导等于0直接解出参数。下一章我会把这两种方式都写一遍代码直观感受它们的差别。1.3 一口气看懂矩阵形式 w (X^T X)^{-1} X^T y如果把所有样本堆叠成矩阵X每行是一个样本每列是一个特征线性回归的损失函数就能写成向量形式。对向量形式的loss求导并令其等于0就能得到传说中的正规方程w (X^T X)^{-1} X^T y不理解这个公式的人会死记硬背理解的人会从几何角度把它看穿。X的每一列是一个特征向量这些特征向量张成一个空间Xw就是对这个空间里的所有点做线性组合。我们要找的组合是让Xw这个预测向量离y这个目标向量欧氏距离最近也就是把y投影到特征张成的空间上。投影点就是Xw投影系数就是w。正规方程的本质是一次正交投影。这个公式能成立有个前提X^T X必须可逆。翻译成大白话就是特征之间不能存在完全共线性一个特征是另一个特征的线性组合样本个数要大于特征个数。如果X^T X接近奇异直接求逆数值上会非常不稳定这种时候用np.linalg.lstsq会比手动算逆矩阵稳妥得多。后面踩坑章节里会具体讲多重共线性是怎么让系数符号变得反直觉的。2. 手写一个线性回归numpy版的完整训练循环2.1 造一份带噪声的数据把最小流程跑通学回归最忌讳直接拿真实数据开干因为你不知道正确的参数长什么样出了问题也很难判断是自己写错了还是数据本身有噪声。我建议先造一份完全可控的数据把整个训练周期跑通再换真实数据。import numpy as np rng np.random.default_rng(42) X np.linspace(0, 10, 100).reshape(-1, 1) # 100个样本1个特征 true_w, true_b, noise_scale 3.0, 2.0, 1.5 y true_w * X.ravel() true_b rng.normal(0, noise_scale, size100)这里的X从0均匀取到10真实斜率是3截距是2再加上标准差为1.5的高斯噪声。先用散点图看一眼数据分布再开始建模。注意我用了rng np.random.default_rng(42)而不是老式的np.random.seed这是numpy现在推荐的做法每次生成的数据可复现也避免全局随机种子污染其他模块。2.2 梯度下降核心循环参数更新与收敛观察下面这段是我最喜欢用来给新人演示的完整训练循环全部代码只有十几行n len(X) w, b 0.0, 0.0 lr 0.01 epochs 1000 losses [] for epoch in range(epochs): y_pred w * X.ravel() b loss np.mean((y_pred - y) ** 2) losses.append(loss) grad_w 2 / n * np.sum(X.ravel() * (y_pred - y)) grad_b 2 / n * np.sum(y_pred - y) w - lr * grad_w b - lr * grad_b if epoch % 100 0: print(fepoch {epoch}, loss {loss:.4f}, w {w:.3f}, b {b:.3f})跑完之后w会收敛到3.0附近b收敛到2.0附近loss从初始的几十一路降到2左右。注意loss最后不会等于0因为有噪声存在这是正常现象不要为了追求loss为0而做多余的事。学习率lr是整个过程中最敏感的旋钮。lr设得太小比如0.0001跑1000轮还没走到最低点lr设得太大比如0.5loss会在某轮突然变成几千参数直接发散。我自己的经验是先决定用批量梯度下降、随机梯度下降还是小批量。上面的代码每次更新用全量数据算梯度是小数据集最稳的做法数据量大了之后每轮迭代只随机抽一批样本计算梯度能大幅加快收敛速度但梯度的噪声会大一些。实际项目中线性回归很少会真的手写训练循环但这个循环给你一种“参数到底怎么被更新”的手感后续调任何模型都有帮助。2.3 闭式解与梯度下降怎么选正规方程可以一行代码求解X_design np.hstack([X, np.ones((len(X), 1))]) w_opt, residuals, rank, s np.linalg.lstsq(X_design, y, rcondNone) print(w_opt) # 输出类似 [3.02, 1.91]斜率接近3截距接近2np.linalg.lstsq实际上是用SVDSVD分解来求解最小二乘问题比直接算np.linalg.inv(X.T X) X.T y数值上更稳定即使X^T X接近奇异也能给你一个合理的结果。我强烈建议以后凡是要求线性回归系数优先用lstsq而不是手写求逆。两者怎么选我在实际工作中按这个原则来场景推荐方式原因样本量小、特征数少比如几千行、几十个特征闭式解一次性精确求解无收敛问题样本量百万级、特征维度高梯度下降/小批量梯度下降闭式解要构造X^T X内存和时间都扛不住X^T X接近奇异闭式解用lstsqSVD数值稳定性更好后续要加L1/L2正则化梯度下降或专用求解器带约束的最优化问题不适合直接求逆还有一个坑闭式解的复杂度大约O(n*d²)当特征维度d上万时光算一次矩阵乘法就可能内存爆炸更不要说求逆了。所以“特征多”和“样本多”这两个词在实际建模中会把你推向完全不同的技术路线。2.4 评估模型R²、RMSE、MAE别搞混训练完成之后第一个感觉是loss变低了但这还不够需要一套能骗过自己的评估指标。我常用的三个是from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error # 假装已经用训练好的w和b做了预测 y_pred w * X.ravel() b r2 r2_score(y, y_pred) rmse np.sqrt(mean_squared_error(y, y_pred)) mae mean_absolute_error(y, y_pred)R² 的含义是模型解释了目标变量多少比例的方差取值最大为1但可以变成负的。如果R²是0.94可以粗粗理解为“94%的波动能被特征解释掉”。RMSE和MAE都是误差量纲RMSE因为平方的存在对大误差更敏感所以RMSE一般会大于MAE如果两者差距很大说明数据里有少数样本误差特别大要小心异常值。最关键的评估纪律是必须在测试集上算指标不能用训练集。很多人训练完顺手在训练集上打印R²看到0.98就兴奋上线之后被打回原形。我习惯是先train_test_split划分数据train上训练test上评估并且把两个数据集上的R²一起打印出来。如果train的R²是0.98test是0.72那大概率是过拟合或者数据分布不一致而不是模型不行。3. 从手写到库调用sklearn 与 statsmodels 的分工3.1 sklearn的LinearRegressionfit之后你该拿什么手写完一遍之后再看sklearn你会觉得整个世界都清爽了但也会发现一些容易误用的细节。最基本的用法from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) model LinearRegression() model.fit(X_train, y_train) print(model.coef_) print(model.intercept_)fit完成之后常用的就是coef_和intercept_分别对应w和b。注意sklearn的LinearRegression默认是普通最小二乘OLS内部用的就是类似lstsq的解析解法不是梯度下降所以它对特征是否归一化不敏感这也是它为什么经常被人直接拿来fit的原因。但有几个限制它不会主动告诉你。第一它不返回特征的p值你无法直接知道哪个特征在统计上不显著第二它不做特征选择100个特征它会全给你系数第三它遇到缺失值直接报错需要你提前处理NaN。很多场景下线性回归的目标不只是预测还要解释特征效果这时只靠sklearn是不够的需要请出statsmodels。3.2 statsmodels给回归补上统计推断这一课如果你要给业务方汇报“广告投放对转化的影响是否显著”最合适的工具不是sklearn而是statsmodelsimport statsmodels.api as sm X_with_const sm.add_constant(X_train) # 必须自己加截距列 ols_model sm.OLS(y_train, X_with_const).fit() print(ols_model.summary())输出会包含一张大表里面有coef、std err、t值、P|t|、置信区间以及顶部的R-squared和F-statistic。对业务评审来说P|t|是最值得看的它告诉你这个系数是否显著区别于0。如果某个特征的p值大于0.05不管系数绝对值多大都不能拍着胸脯说它对目标有稳定的影响。还有一个细节sklearn的LinearRegression会自动帮你拟合截距但statsmodels的OLS不会。忘记加sm.add_constant的话等于强迫回归线穿过原点拟合结果会和sklearn完全不同。这是我见过新手最常犯的错误没有之一。什么时候用sklearn什么时候用statsmodels我的分工是预测任务优先sklearn因为它与Pipeline、cross_val_score这些工具配合流畅需要出回归报告、看显著性、算置信区间就切到statsmodels。两者训出来的系数在相同数据下几乎一致但输出信息量差很多。3.3 回归图表的正确打开方式建模报告里最少不了画像。最常见的画法是散点图加一条回归直线import matplotlib.pyplot as plt plt.scatter(X_test, y_test, alpha0.6) plt.plot(X_test, model.predict(X_test), colorred, linewidth2) plt.xlabel(feature) plt.ylabel(target)但这条线很容易掩盖问题。真正该看的图是残差图横轴是预测值或某个特征纵轴是残差真实值减预测值。如果残差随机分布在0附近说明模型拟合合理如果残差呈喇叭形说明方差不是常数可能需要对目标做对数变换如果残差有明显曲线趋势说明漏掉了非线性特征。residuals y_test - model.predict(X_test) plt.scatter(model.predict(X_test), residuals) plt.axhline(y0, colorgray, linestyle--)我在项目里几乎不看那条回归线只看残差图。一张好看的残差图能省下很多试错时间。4. 特征工程与正则化线性回归从能用变好用4.1 特征缩放梯度下降的隐形关卡如果你一直用sklearn默认参数做线性回归可能从来没感知过特征缩放的重要性因为在普通最小二乘里它确实无所谓。但一旦你开始用Ridge、Lasso或者自己写梯度下降特征缩放就直接决定模型能不能收敛。原因很简单不同特征的数值尺度差得远时损失函数的等高线会变成细长的椭圆梯度方向一会儿偏向这个特征一会儿偏向那个特征形成Z字形震荡收敛速度慢得像蜗牛。把特征都缩放到均值0、标准差1之后等高线更接近圆形梯度方向能直接指向最低点。from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline pipe make_pipeline(StandardScaler(), LinearRegression()) pipe.fit(X_train, y_train)注意两个容易踩的细节。第一缩放器只能用训练集拟合测试集上只调用transform不能把train和test拼在一起fit否则测试集的信息会通过scaler的均值和方差泄漏到训练流程里。第二如果是做可解释性报告缩放过后的系数就不再是“x每增加一个单位y增加多少”而是“x每增加一个标准差y增加多少”给业务方解释前一定要先想清楚。4.2 PolynomialFeatures让直线学会弯曲线性回归只能拟合直线但真实关系常常是曲线。解决办法不是换模型而是把原始特征扩展成多项式特征再让线性模型去拟合这些新特征。比如只有一个特征x扩展2次方之后得到[x, x²]线性回归就变成二次曲线拟合扩展到3次方可以拟合更复杂的弯曲形状。from sklearn.preprocessing import PolynomialFeatures poly PolynomialFeatures(degree2, include_biasFalse) X_poly poly.fit_transform(X_train)这招简单有效但很容易过头。degree5的时候训练集R²可以无限逼近1测试集R²反而崩掉因为模型开始死记硬背训练集中的每个点曲线的抖动越来越大。我在实验里见过不少二次方、三次方效果很好一加到五、六次方就完全失控的案例。控制办法是交叉验证。把PolynomialFeatures和线性回归放进Pipeline再用GridSearchCV去搜索degree比手动尝试靠谱得多from sklearn.model_selection import GridSearchCV param_grid {polynomialfeatures__degree: [1, 2, 3, 4]} grid GridSearchCV(make_pipeline(PolynomialFeatures(), LinearRegression()), param_grid, cv5) grid.fit(X_train, y_train) print(grid.best_params_)4.3 L1和L2正则化的选择逻辑当特征数量变多线性回归的系数会变得很不稳定甚至出现数值巨大的正负抵消。正则化就是在损失函数后面加一个惩罚项用一点点偏差换取方差的大幅下降。RidgeL2惩罚系数平方效果是让所有系数都往0缩但不会等于0LassoL1惩罚系数绝对值会让一部分系数被压成真正的0相当于自带特征选择。实际选择时我这么判断如果只需要防过拟合、提升稳定性用Ridge如果需要从几百个特征里筛出少量关键特征用Lasso如果既需要分组特征挑选又想要稳定可以试试ElasticNet。from sklearn.linear_model import Ridge, Lasso ridge Ridge(alpha1.0) lasso Lasso(alpha0.01)alpha是正则化强度它越大惩罚越重系数越趋近0。alpha的选择不要靠拍脑袋优先用RidgeCV、LassoCV或者GridSearchCV做交叉验证。一个常见误解是“加了正则化就一定更好”不一定在特征少且相对独立的数据上普通最小二乘往往就是最好的正则化反而会让模型变钝。5. 上线前最值得排查的四个问题5.1 多重共线性系数符号反直觉先讲一个真实案例。某次广告效果分析里我把“广告费用”和“曝光量”同时作为特征放进线性回归结果广告费用的系数居然是负的意思是花钱越多销售越少。业务方看到这个结果直接炸了但单独跑广告费用一个特征时系数明明是正的。原因是这两个特征高度相关广告费用高曝光量就大它们几乎在表达同一件事。模型在分配解释权时把大部分效果归给了曝光量广告费用的系数就被挤成了负值。这不是数据造假而是多重共线性造成的系数不稳定。检测方法很简单算一下方差膨胀因子VIF一般超过10就要警惕。处理办法有几种删掉相关性高的特征之一、用PCA压缩成主成分、或者改用Ridge让系数更稳定。最要紧的是不要看到系数符号就直接下业务结论先检查特征之间的相关性矩阵。5.2 一个异常值能把斜率拉歪多少MSE对异常值异常敏感。我做过一次小实验100个正常样本拟合出斜率3.0往里放一个x100、y5的极端点斜率直接掉到1.8。那个点本身离正常模式很远但因为平方误差的存在模型宁可牺牲大量正常样本的拟合精度也要把直线往那个点那边拽。排查方法训练完之后先看残差图标出标准化残差绝对值大于3的样本再用IQR或Z-score检测异常值。处理方式要分情况如果异常值明显是录入错误或传感器故障删掉或修正如果是真实业务极端事件比如双十一当天的销量贸然删除反而会把模型训练偏。更稳的做法是用HuberRegressor它对异常值的敏感度比普通最小二乘低得多不需要手工删点。5.3 R²很高但预测很差先查数据泄露有一种情况特别迷惑人训练集R²非常高测试集指标也还不错但模型上线后预测结果一塌糊涂。这种事我排查过好几次根因几乎都是数据泄露也就是模型在训练时偷看到了不该看到的信息。最常见的三种泄漏源第一特征里包含了目标变量的未来信息比如用“当天是否已售完”来预测“当天销量”这是拿结果预测结果第二特征缩放时把全量数据的均值方差一起算train和test的信息混在一起第三时序数据没有按时间切分而是随机shuffle导致模型提前“见过”未来。排查链路我自己是固定的先检查特征构造代码里有没有直接或间接引用目标然后确认train/test划分边界时间序列一律用时间切分最后在测试集上画“预测值vs真实值”散点图如果分布明显偏离再回去看数据管道。R²高从来不是目标稳定性才是。5.4 上线后的漂移监控与重训触发模型不是上线就完事。运营策略调整、用户习惯变化、外部环境改变都会导致训练时学到的规律慢慢失效。我的做法是上线时就预留两个监控指标一是模型预测均值二是特征分布。用PSI群体稳定性指标对比近30天特征分布与训练集分布的差异PSI如果超过0.25基本可以判定特征漂移严重。监控指标下降时不要急着盲目重训。先把新旧模型的预测结果在同一个测试集上跑一遍确认新模型是确实更好还是只是拟合了最近的短期波动。很多团队踩过这个坑一看到线上指标下滑立刻用最近一个月数据重训结果新模型更差因为最近一个月正好赶上活动期数据分布是扭曲的。稳妥的做法是设定重训触发条件比如“连续7天RMSE超过基线的1.2倍”而不是遇到波动就手动操作。这个问题的处理没有银弹但有一点可以确定你手里那版最朴素的线性回归baseline是所有后续复杂模型对比的锚点。如果连baseline的指标都没有记录后面就谈不上监控和优化。我个人这几年的体会是线性回归从来不是一个“只能用来练手”的玩具算法。它是一切线性模型的骨架也是理解岭回归、Lasso、逻辑回归甚至神经网络第一层的起点。真正考验人的不是那几行fit代码而是你能不能解释每个系数、能不能在数据怪异时稳住阵脚、能不能在指标异常时快速定位原因。如果你现在正准备做第一个回归项目我的建议是先花十分钟手写一遍numpy版的梯度下降再换到sklearn然后认认真真存一份baseline的R²和RMSE。这个习惯会在之后的每一次建模中帮你省下大量的返工时间。
阅读完成 · 觉得有帮助?