深度学习中的一些理解,pytorch中损失函数怎么操作的?优化器如何优化的?
这篇文章主要是小编完成 LeNet-5 的代码编写以后,对其中的一些细节,想了解的更清楚一点训练的过程中做了什么是如何计算损失的?参数是怎么更新的?
上一篇文章
本篇任务理解代码:
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
# 训练和测试函数
def train(model, device, train_loader, optimizer, criterion, epoch):model.train()best_model = modelmin_loss = 1for batch_idx, (data, target) in enumerate(train_loader):data, target = data.to(device), target.to(device)optimizer.zero_grad()output = model(data)loss = criterion(output, target)loss.backward()optimizer.step()if min_loss > loss.item():best_model, best_loss = model, loss.item()if batch_idx % 100 == 0:print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} ({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')print("模型训练结束")print("保存最好 loss 模型,loss:",min_loss)torch.save(best_model.state_dict(),'best-lenet5.pth')
损失的计算
先简单了解代码中的过程:
训练过程中使用的交叉熵损失函数(nn.CrossEntropyLoss
)计算模型输出和目标标签之间的误差。具体步骤如下:
-
模型前向传播:
- 模型将输入数据(
data
)传递过来,并计算输出(output
)。
- 模型将输入数据(
-
计算损失:
criterion(output, target)
计算模型输出(output
)与目标标签(target
)之间的交叉熵损失。- 交叉熵损失函数(
nn.CrossEntropyLoss
)首先对模型的输出进行 softmax 操作,然后计算真实标签与预测标签之间的负对数似然损失(negative log-likelihood loss)。 - 公式表示为:
$
\text{CrossEntropyLoss} = -\sum_{i} y_i \log(\hat{y}_i)
$
其中 $ y_i 是目标类别的 o n e − h o t 编码, 是目标类别的 one-hot 编码, 是目标类别的one−hot编码, \hat{y}_i $ 是模型的 softmax 输出。
-
反向传播:
loss.backward()
通过计算损失相对于模型参数的梯度,将这些梯度反向传播回模型。
-
优化更新:
optimizer.step()
使用优化算法(如 SGD)更新模型的参数,以最小化损失。
-
保存最优模型:
- 每个批次(
batch_idx
)的训练过程中,检查当前损失是否为最小值。如果是,则保存当前模型及其损失。 - 在所有批次结束后,保存具有最小损失的模型参数。
- 每个批次(
相关的解释和注释:
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)def train(model, device, train_loader, optimizer, criterion, epoch):model.train()best_model = modelmin_loss = 1for batch_idx, (data, target) in enumerate(train_loader):data, target = data.to(device), target.to(device)optimizer.zero_grad()# 前向传播output = model(data)# 计算损失loss = criterion(output, target)# 反向传播loss.backward()# 参数更新optimizer.step()# 保存最优模型if min_loss > loss.item():best_model, best_loss = model, loss.item()# 打印训练信息if batch_idx % 100 == 0:print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} ({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')print("模型训练结束")print("保存最好 loss 模型,loss:", min_loss)torch.save(best_model.state_dict(), 'best-lenet5.pth')
在训练过程中,nn.CrossEntropyLoss()
会自动处理模型输出和目标标签的比较,进行 softmax 操作并计算损失值。上述代码的每一步都按顺序执行,确保模型在训练过程中不断调整参数,以最小化损失函数,从而提高模型的预测精度。
过程看完了还是很迷糊,进一步了解。
我们可以更详细地了解交叉熵损失函数是如何处理模型输出和目标标签,并计算损失值的。
1. 模型输出
假设我们有一个分类任务,模型的最后一层是全连接层,其输出是一个未归一化的分数向量(logits),表示每个类别的得分。例如,对于 3 个类别,模型输出可能是 output = [2.0, 1.0, 0.1]
。
2. Softmax 操作
在计算交叉熵损失之前,首先需要将模型的未归一化输出转换为概率分布。这个过程通过 softmax 函数实现:
y ^ i = exp ( o i ) ∑ j exp ( o j ) \hat{y}_i = \frac{\exp(o_i)}{\sum_{j} \exp(o_j)} y^i=∑jexp(oj)exp(oi)
其中 o i o_i oi 是第 i i i 个类别的得分, y ^ i \hat{y}_i y^i 是第 i i i 个类别的预测概率。
对于 output = [2.0, 1.0, 0.1]
,softmax 计算如下:
y ^ 1 = exp ( 2.0 ) exp ( 2.0 ) + exp ( 1.0 ) + exp ( 0.1 ) ≈ 0.659 \hat{y}_1 = \frac{\exp(2.0)}{\exp(2.0) + \exp(1.0) + \exp(0.1)} \approx 0.659 y^1=exp(2.0)+exp(1.0)+exp(0.1)exp(2.0)≈0.659
y ^ 2 = exp ( 1.0 ) exp ( 2.0 ) + exp ( 1.0 ) + exp ( 0.1 ) ≈ 0.242 \hat{y}_2 = \frac{\exp(1.0)}{\exp(2.0) + \exp(1.0) + \exp(0.1)} \approx 0.242 y^2=exp(2.0)+exp(1.0)+exp(0.1)exp(1.0)≈0.242
y ^ 3 = exp ( 0.1 ) exp ( 2.0 ) + exp ( 1.0 ) + exp ( 0.1 ) ≈ 0.099 \hat{y}_3 = \frac{\exp(0.1)}{\exp(2.0) + \exp(1.0) + \exp(0.1)} \approx 0.099 y^3=exp(2.0)+exp(1.0)+exp(0.1)exp(0.1)≈0.099
此时,模型的输出概率分布为 output_probs = [0.659, 0.242, 0.099]
。
3. 目标标签
目标标签(target)通常是一个标量,表示正确类别的索引。例如,对于类别索引为 0 的目标标签:
target = 0 \text{target} = 0 target=0
4. 交叉熵损失计算
交叉熵损失计算的是目标标签与模型预测概率之间的差异。其公式为:
CrossEntropyLoss = − log ( y ^ i ) \text{CrossEntropyLoss} = -\log(\hat{y}_i) CrossEntropyLoss=−log(y^i)
其中 y ^ i \hat{y}_i y^i 是目标标签对应的预测概率。
对于目标标签 0 和输出概率分布 output_probs = [0.659, 0.242, 0.099]
,交叉熵损失计算如下:
CrossEntropyLoss = − log ( 0.659 ) ≈ 0.417 \text{CrossEntropyLoss} = -\log(0.659) \approx 0.417 CrossEntropyLoss=−log(0.659)≈0.417
PyTorch 中的实现
在 PyTorch 中,nn.CrossEntropyLoss
会自动处理 softmax 操作,并计算交叉熵损失。其使用方法如下:
import torch
import torch.nn as nn# 模型输出 logits
output = torch.tensor([[2.0, 1.0, 0.1]], requires_grad=True)# 目标标签
target = torch.tensor([0])# 定义交叉熵损失函数
criterion = nn.CrossEntropyLoss()# 计算损失
loss = criterion(output, target)
print(f"Loss: {loss.item()}")
在这个例子中:
output
是模型的 logits 输出,形状为[1, 3]
,表示批次大小为 1,有 3 个类别。target
是目标标签,形状为[1]
,表示正确类别的索引。
nn.CrossEntropyLoss
会先对 output
进行 softmax 操作,然后计算目标标签与预测概率之间的交叉熵损失。
到这里的话差不多就是理解了!
如何进行优化的?
优化器(optimizer)在训练过程中主要负责更新模型的参数,以最小化损失函数。具体来说,优化器通过使用反向传播算法计算的梯度信息来调整模型参数,从而使模型在训练数据上的表现逐渐改善。
以 SGD
(随机梯度下降)优化器为例,optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
主要完成以下几个任务:
1. 初始化优化器
创建一个 SGD
优化器实例,并将模型的参数、学习率和动量作为输入参数传递给它。
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
model.parameters()
:模型的可训练参数。lr=0.01
:学习率,控制参数更新的步长。momentum=0.9
:动量项,帮助加速收敛并减小振荡。
2. 清零梯度
在每个训练批次开始前,清零所有模型参数的梯度。
optimizer.zero_grad()
这是因为 PyTorch 中的梯度是累积的,因此需要在每次反向传播前清零。
3. 反向传播计算梯度
计算损失函数相对于模型参数的梯度。
loss.backward()
这一操作通过链式法则计算出损失函数相对于每个参数的偏导数,并存储在每个参数的 grad
属性中。
4. 更新参数
使用计算得到的梯度更新模型参数。
optimizer.step()
这是优化器的核心操作,它利用当前的梯度信息和优化算法(如随机梯度下降)来更新模型的参数。具体步骤如下:
4.1 基础 SGD 更新公式
对于每个参数 θ \theta θ,其更新公式为:
θ = θ − η ⋅ ∇ θ L ( θ ) \theta = \theta - \eta \cdot \nabla_{\theta} L(\theta) θ=θ−η⋅∇θL(θ)
其中:
- θ \theta θ:模型参数
- η \eta η:学习率
- ∇ θ L ( θ ) \nabla_{\theta} L(\theta) ∇θL(θ):损失函数 L L L 对参数 θ \theta θ 的梯度
4.2 带动量的 SGD
动量的引入可以加速收敛,并减小训练过程中的振荡。动量项 v \mathbf{v} v 的更新公式为:
v = α ⋅ v + η ⋅ ∇ θ L ( θ ) \mathbf{v} = \alpha \cdot \mathbf{v} + \eta \cdot \nabla_{\theta} L(\theta) v=α⋅v+η⋅∇θL(θ)
参数的更新公式为:
θ = θ − v \theta = \theta - \mathbf{v} θ=θ−v
其中:
- α \alpha α:动量系数(例如 0.9)
- v \mathbf{v} v:动量项,存储了累积的梯度信息
结合动量的 SGD 更新步骤:
- 计算并更新动量项。
- 使用动量项更新模型参数。
例子
以下是一个完整的训练步骤的代码示例,展示了优化器如何与模型、数据和损失函数一起工作:
import torch
import torch.nn as nn
import torch.optim as optim# 假设已经定义并实例化了模型和数据加载器
model = ... # 模型实例
train_loader = ... # 训练数据加载器# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)# 训练函数
def train(model, device, train_loader, optimizer, criterion, epoch):model.train()for batch_idx, (data, target) in enumerate(train_loader):data, target = data.to(device), target.to(device)# 清零梯度optimizer.zero_grad()# 前向传播output = model(data)# 计算损失loss = criterion(output, target)# 反向传播loss.backward()# 更新参数optimizer.step()if batch_idx % 100 == 0:print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} ({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')# 假设已经将模型移动到正确的设备上并加载数据
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)# 进行一个训练周期
train(model, device, train_loader, optimizer, criterion, epoch=1)
总结
优化器的主要工作是使用计算得到的梯度来更新模型的参数,以最小化损失函数。通过多次迭代这一过程,模型的性能在训练数据上逐渐提高,并在验证和测试数据上取得更好的表现。