ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

线性回归从原理到实战:手写实现、sklearn流程与常见坑排查

线性回归从原理到实战:手写实现、sklearn流程与常见坑排查 很多人第一次接触机器学习不是被神经网络拉进坑的而是被一行“linear代码线性回归”拉进坑的。十几行代码跑完屏幕上跳出斜线穿过散点图当时觉得“就这”。但后来回头看线性回归模型把机器学习的完整链路全串起来了数据、假设、损失、优化、评估、调参一个都没少。这篇文章会从线性回归算法的核心思路拆起带你手写一轮线性回归python实现再走一遍sklearn的工程化流程最后把实验里最容易踩的坑和排查技巧一次讲清楚。无论你是刚起步做机器学习线性回归实验还是想系统梳理线性回归代码细节都能直接照着复现。1. 线性回归是什么先搞清楚在预测什么1.1 从房价预测聊起回归任务和分类任务有什么不同如果你在租房平台看到一套房子面积90平米、朝南、10楼你会下意识估一个月租金大概多少。这个“估多少钱”的行为就是回归任务输入房子的特征输出一个连续数值。分类任务则是判断这房子“值不值”输出的是类别标签。同样是预测前者是连续实数后者是离散标签这两类问题的模型逻辑完全不同。线性回归模型的输出是一个实数可以是房价、销售额、温度也可以是某个指标的未来值。它解决的就是这类连续值预测问题。很多人刚接触时会把线性回归和逻辑回归搞混逻辑回归虽然名字里带“回归”但它解决的是分类问题本质是在线性回归外面套了一层sigmoid函数把输出压到0到1之间。后面我会专门讲这个延续关系这里先聚焦纯粹的线性回归。1.2 核心假设特征和目标之间是线性关系线性回归假设目标值 y 可以通过特征的线性组合来近似预测y ≈ w1x1 w2x2 ... w_p*x_p b其中 w1 到 w_p 是每个特征的权重b 是偏置项。这个式子看起来简单但它隐含了一个很强的前提每个特征单独对目标的影响是线性的特征之间没有交互作用。比如房价和面积的关系如果假设是线性的那就意味着面积每增加一平米房价增加固定的金额。这个假设在实际数据中很少完全成立但很多情况下它足够近似。面积和房价在常见区间内就接近线性即使有弯曲也可以通过特征工程比如加平方项来拟合。换句话说线性回归模型不是只能画直线它也可以拟合曲线但前提是你先把原始特征做非线性变换再把变换后的特征喂给线性模型。这个技巧我放到特征工程部分详细说。1.3 损失函数为什么非要用最小二乘有了模型形式下一步要定义“预测得多差”。最常用的损失函数是均方误差MSE对应的优化目标是最小化残差平方和SSESSE Σ(y_i - ŷ_i)^2为什么用平方而非绝对值三个原因。第一平方函数处处可导梯度下降时方便求导绝对值在零点不可导求导麻烦。第二平方误差会放大大误差的惩罚让模型更注意那些预测偏差很大的样本这在很多场景下是优点。第三如果噪声服从高斯分布最小化平方误差等价于最大似然估计有明确的统计解释。需要注意平方误差对大误差敏感的同时也意味着它对异常值非常脆弱。一个偏离很远的点它的残差平方可能比所有正常点的平方和还大模型会拼命去拟合那个脏点导致整体预测偏移。所以后面我会专门讲异常值检测和稳健回归的兜底方案。2. 手写线性回归代码一步步拆解核心细节2.1 先造一份带噪声的实验数据为了看清线性回归是怎么工作的我习惯先用numpy造一份已知答案的数据。下面这条代码会生成100个样本特征 x 在0到10之间均匀分布真实关系是 y 3x 2然后加上标准差为1.5的高斯噪声import numpy as np np.random.seed(42) X np.linspace(0, 10, 100).reshape(-1, 1) true_w, true_b 3.0, 2.0 y true_w * X.squeeze() true_b np.random.randn(100) * 1.5这份数据的妙处在于“答案已知”。后面不管用哪种方法求权重我们都期待得到接近 w≈3.0、b≈2.0 的结果。如果算出来的参数明显偏离说明实现的梯度更新、学习率或数据处理有问题。这是检验手写代码最直观的方法。2.2 梯度下降实现权重是怎么一步步学出来的线性回归的损失函数是一个凸函数意味着只要沿着梯度反方向走一定能找到全局最优解不存在局部最优的困扰。我们要做的事情就是反复计算损失对每个参数的偏导数然后更新参数。假设特征矩阵是 X目标向量是 y把偏置项并到权重里即 X_b [1, X]θ [b, w]^T。那么均方误差的梯度是∂MSE/∂θ (2/m) * X_b^T * (X_b·θ - y)写代码时我建议先做特征标准化。如果不标准化x的取值范围和偏置项对应的常数1差一个量级梯度下降很容易震荡甚至发散。下面这段代码先用均值和标准差把 x 缩放到均值为0、方差为1的范围再执行批量梯度下降m len(X) X_mean X.mean() X_std X.std() X_norm (X - X_mean) / X_std X_b np.c_[np.ones((m, 1)), X_norm] theta np.random.randn(2, 1) learning_rate 0.1 n_iterations 1000 for iteration in range(n_iterations): gradients (2 / m) * X_b.T.dot(X_b.dot(theta) - y.reshape(-1, 1)) theta - learning_rate * gradients print(theta , theta.ravel())标准化后学习率可以设到0.1以上迭代几百次就能收敛得很干净。如果拿原始X直接跑同样学习率大概率会看到损失曲线剧烈跳动。这个坑我踩过好几次加了特征标准化之后梯度下降才稳定。2.3 正规方程闭式解不用迭代也能求权重梯度下降是用迭代逼近最优解但线性回归还有一条更省事的路直接求闭式解。把损失函数对θ求导并令其为零可以得到θ (X_b^T * X_b)^(-1) * X_b^T * y这就是正规方程。只要 X_b^T * X_b 可逆一步就能得到解析解。代码实现如下X_b np.c_[np.ones((m, 1)), X] theta_closed np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y) print(theta closed , theta_closed.ravel())需要注意当特征数量特别多或者特征之间存在高度相关性时X_b^T * X_b 可能接近奇异矩阵直接求逆会得到数值不稳定的结果。这时候更稳妥的做法是用np.linalg.pinv求伪逆或者退化到梯度下降。对于数据量特别大的场景正规方程涉及矩阵乘法和求逆计算复杂度接近 O(n^3)远不如梯度下降扩展性好。手写实验可以都用一遍加深理解实际工程里优先用sklearn封装的求解器。2.4 结果验证两种方法得到的参数是否一致我在jupyter里跑完上面两段代码打印出的结果大致是这样的梯度下降法theta [1.98, 3.04]正规方程法theta [1.99, 3.03]因为噪声存在b 不是精确的2.0w 也不是精确的3.0但在95%置信区间内都落在真实值附近。这个一致性说明两个实现逻辑都没问题。接着我会画真实值和预测值的散点图用matplotlib把数据和拟合线叠在一起看import matplotlib.pyplot as plt x_line np.linspace(0, 10, 100) y_line theta_closed[0] theta_closed[1] * x_line plt.scatter(X.squeeze(), y, alpha0.6, labeldata) plt.plot(x_line, y_line, colorred, labellinear regression) plt.legend() plt.show()如果拟合线能穿过点云中间且残差看起来随机分布说明模型结构基本正确。如果残差有明显弯曲比如数据是抛物线而模型画的是直线说明线性假设不成立需要做特征变换或换模型。3. 用sklearn实现线性回归工程化的正确姿势3.1 从数据划分到模型训练完整的sklearn流程手写一遍代码理解原理之后实际项目里我们直接用sklearn因为它把数值求解、矩阵分解、异常处理都优化过了。一个标准的sklearn线性回归流程包含六个步骤数据准备、划分训练集和测试集、创建模型、训练、预测、评估。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score 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) y_pred model.predict(X_test) print(w:, model.coef_) print(b:, model.intercept_) print(MSE:, mean_squared_error(y_test, y_pred)) print(R2:, r2_score(y_test, y_pred))这里有个容易被忽略的点测试集必须只在最后使用不能拿它去调整超参数。有人会在训练过程中反复看测试集指标这相当于把测试集信息泄露进了模型选择过程最后得到的评估结果会偏乐观上线后表现会打折扣。正确做法是再切一份验证集或者在sklearn里用交叉验证把测试集留到最终验收。3.2 评估指标R²、MSE、RMSE、MAE到底看哪个sklearn里最常见的回归评估指标有四个不同场景看不同的指标MSE均方误差所有残差平方的平均值。它和大误差绑定适合需要重点惩罚大偏差的场景。但数值受量纲影响不好直观理解。RMSE均方根误差MSE开方后和原始目标同量纲比如房价预测的RMSE是几万块更容易解释。MAE平均绝对误差所有误差绝对值的平均。它对异常值更稳健反映的是“平均差多少”。R²决定系数表示模型解释了多少目标变量的方差取值最高为1越接近1越好。但R²对新增特征永远不会降低所以比较不同复杂度模型时要用调整R²。实际使用时我会同时看RMSE和R²。如果RMSE很小但R²很低说明目标本身的方差很小模型解释力仍然不足如果R²接近0.9但RMSE依然很大说明数据里有个别极端值拉高了误差。两个指标互补才能看清问题。3.3 多特征场景特征预处理比模型本身更重要前面我们只用了面积一个特征真实场景里会扩展成“面积卧室数楼龄朝向地段”可能有几十个特征。特征多起来之后首先要做的是量纲归一化。比如面积是几十到几百楼龄是个位数到几十差距不大但如果某个特征取值范围是0到1另一个是0到100000线性回归的系数集会变得很难解释梯度下降也容易不稳定。sklearn里用StandardScaler做标准化from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)注意这里有个细节先用训练集fit得到均值和标准差再分别 transform 训练集和测试集不能拿测试集重新 fit。否则测试集的信息会污染训练过程等于提前让模型看到了“考试答案”。3.4 正则化岭回归和Lasso在什么时候出手当特征数量接近甚至超过样本数量时普通线性回归会疯狂拟合训练数据权重变得极大泛化能力极差。这时候需要给损失函数加上权重惩罚项让权重尽量小但又不至于让预测失效。岭回归Ridge加的是L2正则项λΣw²效果是让权重整体收缩但不会把特征系数压到零。它适合特征之间相关性较强的场景。Lasso加的是L1正则项λΣ|w|效果是让部分不重要的特征系数直接变成0天然做特征选择。ElasticNet则是两者的混合。sklearn里的用法几乎一样from sklearn.linear_model import Ridge, Lasso ridge Ridge(alpha1.0) ridge.fit(X_train_scaled, y_train)alpha是正则化强度越大权重惩罚越强。实际调alpha要靠交叉验证sklearn提供了RidgeCV和LassoCV可以直接在给定范围内自动选最优alpha。4. 线性回归实验中的常见坑与排查技巧4.1 梯度下降不收敛或震荡先检查两件事手写梯度下降时最典型的问题是损失曲线像心电图一样上下乱跳。出现这种情况第一检查学习率是否过大第二检查特征是否标准化。学习率过大的解决办法是把它调小一个量级比如从0.1调到0.01标准化则需要保证所有特征都在相近的尺度内。另一个排队时容易犯的错是梯度更新方向写反。如果损失曲线不降反升大概率是theta - learning_rate * gradients写成了加号或者梯度的表达式里转置写错了。遇到这种问题最好的调试方式是在每次迭代后打印一次损失值观察前几十轮的变化趋势就能很快定位。4.2 多重共线性导致系数解释像“猜谜”如果两个特征高度相关比如“房屋面积”和“房间数量”线性回归会把解释力劈给两个特征导致其中一个系数变成正的很大另一个变成负的很大两者抵消后模型效果可能还可以但单看系数你会怀疑人生。这时候需要检查特征之间的相关系数矩阵一般相关系数超过0.8就要警惕。解决办法有几种直接删掉一个相关特征用PCA做降维或者改用带L2正则的岭回归让系数收缩。如果你的目标是“解释每个特征的影响”共线性问题不解决论文和业务报告里的结论都站不住脚。4.3 欠拟合和过拟合光看训练集永远看不出问题线性回归也会过拟合吗会尤其当特征维度很高、样本量很小时普通线性回归可以把训练集的损失降到几乎为零但测试集一塌糊涂。判断欠拟合和过拟合最实用的工具是学习曲线画训练误差和验证误差随样本量变化的曲线。欠拟合训练误差和验证误差都高且逼近时二者趋于一致说明模型太简单。过拟合训练误差很低验证误差明显更高且两者之间有gap说明模型太复杂或训练数据不足。线性回归场景下过拟合多来自特征太多、样本太少解决办法是加正则化、增加样本量或减少特征。欠拟合则多来自特征本身表达不足比如把面积和房价的非线性关系硬当成直线这时候要做多项式特征或改用非线性模型。4.4 异常值和非线性关系回归模型的两大软肋异常值对最小二乘的破坏力极大。我做过一个实验在100个正常样本里塞1个偏离10个标准差的脏点普通线性回归的权重直接偏了将近30%。这时候要么先用箱线图或Z-score把异常点找出来剔除要么改用稳健回归比如RANSAC或Huber回归它们对异常值有更高容忍度。非线性关系则有两类处理思路。一类是特征变换对x加平方项、三次项、log项把非线性关系变成线性关系再喂给线性模型。另一类是换模型比如决策树、随机森林、XGBoost它们天然能拟合非线性关系。但换模型也要付出代价可解释性会明显下降所以业务需要解释时我通常先用线性模型加多项式特征试一轮看效果差距再决定。5. 从线性回归到更复杂的模型延展思路5.1 广义线性模型从回归到分类的桥梁线性回归是广义线性模型里最基础的一种。广义线性模型包含三个部分线性预测器、连接函数、随机分布。逻辑回归就是在线性预测器外面套一个logit连接函数对应二分类的伯努利分布泊松回归套log函数对应计数数据。理解这个框架之后你会知道线性回归不是孤立的知识点而是一整套建模方法的基础。实际工程里线性回归的结论可以直接迁移到逻辑回归上特征标准化、多重共线性、正则化、评估方式这些坑在逻辑回归里同样存在。先吃透线性回归后面学其他模型的速度会快很多。5.2 特征工程给线性模型“换武器”才是决胜手很多人在数据集上直接调用LinearRegression.fit效果不好就怪模型太弱。但线性回归真正强的地方在于特征表达。比如房价和面积的关系如果是曲线我加一个area^2特征线性模型就能拟合抛物线再加一个area*bedrooms交互项模型就能捕捉“大房子多卧室和小房子多卧室”的不同影响。sklearn里用PolynomialFeatures可以一行生成多项式特征和交互特征from sklearn.preprocessing import PolynomialFeatures poly PolynomialFeatures(degree2, include_biasFalse) X_poly poly.fit_transform(X)但特征多了之后过拟合和共线性问题会同时冒出来所以多项式特征要和正则化配合使用。我的经验是先加degree2的多项式和少量交互项再套RidgeCV比一上来就用随机森林更可控、更容易解释。5.3 实战建议什么时候该坚持线性回归做项目时会遇到一个常见争论线性回归这么简单为什么业务方还在用其实很多场景里简单模型的优势远大于复杂模型。第一线性回归可解释性强每个特征系数可以直接拿到业务会上讲第二它训练快数据量百万级也能秒级完成第三它适合做基线模型用来衡量复杂模型到底值不值得上。我的建议是拿到任何回归任务第一步先用线性回归跑了基线记录RMSE和R²再做特征工程看指标提升多少。如果提升不明显说明数据本身的信息量有限换复杂模型大概率也不会好太多如果提升显著再用非线性模型进一步挖掘。这样你的项目每一步都有据可循而不是盲目上重型模型。最后再分享一个小技巧线性回归代码跑通之后一定要把训练集误差和测试集误差打印出来对比再画出残差图。如果残差随预测值的增加呈现喇叭状说明均方差假设不成立可能需要log变换目标变量。这个细节很多人忽略但一旦遇到排查起来会让你少掉很多头发。我自己的习惯是每次实验都留一份这样的检查清单花两分钟看一眼能避免上线后多折腾一整天。
返回列表