Books/밑바닥부터 시작하는 딥러닝1

[Books] 신경망 학습하기 (loss function, gradient descent)

언킴 2022. 3. 7. 10:02
반응형

밑바닥부터 시작하는 딥러닝1을 리뷰합니다.

 

이전 글에서 선형으로 분리가 되는 문제의 경우 데이터로부터 자동으로 학습할 수 있다. 하지만 비선형 분리의 문제의 경우 자동으로 학습하는 것이 불가능하기 때문에 비선형 문제에서는 손실 함수(loss function)라는 개념이 나오게 된다. 본 책에서는 신경망 학습에서는 현재의 상태를 '하나의 지표'로 표현한다고 언급했다. '지표'를 가장 좋게 만들어주는 가중치를 탐색하는 것이 목표라고 할 수 있다. 여기서 '지표'는 바로 손실 함수이다. 손실함수는 다양한 함수들이 존재하지만, 일반적으로 오차제곱합(Sum of Squares for Error, SSE), 교차 엔트로피(Cross Entropy)를 많이 사용한다. 

 

 

손실함수(loss function)

오차제곱합(Sum of Squares for Error, SSE)

만약 통계학을 배운 사람이라면 SSE는 많이 보았을 것이다. 말 그대로 오차의 제곱을 합산한 값을 의미하며, 수식은 다음과 같다. 

 

\[ SSE = \frac{1}{2} \sum_k (y_k - t_k)^2 \]

 

$y_k$는 출력, $t_k$는 정답 라벨, $k$는 데이터 차원의 수를 의미한다.[코드] 실제 정답이 '3'이지만 모델은 '2'라고 예측할 경우 SSE는 높은 값을 출력하고, 3이라고 예측할 경우 낮은 SSE 를 보인다. 

 

# def SSE
def SSE(pred_y, true_y):
	return 0.5 * np.sum((true_y-pred_y)**2)
    
pred_y = np.array([0.1, 0.05, 0.05, 0.6, 0.0, 0.0, 0.1, 0.1, 0.0, 0.0])
true_y = np.array([0, 0, 0, 1, 0, 0, 0, 0, 0, 0])

# '3'으로 예측한 경우 
SSE(pred_y, true_y) # 0.0975


pred_y = np.array([0.1, 0.05, 0.05, 0.0, 0.6, 0.0, 0.1, 0.1, 0.0, 0.0])

# '4'로 예측한 경우 
SSE(pred_y, true_y) # 0.6975

 

 

교차 엔트로피(cross entropy)

교차 엔트로피(Cross Entropy, CE)도 자주 사용된다. 본인은 SSE보다는 CE를 더 많이 사용하는 것 같다. CE는 이진 분류일 경우 BCE를 사용하고, 라벨의 수가 많은 다중 분류일 경우 Categorical CE를 사용한다. 교차 엔트로피의 수식은 다음과 같다. 

 

\[ CE = -\sum_k t_k \log y_k \]

 

CE의 개념을 제대로 이해하기 위해서는 Entropy를 한 번 공부해보는 것을 추천한다. 추가적으로 Entropy와 CE를 결합한 쿨백-라이블러 발산(Kullback-Leibler Divergence, KLD)도 함께 공부하는 것을 추천한다. 마지막으로 최대우도추정법(Maximum Likelihood Estimate)의 내용도 중요하다... MLE는 여기를 참고하자. 이 내용에 대해서는 다음에 한 번 다루어 볼 것이다.

 

CE에서 사용한 $\log$는 밑이 $e$인 자연로그를 사용한다. 자연로그의 그래프를 확인해보면 $x$가 1일때는 $y$는 0이 되고, $x$가 0에 가까워질수록 $y$는 점점 작아진다. [코드]

 

 

def CE(pred_y, true_y):
    delta = 1e-7
    return -np.sum(true_y * np.log(pred_y + delta))
    
    

pred_y = np.array([0.1, 0.05, 0.05, 0.6, 0.0, 0.0, 0.1, 0.1, 0.0, 0.0])
true_y = np.array([0, 0, 0, 1, 0, 0, 0, 0, 0, 0])

CE(pred_y, true_y) # 0.5108

pred_y = np.array([0.1, 0.05, 0.05, 0.0, 0.6, 0.0, 0.1, 0.1, 0.0, 0.0])

CE(pred_y, true_y) # 16.1181

 

$\delta$를 더해주는 이유는 $\log(x)$를 한번 생각해보면 된다. 로그함수는 $x$가 0일때 정의할 수 없기 때문에 0이 되지 않도록 하기 위해 $\delta$를 더해주는 것이다. 

 

 

미니배치 학습(mini-batch)

지금까지는 데이터가 존재하면 각 데이터마다 손실 함수를 구하는 방법에 대해서 알아보았다. 지금은 데이터를 배치 단위로 묶어 손실 함수를 계산하는 방법에 대해서 다루어볼 것이다. CE를 기준으로 한다면 수식은 다음과 같이 정의한다. 

 

\[ CE = \frac{1}{N} \sum_n \sum_k t_{nk} \log y_{nk} \]

 

데이터가 $N$개라면 $t_{nk}$는 $n$번째 데이터의 $k$번째 값을 의미한다. 이전에는 하나의 데이터에 대한 손실 함수였다면 이번에는 $N$개에 대한 손실함수를 의미한다. $N$개에 대한 손실 함수를 계산하고 마지막에 $\frac{1}{N}$으로 나누어줌으로써 $N$개의 데이터에 대한 평균 손실 함수를 구하는 것이다. 이러한 방식을 미니배치 학습이라고 부른다. [코드]

 

그렇다면 왜 손실 함수를 정의할까? 신경망 학습에서는 최적의 가중치와 편향을 탐색할 때 손실 함수의 값이 가능한 한 작게 하는 매개변수 값을 찾는다. 이때 매개변수를 미분하여 기울기를 계산하고, 그 값을 기준으로 움직여가며 갱신하는 과정을 반복하게 된다. 가중치를 조금씩 변화하면서 손실 함수가 어떻게 변하는지를 확인하는 것이다. 정확도를 지표로 삼아서는 안되는 이유는 미분 값이 대부분의 장소에서 0이 되어 가중치를 갱신할 수 없기 때문이다. 

 

정확도를 지표로 삼으면 왜 대부분의 장소에서 0이 될까? 우리는 가중치를 조금씩 변화하면서 손실 함수의 변화를 확인한다고 했다. 이렇게 가중치를 조금 변화시킨다고 하더라도 정확도는 개선되지 않을 가능성이 높다. 또한, 정확도는 손실 함수처럼 미세하게 변화하는 것이 아니라 100개를 기준으로 할 경우 50%, 51% 와 같은 형태로 변화하기 때문에 불연속적인 값을 가진다. 

 

 

수치 미분(numerical differentiation)

위에서 손실 함수를 계산할 때 가중치를 미분한다고 했다. 우리는 경사법(Gradient)에서는 기울기를 기준으로 왼쪽으로 갈지, 오른쪽으로 갈지 나아갈 방향을 정한다. 수치 미분은 변화량을 기반으로 계산하는 것을 의미하며, 해석적 미분과는 다르다. 수치 미분의 수식은 다음과 같다. 

 

\[ \frac{df(x)}{dx} = \lim_{h \rightarrow 0} \frac{f(x+h) - f(x)}{h} \]

 

수치 미분은 차분을 통해 미분하는 것을 의미한다. 또한 해석적 미분과는 다르게 수치 미분에서는 오차가 발생한다. 우리가 아무리 h를 최소한으로 작은 값을 대입한다고 하더라도 반올림을 통해 발생하는 오차가 존재하기 때문이다. 반올림 오차를 발생하지 않기 위해 일반적으로 $10^{-4}$를 많이 쓴다고 알려져있다. 앞서 다룬 차분은 전방 차분을 의미한다. 아래의 그림과 같이 전방 차분을 사용할 경우 실제 접선과 전방 차분으로 구한 접선 간의 괴리를 확인할 수 있다.

전방 차분

 

우리는 이러한 오차를 줄이기 위해 중앙 차분을 사용한다. 중앙 차분은 오차를 줄이기 위해 $(x+h)$와 $(x-h)$ 사이의 값을 계산하는 것이며, 수식은 다음과 같다. [코드]

 

\[ \frac{df(x)}{dx} = \lim_{h \rightarrow 0} \frac{f(x+h) - f(x-h)}{2h} \]

 

차분에 대해서는 다루어보았으니, 간단한 예시를 통해 미분을 확인해보자. 우리는 다음과 같이 함수 $f(x)$를 정의해보자.

 

$ f(x) = 0.01 x^2 + 0.1 x $

 

 

우리는 임의의 점에서 접선의 기울기를 구하기 위해서는 다음과 같이 미분을 진행해야 한다. 

 

\[ \frac{df(x)}{dx} = 0.02x + 0.01 \]

 

단순한 미분은 아주 간단하게 구할 수 있을 것이다. 하지만 딥러닝에서는 이와 같이 하나의 변수만 존재하는 경우는 매우 드물기 때문에 일반적인 미분이 아닌 편미분을 사용한다. 편미분은 $ \frac{\partial f}{\partial x_0}$ 나 $ \frac{\partial f}{\partial x_1} $ 형태로 사용한다. 각각의 편미분에 대한 값은 다음과 같다. [코드]

 

\[ f(x_0, x_1) = x_0^2 + x_1^2 \]

 

\[ \frac{\partial f(x)}{\partial x_0} = 2*x_0 \]

 

\[ \frac{\partial f(x)}{\partial x_1} = 2*x_1 \]

 

편미분은 미분하는 변수가 아닌 다른 변수는 상수로 취급하고 미분을 진행한다. 상수값의 미분은 0이기 때문에 0으로 처리되어 해당 변수에 대한 미분값만 남게 되는 것이다. 

 

$f(x) = x_0^2 + x_1^2 $

 

 

지금까지는 각각의 변수에 대해서 편미분을 계산했다. $x_0, x_1$에 대한 편미분을 동시에 계산하고 싶을 때 모든 변수의 편미분을 벡터로 정리한 것을 우리는 기울기$^{\mathsf{gradient}}$ 라고 부른다. [코드] 각 점에서의 기울기를 시각화하면 아래와 같은 그림이 도출된다. 

 

$ f(x_0, x_1) = x^2_0 + x^2_1 $의 기울기

def numerical_gradient(f, x):
    h = 1e-4 
    grad = np.zeros_like(x)
    
    for idx in range(x.size):
        tmp_val = x[idx]
        
        # f(x+h)
        x[idx] = tmp_val + h 
        fxh1 = f(x)
        
        # f(x-h)
        x[idx] = tmp_val - h
        fxh2 = f(x)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val 
        
    return grad

우리는 grad라는 변수에 각각의 변수에 대한 기울기를 저장할 수 있다. 

numerical_gradient(function_2, np.array([3.0, 4.0])) # array([6., 8.])

numerical_gradient(function_2, np.array([0.0, 2.0])) # array([0., 4.])

numerical_gradient(function_2, np.array([3.0, 0.0])) # array([6., 0.])

 

 

경사 하강법

우리는 경사 하강법을 통해 최적의 값을 찾는 방향으로 접근한다. 하지만 우리는 실제 그래프의 생김새를 확인할 수 없기 때문에 여기가 최솟값인지, 극솟값인지를 알수가 없다. 경사 하강법에 대한 개념은 여기를 참고하면 상세하게 설명해두었다. 여기서는 간략하게 설명하고 넘어가자. 

 

우리는 가중치를 조정하면서 최솟값을 찾는다고 이야기 했다. 이때 어느정도 가중치를 갱신할 것인가를 정해주는 값이 바로 $\eta$이다. $\eta$는 학습률(learning rate)라고도 불리며 한 번에 얼마만큼 학습할 지를 정해준다. [코드]

 

\[ x_0 = x_0 - \eta \frac{\partial f }{\partial x_0} \]

 

\[ x_1 = x_1 - \eta \frac{\partial f }{\partial x_1} \]

 

학습률이 너무 클 경우 발산해버리고, 너무 작은 경우 제대로 갱신되지 않은 채로 끝나버리게 된다. 딥러닝에서는 학습률도 학습에 중요한 파라미터로 불리운다. 

 

지금까지 배운 내용을 실습하는 예제 코드는 여기를 참고하면 된다.