본문 바로가기

Python/Pytorch

[Pytorch] 텐서를 복사하는 방법 : clone, detach

반응형

이번 포스팅에서는 최근에 Pytorch로 GAN 모델을 구현하면서 알게된 Pytorch의 clone() 메소드와 detach() 메소드에 대해 알아보려고 한다.

 

오픈소스 딥러닝 프레임워크 중 하나인 Pytorch


 

파이토치에서 텐서를 복사하는 메소드에는 대표적으로 clonedetach가 존재한다. 하지만 이 2가지 사이에는 무슨 차이점이 있을까? 또한 이 2가지 메소드를 조합해서도 사용할 수 있는데, 그러면 조합할 때와 단일하게 사용할 때 간의 차이점은 무엇일까? 우선 이 차이점을 알기 전에 파이토치의 계산 그래프와 AutoGrad 메커니즘에 대해 알아야 이해가 수월할 수 있다. 이 2개는 딥러닝 모델의 순전파, 그리고 역전파 과정을 이해하는 데 필수이다. 필자도 [밑바닥부터 시작한느 딥러닝 시리즈]를 읽으면서 계산 그래프에 대해서 deep-dive하게 배웠었다. 필자의 글중 계산 그래프에 대한 기초 글을 참고하거나 파이토치 공식문서에서 작성된 AutoGrad 소개에 대한 글을 읽어보도록 하자.

1. 파이토치에서 텐서 생성하기

포스팅 본문 내용에 들어가기에 앞서 파이토치에서 텐서를 생성하는 방법에 대해 알아보자. 그리고 간단한 수식과 더불어서 역전파를 수행해보도록 하자. 먼저 딥러닝에서는 계산 그래프라는 것을 순전파 시에 생성하는 동시에 딥러닝 레이어들 중간 중간 각 레이어들의 출력값들을 캐싱해둔다. 그리고 역전파 시 (순전파 시에 생성된) 계산 그래프와 캐싱해둔 출력값들을 이용해 역전파를 수행한다.

 

밑바닥부터 시작하는 딥러닝 시리즈에서도 알아보았지만 순전파 시에 출력값들을 캐싱해두는 이유역전파 시에 이 캐싱해둔 출력값을 활용하게 되면 역전파 수식이 훨씬 간단해지기 때문이다.(이에 대해 자세한 내용이 궁금하다면 여러가지 연산에 대한 역전파 계산에 대해 소개했던 포스팅을 참고해보자)

 

우선 파이토치에서는 임의의 텐서를 생성하는데, requires_grad 라는 인자를 넣을 수 있다. 기본적으로 False가 부여되는데, 이를 True로 변경해 텐서를 생성하면 순전파 시 계산 그래프가 생성될 때 해당 텐서를 포함시키고, 역전파 시 기울기를 전파시키도록 한다. 아래처럼 임의의 텐서를 생성하고 간단한 계산식을 넣은 후 역전파르 수행했다.

 

import torch

x = torch.ones(5, requires_grad=True)
y = torch.ones(5, requires_grad=True)
z = (x * 2 + y / 5).sum()
z.backward()

print('x gradients:', x.grad)  # x gradients: tensor([2., 2., 2., 2., 2.])
print('y gradients:', y.grad)  # y gradients: tensor([0.2000, 0.2000, 0.2000, 0.2000, 0.2000])

 

위처럼 True 값을 넣어 x, y 텐서 모두 역전파가 가능한 것을 볼 수 있다. 이제 그럼 y 텐서에 False를 넣어보도록 하자.

 

import torch

x = torch.ones(5, requires_grad=True)
y = torch.ones(5, requires_grad=False)
z = (x * 2 + y / 5).sum()
z.backward()

print('x gradients:', x.grad)  # x gradients: tensor([2., 2., 2., 2., 2.])
print('y gradients:', y.grad)  # y gradients: None

 

 

 

y 텐서의 기울기 값이 None이 된 것을 볼 수 있다. 즉, requires_grad를 False로 지정하면 해당 텐서는 역전파 시 기울기를 전달하지 않도록 한다. 보통 기울기 전파 및 계산이 가능한 즉, requires_grad = True로 지정한 텐서를 리프 노드(leaf-node)라고 칭하고, False로 지정한 텐서를 논리프 노드(Non-leaf node)라고 이야기하기도 한다. 참고로 특정 텐서가 리프 노드인지 여부를 확인할 수 있도록 텐서에 is_leaf 라는 attribute 값도 제공이 된다.

 

또한 리프 노드인 경우에는 기울기 전파가 가능하기 때문에 원본의 원소 값을 수정하는 in-place 연산이 불가능하다. 반면, 논리프 노드의 경우에는 기울기 전파가 불가능하기 때문에 in-place 연산이 허용된다. 실제로 테스트해보면 아래와 같다.

 

import torch

x = torch.ones(5, requires_grad=True)
x[0] = 100
print(x) # RuntimeError: a view of a leaf Variable that requires grad is being used in an in-place operation.

 

import torch

x = torch.ones(5, requires_grad=False)
x[0] = 100
print(x)   # tensor([100.,   1.,   1.,   1.,   1.])

2. 텐서를 새로운 메모리에 할당하고 역전파를 허용한다 : clone()

위 1번 목차 내용을 이제 머릿속에 두고 본격적으로 텐서를 복사하는 방법 중 하나인 clone 메소드에 대해 알아보자. clone 메소드는 한 마디로 정리하면 원본 텐서를 deep-copy 즉, 새로운 메모리에 할당하고 기울기 함수를 갖고 있게 되어 기울기 전파가 가능하다. 하지만 복사된 텐서의 기울기 자체 값은 갖지 않는다. 왜냐하면 clone 으로 복사된 텐서는 리프 노드가 아니기 때문이다. 또한 새로운 메모리에 할당했기 때문에 in-place 연산을 하여도 원본 텐서의 값이 수정되지는 않는다.

 

실제로 아래처럼 테스트하면 결과가 다음과 같다.

 

import torch

x = torch.ones(5, requires_grad=True)
y = x.clone()

z = (2 * x + 5 / y).sum()
z.backward()

print('x:', x, '| x grad:', x.grad) # x: tensor([1., 1., 1., 1., 1.], requires_grad=True) | x grad: tensor([-3., -3., -3., -3., -3.])
print('y:', y, '| y grad:', y.grad) # y: tensor([1., 1., 1., 1., 1.], grad_fn=<CloneBackward0>) | y grad: None

 

clone 된 y 텐서를 출력해보니 기울기 함수 즉, grad_fn 은 갖고 있지만 기울기 자체값은 None인 것을 볼 수 있다. 기울기 함수도 네이밍을 보니 Clone 된 것임을 유추할 수 있다.

3. 원본 텐서와 메모리를 공유하고 역전파를 금지한다 : detach()

다음은 detach 메소드이다. detach 메소드를 정의하자면 원본 텐서를 swallow-copy 즉, 메모리를 새로 할당하지 않고 원본 메모리를 공유하지만 기울기 함수도 갖고 있지 않아 기울기 전파가 불가능한 방법이다. 기울기 함수도 갖고 있지 않기 때문에 기울기 값 자체도 역시 갖지 않는다. 또한 원본 텐서와 메모리를 공유하기 때문에 in-place 연산을 수행하면 원본 텐서의 원소값도 동일하게 수정된다.

 

테스트한 결과는 아래와 같다.

 

import torch

x = torch.ones(5, requires_grad=True)
y = x.detach()
z = (2 * x + 5 / y).sum()

z.backward()
print('x:', x, '| x grad:', x.grad) # x: tensor([1., 1., 1., 1., 1.], requires_grad=True) | x grad: tensor([2., 2., 2., 2., 2.])
print('y:', y, '| y grad:', y.grad) # y: tensor([1., 1., 1., 1., 1.]) | y grad: None

 

 

import torch

x = torch.ones(5, requires_grad=True)
y = x.detach()
y[0] = 100
print(x)

4. 그래서 뭘 써야되는데?

아마 이 부분이 가장 궁금할 것이다. clone도 알겠고 detach도 알겠는데, 그러면 실전에 대체 어떤걸 사용하라는 것인가? 정답은 둘 다 사용해야 가장 깔끔한 처리가 가능하다. 2개를 조합해서 사용하는 경우는 2가지가 있는데, clone().detach() 와 detach().clone() 이다. 사실 2개 모두 결과 형태는 동일하다. clone을 먼저 하냐, detach를 먼저하느냐의 차이이다.

 

clone과 detach 를 함께 사용한다는 것은 복사된 텐서를 계산 그래프에서 제외시키고 기울기 역전파를 시키지도 않는다는 의미이다. 굳이 차이가 있다라고 한다면 clone().detach()는 복사된 텐서를 새로운 메모리에 할당하고 난 뒤 계산 그래프에서 복사된 텐서를 제외시키는 것이다. 반면에 detach().clone()은 계산 그래프에서 복사된 텐서를 제외시킨 뒤 새로운 메모리에 할당하는 것이다. 이 순서가 달라짐에 따라 차이점이 존재하긴 하는데, detach를 먼저하는 것이 계산 그래프에서 먼저 제외시켰으므로 그 이후에 clone operation이 트래킹되지 않기 때문에 조금 더 빠르다고 한다.(출처: https://hongl.tistory.com/363)

 

그래서 위 clone, detach를 조합해서 사용하는 것을 최근에 GAN을 구현하면서 사용해보았다. 사용한 부분 코드는 여기를 참조해보자.

 

 

반응형

'Python > Pytorch' 카테고리의 다른 글

[pytorch] pytorch로 구현하는 Attention이 적용된 Seq2Seq  (1) 2024.01.21