模块 08 · 第 2 课

梯度下降

不再盲目地试,而是每一步都算出往哪个方向改参数、改多少。从斜率讲起理解导数,手算梯度,写出梯度下降的循环,再亲眼看看学习率太小、太大和数据没有标准化时会发生什么。

  • 约 45 分钟
  • 难度:入门
  • 实测:2026-09-14 numpy 2.5,固定随机种子

上一课用网格搜索找到了最好的直线,但也看到了它的死路:参数一多,要试的组合就是天文数字。

想象你蒙着眼睛站在一座山的山坡上,要走到山谷的最低点。你看不见整座山,但能用脚感觉到脚下的地面往哪边倾斜。那就往下坡的方向迈一小步,再感觉一下,再迈一步。一直走下去,最后就到了谷底。

这就是梯度下降(gradient descent)。"山"是损失,你站的位置是当前的参数,"往哪边倾斜"就是梯度。

导数:就是斜率

先只看一个参数 w,固定 b = 0。把 w 往右挪一点点,损失会变多少?变化量除以挪动的距离,就是损失在这一点的斜率。用数值的办法直接量一量:

def mse(w, b, x=area, y=price):
    return np.mean((w * x + b - y) ** 2)


w, b = 1.0, 0.0
for h in [0.1, 0.01, 0.001]:
    slope = (mse(w + h, b) - mse(w, b)) / h
    print(f"  w 往右挪 {h:<6},损失变化 / 挪动距离 = {slope:10.2f}")
== 1. 在 w=1.0, b=0 这一点,损失对 w 的斜率
  w 往右挪 0.1   ,损失变化 / 挪动距离 =   -7300.21
  w 往右挪 0.01  ,损失变化 / 挪动距离 =   -8256.23
  w 往右挪 0.001 ,损失变化 / 挪动距离 =   -8351.83

挪动的距离越小,量出来的斜率越稳定,逐渐接近一个固定的值。这个"挪动距离无限小时的斜率",就是导数。它告诉我们两件事:

  • 符号:这里是负数,意思是 w 往右挪(变大),损失会变小。所以应该把 w 调大。
  • 大小:绝对值很大,意思是这里的坡很陡,损失对 w 非常敏感。

梯度:每个参数一个导数

我们有两个参数,wb。对每个参数分别求导(求 w 的导数时把 b 当成常数,反过来也一样),得到的两个数合在一起,叫做梯度

用数值的办法量太慢,也不精确。好在均方误差的导数可以直接写出公式。损失是 平均((w×x + b - y)²),按求导的规则:

损失对 w 的导数 = 平均( 2 × (w×x + b - y) × x )
损失对 b 的导数 = 平均( 2 × (w×x + b - y) )

不需要记这个推导,只要知道它就是"把上面量斜率的过程,用代数一次算准"。写成代码:

def gradients(w, b, x=area, y=price):
    error = w * x + b - y
    dw = np.mean(2 * error * x)  # 损失对 w 的导数
    db = np.mean(2 * error)  # 损失对 b 的导数
    return dw, db
== 2. 公式算出的梯度:dw=-8362.45,db=-79.98

dw = -8362.45,和上面数值量出来的结果(挪动 0.001 时是 -8351.83)非常接近。这是一个好习惯:自己推导的梯度公式,用数值的办法核对一下,能发现很多错误。下一课写反向传播时,我们还会这样做。

梯度下降的循环

梯度指向损失增加最快的方向。我们要让损失减小,就往梯度的反方向走一小步:

w = w - 学习率 × dw
b = b - 学习率 × db

学习率(learning rate)控制每一步迈多大。从 w = 0, b = 0 出发,重复这一步:

def descend(lr, steps, x=area, y=price, report=()):
    """从 w=0, b=0 出发走 steps 步。发散时返回 None。"""
    w, b = 0.0, 0.0
    for step in range(1, steps + 1):
        dw, db = gradients(w, b, x, y)
        w -= lr * dw
        b -= lr * db
        if abs(w) > 1e6:  # 越走越远,已经发散了
            print(f"  第 {step} 步:w 已经变成 {w:.3g},发散了")
            return None
        if step in report:
            print(f"  第 {step:5d} 步:w={w:7.4f} b={b:7.3f} 损失 {mse(w, b, x, y):10.2f}")
    return w, b

这就是训练的全部核心逻辑。第 09 模块训练 GPT 时,循环的结构一模一样,只是参数多了、算梯度的办法换成了下一课的反向传播。

走一走:没那么顺利

用学习率 0.00005 走 2 万步:

== 3. 学习率 0.00005,走 20000 步
  第     1 步:w= 1.4804 b=  0.014 损失     244.38
  第    10 步:w= 1.3935 b=  0.014 损失     164.22
  第   100 步:w= 1.3934 b=  0.027 损失     164.18
  第  1000 步:w= 1.3922 b=  0.155 损失     163.81
  第  5000 步:w= 1.3871 b=  0.711 损失     162.27
  第 20000 步:w= 1.3695 b=  2.615 损失     157.43

第一步,损失就从几千降到了 244;10 步之后降到 164。然后就几乎不动了:走了 2 万步,损失才降到 157,而上一课网格搜索找到的最低损失是 143。b 更是慢得出奇,2 万步只从 0 挪到了 2.6,离最好的值(约 14.5)还很远。

为什么会这样?下面两个实验会揭开原因。

学习率:太小太慢,太大就炸

换几个学习率,都走 1000 步:

== 4. 不同的学习率(都走 1000 步)
  学习率 1e-05  :w=1.3932 b=0.041 损失 164.14
  学习率 5e-05  :w=1.3922 b=0.155 损失 163.81
  学习率 9e-05  :w=1.3912 b=0.268 损失 163.50
  第 115 步:w 已经变成 1.03e+06,发散了
  学习率 0.0001 :发散

学习率越大,走得越快,b 挪得越远。可学习率从 0.00009 提高到 0.0001,只差了一点点,就彻底炸了:第 115 步,w 变成了一百多万。

发散是这样发生的:w 方向的坡太陡(上面量到斜率是 -8362),步子稍微大一点,一步就跨过了谷底,落到对面更高的地方;那里的坡更陡,下一步跨得更远……每一步都比上一步离谷底更远,最后越来越大,直到数值溢出。

问题的根源在于:w 方向的坡太陡,b 方向的坡太缓dw 是 -8362,db 只有 -80,差了一百倍。学习率要小到不让 w 发散,b 就只能挪得极慢。一个学习率没法同时照顾两个方向。

标准化:让每个方向的坡差不多陡

w 方向的坡为什么这么陡?因为面积的数值很大,几十到一百多平方米。梯度公式里 dw = 平均(2 × 误差 × x)x 越大,dw 就越大。

办法是把面积换算成"均值为 0、标准差为 1"的数,这叫标准化

mean, std = area.mean(), area.std()
area_n = (area - mean) / std

标准化之后,面积的数值都在 -2 到 2 左右,wb 两个方向的坡差不多陡了。这时学习率可以放心地用 0.1,只走 100 步:

== 5. 面积标准化之后(均值 98.0,标准差 32.1),学习率 0.1 只走 100 步
  第     1 步:w= 8.0605 b= 27.588 损失   13360.27
  第    10 步:w=35.9749 b=123.129 损失     381.15
  第    50 步:w=40.3018 b=137.939 损失     143.05
  第   100 步:w=40.3023 b=137.941 损失     143.05
  换算回原单位:w=1.2571 b=14.806

50 步就到了最低点,损失 143.05,比上一课网格搜索的 143.1 还低一点(网格搜索每隔 0.01 才试一次,最好的点可能落在两个格子之间)。换算回原来的单位,w = 1.2571, b = 14.806

之前 2 万步都做不到的事,标准化之后 50 步就完成了。这是训练神经网络时一个非常实用的经验:输入的数值范围要差不多。以后你会看到的各种"归一化"(LayerNorm、BatchNorm),出发点都和这里一样。

小结一下梯度下降

  • 损失对每个参数的导数合起来叫梯度,它指向损失增加最快的方向。
  • 每一步都往梯度的反方向走一小步,步长由学习率决定。
  • 学习率太小,走得慢;太大,会发散。它是训练中最需要调的一个数。
  • 输入的数值范围差别很大时,训练会很难调,先做标准化。

我们的直线只有两个参数,梯度公式可以手推。可一个神经网络有成百上千个参数,一层套一层,手推每个参数的导数是不可能的。下一课讲怎么让计算机自动算出所有参数的梯度:反向传播。

练习

  1. 在标准化的数据上,把学习率改成 0.5、1.0、1.1,各走 100 步,看看结果。临界的学习率大约是多少?
  2. 不做标准化,只用学习率 0.00009,要走多少步,损失才能降到 144 以下?估算一下,再运行验证。
  3. 数值求导时,h 越小越好吗?把 h 改成 1e-101e-15,看看量出来的斜率。结果是不是反而变差了?想一想为什么(提示:计算机里的小数精度是有限的)。

自测

1. 梯度下降为什么要往梯度的"反方向"走?

梯度指向损失增加最快的方向。我们的目标是让损失减小,所以要往相反的方向走。

2. 学习率只从 0.00009 提高到 0.0001,训练就发散了。发生了什么?

在 w 方向,损失的坡非常陡。学习率稍微大一点,一步就跨过了最低点,落到对面更高的地方,那里的坡更陡,下一步跨得更远。每一步都比上一步离最低点更远,最后数值越来越大,直到溢出。

3. 为什么对面积做了标准化之后,训练快了几百倍?

没有标准化时,面积的数值很大,w 方向的梯度比 b 方向大了一百倍。学习率只能迁就陡的 w 方向,设得很小,结果 b 方向走得极慢。标准化后两个方向的坡差不多陡,可以用大得多的学习率,两个参数都能很快走到最低点。

提问与讨论

这一课没看懂的地方,在这里问。看到别人的问题,也欢迎你来回答。

提问 +3 积分,回答别人 +6 积分。内容经审核后公开。

正在加载讨论…