pytorch之梯度累加

1.什么是梯度?

梯度可以理解为一个多变量函数的变化率,它告诉我们在某一点上,函数的输出如何随输入的变化而变化。更直观地说,梯度指示了最优化方向。

  • 在机器学习中的作用:在训练模型时,我们的目标是最小化损失函数,以提高模型的准确性。损失函数是衡量模型预测值与真实值之间差距的函数。梯度告诉我们如何调整模型参数,以使损失函数的值减小。

2. 模型参数的优化

考虑一个简单的线性模型:

y=wx+b

  • 其中,yy 是输出,xx 是输入,ww 是权重,bb 是偏置。
  • 为了训练模型,我们使用损失函数(例如均方误差)来衡量模型输出与真实输出之间的差距。损失函数通常定义为:

Loss=1/N∑i=1N(ypred,i−ytrue,i)^2

  • 这里 NN 是样本数,ypred是模型计算的预测值,ytrue是真实值。

3. 反向传播

反向传播是一种高效计算梯度的算法,尤其在深度学习中使用广泛。

3.1 前向传播

在前向传播中,我们将输入数据通过模型传递,计算出预测结果,并基于预测结果与真实结果计算损失。

3.2 计算梯度

反向传播通过链式法则计算梯度,以更新模型参数。通过反向传播,我们可以得到损失函数对每个参数(比如 ww 和 bb)的导数,这些导数就是梯度。

  • 链式法则:假设有一个复合函数 z=f(g(x)),则其导数为:

dz/dx=dz/dg⋅dg/dx

这个法则帮助我们逐层计算梯度。

4. 梯度的累加

4.1 为什么会累加?

在训练过程中,我们可能会处理多个训练样本进行参数更新。如果连续调用多次 loss.backward(),每次都会将计算的梯度值加到之前的梯度上。

  • 这意味着如果我们不清零梯度,梯度会随着样本数的增加而不断增加,可能导致参数更新的步幅变得非常大,影响模型的收敛。
4.2 样例代码

让我们通过具体的代码示例来更好地理解梯度的累加和为什么需要清零。

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

# 定义简单线性模型
model = nn.Linear(1, 1)  # 线性模型:1个输入,1个输出
optimizer = optim.SGD(model.parameters(), lr=0.01)  # 使用 SGD 优化器

# 模拟一些数据
x = torch.tensor([[1.0], [2.0], [3.0]], requires_grad=False)  # 输入
y = torch.tensor([[2.0], [3.0], [4.0]], requires_grad=False)  # 目标输出

# 训练循环
for epoch in range(5):  # 假设训练 5 轮
    for i in range(len(x)):  # 遍历每个训练样本
        optimizer.zero_grad()  # 清零梯度,确保只考虑当前样本

        # 前向传播
        output = model(x[i])  # 计算当前样本的输出

        # 计算损失
        loss = (output - y[i]) ** 2  # 均方误差损失

        # 反向传播
        loss.backward()  # 计算梯度,累加到 model 的参数中

        # 更新参数
        optimizer.step()  # 使用累加的梯度更新参数

        print(f"Epoch: {epoch}, Sample: {i}, Loss: {loss.item()}, W: {model.weight.data}, b: {model.bias.data}")

5. 源代码解释

  1. 清零梯度:在每次处理新的训练样本前,调用 optimizer.zero_grad() 清空梯度。这是为了确保每个训练样本只对当前的梯度产生影响。

  2. 前向传播:计算当前输入的输出。

  3. 损失计算:计算输出与真实值之间的差距。

  4. 反向传播:通过 loss.backward() 计算当前样本对模型参数的梯度并将其累加到 model.parameters() 的 grad 属性上。

  5. 参数更新:调用 optimizer.step() 进行参数更新。

在 PyTorch 中,梯度的累加是一种非常重要且实用的特性,其设计有几个原因:

6. 支持小批量(Mini-batch)训练

在实践中,由于计算资源的限制,通常使用小批量数据进行训练。这意味着我们不会一次性使用整个数据集来更新模型,而是对一小部分数据频繁进行计算。

  • 梯度累加允许我们在多个小批量上计算梯度,并在适当的时候一并更新模型参数。这种策略被称为“累积梯度”。

例如,如果我们有一个较大的数据集,可以将其分为多个小批量,然后在每个小批量上计算梯度。在所有小批量处理完成后,再进行一次参数更新。这种方法可以模拟使用更大批量数据的效果,提高模型的表现。

7. 提高训练灵活性

梯度累加允许用户在特定情况下有效地控制参数更新的频率。例如:

  • 如果处理每个样本时都立即更新权重,可能会导致训练过程不稳定。而通过在多个样本上累加梯度,可以缓解这种波动性,平滑参数的更新过程。

  • 用户可以决定什么情况下清零梯度,例如只有在处理完一个完整的训练周期(epoch)后,或在经历多个小批量后再更新一次参数。这种控制在很多情况下可以提高性能和收敛性。

8. 节省内存

对于一些深度学习模型,特别是当模型较大,或者在训练过程中使用大量数据时,清零梯度后再进行反向传播通常需要的内存较少。没有累加的梯度能够避免内存的额外消耗,进而提高整个训练过程的效率。

9. 灵活的梯度管理

开发者可以基于需求自定义梯度累加的策略。例如,有时我们可能希望实现一些特殊的训练策略,比如调整学习率、动态更改模型的训练方式等。在这些情况下,梯度的管理就显得至关重要。

10. 应用在不同的训练模式

在一些变种训练方式中,如强化学习或一些优化器的特殊需求,可能需要在更新权重前手动控制梯度。这对开发者提供了更大的灵活性和更丰富的训练策略。

数学推导

假设我们有一个线性回归模型,其数学表达式为:

y = W \cdot x + b

均方误差(MSE)损失函数:

\mathcal{L} = \frac{1}{N} \sum_{i=1}^{N} (y_i - \hat{y}_i)^2

梯度计算:

对于权重W

\frac{\partial \mathcal{L}}{\partial W} = \frac{2}{N} \sum_{i=1}^{N} x_i (\hat{y}_i - y_i)


对于偏置 b

\frac{\partial \mathcal{L}}{\partial b} = \frac{2}{N} \sum_{i=1}^{N} (\hat{y}_i - y_i)

假设我们将数据集分成若干小批量,每个小批量包含 m个样本。我们累加这些梯度,并在累积一定数量的小批量k后更新参数。

对于第j个小批量,计算梯度:

对于权重W

\nabla W_j = \frac{2}{m} \sum_{i=1}^{m} x_i (\hat{y}_i - y_i)

对于偏置 b

\nabla b_j = \frac{2}{m} \sum_{i=1}^{m} (\hat{y}_i - y_i)

梯度累加

\nabla W_{\text{accumulated}} = \sum_{j=1}^{k} \nabla W_j

\nabla b_{\text{accumulated}} = \sum_{j=1}^{k} \nabla b_j

g更新参数

W \leftarrow W - \eta \cdot \nabla W_{\text{accumulated}}

b \leftarrow b - \eta \cdot \nabla b_{\text{accumulated}}

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

# 创建数据集
x_train = torch.tensor([[1.0], [2.0], [3.0], [4.0], [5.0]])
y_train = torch.tensor([[2.0], [4.0], [6.0], [8.0], [10.0]])

# 简单的线性回归模型
class LinearRegression(nn.Module):
    def __init__(self):
        super(LinearRegression, self).__init__()
        self.linear = nn.Linear(1, 1)

    def forward(self, x):
        return self.linear(x)

# 初始化模型、损失函数和优化器
model = LinearRegression()
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 定义小批量大小和累积步数
batch_size = 2
accumulation_steps = 2

# 训练过程
for epoch in range(5):
    optimizer.zero_grad()  # 清零梯度
    for i in range(0, len(x_train), batch_size):
        # 获取小批量数据
        x_batch = x_train[i:i + batch_size]
        y_batch = y_train[i:i + batch_size]

        # 前向传播
        outputs = model(x_batch)
        loss = criterion(outputs, y_batch)

        # 反向传播,累加梯度
        loss.backward()

        # 每处理完指定的累积步骤后,更新参数并清零梯度
        if (i // batch_size + 1) % accumulation_steps == 0:
            optimizer.step()
            optimizer.zero_grad()

    # 打印损失
    print(f'Epoch [{epoch + 1}/5], Loss: {loss.item():.4f}')

# 打印模型参数
print(f'Final Parameters: W: {model.linear.weight.item():.4f}, b: {model.linear.bias.item():.4f}')

PyTorch中的梯度累积是指在训练过程中,将多个小批量数据的梯度进行累加,而不是每次反向传播后自动清零梯度。这个特性可以通过调用`loss.backward()`来实现,但在梯度累积时需要手动将梯度清零。 梯度累积的好处在于可以在内存有限的情况下使用更大的批量大小,从而提高模型的训练效果。另外,梯度累积还能够支持多任务训练,因为在多任务中共享的张量的梯度会自动累加。 具体实现梯度累积的代码示例如下: ``` optimizer.zero_grad() # 将梯度清零 for i, data in enumerate(train_loader): inputs, labels = data # 前向传播 outputs = model(inputs) loss = criterion(outputs, labels) # 反向传播 loss.backward() if (i+1) % accum_steps == 0: # 每经过 accum_steps 个小批量数据进行一次梯度更新 optimizer.step() # 更新参数 optimizer.zero_grad() # 将梯度清零 ``` 在这个示例中,我们在每经过 `accum_steps` 个小批量数据时进行一次参数更新,并在更新之后将梯度清零。这样就实现了梯度累积的效果。需要注意的是,`accum_steps`需要根据具体的情况进行调整,以平衡内存占用和训练效果。 参考资料: PyTorch默认会对梯度进行累加。即,PyTorch会在每一次backward()后进行梯度计算,但是梯度不会自动归零,如果不进行手动归零的话,梯度会不断累加梯度累积时,每个batch仍然正常前向传播以及反向传播,但是反向传播之后并不进行梯度清零,因为PyTorch中的backward()执行的是梯度累加的操作,所以当我们调用N次loss.backward()后,这N个batch的梯度都会累加起来。 在PyTorch的设计原理上,利用梯度累加可以在最多保存一张计算图的情况下进行多任务的训练。另外一个理由是在内存不足的情况下,可以叠加多个batch的梯度作为一个大batch进行迭代。由于PyTorch的动态图和autograd机制,设置梯度为0比较复杂。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

背水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值