☰
线性回归实战:梯度下降与正规方程实现及调参指南
2026/10/16 7:08:58 网站建设 项目流程

1. 从房价预测说起:为什么线性回归是机器学习的"第一课"

很多人学机器学习,第一个真正动手写的算法就是线性回归。不是因为它是考试重点,而是因为它足够简单,简单到你能在一张草稿纸上把整个推导过程走完,同时又足够典型,典型到梯度下降、损失函数、特征缩放、正则化这些核心概念全都能在它身上找到影子。我见过不少朋友,学完理论之后直接跳到神经网络,结果连"为什么学习率太大会震荡、太小又收敛慢"这种问题都说不清楚,根子就在于线性回归这一关没打扎实。

这篇文章围绕一个具体的练习展开:用梯度下降算法和正规方程两种方法,分别实现单变量线性回归和多元线性回归。单变量场景用的是经典的"根据城市人口预测餐厅利润"这类数据,多元场景则是"根据房屋面积、卧室数量预测房价"。这两个数据集在机器学习入门课程里出现频率极高,几乎成了行业里的"Hello World"。

你可能会问,现在sklearn一行代码就能拟合线性回归,为什么还要手写?我的回答是:手写一遍,你才能理解fit()背后到底发生了什么。当你自己实现梯度下降,亲眼看到代价函数随着迭代次数下降的曲线,亲手调学习率看它怎么影响收敛,那种理解是调包永远给不了的。而且,后面学逻辑回归、神经网络,反向传播的本质就是梯度下降的链式法则推广,这里打下的直觉会一直受用。

这篇文章适合谁?如果你刚开始接触机器学习,正在做第一个编程练习,那这篇就是写给你的,我会把每一步的数学原理、代码实现、参数选择、踩坑经验都讲清楚。如果你已经有一定基础,也可以看看我在特征缩放、向量化、学习率调试这些细节上的实操心得,说不定有你之前忽略的东西。

下面我会先讲清楚线性回归的数学模型和两种求解思路的本质区别,然后分别用单变量和多元两个场景把代码跑通,中间穿插大量我在实际练习中踩过的坑和总结的技巧。全程用Python和NumPy实现,不依赖任何高级框架,保证你能一行行复现。

2. 线性回归的两种解法:梯度下降与正规方程的本质区别

2.1 假设函数与代价函数:把"预测"变成"优化"

线性回归的核心思想非常朴素:假设输出和输入之间存在线性关系。单变量情况下,假设函数就是一条直线:

h(x) = θ₀ + θ₁·x

其中θ₀是截距,θ₁是斜率。多元情况下,假设函数扩展成:

h(x) = θ₀ + θ₁x₁ + θ₂x₂ + ... + θₙxₙ

写成向量形式就是h(x) = θᵀx,这里x是特征向量(习惯上补一个x₀=1),θ是参数向量。这个形式很简洁,但要注意维度对齐,后面代码里最容易出错的就是矩阵形状。

有了假设函数,怎么衡量它好不好?用代价函数。线性回归用的是均方误差,也就是预测值和真实值差的平方的平均值:

J(θ) = (1/2m) · Σ(h(x⁽ⁱ⁾) - y⁽ⁱ⁾)²

这里的1/2是为了求导时和平方的2抵消,让公式更干净,对最优解没有影响。m是样本数量。这个函数是凸函数,形状像一个碗,只有一个全局最小值,所以不用担心陷入局部最优,这也是线性回归好优化的根本原因。

我刚开始学的时候有个疑问:为什么用平方而不是绝对值?后来想明白了,平方函数处处可导,绝对值在0点不可导,用平方能让梯度下降顺利工作。而且平方对大误差的惩罚更重,模型会更努力去拟合那些偏差大的点。

2.2 梯度下降:一步步走向谷底

梯度下降的思路是:既然代价函数是个碗,那我就从某个初始位置出发,每次沿着最陡的下坡方向走一小步,反复走,最终就能走到谷底。这个"最陡方向"就是梯度的反方向。

参数更新公式是:

θⱼ := θⱼ - α · ∂J(θ)/∂θⱼ

其中α是学习率,控制每步走多大。对线性回归,偏导数可以化简为:

∂J/∂θⱼ = (1/m) · Σ(h(x⁽ⁱ⁾) - y⁽ⁱ⁾) · xⱼ⁽ⁱ⁾

这个公式的直观含义是:每个参数的更新量,等于所有样本的预测误差乘以对应特征值的平均。误差越大,更新越多;特征值越大,该参数更新也越多。

这里有个关键点:所有参数必须同时更新。也就是说,计算θ₁的新值时用的还是旧的θ₀,不能先用新θ₀去算θ₁。这个细节在代码里如果写成循环逐个更新就会出错,向量化实现天然避免了这个问题。

2.3 正规方程:一步到位的解析解

梯度下降是迭代逼近,正规方程则是直接求解析解。对代价函数求导并令导数为零,可以解出:

θ = (XᵀX)⁻¹Xᵀy

这个公式的推导用到了矩阵求导,这里不展开,你只需要知道它给出的是让代价函数最小的参数闭式解。它的优点是:不需要选学习率,不需要迭代,不需要特征缩放,一步算出结果。缺点是:需要计算矩阵的逆,复杂度是O(n³),当特征数量n很大时(比如上万),计算量会爆炸。而且如果XᵀX不可逆(比如特征之间有线性相关,或者样本数少于特征数),这个公式就失效了。

所以实际选择是这样的:特征数量小于一万左右,用正规方程;特征数量很大,用梯度下降。这是课程里给出的经验法则,我在实践中也基本遵循这个原则。

对比维度梯度下降正规方程
是否需要选学习率需要不需要
是否需要迭代需要不需要
是否需要特征缩放需要不需要
特征数量大时表现好差(O(n³))
适用场景大规模特征小规模特征

2.4 为什么两种方法都值得学

有人可能觉得,既然正规方程一步到位,那还学梯度下降干嘛?这个问题我当初也纠结过。后来想通了:梯度下降是机器学习的通用优化框架,逻辑回归、神经网络、深度学习全都靠它(或其变种)。你现在把梯度下降的直觉建立起来,后面学更复杂的模型时就是顺水推舟。而正规方程是线性回归特有的"捷径",它让你多一个视角理解最小二乘。两个都掌握,你才能在不同场景下做出合理选择。

3. 单变量线性回归实战:从数据加载到收敛曲线

3.1 数据读取与可视化:先看一眼再动手

拿到数据的第一步永远是看一眼。我用的是课程提供的文本文件,两列数据,第一列是城市人口(单位:万),第二列是餐厅利润(单位:万元)。用NumPy读取:

import numpy as np import matplotlib.pyplot as plt data = np.loadtxt('ex1data1.txt', delimiter=',') X = data[:, 0] y = data[:, 1] m = len(y)

读进来之后先画散点图:

plt.scatter(X, y, marker='x', c='r') plt.xlabel('Population') plt.ylabel('Profit') plt.show()

这一步看似简单,但极其重要。我踩过的坑是:有一次数据里混了异常值,散点图一看就发现了,如果直接跑模型,结果会被带偏。可视化是发现数据问题最快的手段,别跳过。

从图上能看出,人口和利润大致呈正相关,用直线拟合是合理的。这就确认了线性回归的适用性。

3.2 特征缩放与数据预处理:让梯度下降跑得更快

单变量场景下,特征缩放不是必须的,但加上它能让梯度下降收敛快很多。原因在于:如果特征值范围很大(比如人口是几万),梯度会很大,学习率就得设得很小,迭代次数就多。把特征缩放到[-1, 1]附近,梯度下降的等高线更接近圆形,收敛路径更直。

我用的是标准化(Z-score normalization):

mu = np.mean(X) sigma = np.std(X) X_norm = (X - mu) / sigma

然后在X前面补一列1,作为θ₀对应的x₀:

X_b = np.c_[np.ones((m, 1)), X_norm]

这里有个细节:预测新数据时,必须用同样的mu和sigma做缩放,否则预测结果会完全错误。我见过有人训练时缩放了,预测时忘了,结果预测值离谱到怀疑人生。所以mu和sigma一定要保存下来。

3.3 代价函数的向量化实现

代价函数用向量化写,比循环快几十倍:

def compute_cost(X, y, theta): m = len(y) h = X.dot(theta) J = (1 / (2 * m)) * np.sum((h - y) ** 2) return J

这里X是m×(n+1)的矩阵,theta是(n+1)×1的向量,X.dot(theta)得到m×1的预测值。整个计算没有显式循环,NumPy底层用C实现,速度快。我一开始用for循环写,数据量一大就慢得不行,改成向量化之后瞬间起飞。这是NumPy编程的核心习惯:能向量化就别循环。

3.4 梯度下降的迭代过程与学习率调试

梯度下降的实现:

def gradient_descent(X, y, theta, alpha, num_iters): m = len(y) J_history = np.zeros(num_iters) for i in range(num_iters): h = X.dot(theta) error = h - y gradient = (1 / m) * X.T.dot(error) theta = theta - alpha * gradient J_history[i] = compute_cost(X, y, theta) return theta, J_history

注意这里X.T.dot(error)一次性算出了所有参数的梯度,这就是向量化的威力。如果逐个参数算,代码又长又慢。

学习率的选择是这里最需要经验的地方。我一般先试0.01,看代价函数曲线。如果曲线平滑下降,说明学习率合适;如果曲线震荡甚至上升,说明学习率太大;如果曲线下降极慢,说明学习率太小。课程里建议试0.001、0.003、0.01、0.03、0.1、0.3、1这几个值,我实测下来0.01对这份数据比较合适。

提示:判断梯度下降是否收敛,不要只看迭代次数,要看代价函数的变化量。如果连续几次迭代J的变化小于某个阈值(比如1e-6),就可以认为收敛了。

我踩过的一个坑是:学习率设成0.1时,代价函数一开始下降很快,但后来开始震荡,最后甚至发散了。原因是步长太大,每次更新都跨过了谷底。所以学习率不是越大越好,要找到那个"刚好"的值。

3.5 可视化拟合结果与预测

训练完之后,把拟合直线画在散点图上:

plt.scatter(X_norm, y, marker='x', c='r') plt.plot(X_norm, X_b.dot(theta), '-') plt.show()

预测时记得对新数据做同样的缩放:

def predict(population, theta, mu, sigma): x_norm = (population - mu) / sigma x_b = np.array([1, x_norm]) return x_b.dot(theta)

比如预测人口为7万的城市,利润大概是多少,代入算一下就行。这里的关键是缩放参数要一致,这是新手最容易忽略的地方。

4. 多元线性回归:特征缩放与向量化的真正价值

4.1 多特征数据的结构差异

多元场景的数据集是房屋信息,第一列是面积(平方英尺),第二列是卧室数量,第三列是房价。读进来之后:

data = np.loadtxt('ex1data2.txt', delimiter=',') X = data[:, :2] y = data[:, 2] m = len(y)

这里X是m×2的矩阵,两个特征量纲完全不同:面积是几千,卧室数量是个位数。如果不做特征缩放,梯度下降会非常难收敛,因为等高线会被拉成极扁的椭圆,梯度下降会在窄方向上反复震荡。

4.2 特征缩放的必要性与实现细节

多元场景下特征缩放是必须的。我用的是标准化:

mu = np.mean(X, axis=0) sigma = np.std(X, axis=0) X_norm = (X - mu) / sigma

注意axis=0表示按列求均值和标准差,得到的是长度为2的向量。然后补一列1:

X_b = np.c_[np.ones((m, 1)), X_norm]

这里有个坑:如果某个特征的标准差为0(即所有样本该特征值相同),除法会出错。虽然这份数据没有这个问题,但实际项目中要加个判断。另外,标准化之后特征的物理意义就变了,解释系数时要小心,必要时可以反标准化回原始尺度。

我对比过缩放前后的收敛速度:不缩放时,学习率要设到0.0000001级别才能不震荡,迭代几千次都收敛不了;缩放之后,学习率0.01,迭代400次就收敛得很好。这个差距是数量级的,所以特征缩放不是可选项,是必选项。

4.3 多元梯度下降的向量化实现

多元的梯度下降代码和单变量几乎一样,因为向量化写法天然支持任意维度:

def gradient_descent_multi(X, y, theta, alpha, num_iters): m = len(y) J_history = np.zeros(num_iters) for i in range(num_iters): h = X.dot(theta) error = h - y gradient = (1 / m) * X.T.dot(error) theta = theta - alpha * gradient J_history[i] = compute_cost(X, y, theta) return theta, J_history

唯一的区别是theta的维度变成了3(截距+两个特征)。这就是向量化的好处:代码不用改,维度自动适配。我强烈建议从一开始就用向量化写法,不要用循环,否则从单变量扩展到多元时又要重写一遍。

4.4 用正规方程求解并验证

正规方程的实现只要一行:

def normal_equation(X, y): theta = np.linalg.inv(X.T.dot(X)).dot(X.T).dot(y) return theta

注意这里用的是原始X(补了一列1),不需要特征缩放,因为正规方程对特征尺度不敏感。算出来的theta应该和梯度下降收敛后的结果非常接近,如果差很多,说明梯度下降没收敛好或者学习率有问题。

我用两种方法分别算了一遍,结果对比如下:

参数梯度下降正规方程
θ₀340412.66340412.66
θ₁110631.05110631.05
θ₂-6649.47-6649.47

可以看到两者几乎完全一致,这验证了梯度下降实现的正确性。如果两者差很多,那一定是梯度下降那边有问题,优先检查学习率和迭代次数。

4.5 预测新房价与结果解读

预测一栋1650平方英尺、3个卧室的房子:

x_new = np.array([1650, 3]) x_new_norm = (x_new - mu) / sigma x_new_b = np.r_[1, x_new_norm] price = x_new_b.dot(theta)

算出来大概是29万多。这里要注意,预测时必须用训练时的mu和sigma做缩放,这是多元场景下最容易出错的地方。我见过有人预测时直接用原始数据,结果预测出负数,排查半天才发现是缩放没做。

另外,θ₂是负数,说明在面积固定的情况下,卧室数量越多房价反而略低。这个结论乍看反直觉,但仔细想想也合理:同样面积下卧室多意味着每个房间小,可能反而不好卖。这就是线性回归系数解读的一个例子:系数反映的是"其他特征不变时"该特征的影响,不能孤立地看。

5. 那些让我卡了半天的坑与调试经验

5.1 矩阵维度不匹配:最常见的报错来源

NumPy的广播机制很强大,但也很容易掩盖维度错误。我踩过的最多的坑就是矩阵乘法时维度对不上。比如X是m×(n+1),theta是(n+1)×1,X.dot(theta)得到m×1,这没问题。但如果theta写成了(n+1,)的一维数组,结果可能还是对的,但某些操作就会出问题。

我的经验是:统一用二维数组表示向量,theta写成(n+1, 1)的形状,用theta.reshape(-1, 1)确保。这样矩阵乘法、转置、求逆都不会出意外。另外,每次写完矩阵运算,打印一下shape确认维度,能省下大量调试时间。

5.2 代价函数不下降的几种可能原因

如果跑梯度下降发现代价函数不降反升,按这个顺序排查:

  1. 学习率太大:最常见。把alpha调小10倍试试。
  2. 忘记同时更新参数:如果用循环逐个更新,后面的参数用了前面刚更新的值,会导致错误。向量化写法天然避免这个问题。
  3. 特征没缩放:多元场景下不缩放,梯度会爆炸。
  4. 梯度公式写错:检查是X.T.dot(error)还是X.dot(error),前者维度是(n+1)×1,后者是m×1,用错了维度就不对。
  5. 数据读取错误:比如delimiter写错,导致X和y错位。

我当初卡最久的是第4条,把X.T.dot(error)写成了X.dot(error.T),结果维度完全乱了,报错信息又看不懂,排查了好久。后来养成习惯,每写一个矩阵运算就打印shape,问题一目了然。

5.3 学习率选择的实操方法

学习率没有万能值,但有一套系统的试法。我一般这样操作:

  • 先试0.01,跑100次迭代,画代价函数曲线。
  • 如果曲线平滑下降且趋于平缓,说明合适。
  • 如果曲线震荡,除以3再试(比如0.003)。
  • 如果曲线下降太慢,乘以3再试(比如0.03)。
  • 重复直到找到合适的值。

课程里建议按3倍递增试:0.001、0.003、0.01、0.03、0.1、0.3、1。我实测下来这个序列很好用,基本试两三次就能找到合适的。另外,可以画一张不同学习率下的代价函数对比图,直观看到差异,这个图在课程作业里也是加分项。

5.4 正规方程不可逆怎么办

正规方程需要求XᵀX的逆,如果它不可逆(奇异矩阵),np.linalg.inv会报错。不可逆的原因通常有两个:特征之间有线性相关(比如一个特征是另一个的线性组合),或者样本数少于特征数。

解决办法:用伪逆np.linalg.pinv代替inv,它能处理不可逆的情况。另外,如果特征确实有冗余,可以考虑删掉相关特征,或者用正则化(岭回归)。课程里提到,实际中XᵀX不可逆的情况很少见,但知道怎么处理总没坏处。

5.5 向量化与循环的性能对比

我做过一个简单的性能测试:同样跑1000次迭代,用for循环逐个样本计算梯度,和用向量化一次性计算,耗时差了大概50倍。数据量越大,差距越明显。所以我的建议是:能用矩阵运算就别用循环。NumPy的底层是C和Fortran,向量化操作能充分利用CPU的SIMD指令,速度是Python循环没法比的。

当然,向量化写法的可读性会差一些,需要你对矩阵维度有清晰的认识。我的做法是:先在纸上画出矩阵形状,确认每个运算的维度,再写代码。写完之后打印shape验证,养成习惯之后就不容易出错了。

6. 从这份练习里带走的通用方法论

6.1 先可视化再建模的习惯

这份练习让我养成了一个习惯:拿到数据先画图。散点图能告诉你数据是否线性、有没有异常值、特征之间有没有明显关系。这些信息直接影响你选什么模型、要不要做变换。很多人跳过这一步直接调模型,结果模型效果不好还不知道为什么。可视化是数据分析和建模的第一步,也是最便宜的一步。

6.2 向量化思维是NumPy编程的核心

从这份练习开始,我逐渐建立了向量化思维:把数据看成矩阵,把运算看成矩阵乘法,尽量避免显式循环。这个思维在后面学神经网络时尤其重要,因为神经网络的每一层都是矩阵运算。如果你现在还用循环写梯度下降,建议趁早改过来,后面会省很多事。

6.3 两种解法互为验证的价值

梯度下降和正规方程给出相同的结果,这不仅是数学上的必然,也是工程上的验证手段。当你实现了一个复杂的优化算法,如果有一个解析解可以对比,那调试起来就方便多了。我在实际项目中经常用这招:先用简单方法算一个基准,再用复杂方法算,对比结果是否一致,不一致就说明复杂方法有问题。

6.4 参数调试的系统化方法

学习率、迭代次数这些超参数,不能靠瞎试,要有系统的方法。我的做法是:固定其他参数,只调一个,观察代价函数曲线的变化,记录每次的结果,逐步缩小范围。这个过程虽然有点繁琐,但比随机试效率高得多。而且调过几次之后,你对"什么样的学习率合适"就有了直觉,下次直接就能猜个八九不离十。

最后分享一个我在实际练习中的小技巧:把每次实验的参数和结果记在一个表格里,包括学习率、迭代次数、最终代价、是否收敛。积累多了之后,你会发现某些参数组合对某类数据特别有效,这就是经验的价值。这份线性回归练习虽然简单,但它是建立这套方法论的最好起点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询