본문 바로가기
Book Review/[파이토치 딥러닝 마스터] 리뷰

모델 학습 기법 기초

by 3n952 2023. 4. 5.

인공지능, 머신러닝, 딥러닝은 데이터로부터 무언가를 배우는 작업입니다.
데이터에 맞춰가는 과정과 학습하는 알고리즘을 만드는 과정이 그것인데요.
그렇다면 "데이터로부터 배운다"라는 것이 무엇을 의미하는 걸까요?
데이터로부터 배우는 것은 간단하게 말하면 특정 작업에 대한 데이터로 학습한 일반화된 모델을 만드는 것이라고 
할 수 있겠습니다.

딥러닝의 핵심은 일반함수를 fitting하는 것에 있습니다. 
이를 위해 이번 포스팅에서는 학습 알고리즘의 동작 방식에 대해 알아보도록 하겠습니다.

 


1.학습의 과정

딥러닝에서 학습은 결국 파라미터(가중치)를 추정하는 것에 불과합니다.

입력 및 출력에 대응하는 ground truth와 가중치 초깃값이 주어지면, 

모델에 입력이 들어가서 실측값과 모델의 예측값을 비교하여 그 오차를 계산하여 줄이는 것입니다.

오차가 줄어든다는 것은 실제의 값과 가장 비슷한 함수 식을 찾았다고 볼 수 있겠네요.

 

이때 오차 측정을 어떻게 해야 할지를 구체적으로 정의해야 합니다.

손실함수(loss function)을 통해 오차를 측정할 수 있습니다.

 

2. 오차를 줄이는 손실함수

오차가 크면 손실함수 출력값도 높아지도록 정의하여 함수의 출력값을 가능한 최고로 작게

만들면 그 지점에서 가중치를 찾을 수 있습니다. 이를 최적화 과정이라고 합니다.

즉, 훈련 샘플로부터 기대하는 출력값과 모델이 샘플에 대해 실제로 출력한 값 사이의 차이를 계산하는 것입니다.

개념적으로 파라미터 조정은 손실값이 적은 샘플의 출력을 변경하기보다 가중치가 큰 샘플을 우선적으로 보정합니다.

 

3. 경사 하강 알고리즘 

파라미터 관점에서 손실 함수를 최적화하는 데는 경사 하강 알고리즘을 사용합니다.

어떠한 경우에 손실 값을 줄일 수 있는지를 하나하나 파악해 나가면서 최적의 지점을 찾는 것을 의미합니다.

경험적으로 파악해야 하기 때문에 손실이 줄어드는 방향으로 조금씩 오차를 줄여갑니다.

손실값이 최소로 가려면 파라미터 값을 서서히 변하게 조정하여 최솟값으로 수렴해야 할 것입니다.

조정할 변화율을 머신러닝, 딥러닝에서는 학습률(learning rate이라고 합니다.

 

-> 학습률과 파라미터 값을 조정함으로써 손실 함수의 값을 최소화해야 한다!

 

2~3의 과정을 (=손실함수로 부터 오차를 줄이기 위해 경사 하강 알고리즘을 적용시키기) 코드로 구현하여 이해하면 다음과 같습니다.

import numpy as np
import torch

#입력 텐서 만들기(t_c는 실제값, t_u는 모델 입력값)
t_c = [0.5,  14.0, 15.0, 28.0, 11.0,  8.0,  3.0, -4.0,  6.0, 13.0, 21.0]
t_u = [35.7, 55.9, 58.2, 81.9, 56.3, 48.9, 33.9, 21.8, 48.4, 60.4, 68.4]
t_c = torch.tensor(t_c)
t_u = torch.tensor(t_u)

#선형 모델 정의
def model(t_u, w, b):
    return w * t_u + b
    
#손실함수로 평균 제곱 오차를 사용
def loss_fn(t_p, t_c):
    squared_diffs = (t_p - t_c)**2
    return squared_diffs.mean()

#모델에 데이터 입력-> t_p로 예측
w = torch.ones(())
b = torch.zeros(())
t_p = model(t_u, w, b)

#오차 값 계산
loss = loss_fn(t_p, t_c)

#특정 단위 만큼 증가 했을 때의 손실의 변화율
delta = 0.1

loss_rate_of_change_w = \
    (loss_fn(model(t_u, w + delta, b), t_c) - 
     loss_fn(model(t_u, w - delta, b), t_c)) / (2.0 * delta)

#학습률
learning_rate = 1e-2

#경사 하강법으로 파라미터 값(w)을 조정
w = w - learning_rate * loss_rate_of_change_w

 

손실의 변화율을 보기 위해 인접 영역을 지정해 주었습니다.(delta = 0.1)

인접 영역을 극단적으로 줄이면 어떻게 될까? 이는 파라미터에 대한 손실 함수를 미분하는 것과 같습니다.

두 개 이상의 파라미터를 가진 모델에서는 각 파라미터에 대한 손실 함수의 편미분을 구하고 이 편미분 값들을

미분 벡터에 넣습니다. 이것을 기울기gradient라고 부릅니다

이때 등장하는 개념이 오차역전파 개념입니다.

가중치에 대한 손실 함수의 미분을 계산하게 해주는 것이죠. 보다 자세한 내용은 밑의 포스팅에서 확인해 주세요!

https://sanmldl.tistory.com/8

 

역전파 알고리즘 (backpropagation)

이전 포스트에서 손실 함수에 대한 그레이디언트를 계산하여 손실 함수가 최소가 되는 지점을 찾는 방식을 배웠습니다. 그렇다면 수 천, 수 만개로 이루어진 모델 파라미터의 그레이디언트를

sanmldl.tistory.com

 

4. learning rate의 조정 / 입력 정규화

앞선 과정을 통해 모델을 학습시킬 때 직면하는 문제가 있습니다.

1. learning rate의 조정

2. 입력 정규화

 

먼저 learning rate의 문제는 다음과 같습니다.

learning rate * grad를 해서 파라미터를 조정하는 것인데, learning rate가 너무 크면 발산하게 되고, 너무 작으면 오차가 최소로 가는데

시간이 오래 걸린다는 단점이 있습니다.

따라서 learning rate를 단계적으로 그 규모를 조정할 필요가 있습니다. 이에 대한 자세한 내용은 다른 포스팅에서 다루도록 하겠습니다.

 

입력 정규화의 문제는 가중치에 대한 기울기와 편향값에 대한 기울기의 scale차이에서 옵니다.

가중치에 대한 기울기가 편향값에 대한 기울기보다 훨씬 크다면 하나의 학습률을 가지고 동시에 값을 업데이트한다면 

파라미터가 불안정해집니다. 이를 해결하기 위해 입력값을 변경하여 기울기가 서로 큰 차이가 나지 않게 하는 것이 필요한데,

이를 입력 정규화 과정으로 해결합니다.

위의 예제코드로 예를 들면 입력 텐서의 범위를 비슷하게 조정하여 기울기가 서로 큰 차이가 나지 않게 만들어 주는 것입니다.

t_un = 0.1 * t_u

입력 정규화를 했으면 이제 훈련 루프를 통해 학습시킬 차례입니다.

위의 코드 들과 이어서 작성한 코드는 다음과 같습니다.

#도함수 정의
def dloss_fn(t_p, t_c):
    dsq_diffs = 2 * (t_p - t_c) / t_p.size(0) 
    return dsq_diffs

#모델에 식의 각각의 미분 값 계산
def dmodel_dw(t_u, w, b):
    return t_u
def dmodel_db(t_u, w, b):
    return 1.0

#다 합쳐 w와 b에 대한 손실값의 미분을 반환하게 함
def grad_fn(t_u, t_c, t_p, w, b):
    dloss_dtp = dloss_fn(t_p, t_c)
    dloss_dw = dloss_dtp * dmodel_dw(t_u, w, b)
    dloss_db = dloss_dtp * dmodel_db(t_u, w, b)
    return torch.stack([dloss_dw.sum(), dloss_db.sum()])

#훈련 루프
def training_loop(n_epochs, learning_rate, params, t_u, t_c):
    for epoch in range(1, n_epochs + 1):
        w, b = params

        t_p = model(t_u, w, b)  
        loss = loss_fn(t_p, t_c)
        grad = grad_fn(t_u, t_c, t_p, w, b)  

        params = params - learning_rate * grad

        print('Epoch %d, Loss %f' % (epoch, float(loss))) 
            
    return params

training_loop(
    n_epochs = 100, 
    learning_rate = 1e-2, 
    params = torch.tensor([1.0, 0.0]), 
    t_u = t_un, 
    t_c = t_c)

 

5.optimizer(옵티마이저)

이전의 최적화 과정에서 경사 하강법을 사용했습니다.

모델이 복잡해지면 다른 여러 가지 최적화 전략을 사용해야 성능이 향상됩니다.

파이토치에는 다양한 최적화 지원 기능이 있는데 torch의 서브 모듈인 optim에서

다양한 최적화 알고리즘 구현 클래스를 찾을 수 있습니다.

import torch.optim as optim

dir(optim)

심지어 파이토치의 자동 미분 기능(autograd)이 있어 텐서의 미분 값들을 일일이 정해주지

않아도 연쇄법칙에 의한 역전파 방식으로 미분한 값들을 알려줍니다.

파라미터 텐서를 지정할 때, requires_grad = True로 설정해 주면 됩니다.

 

결국 모든 옵티마이저의 생성자는 파라미터 리스트를 받아 옵티마이저 객체 내부에 유지되면서 

자동 미분 기능으로 grad속성에 접근하여 값을 조정합니다.


옵티마이저 중 가장 기본이 되는 경사 하강 옵티마이저를 사용하여 이해를 돕겠습니다.

우선 params에 텐서를 만들어 주고(자동 미분을 위해 requires_grad = True)

이후 학습률과 옵티마이저 인스턴스를 생성해 줍니다. 

params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-5
optimizer = optim.SGD([params], lr=learning_rate)

옵티마이저를 돌려보기 위해서는 optimizer의 step 메서드를 사용하면 됩니다.

step 메서드는 옵티마이저별로 구현된 최적화 전략에 따라 파라미터 값을 조정합니다.

t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
loss.backward()

optimizer.step()

params

# out: tensor([ 9.5483e-01, -8.2600e-04], requires_grad=True)

이제 직접 도함수를 정의해주지 않고, 조정 식을 지정해주지 않아도 

step메서드를 통해 알아서 조정할 수 있습니다.

그렇다면 이대로 훈련 루프에 적용하면 될지 의문이 생깁니다.

정답은 하면 안 된다입니다.

그 이유는 기울기 값은 grad에서 누적되어 경사 하강이 모든 곳에서 일어날 것이기 때문입니다.

따라서 grad값을 0으로 초기화해주는 작업이 필요합니다.

 

backward호출 직전에 zero_grad를 추가하여 루프에 넣으면 해결이 될 것입니다.

params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-2
optimizer = optim.SGD([params], lr=learning_rate)

t_p = model(t_un, *params)
loss = loss_fn(t_p, t_c)

optimizer.zero_grad() # zero_grad추가
loss.backward()
optimizer.step()

params

옵티마이저까지 적용한 전체 코드를 구성하면 다음과 같습니다.

def training_loop(n_epochs, optimizer, params, t_u, t_c):
    for epoch in range(1, n_epochs + 1):
        t_p = model(t_u, *params) 
        loss = loss_fn(t_p, t_c)
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        if epoch % 500 == 0:
            print('Epoch %d, Loss %f' % (epoch, float(loss)))
            
    return params
  
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-2
optimizer = optim.SGD([params], lr=learning_rate) # 이전의 코드에서 옵티마이저 추가

training_loop(
    n_epochs = 5000, 
    optimizer = optimizer,
    params = params, 
    t_u = t_un,
    t_c = t_c)

 

이번 포스팅에서는 모델이 학습하는 알고리즘의 전반적인 내용에 대해 간략하게 살펴보았습니다.