컴퓨터비전(Computer Vision)

파이토치(Pytorch)[2] - 경사하강법

zzoming 2023. 10. 24. 16:42

경사하강법 

경사하강법은 미분가능한 복잡한 함수가있을 때 해당 함수의 최소점을 찾기 위한 방법이다. 기본 개념은 함수의 기울기(경사)를 구하고 경사의 반대 방향으로 계속 이동시켜 극값에 이를 때 까지 반복시키는 것이다.

이러한 방법은 다음과 같이 많이 비유되기도 한다

앞이 보이지 않는 안개가 낀 산을 내려올 때는 모든 방향으로 산을 더듬어 가며 산의 높이가 가장 낮아니는 방향으로 한 발씩 내딛어 갈 수 있

 


🤔 경사하강법을 왜 사용할까? 

  • 일반적으로 인공지능은 손실함수를 통해 자신의 파라미터를 검증한다.
  • 손실함수는 인공지능의 파라미터를 통해 나온 예측값과 실제 값의 차이라고 볼 수 있으며, 손실 함수 값을 가장 낮게 나오게 하는 파라미터들이 바로 최적의 파라미터이다. 
  •  

그렇다면 손실함수를 미분하여 최소,최대값을 찾으면 되지 않을까? 하는 의문이 들 수 있지만 경사하강법을 이용하여 최소의 근삿값을 찾는 이유는 다음과 같다 

  • 실제 우리가 마주치는 함수는 간단한 함수가 아닌 복잡하고, 비선형적인 함수가 대부분이다. 따라서 미분을 통하여 그 값을 계산하기 어려운 경우가 많다 
  • 미분을 구현하는 과정보다 경사하강법을 구현하여 최소값을 찾는 것이 실질적으로 더 효율적이다 

학습률(learning rate)

x가 위치한 지점이 양의 기울기라면 음의 방향으로 x를 옮겨야 할 것이고, 반대로 x 가 위치한 지점이 음의 기울기를 가지고 있으면 양의 방향으로 x를 움직여야 한다

 

이 논리를 수식으로 쓰면 다음과 같다

 

그렇다면 위 식에서 이동거리는 어떻게 생각해야 할까? 

이 문제에 대해서 다시 생각해본다면 미분계수(기울기 혹은 gradient) 값은 극소값에 가까울 수록 그 값이 작아진다.

사실 극대값에 가까울 때도 미분계수는 작아지기 마련인데, 경사하강법 과정에서 극대값에 머물러있는 경우는 매우 드물기에 이 문제에 대해서는 고려하지 않겠다.

 

따라서 이동거리에 사용할  값을 gradient의 크기와 비례하는 factor을 이용하면 현재 x의 값이 극소값에서 멀 때는 많이 이동하고, 극소값에 가까워졌을때는 조금씩 이동할 수 있게된다. 

이동거리는 gradient(기울기)를 직접 이용하되,  gradient에 상수를 곱해주어 움직임의 속도를 조정할 수 있다. 이때  상수 를 학습률이라고 부른다. 


학습률(learning rate)

학습률이 0에 가까울 수록 파라미터 업데이트의 양은 줄어들 것이고 커질수록 파라미터 업데이트의 양은 늘어날 것이다. 또한, 파라미터가 한 번 업데이트되는 것을 한 스텝 (step) 움직인다고 말한다.  

 

 

위의 그림과 같이 학습률이 큰 경우 한 번 이동하는 거리가 커지므로 빠르게 수렴할 수도 있지만, 최소값을 계산하지 못하고 함수 값이 계속 커지는 방향으로 진행될 수도 있다. 

 

또, 너무 작은 경우에는 발산하지는 않겠지만, 최적의 x를 구하는데 소요되는 시간이 오래 걸릴 수 있으먀, 자칫 지역 최소점에 빠질 수 있다. 

 

데이터를 통해 학습이 진행되는 가중치 파라미터와 달리 모델의 성능에 영향을 줄 수 있지만 사용자에 의해 직접 설정 되어야하는 파라미터를 하이퍼파라미터라고 한다

 

학습률과 같은 하이퍼파라미터들은 모델의 성능에 영향을 끼치지만 최적의 값으로 자동 학습 되거나 설정되지 않기 때문에 지리멸렬한 실험과 직접적인 튜닝 과정을 거치기도 하며, 결국 사용자의 경험과 능력에 의해 좌지우지 되는 경우가 많다.


전역 최소점(global minima) 과 지역 최소점 (local minima)

우리가 목표로 하는 지점은 전체 구간에서 가장 낮은 함수의 출력값을 갖는 지점인 global minima 지점이다. 경사하강법 알고리즘을 시작하는 위치는 매번 랜덤하기 때문에 어떤 경우에는 지역 최소점 (Local minimua) 에 빠질 우려가 있다. 


경사하강법 구현 

경사하강법을 통해 함수의 출력을 최소화하는 함수의 입력을 찾을 수 있다.

import torch
import numpy as np
import torch.nn.functional as F

1. 목표 텐서 생성 

target = torch.FloatTensor([[1,2,3],
                           [4,5,6],
                           [7,8,9]])
print(target.size())

#출력결과
torch.Size([3, 3])

2. 랜덤 값을 갖는 텐서를 생성한다. 우리는 랜덤 텐서와 목표 텐서 사이의 차이가 최소화가 되도록 할 것이다. 

x = torch.rand_like(target)
x.requires_grad = True
print(x)

#실행결과
tensor([[0.6979, 0.7487, 0.4355],
        [0.0163, 0.1890, 0.9071],
        [0.7579, 0.2994, 0.0791]], requires_grad=True)

 

이 과정에서  텐서의 requires_grad 속성이 True 가 되도록 설정해줘야 한다 

🤔requires_grad = True 로 설정하면 해당 텐서에서 이루어지는 모든 연산(operation)들을 추적하기 시작 

기록을 추적하는 것을 중단하게 하려면 .detach() 를 호출하여 연산기록으로 부터 분리 

 

3. 두 텐서 사이의 손실 값을 계산한다

loss = F.mse_loss(x, target)
print(loss)

#실행결과
tensor(27.8926, grad_fn=<MseLossBackward0>)

4. 이제 while 반복문을 사용하여 두 텐서 값의 차이가 변수 threshold의 값보다 작아질 때까지 미분 및 경사하강법을 반복 수행한다

 

여기에서는 backward 함수를 통해 편미분을 수행한다. 편미분을 통해 얻어진 gradient이 x.grad에 자동으로 저장되고 이 값을 활용하여 경사하강법을 수행한다.

requires_grad_(...)는 기존 텐서의 requires_grad 값을 바꿔치기(in-place)하여 변경한다. 

threshold = 1e-5 
learning_rate = 1 #학습률 설정
iter_cnt = 0 

while loss > threshold :
    iter_cnt += 1
    loss.backward() #기울기 계산

    x = x- learning_rate * x.grad 

    x.detach_()
    x.requires_grad_(True)

    loss = F.mse_loss(x, target) 

    print('%d-th Loss : %.4e' % (iter_cnt ,loss))
    print(x)
    
#실행결과
1-th Loss : 1.6545e+01
tensor([[0.3846, 0.6731, 1.4078],
        [0.9227, 1.6378, 1.8003],
        [2.0462, 2.2522, 2.0542]], requires_grad=True)
2-th Loss : 1.0009e+01
tensor([[0.5213, 0.9680, 1.7616],
        [1.6066, 2.3850, 2.7335],
        [3.1471, 3.5295, 3.5977]], requires_grad=True)
3-th Loss : 6.0547e+00
tensor([[0.6277, 1.1973, 2.0368],
        [2.1384, 2.9661, 3.4594],
        [4.0033, 4.5229, 4.7982]], requires_grad=True)
4-th Loss : 3.6627e+00
tensor([[0.7104, 1.3757, 2.2509],
        [2.5521, 3.4181, 4.0240],
        [4.6692, 5.2956, 5.7320]], requires_grad=True)
5-th Loss : 2.2157e+00
tensor([[0.7748, 1.5144, 2.4173],
        [2.8739, 3.7696, 4.4631],
        [5.1872, 5.8966, 6.4582]], requires_grad=True)      
        
             .....
             
29-th Loss : 1.2780e-05
tensor([[0.9995, 1.9988, 2.9986],
        [3.9973, 4.9970, 5.9963],
        [6.9956, 7.9949, 8.9939]], requires_grad=True)
30-th Loss : 7.7319e-06
tensor([[0.9996, 1.9991, 2.9989],
        [3.9979, 4.9977, 5.9971],
        [6.9966, 7.9961, 8.9953]], requires_grad=True)

코드 실행결과를 보면 손실 값이 점차 줄어드는 것을 볼 수 있고ㅡ 텐서 x의 값이 목표 텐서 값에 근접해 가는 것을 확인할 수 있다. 만약에 학습률 변수를 조절한다면 텐서 x가 목표텐서에 근접해 가는 속도가 달라질 수 있다.


Autograd (자동편미분)

 

  • 파이토치는 오토그래드 알고리즘을 통해서 미분을 자동으로 수행
  • 스칼라 값에 backward함수를 호출하여 자동으로 미분 수행
  • 텐서간의 연산마다 계산 그래프를 자동으로 생성하고 나중에 미분할 때 활용
x = torch.FloatTensor([[1,2],
                       [3,4]]).requires_grad_(True)
print(x)

#실행결과
tensor([[1., 2.],
        [3., 4.]], requires_grad=True)

requires_grad 속성이 True인 텐서가 있을 때 이 텐서가 들어간 연산의 결과가 담긴 텐서도 자동으로 require_grad 속성값이 True를 갖게 된다

x1 = x +2 
print(x1)

#실행결과
tensor([[3., 4.],
        [5., 6.]], grad_fn=<AddBackward0>)
        
x2 = x - 2 
print(x2)

#실행결과
tensor([[-1.,  0.],
        [ 1.,  2.]], grad_fn=<SubBackward0>)

x3 = x1 * x2 
print(x3)

#실행결과
tensor([[-3.,  0.],
        [ 5., 12.]], grad_fn=<MulBackward0>)

y = x3.sum()
print(y)

#실행결과
tensor(14., grad_fn=<SumBackward0>)

grad_fn : 미분값을 계산한 함수에 대한 정보 저장 

생성된 결과들은 모두 grad_fn속성을 갖는다.  예를 들자면 텐서 x1이 덧셈 연산의 결과물이기 때문에 x1의 grad_fn 속성은 AddBackward() 임을 볼 수 있다.텐서 y는 sum함수로 인해 스칼라 값이 되었다.

y.backward()

여기서 backward 함수를 통해 편미분을 수행한 후얻어진 gradient들이 x.grad에 저장되었을 것이다.

print(x.grad)

#실행결과
tensor([[2., 4.],
        [6., 8.]])

📝참고자료 

https://angeloyeo.github.io/2020/08/16/gradient_descent.html

 

경사하강법(gradient descent) - 공돌이의 수학정리노트 (Angelo's Math Notes)

 

angeloyeo.github.io