梯度下降#
想象一下,你是一个勇敢的寻宝者,被空投到了一座连绵不绝的大山上。你的任务是找到这座山脉的最低点,因为传说中宝藏就埋在那里。
但有一个问题:山上起了大雾,你看不见整个山的全貌,能见度只有你脚下的一小块地方。
你该怎么办?
一个非常聪明的策略是:
环顾四周:在你当前的位置,感受一下哪个方向是下坡最陡峭的。
迈出一步:朝着那个最陡的下坡方向,小心地迈出一步。
重复:到达新位置后,再次环顾四周,找到新的最陡下坡方向,再迈一步。
一直这样重复下去,虽然你每次都只看脚下,但最终你很有可能会走到山谷的最低点,找到宝藏!
恭喜你,你刚刚已经理解了梯度下降(Gradient Descent)的核心思想!
现在,我们把这个故事和机器学习联系起来:
那座山 (The Mountain):就是机器学习里的 损失函数 (Loss Function)。它衡量了你的模型有多“差”。山越高,代表模型的预测错误越大;山越低,代表模型越好。
你的位置 (Your Position):就是模型的 参数 (Parameters),比如我们常说的
w
(权重) 和b
(偏置)。调整这些参数,就相当于你在山上移动。宝藏 (The Treasure):就是损失函数的 最小值,也就是模型表现最好的那一组参数
w
和b
。寻找最陡的下坡方向:这个“方向”就是 梯度 (Gradient)。梯度会指向山上坡度最陡峭的上坡方向,那么它的反方向(负梯度)自然就是最陡的下坡方向。
你迈出的那一步的大小:这就是 学习率 (Learning Rate)。步子迈得太大,可能会直接跨过最低点,跑到对面山坡上去了。步子太小,下山会非常非常慢。所以这是一个需要小心调整的超参数。

梯度下降的流程总结
目标:找到一组参数 (
w
,b
),让损失函数 (Loss) 最小。步骤:
随机初始化一组参数 (
w
,b
)。计算当前参数下的损失。
计算损失函数关于每个参数的梯度(也就是“坡度”)。
沿着梯度相反的方向,更新参数(
新参数 = 旧参数 - 学习率 * 梯度
)。重复 2-4 步,直到损失不再明显下降,我们就找到了“宝藏”!
用 PyTorch 来一场真实的“下山寻宝”#
现在我们用代码来实现这个过程。假设我们要解决一个超级简单的问题:我们有一堆数据 X
和 y
,并且我们知道它们的关系大致是 y = 2 * X + 1
。我们假装不知道 2
和 1
这两个数字,让模型自己去学。
第一幕:手动实现梯度下降(帮你理解原理)#
这部分代码会帮你理解底层发生了什么。
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()
才能计算出这个张量(w
和b
)的梯度。loss.backward()
:这是最神奇的一步。它会像多米诺骨牌一样,从loss
开始反向传播,计算出计算图上所有requires_grad=True
的张量的梯度,并把结果存放在它们的.grad
属性里。w.grad
和b.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 提供的一个标准“层”。它替我们管理了w
和b
,我们只需要调用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
。
总结#
梯度下降就像一个在大雾中凭感觉下山的过程:
目标:找到山谷最低点(损失函数的最小值)。
方法:在当前位置,计算梯度(最陡的上坡方向),然后朝着梯度的反方向(最陡的下坡方向)走一小步(由学习率决定大小)。
重复:不断重复这个过程,直到我们足够接近最低点。
PyTorch 的 autograd
引擎 (loss.backward()
) 和 optim
模块(优化器)极大地简化了这个过程,让我们能够专注于构建模型,而不是手动进行数学计算和参数更新。
这个简单而强大的思想,是驱动今天几乎所有深度学习模型训练的核心动力。希望这个“下山寻宝”的故事能让你彻底明白梯度下降!