by museonghwang

경사하강법(Gradient Descent)을 이용한 비용(Cost) 최소화

|

경사하강법(Gradient Descent)

경사하강법 은 ‘데이터를 기반으로 알고리즘이 스스로 학습한다’는 머신러닝의 개념을 가능하게 만들어준 핵심 기법으로, '점진적으로' 반복적인 계산을 통해 $W$ 파라미터 값을 업데이트하면서 오류 값이 최소가 되는 $W$ 파라미터를 구하는 방식 입니다.

image


눈을 가린채 산 정상에서 아래로 내려간다고 가정했을때, 발을 뻗어서 현재 위치보다 무조건 낮은 곳으로 계속 이동하다 보면 마침내 지상에 도착할 수 있을 것입니다. 어떻게 보면 무식해 보이는 방법이지만 직관적이고 빠르게 비용 함수가 최소가 되는 $W$ 파라미터 값을 구할 수 있습니다.

경사하강법은 반복적으로 비용 함수의 반환 값, 즉 예측값과 실제 값의 차이가 작아지는 방향성을 가지고 $W$ 파라미터를 지속해서 보정(오류를 감소시키는 방향으로 $W$ 값을 계속 업데이트)해 나갑니다. 그리고 오류 값이 더 이상 작아지지 않으면 그 오류 값을 최소 비용으로 판단하고 그때의 $W$ 값을 최적 파라미터로 반환합니다.

경사하강법의 핵심“어떻게 하면 오류가 작아지는 방향으로 W 값을 보정할 수 있을까?” 입니다. 예를 들어 비용 함수가 다음 그림과 같은 포물선 형태의 2차 함수라면 경사하강법은 최초 $w$ 에서부터 미분을 적용한 뒤 이 미분 값이 계속 감소하는 방향으로 순차적으로 $w$ 를 업데이트합니다. 마침내 더 이상 미분된 1차 함수의 기울기가 감소하지 않는 지점을 비용 함수가 최소인 지점으로 간주 하고 그때의 $w$ 를 반환합니다.

image


$RSS(w)$ 는 변수가 $w$ 파라미터로 이뤄진 함수로 다음과 같이 작성할 수 있습니다.


\[RSS(w)=\frac{1}{N}\sum^N_{i=1}(y_i-(w_0+w_1*x_i))^2\]


$RSS(w)$ 를 미분해서 미분 함수의 최소값을 구하기 위해 $w_0, w_1$ 각 변수에 편미분을 적용해야 합니다. $RSS(w)$ 를 최소화하는 $w_0$ 와 $w_1$ 의 값은 각각 $RSS(w)$ 를 $w_0$, $w_1$ 으로 순차적으로 편미분을 수행해 얻을 수 있습니다.


\[\frac{\partial RSS(w)}{\partial w_1} = \frac{2}{N}\sum^N_{i=1}-x_i*(y_i-(w_0 + w_1x_i)) = -\frac{2}{N}\sum^N_{i=1}x_i*(실제값_i-예측값_i)\] \[\frac{\partial RSS(w)}{\partial w_0} = \frac{2}{N}\sum^N_{i=1}-(y_i-(w_0 + w_1x_i)) = -\frac{2}{N}\sum^N_{i=1}(실제값_i-예측값_i)\]


$w_1, w_0$ 의 편미분 결과값을 반복적으로 보정하면서 $w_1, w_0$ 값을 업데이트하면 비용 함수 $RSS(w)$ 가 최소가 되는 $w_1, w_0$ 의 값을 구할 수 있습니다. 업데이트는 새로운 $w_1$ 을 이전 $w_1$ 에서 편미분 결과값을 마이너스($-$)하면서 적용 합니다. 또한 편미분 값이 너무 클 수 있기 때문에 보정 계수 $\eta$ 를 곱하는데, 이를 ‘학습률’ 이라고 합니다.


\[w_1^* = w_1 - \eta \left( - \frac{2}{N}\sum^N_{i=1}x_i*(실제값_i-예측값_i) \right) \\ = w_1 + \eta \frac{2}{N}\sum^N_{i=1}x_i*(실제값_i-예측값_i)\] \[w_0^* = w_0 + \eta \frac{2}{N}\sum^N_{i=1}(실제값_i-예측값_i)\]


요약하자면, 경사하강법은 $w_0, w_1$ 를 임의의 값으로 설정하고 첫 비용 함수의 값을 계산한 뒤, 새로운 $w_1$, 새로운 $w_0$ 을 반복적으로 적용하면서 비용 함수가 최소가 되는 값을 찾습니다.

지금까지 정리한 수식과 절차를 이용해 경사하강법을 구현하겠습니다. 간단한 회귀식인 y = 4X + 6 을 근사하기 위한 100개의 데이터 세트를 만들고, 여기에 경사하강법을 이용해 회귀계수 $w_0, w_1$ 을 도출하겠습니다.

import numpy as np
np.random.seed(0)
import matplotlib.pyplot as plt
%matplotlib inline

# y = 4X + 6 식을 근사(w1=4, w0=6). random 값은 Noise를 위해 만듬
X = 2 * np.random.rand(100,1)
y = 4*X + 6 + np.random.randn(100,1)

# X, y 데이터 셋 scatter plot으로 시각화
plt.scatter(X, y)

image


데이터는 y = 4X + 6 을 중심으로 무작위로 퍼져 있습니다. 다음으로 비용 함수를 정의하겠습니다. 비용 함수 get_cost() 는 실제 y 값과 예측된 y 값을 인자로 받아서 $\frac{1}{N}\sum^N_{i=1}x_i*(실제값_i-예측값_i)^2$ 을 계산해 반환합니다.

def get_cost(y, y_pred):
    N = len(y) 
    cost = np.sum(np.square(y - y_pred))/N
    return cost


$w_0$ 과 $w_1$ 의 값을 최소화 할 수 있도록 업데이트를 수행하는 get_weight_update() 함수를 생성하겠습니다. 예측 배열 y_prednp.dot(X, w1.T) + w0 으로 구합니다. 100개의 데이터 X(1,2,…,100) 이 있다면 예측값은 w0 + X(1)w1 + X(2)w1 +..+ X(100)*w1 이며, 이는 입력 배열 Xw1 배열의 내적과 동일합니다. 따라서 넘파이의 내적 연산인 dot() 를 이용해 예측 배열값을 계산합니다.

# w1 과 w0 를 업데이트 할 w1_update, w0_update를 반환. 
def get_weight_updates(w1, w0, X, y, learning_rate=0.01):
    N = len(y)
    
    # 먼저 w1_update, w0_update를 각각 w1, w0의 shape와 동일한 크기를 가진 0 값으로 초기화
    w1_update = np.zeros_like(w1)
    w0_update = np.zeros_like(w0)
    
    # 예측 배열 계산하고 예측과 실제 값의 차이 계산
    y_pred = np.dot(X, w1.T) + w0
    diff = y-y_pred
         
    # w0_update를 dot 행렬 연산으로 구하기 위해 모두 1값을 가진 행렬 생성 
    w0_factors = np.ones((N,1))

    # w1과 w0을 업데이트할 w1_update와 w0_update 계산
    w1_update = -(2/N)*learning_rate*(np.dot(X.T, diff))
    w0_update = -(2/N)*learning_rate*(np.dot(w0_factors.T, diff))    
    
    return w1_update, w0_update


다음은 get_weight_updates() 을 경사하강 방식으로 반복적으로 수행하여 w1w0 를 업데이트하는 함수인 gradient_descent_steps() 함수를 생성하겠습니다.

# 입력 인자 iters로 주어진 횟수만큼 반복적으로 w1과 w0를 업데이트 적용함. 
def gradient_descent_steps(X, y, iters=10000):
    # w0와 w1을 모두 0으로 초기화. 
    w0 = np.zeros((1,1))
    w1 = np.zeros((1,1))
    
    # 인자로 주어진 iters 만큼 반복적으로 get_weight_updates() 호출하여 w1, w0 업데이트 수행. 
    for ind in range(iters):
        w1_update, w0_update = get_weight_updates(w1, w0, X, y, learning_rate=0.01)
        w1 = w1 - w1_update
        w0 = w0 - w0_update

    return w1, w0


이제 gradient_descent_steps() 를 호출해 w1w0 을 구해보겠습니다. 그리고 최종적으로 예측값과 실제값의 RSS 차이를 계산하는 get_cost() 함수를 이용해 경사하강법의 예측 오류도 계산해 보겠습니다.

w1, w0 = gradient_descent_steps(X, y, iters=1000)
print("w1:{0:.3f} w0:{1:.3f}".format(w1[0,0], w0[0,0]))

y_pred = w1*X + w0
print('Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y, y_pred)))
[output]
w1:4.022 w0:6.162
Gradient Descent Total Cost:0.9935


실제 선형식인 y = 4X + 6 과 유사하게 w1 은 4.022, w0 는 6.162가 도출되었습니다. 예측 오류 비용은 약 0.9935입니다. 앞에서 구한 y_pred 에 기반해 회귀선을 그려 보겠습니다.

plt.scatter(X, y)
plt.plot(X,y_pred)

image


경사 하강법을 이용해 회귀선이 잘 만들어졌음을 알 수 있습니다. 일반적으로 경사 하강법은 모든 학습 데이터에 대해 반복적으로 비용함수 최소화를 위한 값을 업데이트하기 때문에 수행 시간이 매우 오래 걸린다는 단점 이 있습니다. 그 때문에 실전에서는 대부분 확률적 경사하강법(Stochastic Gradient Descent) 을 이용합니다.

확률적 경사하강법은 전체 입력 데이터로 $w$ 가 업데이트되는 값을 계산하는 것이 아니라 일부 데이터만 이용해 $w$ 가 업데이트되는 값을 계산하므로 경사 하강법에 비해서 빠른 속도를 보장 합니다.

확률적 경사하강법을 stochastic_gradient_descent_steps() 함수로 구현하겠습니다. gradient_descent_steps() 와 달리, 전체 X, y 데이터에서 랜덤하게 batch_size 만큼 데이터를 추출해 이를 기반으로 w1_update, w0_update 를 계산하는 부분만 차이가 있습니다.

def stochastic_gradient_descent_steps(X, y, batch_size=10, iters=1000):
    w0 = np.zeros((1,1))
    w1 = np.zeros((1,1))
    prev_cost = 100000
    iter_index = 0
    
    for ind in range(iters):
        np.random.seed(ind)
        
        # 전체 X, y 데이터에서 랜덤하게 batch_size만큼 데이터 추출하여 sample_X, sample_y로 저장
        stochastic_random_index = np.random.permutation(X.shape[0])
        sample_X = X[stochastic_random_index[0:batch_size]]
        sample_y = y[stochastic_random_index[0:batch_size]]
        
        # 랜덤하게 batch_size만큼 추출된 데이터 기반으로 w1_update, w0_update 계산 후 업데이트
        w1_update, w0_update = get_weight_updates(w1, w0, sample_X, sample_y, learning_rate=0.01)
        w1 = w1 - w1_update
        w0 = w0 - w0_update
    
    return w1, w0


이렇게 만들어진 stochastic_gradient_descent_steps() 를 이용해 w1, w0 및 예측 오류 비용을 계산해 보겠습니다.

w1, w0 = stochastic_gradient_descent_steps(X, y, iters=1000)
print("w1:",round(w1[0,0],3),"w0:",round(w0[0,0], 3))

y_pred = w1 * X + w0
print('Stochastic Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y, y_pred)))
[output]
w1: 4.028 w0: 6.156
Stochastic Gradient Descent Total Cost:0.9937


지금까지 피처가 1개, 즉 독립변수가 1개인 단순 선형 회귀에서 경사하강법을 적용해 봤습니다. 피처가 여러 개인 경우도 1개인 경우를 확장해 회귀 계수를 유사하게 도출할 수 있습니다.

피처가 한 개인 경우의 예측값 $\hat{Y} = w_0 + w_1 * {X}$ 로 회귀 계수를 도출하며, 피처가 M개($X_1, X_2, …, X_{100}$) 있다면 그에 따른 회귀 계수도 $M + 1$(1개는 $w_0$) 개로 도출됩니다. 즉, $\hat{Y} = w_0 + w_1 * {X_1} + w_2 * {X_2} + … + w_{100} * {X_{100}}$ 과 같이 예측 회귀식을 만들 수 있습니다. 이렇게 회귀 계수가 많아지더라도 선형대수를 이용해 간단하게 예측값을 도출할 수 있습니다.

예제에서 입력 행렬 X 에 대해서 예측 행렬 y_prednp.dot(X, w1.T) + w0 을 이용해 계산했습니다. 마찬가지로 데이터의 개수가 N 이고 피처 M 개의 입력 행렬을 $X_{mat}$, 회귀 계수 $W_1, W_2, …, W_{100}$ 을 $W$ 배열로 표기하면 예측 행렬 $\hat{Y} = np.dot(X_{mat}, W^T)+w_0$ 로 구할 수 있습니다.

image


$w_0$ 를 Weight의 배열인 $W$ 안에 포함시키기 위해서 $X_{mat}$ 의 맨 처음 열에 모든 데이터의 값이 1인 피처 Feat 0 을 추가하겠습니다. 이제 회귀 예측값은 $\hat{Y} = X_{mat} * W^T$ 와 같이 도출할 수 있습니다.

image