FEATURED · 精选文章

从零实现单变量线性回归:掌握机器学习核心三要素与梯度下降实战

发布时间 / 2026/8/28 7:54:28
来源 / 创域科博编辑部
栏目 / 资讯中心
从零实现单变量线性回归:掌握机器学习核心三要素与梯度下降实战 1. 从“预测”开始为什么单变量线性回归是机器学习的“第一课”如果你刚开始接触机器学习面对各种复杂的算法名词感到无从下手那么从单变量线性回归入手绝对是最高效、最明智的选择。这就像学开车你不会一上来就去研究涡轮增压和变速箱原理而是先学会怎么踩油门、打方向盘把车平稳地开起来。单变量线性回归就是机器学习里的“踩油门和打方向盘”——它是最基础、最直观的预测模型能帮你建立起对机器学习核心思想的“手感”。简单来说单变量线性回归要解决的问题是这样的我们手头有一组数据它描述了两个变量之间的关系比如“房屋面积”和“房屋总价”。我们的目标是找到一条最合适的直线用这条直线来刻画“面积”如何影响“总价”。这样当有一个新的、我们知道面积但不知道总价的房子时我们就可以用这条直线来预测它的价格。这个“找直线”的过程就是机器学习模型从数据中“学习”规律的过程。别看它简单线性回归背后蕴含的模型定义、损失函数、优化算法这三大核心思想是所有监督学习模型的基石。吃透了它你再去看逻辑回归、神经网络甚至更复杂的模型都会有一种“哦原来是这个原理的升级版”的豁然开朗感。我见过很多初学者要么被复杂的数学公式吓退要么急于求成直接调用sklearn的LinearRegression结果代码跑通了但完全不知道模型在干什么参数为什么那样调。这篇笔记我就带你从零开始亲手“造”一个单变量线性回归模型。我们会用最直白的语言解释每一个概念用Python一步步实现核心算法并解决你肯定会遇到的几个典型问题。我们的目标不是成为调包侠而是真正理解机器学习的“内功心法”。2. 模型核心如何用一条直线来描述世界2.1 假设函数我们猜测的“规律”长什么样任何机器学习模型的第一步都是先做一个合理的“猜测”或者说先假设数据遵循某种规律。对于单变量线性回归我们的假设非常直接两个变量之间的关系可以用一条直线来近似描述。用数学公式表达就是著名的直线方程h_θ(x) θ₀ θ₁ * x别被符号吓到我们来逐一拆解x这是我们已知的输入特征也叫自变量。在我们房价预测的例子中x就是房屋面积单位平方米。h_θ(x)读作“h theta of x”这是我们的假设函数。它代表模型根据输入x所做出的预测值。在房价例子里h_θ(x)就是模型预测的房屋总价。θ₀和θ₁这是模型的参数也是我们整个学习过程最终要确定的东西。你可以把它们理解为这条直线的“旋钮”。θ₀截距。当x0时直线与y轴的交点。在房价预测中它可以理解为“基础价格”比如地块价值、固定税费等与面积无关的成本。θ₁斜率。它表示x每增加一个单位h_θ(x)会增加多少。在房价中它就是“每平方米的单价”。注意这里有一个非常重要的思维转换。在传统数学中我们已知一条直线知道θ₀和θ₁然后去求y。在机器学习中情况恰恰相反我们已知一大堆(x, y)的数据点即房屋面积和真实售价目标是反推出最合适的θ₀和θ₁使得直线h_θ(x)能最好地拟合这些数据点。θ参数才是我们要求的“未知数”。2.2 损失函数如何判断我们的“猜测”好不好现在我们有了一个假设一条直线但这条直线画在哪里即θ₀和θ₁取什么值才是最好的呢我们需要一个客观的评判标准。这个标准就是损失函数也叫代价函数。它的作用就是量化模型预测值与真实值之间的差距。最常用、最直观的损失函数是均方误差J(θ₀, θ₁) (1 / 2m) * Σ (h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾)²公式看起来又复杂了我们慢慢说m训练样本的总数量。比如我们有100套房屋的历史成交数据m就是100。(i)上标(i)表示第i个训练样本。x⁽ⁱ⁾和y⁽ⁱ⁾就是数据集中第i套房子的面积和真实售价。h_θ(x⁽ⁱ⁾)用我们当前的参数θ和面积x⁽ⁱ⁾预测出的第i套房子的价格。(h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾)这就是第i个样本的预测误差。可能是正数预测高了也可能是负数预测低了。(h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾)²对误差进行平方。平方有两个关键作用第一消除正负号的影响因为无论高估还是低估都是误差第二放大了较大误差的权重让模型对“离谱”的预测更加敏感。Σ求和符号。把数据集中所有m个样本的平方误差都加起来。(1 / 2m)求平均值并乘以1/2。乘以1/2是一个数学上的技巧是为了后续求导数时平方项导数的系数2能与这个1/2抵消让梯度下降的公式更简洁。它对最终求出的θ值没有影响。所以损失函数J(θ₀, θ₁)的本质是什么它是一个关于参数θ₀和θ₁的函数。对于每一组可能的(θ₀, θ₁)取值我们都能计算出一个J值。J值越小说明用这组参数画出的直线其预测结果与真实数据的整体误差越小模型就越好。我们的终极目标就从“找一条直线”具体化为一个数学优化问题找到能使损失函数J(θ₀, θ₁)值最小的那一组参数(θ₀, θ₁)。2.3 梯度下降如何自动找到最好的参数现在问题明确了我们要最小化J(θ₀, θ₁)。但θ₀和θ₁可以取任何值我们不可能手动一个个去试。这就需要引入机器学习的核心优化算法——梯度下降。你可以把梯度下降想象成“蒙眼下山”。你站在山腰对应一组初始的θ值目标是走到山谷最低点损失函数J的最小值点。你看不见全貌但你能用脚感受脚下坡度的最陡方向梯度。梯度下降算法就是让你沿着这个最陡的下坡方向小心翼翼地迈出一步。反复重复这个过程你最终就能走到谷底。它的数学更新公式是同时更新两个参数θ_j : θ_j - α * (∂J(θ₀, θ₁) / ∂θ_j)对于 j0 和 j1:表示赋值更新。α学习率。这是梯度下降中最重要的超参数。它控制着我们每一步迈多大。步子太小α太小下山速度慢需要很多步才能收敛步子太大α太大可能会直接跨过最低点甚至导致损失函数值震荡发散永远找不到最小值。(∂J(θ₀, θ₁) / ∂θ_j)这是损失函数J对参数θ_j的偏导数也就是“坡度”在那个方向上的陡峭程度。对于我们的均方误差损失函数这两个偏导数有具体的表达式推导过程是基础微积分这里直接给出结果对θ₀:(1/m) * Σ (h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾)对θ₁:(1/m) * Σ (h_θ(x⁽ⁱ⁾) - y⁽ⁱ⁾) * x⁽ⁱ⁾实操心得理解“同时更新”。在代码实现时必须注意“同时更新”的含义。正确的做法是先计算出θ₀和θ₁的新值分别存储在临时变量中等两个都算完了再一次性赋值给原来的θ₀和θ₁。如果算出一个就立刻更新那么计算第二个偏导数时用的h_θ(x)就已经是更新后的θ₀和旧的θ₁混合的结果这不符合梯度下降的定义可能导致算法无法正确收敛。这是初学者自己实现时最容易踩的坑。3. 从理论到代码手把手实现梯度下降理解了原理我们立刻用Python来实战。我们将使用一个经典的房价数据集例如波士顿房价数据集的简化版或自己构造的线性数据但为了绝对清晰我们先自己构造一份完美线性数据加一点噪音。3.1 数据准备与可视化首先我们生成一份模拟数据。假设真实的房价规律是基础价格50万每平米单价0.8万。我们在这个规律上加上一些随机扰动来模拟现实世界的不确定性。import numpy as np import matplotlib.pyplot as plt # 设置随机种子确保结果可复现 np.random.seed(42) # 生成特征数据100套房子的面积范围在50到200平米之间 m 100 X_raw np.random.uniform(50, 200, m) # 定义真实参数 true_theta0 50 # 真实截距50万 true_theta1 0.8 # 真实斜率0.8万/平米 # 生成标签数据按照真实规律计算并加入高斯噪声 noise np.random.randn(m) * 10 # 标准差为10万的噪声 y true_theta0 true_theta1 * X_raw noise # 数据可视化 plt.figure(figsize(10, 6)) plt.scatter(X_raw, y, alpha0.7, labelTraining Data) plt.xlabel(House Area (sqm)) plt.ylabel(House Price (10k yuan)) plt.title(Simulated House Price Data) plt.legend() plt.grid(True) plt.show()运行这段代码你会看到100个点大致分布在一条斜线周围。我们的任务就是让模型从这些点中把那条“隐藏”的直线找出来。3.2 特征缩放一个加速训练的关键技巧你可能会问直接用X_raw不行吗行但不好。观察我们的数据房屋面积X_raw的范围大约是50-200而参数θ₀和θ₁的初始值我们通常会设为0。在计算h_θ(x) θ₀ θ₁*x时x的值很大会导致h_θ(x)和后续的梯度值也很大。更重要的是如果特征尺度差异巨大比如另一个特征是房间数范围1-5会导致损失函数的“等高线”图变得又扁又长梯度下降会沿着陡峭的方向来回震荡收敛路径变得极其曲折缓慢。因此我们几乎总是要对特征进行标准化处理x_scaled (x - μ) / σ其中μ是特征的平均值σ是特征的标准差。标准化后的数据均值为0标准差为1所有特征都处于相近的尺度上能极大提升梯度下降的收敛速度。# 特征标准化 X_mean np.mean(X_raw) X_std np.std(X_raw) X (X_raw - X_mean) / X_std # 缩放后的特征X # 再次可视化可以看到数据分布中心到了0附近 plt.figure(figsize(10, 6)) plt.scatter(X, y, alpha0.7) plt.xlabel(House Area (Standardized)) plt.ylabel(House Price (10k yuan)) plt.title(Standardized Training Data) plt.grid(True) plt.show()注意事项这里我们对y房价没有进行缩放。在单变量线性回归中预测目标y的缩放只会影响损失函数J的数值大小和参数θ₀的尺度不影响θ₁的方向和模型的预测能力。但如果你要进行多变量回归或者使用某些正则化方法对y进行中心化减去均值有时也是有帮助的。一个简单的原则是至少要对特征X进行标准化。3.3 核心算法实现现在我们来实现完整的梯度下降算法。我们会把计算假设函数、损失函数和梯度的步骤都封装成函数。def compute_hypothesis(X, theta): 计算假设函数 h_theta(x) theta0 theta1 * X # X 是一个一维数组 (m,)theta是 [theta0, theta1] return theta[0] theta[1] * X def compute_cost(X, y, theta): 计算损失函数 J(theta) m len(y) predictions compute_hypothesis(X, theta) # 均方误差注意乘以了 1/(2*m) cost (1 / (2 * m)) * np.sum((predictions - y) ** 2) return cost def gradient_descent(X, y, theta, alpha, num_iters): 执行梯度下降 m len(y) cost_history [] # 用来记录每一次迭代的损失值方便查看收敛情况 for i in range(num_iters): # 计算当前参数下的预测值 predictions compute_hypothesis(X, theta) # 计算误差 error predictions - y # 计算梯度 (两个偏导数) # 对 theta0 的偏导: (1/m) * sum(error) # 对 theta1 的偏导: (1/m) * sum(error * X) grad_theta0 (1 / m) * np.sum(error) grad_theta1 (1 / m) * np.sum(error * X) # **同时更新**参数 theta_temp0 theta[0] - alpha * grad_theta0 theta_temp1 theta[1] - alpha * grad_theta1 theta np.array([theta_temp0, theta_temp1]) # 记录当前损失值 cost_history.append(compute_cost(X, y, theta)) # 每1000次迭代打印一次进度可选 if i % 1000 0: print(fIteration {i}: Cost {cost_history[-1]:.6f}, Theta {theta}) return theta, cost_history # 初始化参数通常从0开始 initial_theta np.array([0.0, 0.0]) # 设置学习率和迭代次数 learning_rate 0.01 iterations 5000 # 运行梯度下降 final_theta, cost_history gradient_descent(X, y, initial_theta, learning_rate, iterations) print(f\n梯度下降最终结果) print(f迭代 {iterations} 次后损失函数 J {cost_history[-1]:.6f}) print(f学习到的参数 theta0 {final_theta[0]:.4f}, theta1 {final_theta[1]:.4f})运行这段代码你会看到损失函数J的值随着迭代进行不断下降最终收敛到一个较小的值。我们打印出了最终学习到的参数θ₀和θ₁。3.4 结果分析与反标准化这里有一个关键点我们的特征X是标准化后的。因此我们学习到的参数θ₀和θ₁是针对标准化特征X的。如果我们想用原始的房屋面积单位平米来进行预测需要将参数转换回去。根据标准化公式X_scaled (X_raw - μ) / σ以及我们的假设函数h θ₀_scaled θ₁_scaled * X_scaled代入X_scaled (X_raw - μ) / σ得到h θ₀_scaled θ₁_scaled * (X_raw - μ) / σh (θ₀_scaled - θ₁_scaled * μ / σ) (θ₁_scaled / σ) * X_raw所以对应原始特征的参数为θ₀_raw θ₀_scaled - θ₁_scaled * μ / σθ₁_raw θ₁_scaled / σ# 将参数转换回原始尺度 theta0_raw final_theta[0] - final_theta[1] * X_mean / X_std theta1_raw final_theta[1] / X_std print(f\n对应原始特征房屋面积的参数) print(f原始截距 theta0_raw {theta0_raw:.4f} (万)) print(f原始斜率 theta1_raw {theta1_raw:.4f} (万/平米)) print(f\n真实参数为: theta0 {true_theta0}, theta1 {true_theta1}) print(f模型学习到的参数非常接近真实值)你会发现学习到的theta0_raw和theta1_raw非常接近我们生成数据时使用的真实参数(50, 0.8)。这证明了我们的模型是有效的。最后让我们把学习到的直线画出来看看拟合效果。# 生成预测值使用原始数据X_raw进行预测但用转换后的参数 X_raw_sorted np.sort(X_raw) # 排序是为了画线平滑 # 计算预测值使用转换回原始尺度的参数 y_pred_raw theta0_raw theta1_raw * X_raw_sorted # 或者使用标准化数据预测再转换回来两种方式等价 # X_sorted_scaled (X_raw_sorted - X_mean) / X_std # y_pred_scaled final_theta[0] final_theta[1] * X_sorted_scaled # y_pred_raw y_pred_scaled # 注意这里y没有缩放所以预测值就是y_pred_scaled plt.figure(figsize(12, 5)) # 子图1原始数据与拟合直线 plt.subplot(1, 2, 1) plt.scatter(X_raw, y, alpha0.7, labelTraining Data) plt.plot(X_raw_sorted, y_pred_raw, colorred, linewidth3, labelLinear Regression Fit) plt.xlabel(House Area (sqm)) plt.ylabel(House Price (10k yuan)) plt.title(Linear Regression Fit on Original Data) plt.legend() plt.grid(True) # 子图2损失函数下降曲线 plt.subplot(1, 2, 2) plt.plot(range(iterations), cost_history, colorblue) plt.xlabel(Iteration Number) plt.ylabel(Cost J) plt.title(Gradient Descent Convergence) plt.grid(True) plt.tight_layout() plt.show()左边的图展示了我们学习到的那条红色直线如何穿过数据点的中心右边的图展示了损失函数随着迭代次数增加而平稳下降的过程这表明我们的梯度下降算法运行良好。4. 避坑指南学习率、特征与收敛性自己实现一遍后你可能会遇到一些问题。下面是我总结的几个关键点和排查技巧。4.1 学习率α太大还是太小学习率是梯度下降中最重要的超参数没有之一。我们可以通过画图来直观感受不同学习率的影响。def run_gd_and_plot(alpha, num_iters100): theta np.array([0.0, 0.0]) _, cost_history gradient_descent(X, y, theta, alpha, num_iters) plt.plot(range(num_iters), cost_history, labelfα{alpha}) plt.figure(figsize(10, 6)) learning_rates [0.001, 0.01, 0.1, 0.5] for lr in learning_rates: run_gd_and_plot(lr, 100) plt.xlabel(Iteration) plt.ylabel(Cost J) plt.title(Effect of Learning Rate on Convergence) plt.legend() plt.grid(True) plt.yscale(log) # 使用对数坐标轴更容易观察变化 plt.show()α 0.001太小曲线下降非常缓慢需要极多的迭代次数才能收敛。计算资源浪费。α 0.01合适曲线平滑、稳定地下降到最低点这是我们期望的效果。α 0.1偏大曲线下降很快但可能在最低点附近震荡。有时也能接受。α 0.5太大损失函数值不仅不下降反而可能爆炸式增长在图中会直接飞出去因为对数坐标下负值或极大值无法显示。这是因为每一步迈得太大直接跳到了损失函数更高的地方并且越跳越高导致发散。实操心得如何选择学习率一个实用的方法是进行学习率扫描。尝试一系列呈指数增长的值如[0.001, 0.003, 0.01, 0.03, 0.1, 0.3]。观察损失函数在前100次迭代内的下降情况。选择那个能使损失函数快速且平稳下降的最大学习率。也可以使用自适应学习率算法如Adam但理解固定学习率是基础。4.2 特征工程当直线不够用的时候单变量线性回归假设关系是线性的。但如果真实关系是非线性的呢比如房价和面积可能不是严格的直线关系小户型的单价可能更高。这时强行用直线拟合效果会很差。解决方案是特征工程我们创造新的特征。例如我们可以把面积x的平方x²也作为一个新特征。这样我们的假设函数就变成了h_θ(x) θ₀ θ₁*x θ₂*x²这本质上是在用一条二次曲线来拟合数据但它对于模型形式来说仍然是“线性”的因为它是关于参数θ的线性函数。这种方法可以推广到更高次的多项式。# 多项式特征示例使用二次项 X_poly np.column_stack((X, X**2)) # 现在X_poly有两个特征X 和 X^2 # 注意此时X_poly需要重新标准化或者分别标准化。更通用的做法是使用PolynomialFeatures。这就是从单变量线性回归迈向多项式回归乃至更复杂模型的第一步。理解这一点你就理解了模型灵活性的来源。4.3 常见问题速查表问题现象可能原因排查与解决方法损失函数J不下降或下降极其缓慢。1. 学习率α太小。2. 特征未标准化不同特征尺度差异巨大。3. 代码有Bug如梯度计算错误、参数未同时更新。1. 增大α尝试0.01, 0.03, 0.1。2. 检查并确保对每个特征进行了标准化减去均值除以标准差。3. 用一个小数据集如3个点手动计算前几步的损失和梯度与代码输出对比。损失函数J震荡下降或突然变成NaN无穷大。1. 学习率α太大。2. 特征标准化时除数为0某个特征标准差为0。1. 显著减小α尝试0.001, 0.0001。2. 检查数据如果某个特征所有值相同方差为0则直接移除该特征。模型在训练集上表现很好但对新数据预测很差。过拟合。在多项式回归中尤其常见模型过于复杂拟合了数据中的噪声。1. 获取更多训练数据。2. 减少特征数量如降低多项式次数。3. 使用正则化如岭回归、Lasso在损失函数中加入对参数大小的惩罚项。预测结果存在系统性偏差全部偏高或偏低。可能忽略了重要的特征或关系不是线性的。1. 检查残差图预测值 vs 误差。如果呈现规律性如U型说明存在非线性。2. 尝试添加多项式特征或交互项。4.4 向量化实现提升计算效率我们上面的实现使用了for循环来计算梯度和求和。当数据量m很大时这会很慢。在NumPy中我们可以利用向量化操作来一次性完成所有计算这能极大提升代码效率也是实际项目中的标准写法。向量化的核心思想是将整个训练集视为矩阵用矩阵运算代替循环。def gradient_descent_vectorized(X, y, theta, alpha, num_iters): 向量化实现的梯度下降 m len(y) # 为了便于向量化我们在X前面添加一列1对应theta0 X_b np.c_[np.ones((m, 1)), X] # X_b 形状: (m, 2) cost_history [] for i in range(num_iters): # 向量化计算预测值和误差 predictions X_b.dot(theta) # (m,2) * (2,1) - (m,1) 广播后得到 (m,) error predictions - y # 向量化计算梯度 (1/m) * X_b.T.dot(error) # X_b.T 形状 (2, m), error 形状 (m,), 点积后形状 (2,) gradients (1 / m) * X_b.T.dot(error) # 更新参数 theta theta - alpha * gradients # 计算损失也可向量化 cost (1 / (2 * m)) * np.sum(error ** 2) cost_history.append(cost) return theta, cost_history # 使用向量化版本 initial_theta_vec np.array([0.0, 0.0]) final_theta_vec, cost_history_vec gradient_descent_vectorized(X, y, initial_theta_vec, 0.01, 5000) print(f向量化结果: {final_theta_vec})向量化代码不仅更简洁而且由于底层调用的是高度优化的C/Fortran库运行速度比纯Python循环快成百上千倍。这是机器学习工程实践中必须掌握的技能。单变量线性回归就像一颗种子它简单却包含了机器学习森林的所有遗传密码。理解它如何从数据中学习参数如何通过优化损失函数来改进以及梯度下降这个核心优化器是如何工作的为你打开了理解更复杂模型的大门。当你下次看到神经网络中那庞大的参数数量和复杂的反向传播算法时请记住它的本质思想和我们今天手动实现的这个简单线性模型是一脉相承的——都是在一堆数据中寻找一组参数使得预测的误差最小。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻