前面笔记我们说了损失函数,那么现在损失函数确定了,优化器就登场了!
如果说深度学习模型是一个在群山中寻找最低点(最优解)的登山者,那么:
- Loss Function 是高度计,告诉登山者现在的海拔(误差)有多高。
- Optimizer 是导航员,根据地形(梯度)决定下一步往哪个方向走,走多大一步。
ResNet-50 拥有约 2300 万个参数,暴力穷举是不可能的。优化器的核心任务(BP+逼近函数求解)就是利用反向传播计算出的梯度信息,通过特定的算法(如SGD, Adam)来更新这些参数,使Loss不断降低,让模型输出更加接近真实标签。
一、优化器
PyTorch中的优化器:管理并更新模型中可学习参数的值,使得模型输出更接近真实标签。
导数:函数在指定坐标轴上的变化率——只看一个方向(某个参数)怎么变
方向导数:指定方向上的变化率——沿着你指定的方向走,Loss 变多快
梯度:一个向量,方向为方向导数取得最大值的方向——所有方向里,Loss 上升最快的那个方向
👉 所以 负梯度 = Loss 下降最快的方向
Optimizer本质上就是:“拿到梯度 → 沿着负梯度方向走一步(步长由 lr 决定)”
二、Optimizer基类详解
在PyTorch中,所有优化器如optim.SGD,optim.Adam都继承自基类 torch.optim.Optimizer
详细文档:torch.optim
class Optimizer(object):
def __init__(self, params, defaults):
self.defaults = defaults
self.state = defaultdict(dict)
self.param_groups = []
1.核心属性
优化器初始化时,会创建几个关键属性来管理参数。
defaults:存储优化器的全局超参数默认值。
"""
例子:当你定义下面的用法时,defaults 就是 {'lr': 0.01, 'momentum': 0.9, ...}
"""
optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
param_groups:这是最关键的属性。它是一个 List,其中的每个元素是一个 Dict。这让我们可以对网络的不同部分设置不同的学习率。
"""
深度网络中,不同层“成熟度”不同
backbone:已经学到通用特征 → 小 LR
head / classifier:任务相关 → 大 LR
param_groups 让“同一个优化器,用不同策略更新不同参数”成为可能
"""
optimizer.param_groups = [
{'params': [conv_weight_ptr...], 'lr': 0.001, 'momentum': 0.9}, # 组1
{'params': [fc_weight_ptr...], 'lr': 0.01, 'momentum': 0.9} # 组2
]
state:存储参数的历史状态(缓存)。有些优化器(如 Adam)需要记录参数的动量(Momentum)或平方梯度,这些数据就存在这里,不随模型参数保存,但随优化器保存。
2.核心方法
zero_grad():清空梯度。将所管理参数的.grad属性置零,PyTorch的特性是张量的梯度不自动清零,因此每次反向传播后都需要清空梯度。step():执行更新。根据梯度和更新公式(如SGD的 w = w − l r × g r a d w = w - lr \times grad w=w−lr×grad),实际修改参数的值。state_dict():获取优化器当前的状态(包含超参数和缓存)。load_state_dict():加载状态参数字典。- 应用场景:断点续训。训练中断后,不仅要加载模型的权重,必须也要加载优化器的状态,否则动量等信息丢失,模型性能会抖动。
⚠️注意:state_dict() 与 load_state_dict() 一般经常用于模型训练中的保存和读取模型参数,防止断电等突发情况导致模型训练强行中断而前功尽弃。
- 应用场景:断点续训。训练中断后,不仅要加载模型的权重,必须也要加载优化器的状态,否则动量等信息丢失,模型性能会抖动。
三、训练“三部曲”
在 PyTorch 的迭代训练代码中,有三行代码雷打不动,被称为“三部曲”。
# 1. 清空梯度
optimizer.zero_grad()
# 2. 反向传播,计算梯度
loss.backward()
# 3. 更新参数
optimizer.step()
逻辑版示意图
forward backward
X ───▶ Model(W) ───▶ Ŷ ───▶ Loss ───▶ ∂Loss/∂W
▲ │
│ │
└──────── optimizer.step() ◀───┘
(更新 W)
❓ 灵魂发问:为什么要分这三步?
optimizer.zero_grad() —— 为什么要清零?
- 机制:PyTorch 设计了梯度累加机制。默认情况下,如果不清零,
.backward()计算出的梯度会累加(+=)到原有的.grad上,而不是覆盖。 - 原因:这种机制在显存有限时非常有用(可以通过多次小 Batch 的累加实现大 Batch 的效果),但在常规训练中,我们希望每一步都是独立的,所以必须手动清零。
loss.backward()—— 计算了什么? - 它利用链式法则,从 Loss 开始,反向计算网络中每个叶子节点(参数)的梯度,并将结果保存在参数的
.grad属性中。此时,参数的值还没有变,只是有了梯度。
optimizer.step()—— 执行更新 - 它读取每个参数的
.grad,结合学习率等策略,直接修改参数的值(In-place原地操作)。 - 形象比喻:
backward是计算“该往哪走”,step是真正“迈出腿”。
四、代码实战
1.简单实践
我们首先来简单实践一下。
本节代码并不用于训练真实模型,而是通过人为构造梯度,观察 optimizer.step() 对参数和状态的影响,目的是理解 Optimizer 的工作机制。
import torch
# 设置权重,服从正态分布 --> 2 x 2
weight = torch.randn(2, 2, requires_grad=True)
# 人为设置梯度为全1矩阵 --> 2 x 2(方便观察)
weight.grad = torch.ones_like(weight)
print("Before step:")
print("weight =\n", weight.data)
print("grad =\n", weight.grad)
"""
这里不走 forward/backward,直接“假设梯度已经算好了”,目的是专注观察 optimizer 的行为
"""
# 实例化优化器,使用SGD更新参数
optimizer = torch.optim.SGD([weight], lr=0.1)
# 进行一步操作
optimizer.step()
print("\nAfter step:")
print("weight =\n", weight.data)
print("grad =\n", weight.grad)
# 权重清零
optimizer.zero_grad()
# 检验权重是否为0
print("The grad of weight after optimizer.zero_grad():\n{}".format(weight.grad))
此时我们可以看到每调用一次optimizer.step(),weight 都会沿着负梯度方向减小 lr,可以参考此公式来理解:
W
new
=
W
old
−
l
r
×
g
r
a
d
W_{\text{new}} = W_{\text{old}} - lr \times grad
Wnew=Wold−lr×grad
# 输出参数
print("optimizer.params_group is \n{}".format(optimizer.param_groups))
# 查看参数位置,optimizer和weight的位置一样,我觉得这里可以参考Python是基于值管理
print("weight in optimizer:{}\nweight in weight:{}\n".format(id(optimizer.param_groups[0]['params'][0]), id(weight)))
optimizer 和 weight 的id一样,由此可见,Optimizer 并不“复制参数”,Optimizer 只是“持有参数的引用”,step() 是对参数进行原地修改(inplace)
# 添加参数:weight2
weight2 = torch.randn((3, 3), requires_grad=True)
optimizer.add_param_group({"params": weight2, 'lr': 0.0001, 'nesterov': True})
# 查看现有的参数
print("optimizer.param_groups is\n{}".format(optimizer.param_groups))
# 查看当前状态信息
opt_state_dict = optimizer.state_dict()
print("state_dict before step:\n", opt_state_dict)
# 进行50次step操作
for _ in range(50):
optimizer.step()
# 输出现有状态信息
print("state_dict after step:\n", optimizer.state_dict())
这里我们会发现,50次step()前后的state_dict()一样,这是为什么?
因为在 step() 之前和之后,没有为 weight2 产生任何梯度,所以 optimizer 根本“没干活”,state_dict 自然不会变
Optimizer.step() 并不是无条件更新参数的。它的前置条件是:parameter.grad ≠ None
weight2 = torch.randn((3, 3), requires_grad=True)
optimizer.add_param_group({"params": weight2, "lr": 1e-4, "nesterov": True})
# 手动制造梯度
weight2.grad = torch.ones_like(weight2)
print("Before step:", optimizer.state_dict())
for _ in range(50):
optimizer.step()
print("After step:", optimizer.state_dict())
这里我们就能观察到 state 里多了 momentum_buffer 数值在变化,这正是动量优化器需要保存的历史状态信息。
这就与前面核心方法load_state_dict()对应上了,断电续训时,不仅仅要加载模型的权重,还要加载优化器的状态(动量等信息)。
# 保存参数信息
torch.save(optimizer.state_dict(),os.path.join(r"your_path", "optimizer_state_dict.pkl"))
print("----------done-----------")
# 加载参数信息
state_dict = torch.load(r"your_path\optimizer_state_dict.pkl")
optimizer.load_state_dict(state_dict)
print("load state_dict successfully\n{}".format(state_dict))
# 输出最后属性信息
print("\n{}".format(optimizer.defaults))
print("\n{}".format(optimizer.state))
print("\n{}".format(optimizer.param_groups))
2.实际训练
上面的代码只是为了理解 Optimizer 的行为,在真实训练中,它通常与 forward / backward 配合使用。下面是更接近真实训练场景的代码实战模板
"""
Version 1 —— 只记录每个 epoch 的平均损失
使用avg_train_loss = running_loss / len(train_loader)的潜在风险:如果 drop_last=False 且数据集不能整除 batch size,最后一个 batch 较小,直接除以 batch 数量算出来的平均 Loss 会有极微小的偏差(通常可忽略,但学术上讲不够严谨)。
&
Version 3 —— num_batch计数器
"""
# ======== 初始化 ========
model = MyModel().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
scheduler = StepLR(optimizer, step_size=10, gamma=0.1)
train_losses, val_losses = [], []
# ======== 训练循环 ========
for epoch in range(num_epochs):
model.train()
running_loss = 0.0 # 用于累加每个 mini-batch 的损失值
# num_batches = 0 # 如果用计数器
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
"""
Optimizer 不负责计算梯度,只负责利用梯度更新参数
是否产生梯度,完全由 forward + backward 决定
"""
optimizer.zero_grad() # ① 清空上一轮梯度
outputs = model(inputs) # ② 前向传播
loss = criterion(outputs, labels) # ③ 计算 loss
loss.backward() # ④ 反向传播计算梯度
optimizer.step() # ⑤ 更新参数
running_loss += loss.item()
# num_batches += 1
# 统计平均 loss 必须在 mini-batch 循环结束后
avg_train_loss = running_loss / len(train_loader) # 硬编码,计算平均损失
# avg_loss = running_loss / num_batches # 自动计数
train_losses.append(avg_train_loss)
# ======== 验证循环 ========
model.eval() # 切换到评估模式
val_loss = 0.0
# num_val_batches = 0
with torch.no_grad(): # 不计算梯度
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
outputs = model(inputs)
loss = criterion(outputs, labels)
val_loss += loss.item()
# num_val_batches += 1
avg_val_loss = val_loss / len(val_loader)
val_losses.append(avg_val_loss)
scheduler.step() # 更新学习率
"""
Version 2 —— 通过列表存储每个 mini-batch 的损失,更加灵活
"""
# ======== 初始化 ========
model = MyModel().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
scheduler = StepLR(optimizer, step_size=10, gamma=0.1)
train_loss, val_loss = [], []
for epoch in range(num_epochs):
model.train()
tl = []
for inputs, labels in train_loader:
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad()
loss = criterion(model(inputs), labels)
loss.backward()
optimizer.step()
tl.append(loss.item())
train_loss.append(np.mean(tl))
model.eval()
vl = []
with torch.no_grad():
for inputs, labels in val_loader:
inputs, labels = inputs.to(device), labels.to(device)
vl.append(criterion(model(inputs), labels).item())
val_loss.append(np.mean(vl))
6740

被折叠的 条评论
为什么被折叠?



