PyTorch - autograd
본 글은 제가 PyTorch Turotial의 autograd
부분을 읽고 이해가 안 되는 부분을 보충하기 위해 정리한 글입니다. 지적은 언제나 환영입니다 :)
읽기자료:
PyTorch Tutorials - AUTOGRAD: AUTOMATIC DIFFERENTIATION
autograd
은 딥러닝의 Backpropagation을 코드로 구현한 PyTorch의 하부 패키지입니다. 그래서 Backpropagation에 대해 먼저 숙지하고 본 글을 읽으시길 바랍니다.
Backpropagation은 각 뉴런의 가중치weight를 업데이트하는 과정이다. 그리고 그 업데이트 과정은 출력층에서 입력층 순서로 진행된다. |
autograd
란?
autograd
는 PyTorch에서 Backpropagation을 수행하는 PyTorch의 하부 패키지이다. Backpropagation을 수행하기 위해선 뉴런에서 행해지는 연산에 대한 Gradient미분값를 구해야 한다. autograd
는 텐서 객체의 변수 grad
에 Gradient 값을 담아준다.
요약: autograd
는 텐서의 연산에 대해 자동으로 Gradient 값을 구해주는 패키지이다.
텐서 객체와 autograd
텐서를 생성할 때 requires_grad
옵션을 True
로 설정하면, 앞으로 그 텐서에 행해지는 모든 연산을 추적하고 그에 대한 Gradient를 계산해준다.
requires_grad
옵션을 True
로 하고 텐서 x
를 만들어 보았다.
x = torch.ones(2, 2, requires_grad=True)
텐서 x
에 덧셈 연산을 수행하여 새로운 텐서 y
를 만든다.
y = x + 2
out
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward0>)
텐서 y
는 grad_fn
이라는 멤버 변수도 가지고 있다. grad_fn
은 해당 텐서 객체가 어떤 연산으로부터 생성되었는지를 알려준다.
단, 텐서 객체가 다른 텐서에서 연산을 통해 생성된 것이 아니라 torch.ones()
나 torch.rand()
와 같이 사용자가 직접 텐서 객체를 생성한 경우는 grad_fn
값이 none
이다.
연산을 더 사용해서 텐서 객체를 더 만들어보자.
z = y * y * 3
out = z.mean()
out
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward0>)
tensor(27., grad_fn=<MeanBackward0>)
Gradient
Backpropagation을 하기 위해선 텐서 객체에서 backward()
함수를 실행해야 한다.
out.backward()
그리고 그 결과인 Gradient 값은 텐서 객체의 grad
에 담긴다.
print(x.grad)
out
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
이때, x.grad
의 값은 d(out)/dx에 대한 값이다.
결과를 신경망의 관점에서 해석해보자.
우리가 지금까지 연산(*
, +
, mean()
)으로 텐서 x
, y
, z
, out
을 만든 과정은 일종의 Loss functionLoss function을 만든 것이다.
처음에 만든 텐서 x
는 가중치이다. 직접 만든 텐서 객체에는 grad_fn
값이 없다고 했는데, 가중치 자체는 어떤 연산으로부터 유도된 값이 아니기 때문에 grad_fn
이 없는 것이 당연하다.
out
은 Loss function의 값 자체를 의미한다. Backpropagation은 “각 뉴런층의 가중치를 갱신해주는 작업”이다. 그리고 그 과정에서 Gradient 값이 사용된다. Gradient 값은 가중치에 해당하는 텐서 객체의 grad
에 담겨있다. 우리는 마지막에 out.backward()
를 호출하고, x.grad
값을 확인하였다. x.grad
는 d(out)/dx 값을 의미한다.
GDGradient Descent에서는 d(Loss)/dw를 구하여 가중치를 갱신한다. 우리가 얻은 x.grad
(=d(out)/dx)가 가중치를 갱신하는 d(Loss)/dw인 것이다.
autograd
패키지를 define-by-run실행시점에 정의 프레임워크라고 설명을 한다. 이것은 Backpropagation이 정적으로, 하나의 형태로 고정된 것이 아니라 python 코드가 실행되는 시점에 동적으로 정의되고, 그에 따라 Backpropgation의 형태가 변할 수 있다는 것이다.
autograd
패키지의 콘셉트을 정리하면 다음과 같다.
- 편리한(?) Backpropagation을 제공
backward()
을 호출하면, Gradient를 자동으로 계산grad_fn
을 통해서 연산을 추적하고 기록
(번외) autograd
와 vector-Jacobian Product
PyTorch Tutorial에서는 vector-Jacobian Product(VJP)를 언급하고 있다.
Jacobian $J$는 vector-valued function1의 Gradient를 표현한 행렬이다.
이번엔 벡터 $\vec { y }$를 입력으로 갖는 스칼라 함수 $L = g\left( \vec { y } \right)$를 살펴보자. 함수 $L$의 $\vec { y }$에 대한 Gradient인 벡터 $\vec { v }$는 다음과 같다.
이제 벡터 $\vec { v }$와 Jacobian $J$를 곱할 것이다. 이때, $J$는 벡터 $\vec { y }$의 벡터 $\vec { x }$에 대한 Gradient이다. 결과는 Chain Rule에 의해 다음과 같다. 2
이제 vector-Jacobian Product가 PyTorch와 무슨 관련이 있는지 살펴보자.
위의 수식에서 $\vec { x }$를 가중치로, $L$을 Loss function이라고 생각해보자. 가중치 $\vec { x }$는 어떤 연산들(=함수 $f$)을 거쳐서 벡터 $\vec { y }$가 된다. 그리고 벡터 $\vec { y }$는 또 어떤 연산(=함수 $g$)를 거쳐서 Loss function $L$을 만든다.
벡터 $\vec { v }$는 Loss $L$의 $\vec { y }$에 대한 Gradient인데, $\vec { v }$는 grad_tensor
로 backward()
함수에 인자로 전달된다.3 (벡터 $\vec { v }$를 전달하는 이유는 원래는 L.backward()
를 호출해서 구했어야 할 $\nabla_{\vec{ y }} {L}$를 $\vec{ v }$로 대체하기 때문이다.)
이제 PyTorch Tutorial에 제시된 코드를 약간 변형하여 살펴보자.
x = torch.randn(3, requires_grad=True)
y = x * 2
v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)
print(x.grad)
tensor([1.0240e+02, 1.0240e+03, 1.0240e-01])
non-scalar function인 y
에 대한 Gradient를 구하려면 vector-Jacobian Product를 사용해야 하고, VJP를 하려면 벡터 $\vec {v}$가 필요하다. 그래서 벡터 v
를 생성한 후, backward()
함수의 인자로 전달하였다.
결론은 “autograd
는 backward()
가 실행될 때, 내부적으로 vector-Jacobian Product를 통해 Gradient를 구한다”는 것이다.
Tips
- 만약
backward()
함수를 실행하지 않고,x.grad
에 접근한다면,None
이 리턴된다. - non-scalar function인
y
에서backward()
를 호출할 때,grad_tensor
없이y.backward()
하게 되면 오류가 발생한다. .grad
에서 주의할 점은 계산 그래프에서 leaf node에 해당하는 텐서의.grad
값에만 접근할 수 있다는 것이다. 만약 non-leaf node 텐서의.grad
에 접근하려 한다면,backward()
함수 호출 전에.retain_grad()
함수를 실행하자!- 위 방법으로 텐서
z
의grad
를 확인해보면,z.grad
는 d(out)/dz가 된다.
- 위 방법으로 텐서
참고자료
-
vector-valued function $\vec { y } =f\left( \vec { x } \right)$은 벡터를 출력하는 함수이다. 이때, 입력이 벡터이기 때문에 함수 $\vec { y }$는 다변수 함수이다. 다변수 함수의 Gradient는 Jacobian $J$ 같은 행렬의 꼴로 표현된다. ↩
-
이름은 “vector-Jacobian”인데 ${ J }^{ T }\cdot v$를 한 이유는 ${ v }^{ T }\cdot J$가 row vector를 리턴하기 때문이다. column vector를 취해야 또다른 Jacobian $J_2$을 함수처럼 ${ J_1 }^{ T }\cdot v$ 결과 앞에 씌울 수 있기 때문이다. ${ J_2 }^{ T }\cdot \left( { J_1 }^{ T }\cdot v \right)$ ↩
-
앞에서는
backward()
함수에 인자를 넣지 않았음에 주목하라. 그 경우는out
이 scalar function이기 때문에 가능한 것이다. 만약 non-scalar function에서backward()
를 호출한다면,grad_tensor
라는 인자가 필요하다! ↩ -
x
를randn()
으로 생성했기 때문에 output 값은 의미가 없다. ↩ -
Variable
이라는 클래스가 언급된다. 그런데Variable
클래스는 지원이 중단된 클래스로 2020년 9월 기준으로는 더이상 사용되지 않는다.linkVariable
을Tensor
클래스로 치환하여 이해하는 것이 좋을 것 같다. ↩