为啥我们在 Pytorch 张量上调用 .numpy() 之前调用 .detach()?
Posted
技术标签:
【中文标题】为啥我们在 Pytorch 张量上调用 .numpy() 之前调用 .detach()?【英文标题】:Why do we call .detach() before calling .numpy() on a Pytorch Tensor?为什么我们在 Pytorch 张量上调用 .numpy() 之前调用 .detach()? 【发布时间】:2020-12-14 08:46:57 【问题描述】:It has been firmly established that my_tensor.detach().numpy()
is the correct way to get a numpy array from a torch
tensor.
我正试图更好地理解原因。
在accepted answer 刚刚链接的问题中,Blupon 指出:
您需要将您的张量转换为另一个除了实际值定义之外不需要梯度的张量。
在他链接到的第一个讨论中,albanD 说:
这是预期行为,因为移动到 numpy 会破坏图形,因此不会计算梯度。
如果你实际上不需要梯度,那么你可以显式地 .detach() 需要 grad 的 Tensor 得到一个内容相同但不需要 grad 的张量。然后可以将这个其他张量转换为 numpy 数组。
在他链接到的第二次讨论中,apaszke 写道:
变量不能转换为 numpy,因为它们是保存操作历史的张量的包装器,而 numpy 没有这样的对象。您可以使用 .data 属性检索变量持有的张量。然后,这应该可以工作:var.data.numpy()。
我研究了 PyTorch 的自分化库的内部工作原理,但我仍然对这些答案感到困惑。为什么它会破坏图表以移动到 numpy?是不是因为对numpy数组的任何操作都不会在autodiff图中被跟踪?
什么是变量?它与张量有何关系?
我觉得这里需要一个彻底的高质量 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))
【问题讨论】:
变量接口已经被弃用了很长时间(从 pytorch 0.4.0 开始)。任何说明它们是必要的 autograd 的描述都已经过时了几年。 official docs 中是否有一些令人困惑的地方。我认为他们很好地封装了如何使用张量的grad_fn
属性构造计算图(当然,numpy 数组没有为数组填充的grad_fn
属性,该属性是由操作产生的,因此无法跟踪梯度对于那些)。
它描述了使用grad_fn
属性跟踪操作,该属性为任何新张量填充,这是涉及张量的可微函数的结果。由于此跟踪功能是张量类的一部分,而不是 numpy 数组,因此一旦转换为 numpy 数组,您将无法再跟踪这些操作,因此无法应用微分链规则(也称为反向传播)
另外,这可能会导致混淆,但没有计算图对象。所谓的计算图实际上是张量和函数的抽象组合。您生成的张量指的是函数(使用grad_fn
),这些函数本身指的是其他张量,这些张量指的是函数等。给定一个张量,您可以通过grad_fn
引用进行追溯,最终将引用您的模型参数(叶张量)。如果在中间转换为 numpy 数组,则无法追溯到这些参数,因为只有张量具有 grad_fn
。
@jodag 我的问题最初是通过阅读docs for detach 提示的。既然我已经看到了您的评论答案,我认为该文档只是说分离的张量没有通过 grad_fn 与另一个张量绑定。但它与另一个张量共享内存的事实让人感觉很奇怪。但我觉得这与我在这里想问的问题不同。
【参考方案1】:
我问,为什么它会破坏图表以移动到 numpy?是不是因为 numpy 数组上的任何操作都不会在 autodiff 图中被跟踪?
是的,新张量不会通过grad_fn
连接到旧张量,因此对新张量的任何操作都不会将梯度带回旧张量。
写my_tensor.detach().numpy()
只是在说,“我将根据这个张量在一个 numpy 数组中的值进行一些非跟踪计算。”
深入学习 (d2l) 教科书 has a nice section describing the detach() method,虽然它没有讨论为什么在转换为 numpy 数组之前分离是有意义的。
感谢 jodag 帮助回答这个问题。正如他所说,变量已过时,因此我们可以忽略该评论。
我认为到目前为止我能找到的最佳答案是jodag's doc link:
要阻止张量跟踪历史,您可以调用 .detach() 将其从计算历史中分离出来,并防止未来的计算被跟踪。
在我在问题中引用的 albanD 的评论中:
如果你实际上不需要梯度,那么你可以显式地 .detach() 需要 grad 的 Tensor 得到一个内容相同但不需要 grad 的张量。然后可以将这个其他张量转换为 numpy 数组。
换句话说,detach
方法的意思是“我不想要梯度”,并且不可能通过 numpy
操作来跟踪梯度(毕竟,这就是 PyTorch 张量的用途!)
【讨论】:
看来您已经很清楚地得到了答案。为什么要赏金?有什么你认为可以更清楚的吗? @hkchengrex 等人。我正在专门寻找一个答案,通过适合新手的数字和简单语言来解释为什么必须调用 detach()。我认为,如果这些数字说明了图表、grad_fn 等,例如我刚刚从 Blupon 借来并粘贴在我上面的问题中,它不仅可以更清楚地解释问题,还可以更清楚地解释 numpy 的 autodiff 功能。我觉得我自己对这个话题的理解相当好,但我认为这样的解释将为 SO 对 .detach() 的覆盖提供更多的理论深度,而不是快速代码解决方案。【参考方案2】:我认为这里要理解的最关键的一点是torch.tensor
和np.ndarray
之间的区别:
虽然这两个对象都用于存储 n 维矩阵(又名"Tensors"),但torch.tensors
有一个额外的“层” - 存储导致相关联的 n 维矩阵的计算图。
因此,如果您只对在矩阵上执行数学运算的高效且简单的方法感兴趣,np.ndarray
或 torch.tensor
可以互换使用。
但是,torch.tensor
s 旨在用于gradient descent 优化的上下文中,因此它们不仅包含具有数值的张量,而且(更重要的是)包含导致这些值的计算图。然后使用这个计算图(使用chain rule of derivatives)来计算损失函数的导数 w.r.t 每个用于计算损失的自变量。
如前所述,np.ndarray
对象没有这个额外的“计算图”层,因此,在将 torch.tensor
转换为 np.ndarray
时,您必须明确删除张量使用detach()
命令。
计算图
从您的comments 看来,这个概念有点模糊。我将尝试用一个简单的例子来说明它。
考虑两个(向量)变量的简单函数,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
如果我们只对z
的值感兴趣,我们不必担心任何图形,我们只需将输入x
和w
向前 移动,以计算@ 987654348@ 然后z
。
但是,如果我们不太关心z
的价值,而是想问一个问题“w
是什么,最小化@987654352 @对于给定的x
"?
要回答这个问题,我们需要计算z
w.r.t w
的导数。
我们怎样才能做到这一点?
使用chain rule,我们知道dz/dw = dz/dy * dy/dw
。也就是说,要计算z
wrt w
的梯度,我们需要将backward 从z
移回w
,计算跟踪时每一步操作的梯度 back 我们从z
到w
的步骤。我们追溯的这条“路径”是z
的计算图,它告诉我们如何计算z
的导数 w.r.t 导致z
的输入:
z.backward() # ask pytorch to trace back the computation of z
我们现在可以检查z
w.r.t w
的梯度:
w.grad # the resulting gradient of z w.r.t w tensor([0.8010, 1.9746, 1.5904, 1.0408])
请注意,这完全等于
2*y*x tensor([0.8010, 1.9746, 1.5904, 1.0408], grad_fn=<MulBackward0>)
因为dz/dy = 2*y
和dy/dw = x
。
路径上的每个张量都存储其对计算的“贡献”:
z tensor(1.4061, grad_fn=<PowBackward0>)
和
y tensor(1.1858, grad_fn=<DotBackward>)
如您所见,y
和 z
不仅存储了 <x, w>
或 y**2
的“前向”值,还存储了计算图——grad_fn
在从z
(输出)到w
(输入)追溯梯度时,需要计算导数(使用链式法则)。
这些grad_fn
是torch.tensors
的基本组成部分,没有它们就无法计算复杂函数的导数。但是,np.ndarray
s 根本没有这个能力,他们也没有这个信息。
有关使用backwrd()
函数追溯导数的更多信息,请参阅this answer。
由于np.ndarray
和torch.tensor
都有一个共同的“层”来存储一个n-d 数组,pytorch 使用相同的存储来节省内存:
numpy() → numpy.ndarray
返回self
张量作为 NumPy ndarray。这个张量和返回的 ndarray 共享相同的底层存储。自张量的变化将反映在 ndarray 中,反之亦然。
另一个方向也是如此:
torch.from_numpy(ndarray) → Tensor
从 numpy.ndarray 创建张量。 返回的张量和 ndarray 共享相同的内存。对张量的修改将反映在 ndarray 中,反之亦然。
因此,当从torch.tensor
创建np.array
或反之亦然时,两个对象引用内存中相同的底层存储。由于np.ndarray
不存储/表示与数组关联的计算图,因此当共享 numpy 和 torch 希望引用相同的张量时,应该使用detach()
显式删除此图。
请注意,如果您出于某种原因希望仅将 pytorch 用于数学运算而不进行反向传播,您可以使用 with torch.no_grad()
上下文管理器,在这种情况下不会创建计算图,并且 torch.tensor
s 和 @ 987654397@s 可以互换使用。
with torch.no_grad():
x_t = torch.rand(3,4)
y_np = np.ones((4, 2), dtype=np.float32)
x_t @ torch.from_numpy(y_np) # dot product in torch
np.dot(x_t.numpy(), y_np) # the same dot product in numpy
【讨论】:
我认为您通常可以很好地保持讨论的简单和准确,但我发现共享内存的讨论令人困惑。我觉得有一些应该很明显的原因,“由于 np.ndarray 不存储/表示与数组关联的计算图,因此当共享 numpy 和 torch 希望引用相同的张量”,但它还不够明显。你能详细说明一下吗? 而且,您认为说明计算图的图(例如,我问题末尾的示例代码)会进一步阐明您的答案吗? @JosiahYoder 我添加了有关计算图的更多信息。 我真的很喜欢你提到with torch.no_grad()
作为分离的替代品。
@DavidWaterworth 因为它们共享相同的存储空间,如果你不明确分离真的很糟糕的事情 csn 会发生并且调试起来非常困难【参考方案3】:
这是一个张量的小展示 -> 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)
输出:
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]
第一个元素的值由张量和numpy数组共享。在张量中将其更改为 10 也会在 numpy 数组中更改它。
【讨论】:
以上是关于为啥我们在 Pytorch 张量上调用 .numpy() 之前调用 .detach()?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 PyTorch nn.Module.cuda() 不移动模块张量而只移动参数和缓冲区到 GPU?