numpy 编写梯度下降的简单例子
根据房屋面积,预测房屋价格。
这样你可以完整看到:
而且这个例子正好能把你最近学习的向量、偏导、梯度、方向全部串起来。
一、先从现实问题开始
假设我们有 5 套房子的历史数据:
| 房屋面积 | 房价 |
|---|---|
| 50 m² | 102 万 |
| 70 m² | 139 万 |
| 90 m² | 181 万 |
| 110 m² | 218 万 |
| 130 m² | 261 万 |
我们希望得到一个程序:
输入房屋面积 → 自动预测房价。
最简单的模型是假设:
其中:
- (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 向量。
二、我们到底要“下降”什么?
这里是理解梯度下降最关键的一步。
模型:
但是我们一开始根本不知道 (w) 和 (b) 应该是多少。
比如随便先设:
那么所有房子的预测价格都是:
显然非常差。
所以我们需要一个办法衡量:
当前的 (w,b) 到底有多差?
于是引入一个函数:
它叫损失函数(Loss Function)。
我们可以使用最经典的均方误差:
由于:
所以:
注意这里发生了一个非常重要的变化:
原来的:
只有一个变量。
现在:
有两个变量。
所以它是一个二维曲面。
三、为什么要平方?
例如真实价格:
预测:
误差:
如果直接把误差加起来:
正负误差可能互相抵消。
所以平方:
于是所有误差都会变成正数,而且较大的错误会受到更大的惩罚。
四、我们真正想做的事情
我们的目标就是:
也就是:
找到一组 (w,b),让损失函数 (J) 尽可能小。
这就是一个典型的优化问题。
五、梯度到底是什么?
现在来到你正在学习的核心。
因为:
所以它分别对 (w)、(b) 有偏导数:
和:
把它们放在一起:
这就是:
你前面学的“梯度是由各个方向的偏导数组成的向量”,在这里就真正落地了。
六、把梯度推导出来
我们从:
开始。
先定义:
那么:
1. 对 (w) 求偏导
根据链式法则:
而:
所以:
因此:
代回:
2. 对 (b) 求偏导
同理:
所以:
于是梯度就是:
这就是我们程序中的核心数学公式。
七、为什么“沿着负梯度方向”走?
这是理解梯度下降的灵魂。
梯度:
指向的是:
我们现在的目的不是让损失增加,而是让损失减少。
所以应该朝着:
方向走。
这就是为什么梯度下降公式是:
这里:
- (\theta):参数
- (\eta):学习率
- (\nabla J):当前梯度
对于我们的两个参数:
八、把数学公式直接翻译成 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
所以模型大约变成:
记得我们的 (x) 单位是 10 m²。
因此:
实际上可以理解成:
每增加 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 数组:
可以查看:
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]
这就是:
被 NumPy 一次性计算完。
这就是所谓的向量化计算。
你没有写:
for i in range(len(x)):
但是 NumPy 帮你对整个向量进行了运算。
十一、error = y_pred - y
error = y_pred - y
数学:
例如:
预测:[52, 72, 92]
真实:[50, 70, 100]
得到:
误差:[2, 2, -8]
NumPy 也是逐元素相减。
十二、这一行非常重要
dw = (2 / len(x)) * np.sum(error * x)
不要把它看成“Python 技巧”。
它实际上就是数学公式:
对应关系:
数学 NumPy
n len(x)
e_i x_i error * x
Σ np.sum(...)
2/n 2 / len(x)
因此:
np.sum(error * x)
就是:
这正是你现在应该训练的思维:
看到程序,能够反推出数学公式。
十三、db
db = (2 / len(x)) * np.sum(error)
对应:
所以:
np.sum(error)
就是:
十四、最核心的一行
w = w - learning_rate * dw
数学:
同理:
b = b - learning_rate * db
数学:
这就是梯度下降。
所以你以后看到:
parameter -= learning_rate * gradient
脑子里应该立刻出现:
十五、为什么需要 learning_rate?
假设:
如果:
非常小:
0.000001
那么每一步只走一点点。
可能:
走得非常慢
如果太大:
10
可能:
跨过最低点
↓
↘
↗
↙
↗
甚至直接发散。
所以学习率控制的是:
这就是它为什么叫:
learning rate
十六、把它和你前面学的二维梯度联系起来
你之前看到的是:
梯度:
现在我们的损失函数:
所以:
其实完全是同一个数学结构。
只是:
原来:
变量 = x, y
现在:
变量 = w, b
所以可以把参数看成一个向量:
于是整个算法就可以写成:
这才是机器学习中真正通用的形式。
十七、进一步把程序也写成“向量形式”
这一步非常适合你现在学。
可以把:
w
b
组合成:
theta = np.array([w, b])
于是参数本身就是一个向量:
梯度也是:
那么更新就是:
theta = theta - learning_rate * gradient
对应:
这就是你前面学习的:
向量沿某个方向移动。
梯度下降在几何上的本质就是:
只不过这个方向是:
十八、为什么梯度下降最终能找到一条“合适的直线”?
把 (w,b) 当成平面上的两个坐标:
对于每一组 (w,b),都有一个损失值:
所以实际上形成了一个三维曲面:
你可以想象成一座山谷:
J
↑
╭────╮
╭──╯ ╰──╮
╭─╯ ╰─╮
╱ ╲
╱ ↓ ╲
╱ 最低点 ╲
╱________________________╲
w b
梯度:
指向上升最快的方向。
所以:
就是下降最快的方向。
于是程序不断:
当前位置
↓
计算梯度
↓
沿负梯度走一步
↓
新位置
↓
重新计算梯度
↓
再走一步
↓
……
最后来到一个低点。
十九、这就是“训练模型”的本质
你以后会看到很多看起来非常复杂的代码:
loss.backward()
optimizer.step()
实际上它们背后干的事情,就是:
计算损失
↓
求梯度
↓
沿负梯度方向移动参数
↓
重新计算
只不过神经网络拥有:
甚至几十亿、几千亿个参数。
于是:
梯度:
仍然只是:
所以你现在学习这个小型房价模型并不是在学一个“简单例子”,而是在学习现代神经网络训练的核心数学骨架。
二十、把整个过程压缩成一条主线
你现在可以把这套知识牢牢记成:
对应代码:
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
这四行代码,实际上已经包含了:
线性模型 + 损失函数 + 链式法则 + 偏导数 + 梯度 + 向量化计算 + 梯度下降。
下一步最值得做的练习
把这个例子从一个特征“房屋面积”升级成两个特征“面积 + 房间数”:
然后自己推导:
再用 NumPy 写出来。
这一步会非常自然地把你从目前的“二维梯度”
推进到机器学习真正使用的参数梯度向量,也是理解后面 PyTorch backward() 最关键的一步。

浙公网安备 33010602011771号