본문 바로가기

Machine Learning Models/Pytorch

Pytorch 텐서 복사 - clone, detach 차이

반응형

Pytorch에서 텐서를 복사하는 방법은 여러 가지가 있지만 대표적으로 clone 메서드와 detach 메서드가 있습니다. 먼저 clone 메서드는 복사한 텐서를 새로운 메모리에 할당합니다. 따라서 복사 대상 텐서의 값이 변해도 복사한 텐서의 값이 변하지 않는 deepcopy라고 볼 수 있습니다. 하지만 detach 메서드는 텐서를 복사하지만 복사 대상 텐서와 메모리를 공유합니다. 따라서 기존 텐서의 값이 변하면 복사한 텐서의 값도 따라서 변하게 됩니다. 다만, back-propagation을 위한 기울기 계산 그래프에서 제외되죠. 먼저 clone 메서드부터 보겠습니다.

clone()

어떠한 텐서를 clone 메서드를 이용해 복사하게 되면 기존 계산 히스토리 이력은 그대로 유지하면서 새로운 메모리를 할당합니다. 따라서 기울기 계산도 그대로 적용되면서, in-place operation error도 피할 수 있습니다.

In-place operation 이란 텐서의 특정 인덱스를 어떤 값으로 치환하는 것을 말합니다. Pytorch에서는 기울기가 계산되는 리프 노드 (requires_grad=True)에 대해서는 in-place operation이 금지되어 있습니다. 따라서 다음과 같이 에러가 발생합니다. In-place operation이 필요한 경우에는 원하는 tensor에 대해 clone 메서드로 복사한 이후에 실행하면 에러가 발생하지 않습니다.
x = torch.ones(5,requires_grad=True)
x[0] = 2
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
<ipython-input-19-7a833b03170b> in <cell line: 2>()
      1 x = torch.ones(5,requires_grad=True)
----> 2 x[0] = 2

RuntimeError: a view of a leaf Variable that requires grad is being used in an in-place operation.

clone 메서드를 써서 복사한 텐서는 원본 텐서의 기울기 함수 (grad_fn)을 가지게 되고 이는 그래프 상에서 기울기가 전파되도록 합니다. 다음과 같이 sum.backward() 함수가 실행되었을 때, $y=x*2, z=x*3$과 같으므로 기울기는 5가 됩니다. 하지만 복사한 $y$ 자체는 grad 값을 가지지 않는데요, 이는 clone 메서드로 복사하게 되면 grad 속성을 가진 리프 노드는 아니기 때문입니다. 즉, grad_fn을 통해 기울기는 전파시키지만 그 자체는 grad 값을 가지지 않는 거죠.

x = torch.ones(5,requires_grad=True)
y = x.clone()*2
z = x.clone()*3
sum = (y+z).sum()
sum.backward()
print(x.grad)
>>> tensor([5., 5., 5., 5., 5.])

print(y.grad, y.requires_grad, y.is_leaf)
>>> None True False
<ipython-input-21-358e98c52022>:1: UserWarning: The .grad attribute of a Tensor that is not a leaf Tensor is being accessed. Its .grad attribute won't be populated during autograd.backward(). If you indeed want the .grad field to be populated for a non-leaf Tensor, use .retain_grad() on the non-leaf Tensor. If you access the non-leaf Tensor by mistake, make sure you access the leaf Tensor instead. See github.com/pytorch/pytorch/pull/30531 for more informations. (Triggered internally at aten/src/ATen/core/TensorBody.h:486.)
  print(y.grad, y.requires_grad, y.is_leaf)

 

detach()

만약 계산 그래프에서 아예 배제하고 싶다면 detach 메서드를 사용하면 됩니다. detach 메서드로 텐서를 복사하게 되면 원본 텐서와 메모리는 공유하지만 requires_grad=False 인 텐서를 만들게 되어 해당 텐서에 대해서는 기울기가 계산되지 않습니다. 다음 예에서처럼 $z$는 detach 되었으므로 $x$의 기울기 계산과정에서 빠지게 되는 거죠. 메모리를 공유하기 때문에 복사한 텐서의 값을 바꾸면 원본 텐서에도 동일하게 적용됩니다. 

x = torch.ones(5,requires_grad=True)
y = x*2
z = x.detach()
sum = (y+z).sum()
sum.backward()
print(x.grad)
>>> tensor([2., 2., 2., 2., 2.])

z[3] = 2
print(x, z)
>>> tensor([1., 1., 1., 2., 1.], requires_grad=True) tensor([1., 1., 1., 2., 1.])

clone 메서드와 달리 detach 메서드로 복사한 텐서에 in-place operation을 적용하게 되면 메모리를 공유하기 때문에 마찬가지로 에러가 발생합니다.

a = torch.ones(5, requires_grad=True)
b = a**2
c = a.detach()
c.zero_()
b.sum().backward()
print(a.grad)
>>> RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation: [torch.FloatTensor [5]] is at version 1; expected version 0 instead. Hint: enable anomaly detection to find the operation that failed to compute its gradient, with torch.autograd.set_detect_anomaly(True).

 

clone().detach() or detach().clone()

결국 텐서 복사는 clone과 detach의 특성을 합쳐서 사용하는 것이 제일 깔끔합니다. clone().detach()와 detach().clone() 모두 계산 그래프에서 제외된 텐서를 새로운 메모리로 할당하는 동작은 같습니다. clone().detach()를 하게 되면 새로운 텐서를 메모리에 할당하고 그것을 기존 계산 그래프와 끊어버립니다. detach().clone()을 하게 되면 먼저 기울기가 계산되지 않은 텐서를 만들고 새로운 메모리에 할당합니다. detach 메서드를 먼저 호출하는 것이 먼저 계산 그래프에서 떼어져 나왔으므로 이후의 clone operation이 트래킹 되지 않기 때문에 조금 더 빠릅니다.

https://seducinghyeok.tistory.com/10

반응형