在pytorch中求模型准确率时会记录梯度吗?

Will the gradients be recorded when finding the model's accuracy in pytorch?

我开始学习 PyTorch,我对一些事情感到困惑。据我了解,如果我们将 .requires_grad_() 设置为我们的参数,那么将记录查找这些参数梯度的必要计算。这样,我们就可以进行梯度下降了。但是,梯度值将 添加 在之前的梯度值之上,因此在我们执行梯度下降步骤后,我们应该使用 param.grad.zero_() 重置我们的梯度,其中 param 是权重或偏差项。我有一个只有输入层和一个输出神经元的模型,所以非常简单的东西(因为我只有一个输出神经元,你可以看出我只有 2 个可能 类)。我的参数名为 weightsbias,我在这两个变量上设置了 requires_grad_()。此外,我将训练数据放在名为 train_dlDataLoader 中,将验证数据放在 valid_dl 中。我使用了 MNIST 数据集的一个子集,但这对这个问题来说并不重要。这些是我使用的函数:

def forward_propagation(xb):
    z = xb @ weights + bias
    a = z.sigmoid()
    return a

def mse_loss(predictions, targets):
    loss = ((predictions - targets) ** 2).mean()
    return loss

def backward_propagation(loss):
    loss.backward()
    weights.data -= lr * weights.grad.data
    weights.grad.zero_()
    bias.data -= lr * bias.grad.data
    bias.grad.zero_()

def train_epoch():
    for xb, yb in train_dl:
        a = forward_propagation(xb)
        loss = mse_loss(a, yb)
        backward_propagation(loss)

如您所见,我使用函数 train_epoch() 执行:前向传播(其中一些梯度计算将被记录,因为这是我们的参数首次使用的地方),计算损失(这一步也将用于计算梯度),然后向后传播,我更新我的参数,然后将梯度重置为 0,这样它们就不会累积。我使用这段代码来训练我的模型并且它运行良好,我对我得到的准确性感到满意。所以我认为它至少在某种程度上有效。

但我也使用此代码来查找我的模型的验证数据准确性:

def valid_accuracy():
    accuracies = []
    for xb, yb in valid_dl:
        a = forward_propagation(xb)
        correct = (a > 0.5) == yb
        accuracies.append(correct.float().mean())
    return round(torch.stack(accuracies).mean().item(), 4)

如您所见,为了找到模型的准确性,我执行了前向传播(上面的函数,我将权重乘以数据并添加偏差)。我的问题是:这里也会记录梯度吗?那么下次当我在 loss 上使用 .backward() 时,梯度是否会受到寻找准确性所采取的步骤的影响?我认为,就像现在一样,每次我发现模型的准确性时都会添加梯度值(我不想要也没有意义),但我不确定。我是否应该在函数 valid_accuracy() 的某处添加另外 2 行 weights.grad.zero_()bias.grad.zero_() 这样就不会发生这种情况?还是这种情况不会自动发生,所以默认情况下我得到了所需的行为,我只是误解了什么?

有两件事需要考虑:一个是梯度本身,另一个是在每次前向传递中构建的计算图。

为了计算前向传播后的梯度,我们需要记录对哪些张量以何种顺序进行了哪些操作,即计算图。因此,无论何时从具有 requires_grad==True 的其他张量计算新张量时,新张量都有一个属性 .grad_fn 指向先前的操作和涉及的张量。这基本上就是 backward() “知道”去哪里的方式。如果你调用 backward() 它会考虑这个 .grad_fn 并递归地做反向传播。

所以目前你这样做的方式实际上会构建这个计算图,即使在计算准确性时也是如此。但是如果这张图永远不会被访问,垃圾收集器最终会销毁它。

需要注意的关键是,每个单独的评估都会产生一个 new 计算图(取决于您的模型,可能共享某些部分),但向后传递只会从您调用 .backward 的“节点”,因此在您的代码片段中,您永远不会从准确度计算中获得梯度,因为您从未调用 a.backward(),您只会调用 loss.backward()

尽管计算图的重新编码确实需要一些开销,但是可以使用 torch.no_grad() context manager 来禁用它,这是针对这个确切的用例而设计的。不幸的是,名称(以及文档)提到了梯度,但它实际上只是关于记录(前向)计算图。但很明显,如果你禁用它,结果你将无法计算向后传递。

存储在叶张量中的梯度(在本例中为 weightsbias)在您调用 .backward() 作为这些张量的后代的张量时更新。由于您没有在 valid_accuracy 期间调用 backward,因此不会影响渐变。也就是说,PyTorch 会将中间信息存储在临时计算图中(当 valid_accuracy 中的程序 returns 和所有引用计算图的张量超出范围时,这些信息将被丢弃),这需要时间和内存。

如果您确定不需要对模型的输出执行反向传播,您可以并且应该使用 torch.no_grad() 上下文。这将禁用中间结果的记录。例如

def valid_accuracy():
    with torch.no_grad():
        accuracies = []
        for xb, yb in valid_dl:
            a = forward_propagation(xb)
            correct = (a > 0.5) == yb
            accuracies.append(correct.float().mean())
        return round(torch.stack(accuracies).mean().item(), 4)