
很多入门课程都会在第2章第2节安排简单的线性回归代码我第一次看到那段示例代码时心里是有点失望的——几行代码而已导入库、建模型、训练、预测好像没什么玄机。但等到自己真正动手写、真正去回答这几行代码背后到底发生了什么时才发现这个看似简单的小节其实卡住了非常多初学者。这篇文章不打算把教材上的公式再抄一遍而是想从实操者的角度把线性回归代码从手写实现到库函数调用、从数据准备到结果评估的完整链条捋一遍。无论你是正在上相关课程、看完示例代码却感觉似懂非懂的学生还是准备把线性回归当作机器学习入门的零基础读者我都希望这篇内容能帮你把那几行会跑但不能完全理解的代码变成真正属于自己的工具。1. 这一小节看似平淡却是整个回归家族的地基1.1 线性回归到底在解决什么问题线性回归要解决的事情用大白话说就是给你一堆有输入有输出的历史样本找一条尽可能贴合这些样本的直线或超平面然后用这条线去预测新输入对应的输出。比如明天某个商品卖多少件、某套房子大概能成交什么价、某个城市明天最高温度是多少这些都是典型的连续值预测问题。只要数据里存在某种近似线性的关系线性回归就能用一条线把它描出来。教科书上会写成 y wx b其中 w 是斜率b 是截距代码里做的所有事情本质都是把 w 和 b 给找出来。很多初学者会误以为这一节只是跑通一个Demo所以潦草地看一遍就跳过去了。但事实恰恰相反线性回归代码体现的是机器学习里最基本的一套工作流准备数据、定义模型、设定损失函数、选择优化方式、评估结果。这套工作流在后面学逻辑回归、神经网络、决策树时会一遍一遍地重复出现。第2章第2节之所以安排在这里就是让学习者通过最简单的问题先熟悉这个永远不会变的套路。1.2 代码和数学公式之间那座桥课程前面大概率已经讲过了最小二乘法公式长这样要想把 y 和 x 之间的关系拟好就是让所有样本的预测误差平方和最小即 J Σ(y_i - (wx_i b))² 尽量小。这里的 J 通常被称为损失函数或代价函数。代码里真正做的事其实就三件首先把一组 x、y 组织成数据结构然后对 J 做最小化来求解 w 和 b最后用求到的 w、b 对新样本做计算。第2章第2节正是这三件事第一次以代码形态出现的节点。我在实际带过一些零基础学员后发现大家卡住的地方几乎都一模一样数学推导部分能看懂代码也会抄但这两者之间对应不起来。所以在下面两节里我会先用最原始的方式手写代码把求解 w 和 b这个过程做一遍再切换到常规工具库的写法让这座桥彻底打通。2. 从零手写线性回归不依赖任何库也能跑通2.1 最小二乘法的正规方程一行矩阵运算搞定如果从数学上直接求 J ||Xθ - y||² 的最小值可以把 J 对 θ 求导并令导数等于0得到一个解析解θ (XᵀX)⁻¹Xᵀy这就是所谓的正规方程Normal Equation。在 numpy 里它真的可以一行写完import numpy as np # 特征X每个样本一行这里先造5个样本每个样本只有1个特征 X np.array([[1], [2], [3], [4], [5]]) # 标签y连续值 y np.array([1.2, 2.1, 3.4, 4.3, 5.6]) # 在X左边拼一列1用来对应截距b X_b np.c_[np.ones((X.shape[0], 1)), X] # 正规方程theta (X_b^T * X_b)^(-1) * X_b^T * y theta np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y) print(截距 b , theta[0]) print(斜率 w , theta[1])这段代码里有几个值得留意的细节。第一矩阵里的一列1很多教材会提但初学者很容易忘。它对应的就是公式里的截距项 b没有这列1模型就只能拟合过原点的直线。第二np.linalg.inv 求的是逆矩阵如果 XᵀX 不可逆比如特征之间有强共线性、或者特征数量大于样本数量代码会直接报错。第三这一写法在特征维度很小、数据量不超过几万条时很快可一旦特征维度拉高到几千上万求逆的计算成本会成倍增长。用这组小数据跑完你会得到一个还算合理的 w 和 b。但大多数情况下我们并不直接用正规方程原因后面会讲到。2.2 梯度下降理解迭代优化的最佳入口正规方程是一步到位而梯度下降是一步步走过去的。它不直接求导数为0的解析解而是先随便给 w、b 一个初始值然后沿着损失函数下降最快的方向不断更新参数。损失函数还是那个 J对 w 和 b 分别求偏导后会得到这样两个梯度公式对 w 的偏导∂J/∂w (2/n) * Σ((y_pred - y) * x) 对 b 的偏导∂J/∂b (2/n) * Σ(y_pred - y)其中 n 是样本数量。更新方式就是让参数减去学习率乘以梯度w w - lr * ∂J/∂w b b - lr * ∂J/∂b把它落到代码里就是下面这个样子import numpy as np X np.array([1.0, 2.0, 3.0, 4.0, 5.0]) y np.array([1.2, 2.1, 3.4, 4.3, 5.6]) w 0.0 # 初始斜率 b 0.0 # 初始截距 lr 0.05 # 学习率 epochs 300 # 迭代次数 n len(X) loss_history [] for epoch in range(epochs): y_pred w * X b error y_pred - y loss (error ** 2).mean() dw (2 / n) * (error * X).sum() db (2 / n) * error.sum() w w - lr * dw b b - lr * db loss_history.append(loss) if epoch % 50 0: print(fepoch {epoch:3d}, loss {loss:.4f}, w {w:.3f}, b {b:.3f}) print(f最终结果: w {w:.3f}, b {b:.3f})这段代码初看起来有点绕但把它拆开就非常清晰先算出预测值 y_pred再算误差 error误差结合 X 求梯度 dw、结合自身求和求 db最后用学习率更新参数。每次迭代都离最优解更近一步loss 会一路降下来。300轮跑完结果和正规方程的答案会非常接近只是中间多了一段爬山下山的过程。2.3 手写版和库函数版结果差多少很多人会好奇自己写的梯度下降结果和工具库算出来的结果能对上吗我经常拿上面这组数据做演示得到的典型结果大致如下求解方式斜率 w截距 b正规方程1.06000.0600梯度下降300轮1.06020.0593sklearn LinearRegression1.06000.0600三者几乎一致微小的差异来自迭代次数、学习率和数值精度。这个结果很有意义它说明工具库并不是什么黑魔法底层原理就是我们手写的那些东西只是它在工程上做了大量优化所以你既可以直接用也不用觉得心里没底。3. 引入sklearn三行代码建模背后发生了什么3.1 数据准备阶段最容易被忽略的几个点工具库里最常用的写法非常简单from sklearn.linear_model import LinearRegression # 数据必须是二维的即使只有一个特征 X np.array([[1], [2], [3], [4], [5]]) # 5行1列 y np.array([1.2, 2.1, 3.4, 4.3, 5.6]) # 1维序列 model LinearRegression() model.fit(X, y) print(斜率:, model.coef_) print(截距:, model.intercept_)这段代码看起来平平无奇但我在实际教学里见过太多人在这上面栽跟头。最典型的一个坑是 X 的形状很多初学者直接用 X np.array([1, 2, 3, 4, 5])结果 fit 一运行就报错提示数据应该是一维还是二维的问题。原因在于 sklearn 规定特征矩阵必须是样本数 × 特征数的二维结构哪怕你只有一个特征也要用 reshape(-1, 1) 把它变形成二维。另一个坑是数据类型和缺失值。线性回归内部做的是纯矩阵运算如果特征列里有空值或者文本型数据fit 时会立刻报错。所以拿到真实数据的第一步永远是先检查缺失值、检查每一列的数据类型这比急着调参重要得多。3.2 fit、predict、score三个方法到底是什么逻辑fit、predict、score 这三方法是 sklearn 统一的 API 设计。第一次接触的人会觉得这只是一个机械的约定俗成但理解它们的内部逻辑会让你用得更放心。fit(X_train, y_train) 做的是拟合根据训练数据求解出模型参数放到 model.coef_ 和 model.intercept_ 里。调用完 fit 之后你其实已经有一个完整的模型表达式了。predict(X_test) 做的是预测把新数据代进学到的表达式里算出一组 y_pred。只要 X_test 的列数和训练时一致就能正常预测。score(X_test, y_test) 做的是评估它会计算模型在这一组数据上的 R² 分数。R² 的最大值是1越接近1说明模型越能解释数据的变化但它的绝对值好坏要根据具体场景判断不能盲信。X_test np.array([[6], [7]]) print(model.predict(X_test)) from sklearn.metrics import r2_score y_true np.array([6.4, 7.7]) print(R²:, r2_score(y_true, model.predict(X_test)))3.3 为什么库函数跑得快底层求解方式和你手动写的不一样sklearn 的 LinearRegression 默认并不走梯度下降它靠的是最小二乘加奇异值分解SVD。SVD 比直接求逆数值上更稳定即使遇到矩阵不可逆的边缘情况也能给出一个可用的解。这就是为什么你调用 fit 时感觉一秒出结果因为它根本不需要像梯度下降那样循环几百轮。这引出一个重要的分工逻辑正规方程适合中小规模数据、特征维度不太高的场景优点是快、确定、不用调学习率梯度下降适合大规模数据因为每次迭代只用一个批次的数据也能更新参数可以把内存占用压下来还可以配合学习率调度慢慢逼近最优解。理解了这点你才知道在什么情况下该信任默认实现什么情况下需要自己写优化循环。4. 新手必踩的坑数据划分与评估指标不能想当然4.1 用同一份数据既训练又评估等于拿原题考试很多课程的示例代码为了省事会把整份数据直接喂给 model.fit然后立刻用同一份数据算 score结果 R² 往往好看得吓人。这个操作在演示时没什么问题但在真实场景里会给你一个非常危险的错觉。原因并不复杂模型在训练时见过这些样本的正确答案它可以背诵答案来压低误差但遇到从未见过的数据时泛化能力可能很糟糕。这就是过拟合的雏形。我常用的一个类比是用原题考试当然能考高分但你真正需要测试的是学生做新题的能力。所以正规做法是把原始数据切成训练集和测试集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 )test_size0.2 表示拿20%的数据当测试集random_state42 是随机种子固定它可以保证每次切分的结果一致。没有这个固定过程你每次运行代码得到的训练集都不一样实验就无法复现排错也会变得非常痛苦。4.2 R²、MSE、RMSE各看什么别只会背公式初学者很容易只盯 R² 一个数但实际工作中我建议至少配合看两类误差指标。下面这个表是我自己总结的按使用频率排序指标全称含义适用场景R²决定系数模型解释了数据总波动的比例快速判断整体拟合效果RMSE均方根误差预测值和真实值差的标准差量级理解误差绝对大小MAE平均绝对误差平均每个样本偏差多少对离群点更稳健的误差表达MSE均方误差误差平方的平均值优化目标、梯度推导常用一个很容易踩的误区是R² 高不代表模型好。比如预测某商品销量时如果数据本身就非常集中、波动很小哪怕模型偏离很多R² 也可能显得还不低。反过来RMSE 和 MAE 更贴近业务语言——平均差几十块钱平均差几十件业务同事一看就懂。计算代码也很简单from sklearn.metrics import mean_squared_error, mean_absolute_error y_pred model.predict(X_test) rmse mean_squared_error(y_test, y_pred) ** 0.5 mae mean_absolute_error(y_test, y_pred) print(fRMSE {rmse:.2f}, MAE {mae:.2f})4.3 一个典型的过拟合案例加高次项后R²暴涨但测试集崩塌有一个我反复讲给学员听的模拟案例某同学用房屋面积预测房价他手头一份数据量不大的样本想提升 R²于是给模型加了面积²、面积³……一直到面积⁹这些高阶多项式特征。训练集上的 R² 确实一路从 0.72 涨到 0.99他兴奋地跑来问我是不是模型变强了。结果一放到测试集上R² 掉到了 0.45。原因很典型多项式阶数越高模型越能弯弯曲曲地穿过每一个训练点代价是它学到的大量都是噪声而不是真实的规律。这就是过拟合的经典样貌——训练误差低到感人测试误差高到离谱。我让他把训练集和测试集上的 R² 画在同一张图上曲线一出来他就明白了训练集曲线一路向上测试集曲线先升后降中间存在一个最佳复杂度的拐点。这个案例我每次都会提因为它比任何定义都直观地说明了为什么不能只用训练集评估模型。5. 一个可以直接复现的完整示例从造数据到结论5.1 先造一份带噪声的已知答案的数据为了观察线性回归到底能不能找回真实规律我习惯先构造一份带噪声的数据。这样做的好处是答案已知比如我知道真实的 w 是 2.5、b 是 -0.8接下来就看模型能不能从噪声中尽量恢复出这两个数。下面是完整的数据生成和建模流程你可以直接复制运行import numpy as np import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, mean_absolute_error # 固定随机种子保证结果可复现 np.random.seed(42) # 生成150个样本特征从0到10均匀分布 X np.linspace(0, 10, 150).reshape(-1, 1) # 真实规律: y 2.5*x - 0.8, 加均值为0、标准差1.2的噪声 y 2.5 * X.ravel() - 0.8 np.random.randn(150) * 1.2 # 切分训练集和测试集 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(训练集 R²:, round(model.score(X_train, y_train), 4)) print(测试集 R²:, round(model.score(X_test, y_test), 4)) print(恢复的斜率 w:, round(model.coef_[0], 4)) print(恢复的截距 b:, round(model.intercept_, 4)) print(RMSE:, round(mean_squared_error(y_test, y_pred) ** 0.5, 4)) print(MAE:, round(mean_absolute_error(y_test, y_pred), 4))如果你运行这份代码大概率会看到这样一组结果训练集 R² 和测试集 R² 都在 0.83 到 0.86 之间恢复出的 w 在 2.4 到 2.6 之间b 在 -1.0 到 -0.6 附近RMSE 大约 1.0 到 1.2。真实规律是 2.5 和 -0.8模型从噪声里把它找回来了误差范围符合数据噪声的强度。5.2 画图这一步不能省看见比背数字更有效# 画散点图和数据拟合线 line_X np.linspace(0, 10, 100).reshape(-1, 1) line_y model.predict(line_X) plt.figure(figsize(8, 5)) plt.scatter(X, y, alpha0.7, label数据点) plt.plot(line_X, line_y, colorred, linewidth3, label回归线) plt.xlabel(特征 x) plt.ylabel(目标 y) plt.legend() plt.show()数据点会围绕红线上上下下分布如果把噪声调成 0所有点就会精确落在红线上。我把画图这步单独拿出来是因为太多人把注意力全放在 R² 数字上却从没看过自己的回归线长什么样。很多问题一眼就能从图上发现数据明显弯曲却用直线拟合、存在几个刺眼的离群点、预测值在某些区间系统性偏高这些用数字很难察觉。5.3 调整学习率和迭代次数观察收敛行为如果回到手写梯度下降那个版本我建议做一组简单实验把学习率分别设成 0.01、0.1、0.5固定迭代次数看 loss 下降曲线。学习率 0.01 时loss 下降得很慢300 轮可能还没完全收敛学习率 0.1 时下降顺滑300 轮基本稳定学习率 0.5 时loss 会剧烈震荡甚至不降反升。初学者看过这一组对比会比背十遍学习率太大容易震荡都要难忘。我的经验是不熟悉的场景把学习率设为 0.01 到 0.1 之间起步loss 不动就调大一点loss 震荡就调小一点。这不是玄学就是看曲线说话的实操策略。6. 代码跑通只是第一步继续深入的正确姿势6.1 从一元到多元代码几乎不用大改当你把第2章第2节的代码彻底搞懂下一步很自然地就会遇到多元线性回归。好消息是 sklearn 代码完全不用改只需要把 X 从一列特征换成多列特征# 假设有3个特征: 面积、房龄、到地铁站距离 X_multi np.array([ [95, 8, 0.3], [72, 15, 0.8], [120, 3, 1.2], [89, 6, 0.5], ]) y_price np.array([310, 230, 420, 330]) model_multi LinearRegression() model_multi.fit(X_multi, y_price) print(回归系数:, model_multi.coef_) print(截距:, model_multi.intercept_)这时 model.coef_ 不再是一个数而是一个数组每个元素代表对应特征对目标的影响程度。从一元到多元思维上的变化比代码上的变化大得多你要开始关心哪些特征放进去是合理的特征之间是否互相干扰特征量纲不同会不会影响可解释性。6.2 残差图是判断模型好坏的第一现场学过回归的人都知道残差 真实值 - 预测值但很多人在实操中根本不画残差图。我强烈建议每次建模之后把残差画成散点图横轴是预测值纵轴是残差。如果残差随机分布在水平线 0 附近、没有明显形态说明模型把能学的线性规律都学走了如果残差呈现喇叭型预测值越大残差越散说明数据里存在异方差性如果残差呈现明显弯曲说明数据可能存在非线性关系只做线性回归是不够的。这个检查几乎不花时间但能帮你挡住大量看起来很猛实际上有问题的模型。我在开源项目里见过不少 R² 漂亮、残差图却歪七扭八的回归模型上线后表现远不如预期问题往往就出在这里。6.3 我给初学者的练习顺序建议最后聊一点学习路径上的体会。很多读者在看完第2章第2节后急着去学逻辑回归、决策树、神经网络我的建议是先别跑那么快把这几个练习做完再说第一把前面的正规方程和梯度下降代码各抄一遍改一改学习率、迭代次数体会收敛过程第二用 sklearn 对一份真实小数据比如某商品的历史销售数据、某地区自行车租赁量数据做完整流程切分、训练、预测、评估、画图第三尝试加入一个额外的类别特征体会把文本特征转成数值特征后回归会发生什么变化第四故意造一个极端离群点看它对拟合线的影响有多大直观理解最小二乘法对离群点敏感这件事。做完这四步你对线性回归的理解深度会远超只看课程示例代码的绝大部分人。我个人这几年每做一个回归相关的项目也还是会先把最简单的线性回归跑一遍。这个习惯帮我避过不少坑——很多问题的答案其实在你用复杂模型之前就已经藏在一条最朴素的直线里了。