본 글은 제가 PyTorch Turotial의 autograd 부분을 읽고 이해가 안 되는 부분을 보충하기 위해 정리한 글입니다. 지적은 언제나 환영입니다 :)

11 minute read

본 글은 제가 PyTorch Turotialautograd 부분을 읽고 이해가 안 되는 부분을 보충하기 위해 정리한 글입니다. 지적은 언제나 환영입니다 :)

읽기자료:
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>)

텐서 ygrad_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 값을 의미한다.

$$w\leftarrow w-\eta \nabla _{ w }Loss\left( w \right)$$

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)를 언급하고 있다.

$$ J = \begin{pmatrix} \cfrac { \partial { y }_{ 1 } }{ \partial { x }_{ 1 } } & \cdots & \cfrac { \partial { y }_{ 1 } }{ \partial { x }_{ n } } \\ \vdots & \ddots & \vdots \\ \cfrac { \partial { y }_{ n } }{ \partial { x }_{ 1 } } & \cdots & \cfrac { \partial { y }_{ n } }{ \partial { x }_{ n } } \end{pmatrix} $$

Jacobian $J$는 vector-valued function1의 Gradient를 표현한 행렬이다.

이번엔 벡터 $\vec { y }$를 입력으로 갖는 스칼라 함수 $L = g\left( \vec { y } \right)$를 살펴보자. 함수 $L$의 $\vec { y }$에 대한 Gradient인 벡터 $\vec { v }$는 다음과 같다.

$$\vec { v } = { \left( \cfrac { \partial L }{ \partial { y }_{ 1 } } , ..., \cfrac { \partial L }{ \partial { y }_{ n } } \right) }^{ T }$$

이제 벡터 $\vec { v }$와 Jacobian $J$를 곱할 것이다. 이때, $J$는 벡터 $\vec { y }$의 벡터 $\vec { x }$에 대한 Gradient이다. 결과는 Chain Rule에 의해 다음과 같다. 2

$${ J }^{ T }\cdot v = \begin{pmatrix} \cfrac { \partial { y }_{ 1 } }{ \partial { x }_{ 1 } } & \cdots & \cfrac { \partial { y }_{ n } }{ \partial { x }_{ n } } \\ \vdots & \ddots & \vdots \\ \cfrac { \partial { y }_{ 1 } }{ \partial { x }_{ n } } & \cdots & \cfrac { \partial { y }_{ n } }{ \partial { x }_{ n } } \end{pmatrix}\begin{pmatrix} \cfrac { \partial L }{ \partial { y }_{ 1 } } \\ \vdots \\ \cfrac { \partial L }{ \partial { y }_{ n } } \end{pmatrix}=\begin{pmatrix} \cfrac { \partial L }{ \partial { x }_{ 1 } } \\ \vdots \\ \cfrac { \partial L }{ \partial { x }_{ n } } \end{pmatrix}$$

이제 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_tensorbackward() 함수에 인자로 전달된다.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)
out
tensor([1.0240e+02, 1.0240e+03, 1.0240e-01])

4

non-scalar function인 y에 대한 Gradient를 구하려면 vector-Jacobian Product를 사용해야 하고, VJP를 하려면 벡터 $\vec {v}$가 필요하다. 그래서 벡터 v를 생성한 후, backward() 함수의 인자로 전달하였다.

결론은 “autogradbackward()가 실행될 때, 내부적으로 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() 함수를 실행하자!
    • 위 방법으로 텐서 zgrad를 확인해보면, z.grad는 d(out)/dz가 된다.

참고자료


  1. vector-valued function $\vec { y } =f\left( \vec { x } \right)$은 벡터를 출력하는 함수이다. 이때, 입력이 벡터이기 때문에 함수 $\vec { y }$는 다변수 함수이다. 다변수 함수의 Gradient는 Jacobian $J$ 같은 행렬의 꼴로 표현된다. 

  2. 이름은 “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)$ 

  3. 앞에서는 backward() 함수에 인자를 넣지 않았음에 주목하라. 그 경우는 out이 scalar function이기 때문에 가능한 것이다. 만약 non-scalar function에서 backward()를 호출한다면, grad_tensor라는 인자가 필요하다! 

  4. xrandn()으로 생성했기 때문에 output 값은 의미가 없다. 

  5. Variable이라는 클래스가 언급된다. 그런데 Variable 클래스는 지원이 중단된 클래스로 2020년 9월 기준으로는 더이상 사용되지 않는다.link VariableTensor 클래스로 치환하여 이해하는 것이 좋을 것 같다. 

Categories:

Updated: