为什么在 Pytorch 张量上调用 .numpy() 之前调用 .detach() ?

Jos*_*der 30 numpy autodiff pytorch

已经确定这my_tensor.detach().numpy()是从torch张量获取 numpy 数组的正确方法。

我试图更好地理解为什么。

在刚刚链接的问题的公认答案中,Blupon 指出:

您需要将您的张量转换为另一个除了其实际值定义之外不需要梯度的张量。

在他链接到的第一个讨论中,albanD 指出:

这是预期的行为,因为移动到 numpy 会破坏图形,因此不会计算梯度。

如果您实际上不需要渐变,那么您可以显式地 .detach() 需要 grad 的 Tensor 以获得具有相同内容但不需要 grad 的张量。然后可以将这个其他 Tensor 转换为一个 numpy 数组。

在他链接到的第二个讨论中,apaszke 写道:

变量不能转换为 numpy,因为它们是保存操作历史的张量的包装器,而 numpy 没有这样的对象。您可以使用 .data 属性检索变量持有的张量。然后,这应该有效:var.data.numpy()。

我研究了 PyTorch 的自动分化库的内部工作原理,但我仍然对这些答案感到困惑。为什么它会破坏图形以移动到 numpy?是否因为在 autodiff 图中不会跟踪对 numpy 数组的任何操作?

什么是变量?它与张量有什么关系?

我觉得这里需要一个彻底的高质量 Stack-Overflow 答案,向尚不了解自动分化的 PyTorch 新用户解释原因。

特别是,我认为通过一个图形来说明图形并显示在此示例中断开连接是如何发生的会很有帮助:

import torch

tensor1 = torch.tensor([1.0,2.0],requires_grad=True)

print(tensor1)
print(type(tensor1))

tensor1 = tensor1.numpy()

print(tensor1)
print(type(tensor1))
Run Code Online (Sandbox Code Playgroud)

Sha*_*hai 48

我认为这里要理解的最关键的一点是a和之间的区别: 虽然这两个对象都用于存储 n 维矩阵(又名“张量”),但都有一个额外的“层”——它存储导致关联的 n 维矩阵。torch.tensornp.ndarray
torch.tensors

因此,如果您只对在矩阵上执行数学运算的高效且简单的方法感兴趣,np.ndarray或者torch.tensor可以互换使用。

然而,torch.tensors 被设计用于梯度下降优化的上下文中,因此它们不仅包含带有数值的张量,而且(更重要的是)包含导致这些值的计算图。然后使用这个计算图(使用导数链式法则)来计算损失函数的导数,并且每个自变量都用于计算损失。

如前所述,np.ndarrayobject 没有这个额外的“计算图”层,因此,在将 a 转换为时torch.tensornp.ndarray您必须使用命令明确删除张量的计算图detach()


计算图
从您的评论来看,这个概念似乎有点模糊。我将尝试用一个简单的例子来说明它。
考虑两个(向量)变量的简单函数,x并且w

x = torch.rand(4, requires_grad=True)
w = torch.rand(4, requires_grad=True)

y = x @ w  # inner-product of x and w
z = y ** 2  # square the inner product
Run Code Online (Sandbox Code Playgroud)

如果我们只对 的值感兴趣z,则无需担心任何图形,我们只需从输入和向前移动,然后计算。xwyz

然而,如果我们不那么在乎的价值会发生什么z,而是要问的问题“什么是w那个最小化 z对于给定的x”?
要回答这个问题,我们需要计算派生zWRT w
我们怎么做?
使用链式法则我们知道dz/dw = dz/dy * dy/dw。也就是说,计算的梯度zWRTw我们需要移动向后zw计算梯度的,我们跟踪的每一步操作的后面,从我们的步骤zw. 我们追溯的这条“路径”是 的计算图z它告诉我们如何计算z导致 的输入的导数z

z.backward()  # ask pytorch to trace back the computation of z
Run Code Online (Sandbox Code Playgroud)

我们现在可以检查zwrt的梯度w

w.grad  # the resulting gradient of z w.r.t w
tensor([0.8010, 1.9746, 1.5904, 1.0408])
Run Code Online (Sandbox Code Playgroud)

请注意,这完全等于

2*y*x
tensor([0.8010, 1.9746, 1.5904, 1.0408], grad_fn=<MulBackward0>)
Run Code Online (Sandbox Code Playgroud)

因为dz/dy = 2*ydy/dw = x

路径上的每个张量都存储其对计算的“贡献”:

z
tensor(1.4061, grad_fn=<PowBackward0>)
Run Code Online (Sandbox Code Playgroud)

y
tensor(1.1858, grad_fn=<DotBackward>)
Run Code Online (Sandbox Code Playgroud)

如您所见,y并且z不仅存储<x, w>or的“前向”值,y**2还存储计算图——grad_fn当从z(输出)到w(输入)追溯梯度时计算导数(使用链式法则)所需的值.

这些grad_fn是必不可少的组成部分torch.tensors,如果没有它们,人们将无法计算复杂函数的导数。然而,np.ndarrays 根本没有这个能力,他们没有这个信息。

有关使用函数追溯导数的更多信息,请参阅此答案backwrd()


由于两个np.ndarraytorch.tensor具有一个共同的“层”存储号码的第二阵列,pytorch使用相同的存储空间以保存存储器:

numpy() ? numpy.ndarray
self张量作为 NumPy ndarray返回。这个张量和返回的 ndarray共享相同的底层存储。自张量的变化将反映在 ndarray 中,反之亦然。

另一个方向也以同样的方式工作:

torch.from_numpy(ndarray) ? Tensor
从 numpy.ndarray 创建一个张量。
返回的张量和 ndarray共享相同的内存。对张量的修改将反映在 ndarray 中,反之亦然。

因此,当创建np.arrayfromtorch.tensor或反之亦然时,两个对象都引用内存中相同的底层存储。由于np.ndarray不存储/表示与数组关联的计算图,因此当共享 numpy 和 torch 希望引用相同的张量时,应明确删除该图detach()


请注意,如果您出于某种原因希望仅将 pytorch 用于没有反向传播的数学运算,您可以使用with torch.no_grad()上下文管理器,在这种情况下,不会创建计算图并且torch.tensors 和np.ndarrays 可以互换使用。

w.grad  # the resulting gradient of z w.r.t w
tensor([0.8010, 1.9746, 1.5904, 1.0408])
Run Code Online (Sandbox Code Playgroud)

  • 我真的很喜欢你提到“with torch.no_grad()”作为分离的替代方案。 (2认同)

Jos*_*der 8

我问,为什么它会破坏图形以移动到 numpy?是否因为在 autodiff 图中不会跟踪对 numpy 数组的任何操作?

是的,新张量不会通过 a 连接到旧张量grad_fn,因此对新张量的任何操作都不会将梯度带回旧张量。

写作my_tensor.detach().numpy()只是说,“我将根据 numpy 数组中这个张量的值进行一些非跟踪计算。”

Dive into Deep Learning (d2l) 教科书有一个很好的部分描述了 detach() 方法,尽管它没有讨论为什么在转换为 numpy 数组之前分离是有意义的。


感谢 jodag 帮助回答这个问题。正如他所说,变量已过时,因此我们可以忽略该评论。

我认为到目前为止我能找到的最佳答案是在jodag 的文档链接中

要阻止张量跟踪历史,您可以调用 .detach() 将其与计算历史分离,并防止跟踪未来的计算。

在我在问题中引用的 albanD 的评论中:

如果您实际上不需要渐变,那么您可以显式地 .detach() 需要 grad 的 Tensor 以获得具有相同内容但不需要 grad 的张量。然后可以将这个其他 Tensor 转换为一个 numpy 数组。

换句话说,该detach方法的意思是“我不想要梯度”,并且不可能通过numpy操作来跟踪梯度(毕竟,这就是 PyTorch 张量的用途!)


pro*_*sti 7

这是张量 -> numpy 数组连接的一个小展示:

import torch
tensor = torch.rand(2)
numpy_array = tensor.numpy()
print('Before edit:')
print(tensor)
print(numpy_array)

tensor[0] = 10

print()
print('After edit:')
print('Tensor:', tensor)
print('Numpy array:', numpy_array)
Run Code Online (Sandbox Code Playgroud)

输出:

Before edit:
Tensor: tensor([0.1286, 0.4899])
Numpy array: [0.1285522  0.48987144]

After edit:
Tensor: tensor([10.0000,  0.4899])
Numpy array: [10.        0.48987144]
Run Code Online (Sandbox Code Playgroud)

第一个元素的值由张量和 numpy 数组共享。将张量中的值更改为 10 也会更改 numpy 数组中的值。