numpy 编写梯度下降的简单例子

根据房屋面积,预测房屋价格。

这样你可以完整看到:

\[\boxed{\text{现实问题}\rightarrow\text{数学模型}\rightarrow\text{损失函数}\rightarrow\text{偏导数}\rightarrow\text{梯度}\rightarrow\text{梯度下降}\rightarrow\text{NumPy程序}} \]

而且这个例子正好能把你最近学习的向量、偏导、梯度、方向全部串起来。


一、先从现实问题开始

假设我们有 5 套房子的历史数据:

房屋面积 房价
50 m² 102 万
70 m² 139 万
90 m² 181 万
110 m² 218 万
130 m² 261 万

我们希望得到一个程序:

输入房屋面积 → 自动预测房价。

最简单的模型是假设:

\[\boxed{\hat y=wx+b} \]

其中:

  • (x):房屋面积
  • (\hat y):模型预测价格
  • (w):面积对价格的影响程度
  • (b):基础价格
  • (w,b):我们真正需要“学习”的参数

为了让数值更容易处理,我们把面积单位改成 10 m²:

50 m²  → 5
70 m²  → 7
90 m²  → 9
110 m² → 11
130 m² → 13

于是:

x = np.array([5., 7., 9., 11., 13.])
y = np.array([102., 139., 181., 218., 261.])

这里 x 和 y 都已经是 NumPy 向量。


二、我们到底要“下降”什么?

这里是理解梯度下降最关键的一步。

模型:

\[\hat y=wx+b \]

但是我们一开始根本不知道 (w) 和 (b) 应该是多少。

比如随便先设:

\[w=0,\quad b=0 \]

那么所有房子的预测价格都是:

\[\hat y=0 \]

显然非常差。

所以我们需要一个办法衡量:

当前的 (w,b) 到底有多差?

于是引入一个函数:

\[\boxed{J(w,b)} \]

它叫损失函数(Loss Function)。

我们可以使用最经典的均方误差:

\[\boxed{ J(w,b)=\frac1n\sum_{i=1}^{n}(\hat y_i-y_i)^2 } \]

由于:

\[\hat y_i=wx_i+b \]

所以:

\[\boxed{ J(w,b)= \frac1n \sum_{i=1}^{n} (wx_i+b-y_i)^2 } \]

注意这里发生了一个非常重要的变化:

原来的:

\[f(x)=x^2 \]

只有一个变量。

现在:

\[J(w,b) \]

有两个变量。

所以它是一个二维曲面。


三、为什么要平方?

例如真实价格:

\[y=100 \]

预测:

\[\hat y=120 \]

误差:

\[20 \]

如果直接把误差加起来:

\[20+(-20)=0 \]

正负误差可能互相抵消。

所以平方:

\[20^2=400 \]

于是所有误差都会变成正数,而且较大的错误会受到更大的惩罚。


四、我们真正想做的事情

我们的目标就是:

\[\boxed{\min_{w,b}J(w,b)} \]

也就是:

找到一组 (w,b),让损失函数 (J) 尽可能小。

这就是一个典型的优化问题。


五、梯度到底是什么?

现在来到你正在学习的核心。

因为:

\[J=J(w,b) \]

所以它分别对 (w)、(b) 有偏导数:

\[\frac{\partial J}{\partial w} \]

和:

\[\frac{\partial J}{\partial b} \]

把它们放在一起:

\[\boxed{ \nabla J= \begin{bmatrix} \frac{\partial J}{\partial w}\\ \frac{\partial J}{\partial b} \end{bmatrix} } \]

这就是:

\[\boxed{\text{梯度}} \]

你前面学的“梯度是由各个方向的偏导数组成的向量”,在这里就真正落地了。


六、把梯度推导出来

我们从:

\[J(w,b) = \frac1n \sum_{i=1}^{n} (wx_i+b-y_i)^2 \]

开始。

先定义:

\[e_i=wx_i+b-y_i \]

那么:

\[J=\frac1n\sum e_i^2 \]


1. 对 (w) 求偏导

根据链式法则:

\[\frac{\partial e_i^2}{\partial w} = 2e_i\frac{\partial e_i}{\partial w} \]

而:

\[e_i=wx_i+b-y_i \]

所以:

\[\frac{\partial e_i}{\partial w}=x_i \]

因此:

\[\frac{\partial J}{\partial w} = \frac2n \sum_{i=1}^{n} e_i x_i \]

代回:

\[\boxed{ \frac{\partial J}{\partial w} = \frac2n \sum_{i=1}^{n} (wx_i+b-y_i)x_i } \]


2. 对 (b) 求偏导

同理:

\[\frac{\partial e_i}{\partial b}=1 \]

所以:

\[\boxed{ \frac{\partial J}{\partial b} = \frac2n \sum_{i=1}^{n} (wx_i+b-y_i) } \]

于是梯度就是:

\[\boxed{ \nabla J= \begin{bmatrix} \frac2n\sum(wx_i+b-y_i)x_i\\[4pt] \frac2n\sum(wx_i+b-y_i) \end{bmatrix} } \]

这就是我们程序中的核心数学公式。


七、为什么“沿着负梯度方向”走?

这是理解梯度下降的灵魂。

梯度:

\[\nabla J \]

指向的是:

\[\boxed{\text{函数上升最快的方向}} \]

我们现在的目的不是让损失增加,而是让损失减少。

所以应该朝着:

\[\boxed{-\nabla J} \]

方向走。

这就是为什么梯度下降公式是:

\[\boxed{ \theta_{\text{new}} = \theta_{\text{old}} -\eta\nabla J } \]

这里:

  • (\theta):参数
  • (\eta):学习率
  • (\nabla J):当前梯度

对于我们的两个参数:

\[\boxed{ w_{\text{new}} = w-\eta\frac{\partial J}{\partial w} } \]

\[\boxed{ b_{\text{new}} = b-\eta\frac{\partial J}{\partial b} } \]


八、把数学公式直接翻译成 NumPy

完整程序:

import numpy as np

# 训练数据
x = np.array([5., 7., 9., 11., 13.])
y = np.array([102., 139., 181., 218., 261.])

# 初始化参数
w = 0.0
b = 0.0

# 学习率
learning_rate = 0.001

# 梯度下降
for epoch in range(10000):

    # 1. 计算预测值
    y_pred = w * x + b

    # 2. 计算误差
    error = y_pred - y

    # 3. 计算梯度
    dw = (2 / len(x)) * np.sum(error * x)
    db = (2 / len(x)) * np.sum(error)

    # 4. 更新参数
    w = w - learning_rate * dw
    b = b - learning_rate * db

print("w =", w)
print("b =", b)

运行后会得到接近:

w = 19.84
b = 1.63

所以模型大约变成:

\[\boxed{\hat y=19.84x+1.63} \]

记得我们的 (x) 单位是 10 m²。

因此:

\[w\approx19.84 \]

实际上可以理解成:

每增加 10 m²,模型预测价格大约增加 19.84 万元。


九、逐行理解 Python / NumPy 语法

现在我们不要把这个程序当成一个整体,而是逐条拆开。

① import numpy as np

import numpy as np

意思:

导入 NumPy,并给它取一个简称 np。

所以:

np.array()
np.sum()

实际上就是调用 NumPy 里的函数。


② np.array()

x = np.array([5., 7., 9., 11., 13.])

创建一个 NumPy 数组:

\[x= \begin{bmatrix} 5&7&9&11&13 \end{bmatrix} \]

可以查看:

print(x.shape)

得到:

(5,)

注意:

(5,)

是一个 一维数组,不是数学上严格区分的行向量或列向量。

这是 NumPy 初学者非常容易混淆的地方。


十、最值得你理解的一行:w * x + b

y_pred = w * x + b

假设:

w = 10
b = 2

而:

x = [5, 7, 9]

那么 NumPy 自动计算:

10 * [5, 7, 9] + 2

变成:

[50, 70, 90] + 2

再因为 broadcasting:

[50, 70, 90]
+
[2,  2,  2]

结果:

[52, 72, 92]

这就是:

\[\hat y_i=wx_i+b \]

被 NumPy 一次性计算完。

这就是所谓的向量化计算。

你没有写:

for i in range(len(x)):

但是 NumPy 帮你对整个向量进行了运算。


十一、error = y_pred - y

error = y_pred - y

数学:

\[e_i=\hat y_i-y_i \]

例如:

预测:[52, 72, 92]
真实:[50, 70, 100]

得到:

误差:[2, 2, -8]

NumPy 也是逐元素相减。


十二、这一行非常重要

dw = (2 / len(x)) * np.sum(error * x)

不要把它看成“Python 技巧”。

它实际上就是数学公式:

\[\frac{\partial J}{\partial w} = \frac2n \sum e_i x_i \]

对应关系:

数学                         NumPy

n                            len(x)

e_i x_i                      error * x

Σ                            np.sum(...)

2/n                          2 / len(x)

因此:

np.sum(error * x)

就是:

\[\sum_i e_i x_i \]

这正是你现在应该训练的思维:

看到程序,能够反推出数学公式。


十三、db

db = (2 / len(x)) * np.sum(error)

对应:

\[\frac{\partial J}{\partial b} = \frac2n\sum e_i \]

所以:

np.sum(error)

就是:

\[e_1+e_2+\cdots+e_n \]


十四、最核心的一行

w = w - learning_rate * dw

数学:

\[w_{t+1} = w_t-\eta\frac{\partial J}{\partial w} \]

同理:

b = b - learning_rate * db

数学:

\[b_{t+1} = b_t-\eta\frac{\partial J}{\partial b} \]

这就是梯度下降。

所以你以后看到:

parameter -= learning_rate * gradient

脑子里应该立刻出现:

\[\boxed{\theta\leftarrow\theta-\eta\nabla J} \]


十五、为什么需要 learning_rate?

假设:

\[x_{\text{new}}=x-\eta f'(x) \]

如果:

\[\eta \]

非常小:

0.000001

那么每一步只走一点点。

可能:

走得非常慢

如果太大:

10

可能:

跨过最低点
      ↓
      ↘
       ↗
      ↙
       ↗

甚至直接发散。

所以学习率控制的是:

\[\boxed{\text{每次沿梯度走多远}} \]

这就是它为什么叫:

learning rate


十六、把它和你前面学的二维梯度联系起来

你之前看到的是:

\[f(x,y)=x^2+y^2 \]

梯度:

\[\nabla f= \begin{bmatrix} 2x\\ 2y \end{bmatrix} \]

现在我们的损失函数:

\[J(w,b) \]

所以:

\[\nabla J= \begin{bmatrix} \frac{\partial J}{\partial w}\\ \frac{\partial J}{\partial b} \end{bmatrix} \]

其实完全是同一个数学结构。

只是:

原来:

变量 = x, y


现在:

变量 = w, b

所以可以把参数看成一个向量:

\[\boxed{ \theta= \begin{bmatrix} w\\ b \end{bmatrix} } \]

于是整个算法就可以写成:

\[\boxed{ \theta_{t+1} = \theta_t-\eta\nabla J(\theta_t) } \]

这才是机器学习中真正通用的形式。


十七、进一步把程序也写成“向量形式”

这一步非常适合你现在学。

可以把:

w
b

组合成:

theta = np.array([w, b])

于是参数本身就是一个向量:

\[\theta= \begin{bmatrix} w\\ b \end{bmatrix} \]

梯度也是:

\[\nabla J= \begin{bmatrix} \frac{\partial J}{\partial w}\\ \frac{\partial J}{\partial b} \end{bmatrix} \]

那么更新就是:

theta = theta - learning_rate * gradient

对应:

\[\boxed{ \theta\leftarrow\theta-\eta\nabla J } \]

这就是你前面学习的:

向量沿某个方向移动。

梯度下降在几何上的本质就是:

\[\boxed{ \text{当前位置} + \text{一个方向} \times \text{一个步长} } \]

只不过这个方向是:

\[-\nabla J \]


十八、为什么梯度下降最终能找到一条“合适的直线”?

把 (w,b) 当成平面上的两个坐标:

\[(w,b) \]

对于每一组 (w,b),都有一个损失值:

\[J(w,b) \]

所以实际上形成了一个三维曲面:

\[(w,b,J) \]

你可以想象成一座山谷:

                 J
                 ↑
              ╭────╮
           ╭──╯    ╰──╮
         ╭─╯          ╰─╮
        ╱                ╲
       ╱       ↓          ╲
      ╱      最低点         ╲
     ╱________________________╲
            w        b

梯度:

\[\nabla J \]

指向上升最快的方向。

所以:

\[-\nabla J \]

就是下降最快的方向。

于是程序不断:

当前位置
   ↓
计算梯度
   ↓
沿负梯度走一步
   ↓
新位置
   ↓
重新计算梯度
   ↓
再走一步
   ↓
……

最后来到一个低点。


十九、这就是“训练模型”的本质

你以后会看到很多看起来非常复杂的代码:

loss.backward()
optimizer.step()

实际上它们背后干的事情,就是:

计算损失
   ↓
求梯度
   ↓
沿负梯度方向移动参数
   ↓
重新计算

只不过神经网络拥有:

\[w_1,w_2,w_3,\ldots,w_N \]

甚至几十亿、几千亿个参数。

于是:

\[\theta= \begin{bmatrix} w_1\\ w_2\\ \vdots\\ w_N \end{bmatrix} \]

梯度:

\[\nabla J= \begin{bmatrix} \frac{\partial J}{\partial w_1}\\ \frac{\partial J}{\partial w_2}\\ \vdots\\ \frac{\partial J}{\partial w_N} \end{bmatrix} \]

仍然只是:

\[\boxed{ \theta\leftarrow\theta-\eta\nabla J } \]

所以你现在学习这个小型房价模型并不是在学一个“简单例子”,而是在学习现代神经网络训练的核心数学骨架。


二十、把整个过程压缩成一条主线

你现在可以把这套知识牢牢记成:

\[\boxed{ \text{数据} \rightarrow \text{模型} \rightarrow \text{损失函数} \rightarrow \text{偏导} \rightarrow \text{梯度} \rightarrow -\text{梯度} \rightarrow \text{参数更新} } \]

对应代码:

y_pred = w * x + b

↓

error = y_pred - y

↓

dw = (2 / len(x)) * np.sum(error * x)
db = (2 / len(x)) * np.sum(error)

↓

w = w - learning_rate * dw
b = b - learning_rate * db

这四行代码,实际上已经包含了:

线性模型 + 损失函数 + 链式法则 + 偏导数 + 梯度 + 向量化计算 + 梯度下降。


下一步最值得做的练习

把这个例子从一个特征“房屋面积”升级成两个特征“面积 + 房间数”:

\[\hat y=w_1x_1+w_2x_2+b \]

然后自己推导:

\[\frac{\partial J}{\partial w_1}, \quad \frac{\partial J}{\partial w_2}, \quad \frac{\partial J}{\partial b} \]

再用 NumPy 写出来。

这一步会非常自然地把你从目前的“二维梯度”

\[\nabla f(x,y) \]

推进到机器学习真正使用的参数梯度向量,也是理解后面 PyTorch backward() 最关键的一步。

posted @ 2026-09-10 21:21  立体风  阅读(16)  评论(0)    收藏  举报