梯度下降#

想象一下,你是一个勇敢的寻宝者,被空投到了一座连绵不绝的大山上。你的任务是找到这座山脉的最低点,因为传说中宝藏就埋在那里。

但有一个问题:山上起了大雾,你看不见整个山的全貌,能见度只有你脚下的一小块地方。

你该怎么办?

一个非常聪明的策略是:

  1. 环顾四周:在你当前的位置,感受一下哪个方向是下坡最陡峭的。

  2. 迈出一步:朝着那个最陡的下坡方向,小心地迈出一步。

  3. 重复:到达新位置后,再次环顾四周,找到新的最陡下坡方向,再迈一步。

一直这样重复下去,虽然你每次都只看脚下,但最终你很有可能会走到山谷的最低点,找到宝藏!

恭喜你,你刚刚已经理解了梯度下降(Gradient Descent)的核心思想!

现在,我们把这个故事和机器学习联系起来:

  • 那座山 (The Mountain):就是机器学习里的 损失函数 (Loss Function)。它衡量了你的模型有多“差”。山越高,代表模型的预测错误越大;山越低,代表模型越好。

  • 你的位置 (Your Position):就是模型的 参数 (Parameters),比如我们常说的 w (权重) 和 b (偏置)。调整这些参数,就相当于你在山上移动。

  • 宝藏 (The Treasure):就是损失函数的 最小值,也就是模型表现最好的那一组参数 wb

  • 寻找最陡的下坡方向:这个“方向”就是 梯度 (Gradient)。梯度会指向山上坡度最陡峭的上坡方向,那么它的反方向(负梯度)自然就是最陡的下坡方向。

  • 你迈出的那一步的大小:这就是 学习率 (Learning Rate)。步子迈得太大,可能会直接跨过最低点,跑到对面山坡上去了。步子太小,下山会非常非常慢。所以这是一个需要小心调整的超参数。

梯度下降的流程总结

目标:找到一组参数 (w, b),让损失函数 (Loss) 最小。

步骤:

  1. 随机初始化一组参数 (w, b)。

  2. 计算当前参数下的损失。

  3. 计算损失函数关于每个参数的梯度(也就是“坡度”)。

  4. 沿着梯度相反的方向,更新参数(新参数 = 旧参数 - 学习率 * 梯度)。

  5. 重复 2-4 步,直到损失不再明显下降,我们就找到了“宝藏”!

用 PyTorch 来一场真实的“下山寻宝”#

现在我们用代码来实现这个过程。假设我们要解决一个超级简单的问题:我们有一堆数据 Xy,并且我们知道它们的关系大致是 y = 2 * X + 1。我们假装不知道 21 这两个数字,让模型自己去学。

第一幕:手动实现梯度下降(帮你理解原理)#

这部分代码会帮你理解底层发生了什么。

import torch

# 1. 准备数据 (我们的“地图”和真实宝藏位置)
X = torch.tensor([[1.0], [2.0], [3.0], [4.0]], dtype=torch.float32)
# 真实的 y = 2 * X + 1
y = torch.tensor([[3.0], [5.0], [7.0], [9.0]], dtype=torch.float32)

# 2. 随机初始化参数 (随机把你空投到山上的某个位置)
# 我们需要找到 w 和 b,让模型 y_pred = w * X + b 尽可能接近真实的 y
# requires_grad=True 告诉 PyTorch: “请帮我追踪这两个变量的梯度!”
w = torch.tensor([[0.0]], requires_grad=True, dtype=torch.float32)
b = torch.tensor([[0.0]], requires_grad=True, dtype=torch.float32)

# 3. 设置学习率 (决定你每一步迈多大)
learning_rate = 0.01

print("开始寻宝!起点 w={}, b={}".format(w.item(), b.item()))

# 4. 开始下山!(迭代训练)
for epoch in range(100):
    # a. 向前走,用当前的 w, b 预测一下
    # 这就是我们的模型
    y_pred = X @ w + b # @ 是矩阵乘法

    # b. 看看离宝藏还有多远 (计算损失)
    # 我们用均方误差 (MSE) 作为损失函数,它就像山的高度计
    loss = torch.mean((y_pred - y) ** 2)

    # c. 关键一步:计算梯度 (环顾四周,找到最陡的下坡方向)
    # PyTorch 的魔法来了!loss.backward() 会自动计算损失对 w 和 b 的梯度
    loss.backward()

    # d. 更新参数 (朝着下坡方向迈出一步)
    # 使用 torch.no_grad() 是因为我们不希望参数更新这个操作本身被追踪梯度
    with torch.no_grad():
        w -= learning_rate * w.grad
        b -= learning_rate * b.grad

        # e. 清空梯度!(非常重要)
        # 梯度是会累加的,每次循环前都要清零,否则就像你闭着眼睛凭记忆走路,会走偏
        w.grad.zero_()
        b.grad.zero_()

    if (epoch + 1) % 10 == 0:
        print("第 {} 步: 损失(山的高度) = {:.4f}, w = {:.3f}, b = {:.3f}".format(
            epoch + 1, loss.item(), w.item(), b.item()
        ))

print("\n寻宝结束!找到的 w ≈ {:.3f}, b ≈ {:.3f}".format(w.item(), b.item()))
开始寻宝!起点 w=0.0, b=0.0
第 10 步: 损失(山的高度) = 1.5412, w = 1.757, b = 0.607
第 20 步: 损失(山的高度) = 0.0518, w = 2.038, b = 0.712
第 30 步: 损失(山的高度) = 0.0126, w = 2.080, b = 0.735
第 40 步: 损失(山的高度) = 0.0109, w = 2.085, b = 0.745
第 50 步: 损失(山的高度) = 0.0102, w = 2.084, b = 0.753
第 60 步: 损失(山的高度) = 0.0096, w = 2.081, b = 0.761
第 70 步: 损失(山的高度) = 0.0091, w = 2.079, b = 0.768
第 80 步: 损失(山的高度) = 0.0085, w = 2.077, b = 0.775
第 90 步: 损失(山的高度) = 0.0080, w = 2.074, b = 0.781
第 100 步: 损失(山的高度) = 0.0076, w = 2.072, b = 0.788

寻宝结束!找到的 w ≈ 2.072, b ≈ 0.788

代码解释:

  • requires_grad=True:这是 PyTorch 自动求导(autograd)引擎的开关。只有打开它,loss.backward() 才能计算出这个张量(wb)的梯度。

  • loss.backward():这是最神奇的一步。它会像多米诺骨牌一样,从 loss 开始反向传播,计算出计算图上所有 requires_grad=True 的张量的梯度,并把结果存放在它们的 .grad 属性里。

  • w.gradb.grad:这就是 loss.backward() 计算出的梯度值。

  • w.grad.zero_()每次更新完参数后,必须清空梯度。 因为PyTorch默认会把梯度累加起来,如果不清零,下一次计算的梯度就会被加到上一次的结果上,导致更新方向错误。

第二幕:使用 PyTorch 的优化器(更简洁的标准方式)#

手动更新参数虽然能帮我们理解原理,但在真实项目中,模型可能有成千上万个参数,手动一个个更新太麻烦了。PyTorch 提供了一个专业的“登山向导”——优化器 (Optimizer)

优化器会帮我们自动完成“更新参数”和“清空梯度”这两件杂事。

import torch
import torch.nn as nn
import torch.optim as optim

# 1. 准备数据 (和之前一样)
X = torch.tensor([[1.0], [2.0], [3.0], [4.0]], dtype=torch.float32)
y = torch.tensor([[3.0], [5.0], [7.0], [9.0]], dtype=torch.float32)

# 2. 定义模型、损失函数和优化器
# nn.Linear(1, 1) 是一个线性层,它内部自动包含了 w 和 b,我们不用自己定义了
# 1 个输入特征,1 个输出特征
model = nn.Linear(1, 1)

# PyTorch 内置的均方误差损失
loss_fn = nn.MSELoss()

# 定义我们的“登山向导”——优化器
# 我们使用 SGD (随机梯度下降),它是梯度下降的一种变体
# 把模型的参数 (model.parameters()) 和学习率告诉它
learning_rate = 0.01
optimizer = optim.SGD(model.parameters(), lr=learning_rate)

print("开始寻宝(专业向导版)!")

# 3. 开始下山!(迭代训练)
for epoch in range(100):
    # a. 预测
    y_pred = model(X)

    # b. 计算损失
    loss = loss_fn(y_pred, y)

    # c. 清空旧梯度 (向导帮你做)
    optimizer.zero_grad()

    # d. 计算新梯度 (和之前一样)
    loss.backward()

    # e. 更新参数 (向导帮你做)
    optimizer.step()

    if (epoch + 1) % 10 == 0:
        # model.parameters() 返回一个生成器,我们可以从中提取 w 和 b
        w, b = model.parameters()
        print("第 {} 步: 损失 = {:.4f}, w = {:.3f}, b = {:.3f}".format(
            epoch + 1, loss.item(), w[0][0].item(), b[0].item()
        ))

print("\n寻宝结束!找到的 w ≈ {:.3f}, b ≈ {:.3f}".format(
    model.weight.item(), model.bias.item()
))
开始寻宝(专业向导版)!
第 10 步: 损失 = 1.9634, w = 1.818, b = 0.299
第 20 步: 损失 = 0.1014, w = 2.129, b = 0.423
第 30 步: 损失 = 0.0502, w = 2.174, b = 0.457
第 40 步: 损失 = 0.0462, w = 2.177, b = 0.476
第 50 步: 损失 = 0.0434, w = 2.173, b = 0.491
第 60 步: 损失 = 0.0409, w = 2.168, b = 0.507
第 70 步: 损失 = 0.0385, w = 2.163, b = 0.521
第 80 步: 损失 = 0.0363, w = 2.158, b = 0.535
第 90 步: 损失 = 0.0342, w = 2.153, b = 0.549
第 100 步: 损失 = 0.0322, w = 2.149, b = 0.562

寻宝结束!找到的 w ≈ 2.149, b ≈ 0.562

代码解释:

  • nn.Linear(1, 1):这是 PyTorch 提供的一个标准“层”。它替我们管理了 wb,我们只需要调用 model(X) 就可以做预测。

  • nn.MSELoss():这是 PyTorch 提供的标准损失函数,不用我们自己手写 torch.mean(...)

  • optim.SGD(...):我们创建了一个 SGD 优化器实例,并告诉它要管理哪些参数(model.parameters())以及学习率是多少。

  • optimizer.zero_grad():一行代码就替代了之前所有的 param.grad.zero_(),非常方便。

  • optimizer.step():这一步会根据之前 loss.backward() 计算出的梯度,自动更新所有它管理的参数。它内部执行的逻辑就是 w -= lr * w.grad

总结#

梯度下降就像一个在大雾中凭感觉下山的过程:

  1. 目标:找到山谷最低点(损失函数的最小值)。

  2. 方法:在当前位置,计算梯度(最陡的上坡方向),然后朝着梯度的反方向(最陡的下坡方向)走一小步(由学习率决定大小)。

  3. 重复:不断重复这个过程,直到我们足够接近最低点。

PyTorch 的 autograd 引擎 (loss.backward()) 和 optim 模块(优化器)极大地简化了这个过程,让我们能够专注于构建模型,而不是手动进行数学计算和参数更新。

这个简单而强大的思想,是驱动今天几乎所有深度学习模型训练的核心动力。希望这个“下山寻宝”的故事能让你彻底明白梯度下降!