PyTorch中不同归约方法计算的梯度差异

数据挖掘 深度学习 火炬 反向传播
2021-10-07 04:40:56

我正在使用内置损失函数中提供的不同减少方法。特别是,我想比较以下内容。

  1. 通过对使用计算的每个损失值执行反向传递的平均梯度 reduction="none"

  2. 通过将批大小除以平均梯度 reduction="sum"

  3. 产生的平均梯度 reduction="mean"

  4. 由 计算的平均梯度reduction="mean",数据点一次输入一个模型。

我的实验代码如下:

def estimate_gradient(model, optimizer, batch):
    criterion_no_reduction = nn.CrossEntropyLoss(reduction="none").cuda()
    criterion_sum = nn.CrossEntropyLoss(reduction="sum").cuda()
    criterion_avg = nn.CrossEntropyLoss().cuda()

    input, target = batch
    input, target = input.cuda(), target.cuda()
    output = model(input)
    n = len(output)

    loss_no_reudction = criterion_no_reduction(output, target)
    grad_list_no_reduction = []
    for i in range(n):
        optimizer.zero_grad()
        loss_no_reudction[i].backward(retain_graph=True)
        for j, param in enumerate(model.parameters()):
            if param.requires_grad:
                grad = param.grad.view(-1, 1)
                if i == 0:
                    grad_list_no_reduction.append(grad)
                else:
                    grad_list_no_reduction[j] = torch.cat((grad_list_no_reduction[j], grad), dim=1)
    grad_out_no_reduction = torch.cat(grad_list_no_reduction, dim=0)
    grad_out_no_reduction = (torch.sum(grad_out_no_reduction, dim=1) / n).cpu().detach().numpy().flatten()

    loss_sum = criterion_sum(output, target)
    optimizer.zero_grad()
    loss_sum.backward(retain_graph=True)
    for j, param in enumerate(model.parameters()):
        if param.requires_grad:
            if j == 0:
                grad_list_sum = param.grad.view(-1)
            else:
                grad_list_sum = torch.cat((grad_list_sum, param.grad.view(-1)))
    grad_out_sum = (grad_list_sum / n).cpu().detach().numpy().flatten()

    loss_avg = criterion_avg(output, target)
    optimizer.zero_grad()
    loss_avg.backward(retain_graph=True)
    for j, param in enumerate(model.parameters()):
        if param.requires_grad:
            if j == 0:
                grad_list_avg = param.grad.view(-1)
            else:
                grad_list_avg = torch.cat((grad_list_avg, param.grad.view(-1)))
    grad_out_avg = grad_list_avg.cpu().detach().numpy().flatten()

    target = target.view(-1, 1)
    grad_list_one_by_one = []
    for i in range(n):
        optimizer.zero_grad()
        curr_output = output[i].view(1, -1)
        loss = criterion_avg(curr_output, target[i])
        loss.backward(retain_graph=True)
        for j, param in enumerate(model.parameters()):
            if param.requires_grad:
                grad = param.grad.view(-1, 1)
                if i == 0:
                    grad_list_one_by_one.append(grad)
                else:
                    grad_list_one_by_one[j] = torch.cat((grad_list_one_by_one[j], grad), dim=1)
    grad_out_one_by_one = torch.cat(grad_list_one_by_one, dim=0)
    grad_out_one_by_one = (torch.sum(grad_out_one_by_one, dim=1) / n).cpu().detach().numpy().flatten()

    assert grad_out_no_reduction.shape == grad_out_sum.shape == grad_out_avg.shape == grad_out_one_by_one.shape
    print("Maximum discrepancy between reduction = none and sum: {}".format(np.max(np.abs(grad_out_no_reduction - grad_out_sum))))
    print("Maximum discrepancy between reduction = none and avg: {}".format(np.max(np.abs(grad_out_no_reduction - grad_out_avg))))
    print("Maximum discrepancy between reduction = none and one-by-one: {}".format(np.max(np.abs(grad_out_no_reduction - grad_out_one_by_one))))
    print("Maximum discrepancy between reduction = sum and avg: {}".format(np.max(np.abs(grad_out_sum - grad_out_avg))))
    print("Maximum discrepancy between reduction = sum and one-by-one: {}".format(np.max(np.abs(grad_out_sum - grad_out_one_by_one))))
    print("Maximum discrepancy between reduction = avg and one-by-one: {}".format(np.max(np.abs(grad_out_avg- grad_out_one_by_one))))

结果如下:

Maximum discrepancy between reduction = none and sum: 0.0316
Maximum discrepancy between reduction = none and avg: 0.0316
Maximum discrepancy between reduction = none and one-by-one: 0.0
Maximum discrepancy between reduction = sum and avg: 0.0
Maximum discrepancy between reduction = sum and one-by-one: 0.0316
Maximum discrepancy between reduction = avg and one-by-one: 0.0316

也就是说,reduction=none一对一的反向传播产生的结果似乎是相同的,而reduciton=sumreduction=mean前一对产生的结果不同。解释差异(可能是由于retain_graph=True?)真的很有帮助,并提前感谢您的帮助!

1个回答

让我们首先回顾一下每一个的含义。减少“无”意味着batch_size针对批次中每个输入的损失独立计算梯度更新,然后应用它们(组成)。减少“平均”和“总和”的意思是应用各自的操作,并针对这个值取梯度。

现在,让我们看看不同的比较。

无与一对一

这些是相同的,因为通过一个接一个地进行,您正在获取 1 项列表的平均值,这只是列表中的值。所以损失总是相同的。换句话说,您基本上只是通过一次将平均值应用于一个损失输出来重新创建代码中的“无”减少。

总和与平均值

接下来让我们比较 sum 和 average,因为这将更容易解释 none 与它们两者。首先,让我们考虑一下我们期望梯度更新在“sum”和“avg”减少损失输出之间有何不同。明确地说,我们知道平均值是

1ni=1nlossi

而总和是

i=1nlossi
所以不管我们的梯度的实际公式是什么,我们期望“总和”减少损失的梯度总是等于 n乘以“平均”的梯度减少了损失。这意味着所有权重都将以相同的比例更新,只是整体幅度不同。

但是,您会看到这两个缩减之间的梯度是相同的。这是为什么?好吧,事实证明,查看您的代码,您将“总和”减少的梯度除以n,所以实际上这正是我们期望看到的。

顺便说一句,还有一个相关的问题,即为什么两者往往会产生相同的结果。我相信答案是,像 Adam 的学习率调整这样的智能优化器将解释两者之间的持续差异,并将其纳入他们的学习率选择中。我还要注意,关于“平均”减少的一个好处是,无论您的批次大小如何(对于相同的数据),它都能保持梯度不变。

总和/平均与一对一/无

现在我们已经解释了为什么这两对给出相同的结果,让我们了解为什么它们之间的结果不同。简短的回答是,这是因为交叉熵不是线性函数。更具体地说,这意味着

xCrossEntropy(x1++xn)x(CrossEntropy(x1)++CrossEntropy(xn)).

所以,无论我们是用“均值”还是“总和”来减少,我们都会得到一个不同的答案,而不是我们单独计算每个输出的损失,然后对每个输出的梯度求和。

您可能会遇到的一个后续问题是,为什么可以使用这些减少呢?基本思想是,我们期望每个批次的梯度更新仍然大致近似于我们通过计算所有单独的梯度更新得到的结果。事实上,事实证明,我们通过减少(使用“均值”或“总和”)得到的梯度更新实际上可以产生比我们通过计算单个梯度得到的更稳健的结果!