numpy 编写梯度下降的简单例子(优化版)
对之前的《numpy编写梯度下降的简单例子》进行了修正,在这里进行总结。由于有了一下变动,但前文也有可取之处,故重开一篇文章,对照学习,能更加深刻理解梯度下降。
整体说,这篇教程主线非常好:从房价问题引出线性模型、均方误差、偏导、梯度、梯度下降和 NumPy 实现,逻辑完整,也很适合初学者。但有几个地方需要修正或补充,否则容易留下隐患。
一、主要错误与需要修正的地方
1. 数据本身几乎完全线性,导致“学习”效果看起来过于完美
原始数据:
| 面积 x | 房价 y |
|---|---|
| 5 | 102 |
| 7 | 139 |
| 9 | 181 |
| 11 | 218 |
| 13 | 261 |
这些点几乎落在一条直线上,所以梯度下降会很容易得到很好的结果。这本身不是错误,但作为教学例子,最好说明:
这是一个人为构造的近似线性数据,所以线性模型效果很好。真实房价数据通常不会这么整齐。
否则初学者可能误以为真实问题也总是这样。
2. w = 19.84, b = 1.63 与数据并不完全吻合
用最小二乘可以算出更精确的结果。对给定数据:
x = [5, 7, 9, 11, 13]
y = [102, 139, 181, 218, 261]
均值为:
计算斜率:
可得:\(w \approx 19.8\)
所以更合理的结果大约是:\(w \approx 19.8,\quad b \approx 2.0\)
原文写:
w = 19.84
b = 1.63
这个 b 偏差较大。虽然梯度下降如果迭代不够、学习率不合适,可能得到近似值,但最好改成更准确的结果,或者说明“运行结果会接近”。
建议改为:
w ≈ 19.8
b ≈ 2.0
模型:\(\hat y \approx 19.8x + 2.0\)
由于 (x) 的单位是 10 m²,所以解释为:
每增加 10 m²,房价大约增加 19.8 万元。
3. 学习率 0.001 和迭代次数 10000 需要检查
对于这个数据,learning_rate = 0.001 偏小,但 10000 次迭代通常可以收敛。不过更好的做法是:
- 使用
0.01或0.005; - 或者打印损失,观察是否收敛;
- 或者说明学习率需要调参。
原文没有展示损失变化,初学者不知道是否真的收敛。建议补充:
if epoch % 1000 == 0:
loss = np.mean(error ** 2)
print(epoch, loss)
这样更直观。
4. 均方误差公式中,是否除以 n 还是 2n 要统一
原文损失函数:\(J(w,b)=\frac1n\sum_{i=1}^n(\hat y_i-y_i)^2\)
偏导:
这是对的。
但很多教材会写成:
这样偏导就没有 2:
原文没有错,但需要明确说明:
本文采用 (\frac1n),所以梯度前面有 2。若采用 (\frac1{2n}),梯度前面的 2 会消失。
否则初学者看别的资料时会混淆。
5. “梯度指向函数上升最快的方向”需要加限定条件
原文说:
梯度指向函数上升最快的方向。
这是对的,但严格说:
梯度是函数在该点上升最快的方向,其大小是最大方向导数的值。
更准确地说:
- (\nabla J) 指向局部上升最快方向;
- (-\nabla J) 指向局部下降最快方向;
- 这是局部性质,不是全局性质。
建议补一句:
注意:这是局部性质。梯度下降每一步只保证在当前点附近下降最快,不保证一步到位,也不保证一定找到全局最优。
6. “梯度下降最终能找到一条合适的直线”需要补充前提
对于线性回归 + 均方误差,损失函数是凸函数,所以梯度下降(学习率合适时)可以收敛到全局最优。
但原文没有强调这一点。建议补充:
对于本文的线性回归和均方误差,(J(w,b)) 是凸函数,所以没有局部最优点陷阱。梯度下降只要学习率合适,就能收敛到全局最优。但神经网络中的损失函数通常不是凸的,所以不一定能找到全局最优。
这个补充非常重要。
7. np.array([5., 7., 9., 11., 13.]) 的 shape 是 (5,)
原文已经提到:
(5,)
是一维数组,不是严格的行向量或列向量。这个提醒很好。
但还可以补充:
在 NumPy 中,一维数组既可以参与按元素运算,也可以在矩阵乘法中根据上下文被解释。初学阶段先把它当作“一串数”即可。
8. 向量化更新部分可以更严谨
原文说可以把:
theta = np.array([w, b])
然后:
theta = theta - learning_rate * gradient
这很好。但需要说明:
这里的
gradient也必须是np.array([dw, db]),否则 NumPy 广播可能不符合预期。
即:
gradient = np.array([dw, db])
theta = theta - learning_rate * gradient
这样才完整。
9. “每增加 10 m²,模型预测价格大约增加 19.84 万元”需要检查单位
原文把面积单位改成 10 m²:
50 m² → 5
70 m² → 7
...
所以 \(x\) 的单位是 10 m²。
如果:\(\hat y = 19.84x + 1.63\)
那么 \(x\) 增加 1,表示面积增加 10 m²,房价增加 19.84 万元。
所以原文说:
每增加 10 m²,模型预测价格大约增加 19.84 万元。
这是对的。
但若用更准确的 \(w \approx 19.8\),则:
每增加 10 m²,房价大约增加 19.8 万元。
10. 可以补充“为什么要特征缩放”
原文把面积从 50、70、90 改成 5、7、9,其实已经做了一次简单的特征缩放。可以明确补充:
这样做是为了让数值更小,梯度下降更稳定。实际机器学习中,特征缩放是非常重要的一步。
二、重点与难点点评
重点 1:从现实问题到数学模型的转换
这是整篇教程最有价值的地方。它展示了:
这个转换是机器学习的核心思维:
把现实问题变成带参数的函数,然后通过数据学习参数。
重点 2:损失函数的意义
损失函数 \(J(w,b)\) 是衡量模型好坏的标尺。没有损失函数,就不知道往哪个方向优化。
初学者常问:
为什么不能直接解方程?
因为真实数据有噪声,而且模型可能不是简单线性方程。梯度下降是一种通用优化方法。
重点 3:偏导与梯度
梯度是偏导组成的向量:
这一步把多元微积分和机器学习连接起来了。
难点 1:链式法则求偏导
对 \(w\) 求偏导:
对 \(b\) 求偏导:
关键是:
初学者容易忘记 \(x_i\) 和 1 的来源。
难点 2:为什么沿负梯度方向走
梯度指向上升最快方向,所以负梯度指向下降最快方向。
这里:
- (\theta):参数;
- (\eta):学习率;
- (\nabla J):梯度。
难点 3:NumPy 向量化
y_pred = w * x + b
这一行同时计算所有样本的预测值。
dw = (2 / len(x)) * np.sum(error * x)
这一行同时计算:\(\sum e_i x_i\)
这就是向量化计算。它比 Python 循环快得多,也更接近数学公式。
三、润色后的全文
下面是重新整理、修正和润色后的版本。
从房价预测理解梯度下降:从现实问题到 NumPy 实现
这个例子非常适合初学者。我们把前面的简单函数:\(f(x)=x^2\)
升级成一个有现实意义的问题:
根据房屋面积,预测房屋价格。
这样你可以完整看到一条主线:
这个例子能把向量、偏导、梯度、方向、参数更新全部串起来。
一、从现实问题开始
假设我们有 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
于是:
import numpy as np
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\)
那么所有预测价格都是 0,显然很差。
所以需要一种方法衡量:
当前的 \(w,b\) 到底有多差?
于是引入损失函数:\(\boxed{J(w,b)}\)
它叫损失函数。
最经典的是均方误差:
因为:\(\hat y_i=wx_i+b\)
所以:
注意一个重要变化:
原来的:\(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(w,b)) 是凸函数,所以没有局部最优陷阱。只要学习率合适,梯度下降可以收敛到全局最优。但神经网络中的损失函数通常不是凸的,所以不一定能找到全局最优。
五、梯度到底是什么?
因为:
[
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}
}
]
这就是梯度。
你前面学的“梯度是由各个方向的偏导数组成的向量”,在这里真正落地了。
六、把梯度推导出来
从:
[
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}
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}
\frac2n
\sum_{i=1}^{n}
e_i x_i
]
代回:
[
\boxed{
\frac{\partial J}
\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}
\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}
}
]
这就是程序中的核心数学公式。
注意:本文采用 (J=\frac1n\sum e_i^2),所以梯度前面有 2。如果采用 (J=\frac1{2n}\sum e_i^2),梯度前面的 2 会消失。两种写法都常见,只要前后一致即可。
七、为什么“沿着负梯度方向”走?
梯度:
[
\nabla J
]
指向函数上升最快的方向。
更准确地说:
梯度是函数在该点局部上升最快的方向,其大小是最大方向导数的值。
我们的目标是让损失减少,所以应该朝着:
[
\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.01
# 梯度下降
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
# 可选:打印损失
if epoch % 1000 == 0:
loss = np.mean(error ** 2)
print(f"epoch={epoch}, loss={loss:.4f}")
print("w =", w)
print("b =", b)
运行后会得到接近:
w ≈ 19.8
b ≈ 2.0
所以模型大约是:
[
\boxed{\hat y \approx 19.8x+2.0}
]
由于 (x) 的单位是 10 m²,所以可以解释为:
每增加 10 m²,模型预测价格大约增加 19.8 万元。
九、逐行理解 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}
\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}
\frac2n\sum e_i
]
所以:
np.sum(error)
就是:
[
e_1+e_2+\cdots+e_n
]
十四、最核心的一行
w = w - learning_rate * dw
数学上:
[
w_
w_t-\eta\frac{\partial J}{\partial w}
]
同理:
b = b - learning_rate * db
数学上:
[
b_
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)=x2+y2
]
梯度:
[
\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_
\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}
]
更新就是:
gradient = np.array([dw, db])
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() 最关键的一步。
四、最终结论
这篇教程整体质量很好,适合初学者建立“从数学到代码”的完整认知。主要需要修正的是:
b的结果应更接近2.0,不是1.63;- 学习率可以改为
0.01,并打印损失观察收敛; - 明确均方误差是否带 (\frac12);
- 强调梯度是局部性质;
- 补充线性回归损失函数是凸函数这一重要前提;
- 向量形式更新时,
gradient也应是 NumPy 数组。
修正后,这篇教程可以成为一篇非常扎实的梯度下降入门材料。

浙公网安备 33010602011771号