우리가 Neural network를 학습시킬 때 사용되는 알고리즘은 '역전파 알고리즘' (Backpropagation)이라고 한다.
이 알고리즘을 활용해서 우리는 모델의 가중치 (weight, parameter)를 loss function의 gradient를 활용해서 조정해간다.
예를 들어 (Mini-batch) Stochastic gradient descent 알고리즘은 다음과 같이 파라미터 업데이트를 진행한다.
$w_{t+1} = w_t - \eta_t \sum_{i=1}^b \nabla \mathcal{L}_i(w_t)$
여기서 $w_t$는 t번째 iteration에서의 파라미터, $\eta_t$는 t번째 iteration에서의 step-size (또는 learning-rate), $\mathcal{L}$은 손실함수이다.
우리는 일반적으로 전체 training dataset에서 mini-batch size ($b$) 만큼 데이터를 sampling하여 Forward 연산을 진행하고 그 결과 얻은 손실 함수 ($\mathcal{L}_i(w_t)$)를 파라미터에 대하여 미분한 $\nabla \mathcal{L}_i(w_t)$를 이용해서 파라미터 업데이트를 진행한다.
이 gradient를 계산하기 위해서 파이토치는 내장 미분 엔진을 가지고 있는데 이를 Automatic diffrentiation 엔진이라고 한다.
그럼 어떻게 Automatic differentiation 엔진을 활용해 gradient를 계산해낼까?
이때 등장하는 개념이 'computational graph' (계산 그래프)이다.
여기서 $x$라고 하는 데이터 (input)은 $w$와 행렬곱이 이뤄지고 $b$와 element-wise 덧셈이 이뤄져서 그 결과 나온 값을 $z$라 정의하였다. 이를 정답 (label)에 해당하는 $y$와의 차를 Cross-entropy loss function을 활용해 계산한 값이 loss 값이다.
우리는 이 loss 값을 활용해서 파라미터 $w$, $b$를 업데이트해야한다.
즉, $w$에 대한 loss의 gradient (${\partial \over \partial w}\mathcal{L}$) , $b$에 대한 loss의 gradient (${\partial \over \partial b} \mathcal{L}$) 를 계산해야 한다.
이를 위해선 Tensor $w$와 $b$에 다음과 같은 속성값이 설정되어져 있어야 한다.
import torch
w = torch.randn(5, 3, requires_grad = True)
b = torch.randn(3, requires_grad = True)
z = torch.matmul(x, w) + b
여기서 우리는 $w$와 $b$라는 Tensor를 생성할 때 requires_grad = True라는 속성값을 설정해주었다.
computational graph는 어떠한 순서와 연산 규칙에 따라 Forward 연산이 진행되었는지를 알고 있다.
즉, 위에 예시에서는 $x$라는 input에 대해 $w$와 행렬곱, $b$와 element-wise 덧셈이 진행되었다는 것을 알고 있다.
그리고 손실함수 값에 대하여 거꾸로 어떻게 $w$와 $b$에 대한 미분값을 구하는지도 알고 있다.
실제 gradient 계산은 다음의 코드를 통해서 이뤄진다.
loss.backward()
.backward() 메소드는 $w$에 대한 loss의 gradient, $b$에 대한 loss의 gradient를 계산할 수 있도록 해준다.
그리고 이 값들은 w.grad, b.grad를 통해서도 확인해볼 수 있다.
한 가지 기억할 요소는 파이토치에서 생성되는 모든 텐서는 'requires_grad = True'가 Default이다.
즉 위의 $w$와 $b$처럼 명시적으로 requires_grad = True라고 하지 않아도 이미 이 값으로 설정이 되어져 있다.
하지만 역으로, 우리는 해당 텐서에 대한 gradient 계산이 필요하지 않을 때도 있다.
가장 대표적인 상황이 모델 Test이다. 이때는 backpropagaion이 필요하지 않다.
혹은 Pretrained model에 대한 Fine tuning을 진행할 떄도 우리는 모델의 일부 parameter에 대해서만 학습하기 때문에 대다수의 parameter에 대해서는 frozen 해놓는다.
with torch.no_grad():
z = torch.matmul(x, w) + b
print(z.requires_grad) # False
위에서처럼 torch.no_grad()로 감싸주면은 그 구문 안에 있는 tensor에 대해서는 computational graph가 추적하지 않기 때문에 gradient 계산이 이뤄지지 않는다.
그러면 위와 같이 계산된 gradient로 어떻게 파라미터를 업데이트할까?
다음과 같은 코드를 통해서 진행된다.
optimizer = torch.optim.SGD(model.parameters(), lr = learning_rate)
optimizer.step()
optimizer.zero_grad()
여기서 optimizer 변수는 파이토치에 내장된 특정 optimizer를 활용하였다.
파이토치에는 SGD, (Heavy-ball, Nesterov, etc) Momentum, RMSProp, Adam, etc 등 다양한 최적화를 위한 알고리즘들이 구현되어져 있다. 우리는 이를 위와 같이 가져다 활용해주기만 하면 된다.
optimizer.step()이 호출되면 우리가 앞서 loss.backward()에서 계산된 gradient를 활용해서 파라미터 업데이트가 진행된다.
그렇다면 optimizer.zero_grad()는 무엇일까?
앞서 이야기한 것처럼 .backward() 메소드가 호출되면 텐서들에 대한 gradient가 계산이 된다.
이 gradient는 optimizer.step()을 통해 파라미터 텐서들의 업데이트에 사용된다고 하여 메모리에서 사라지지 않는다.
그렇기 때문에 이 gradient 값들을 우리는 초기화해주어야 다음 step (iteration)에서 또 gradient를 계산해서 파라미터 업데이트를 진행할 수 있다.
이를 위해서 우리는 .zero_grad() 메소드를 호출해준다.
'Deep dive into Pytorch' 카테고리의 다른 글
Pytorch 6 : Implement CNN (0) | 2023.07.20 |
---|---|
Pytorch 5 : Save and Load (0) | 2023.07.17 |
Pytorch 3 : Neural network 구현 (0) | 2023.07.15 |
Pytorch 2 (0) | 2023.07.11 |
Pytorch 1. Tensor (0) | 2023.07.06 |
댓글