4 분 소요

회귀

비용 최소화하기 - 경사 하강법(Gradient Descent) 소개

W 파라미터의 개수가 적다면 고차원의 방정식으로 비용 함수가 최소가 되는 W 변숫값을 도출할 수 있겠지만, W 파라미터가 많으면 고차원 방정식을 동원하더라도 해결하기가 어렵다.
경사 하강법은 이러한 고차원 방정식에 대한 문제를 해결해주면서 비용 함수 RSS를 최소화하는 방법을 직관적으로 제공하는 뛰어난 방식이다.

-> 많은 W 파라미터가 있는 경우에 경사 하강법은 보다 간단하고 직관적인 비용 함수 최소화 솔루션을 제공

  • 경사 하강법은 반복적으로 비용 함수의 반환 값, 즉 예측값과 실제 값의 차이가 작아지는 방향성을 가지고 W 파라미터를 지속해서 보정해 나간다.
  • 최소 오류 값이 100이었다면 두 번째 오류 값은 100보다 작은 90, 세 번째는 80과 같은 방식으로 지속해서 오류를 감소시키는 방향으로 W 값을 계속 업데이트해 나간다.
  • 오류 값이 더 이상 작아지지 않으면 그 오류 값을 최소 비용으로 판단하고 그때의 W 값을 최적 파라미터로 반환한다.

미분을 통해 비용 함수의 최솟값을 찾기

어떻게 하면 오류가 작아지는 방향으로 W 값을 보정할 수 있을까?

  • 비용 함수가 포물선 형태의 2차 함수라면 경사 하강법은 최초 w에서부터 미분을 적용한 뒤 이 미분 값이 계속 감소하는 방향으로 순차적으로 w를 업데이트 한다.
  • 마침내 더 이상 미분된 1차 함수의 기울기가 감소하지 않는 지점을 비용 함수가 최소인 지점으로 간주하고 그때의 w를 반환한다.

RSS의 편미분

  • R(w)는 변수가 w 파라미터로 이루어진 함수이며, R(w) = $\sum_{i=1}^{N}{(y_i - (w_0 + w_1 \times x_i))^2}$이다.
  • R(w)를 미분해 미분 함수의 최솟값을 구해야 하는데, R(w)는 두 개의 w 파라미터인 w0와 w1을 각각 가지고 있기 때문에 일반적인 미분을 적용할 수가 없고, w0, w1 각 변수에 편미분을 적용해야 한다.
    R(w)를 최소화하는 w0와 w1의 값은 각각 R(w)를 w0, w1으로 순차적으로 편미분을 수행해 얻을 수 있다.

스크린샷 2024-05-10 162222

경사 하강법 정리

w1, w0의 편미분 결괏값인 -$2 \over N$ $\sum_{i=1}^{N}{x_i \times (실제값_i - 예측값_i)}$와 -$2 \over N$ $\sum_{i=1}^{N}{(실제값_i - 예측값_i)}$을 반복적으로 보정하면서 w1, w0 값을 업데이트하면 비용함수 R(w)가 최소가 되는 w1, w0 값을 구할 수 있다.
하지만 실제로는 위 편미분 값이 너무 클 수 있기 때문에 보정계수 $\eta$를 곱하는데 이를 “학습률”이라고 한다.

  • 새로운 $w_1$ = 이전 $w_1$ - (-$\eta$ $2 \over N$ $\sum_{i=1}^{N}{x_i \times (실제값_i - 예측값_i)}$) = 이전 $w_1$ + $\eta$ $2 \over N$ $\sum_{i=1}^{N}{x_i \times (실제값_i - 예측값_i)}$

  • 새로운 $w_0$ = 이전 $w_0$ - (-$\eta$ $2 \over N$ $\sum_{i=1}^{N}{(실제값_i - 예측값_i)}$) = 이전 $w_0$ + $\eta$ $2 \over N$ $\sum_{i=1}^{N}{(실제값_i - 예측값_i)}$

경사하강법 수행 프로세스

  • Step 1: $w_1, w_0$를 임의의 값으로 설정하고 첫 비용 함수의 값을 계산한다.
  • Step 2: $w_1$을 $w_1$ + $\eta$ $2 \over N$ $\sum_{i=1}^{N}{x_i \times (실제값_i - 예측값_i)}$, $w_0$을 $w_0$ + $\eta$ $2 \over N$ $\sum_{i=1}^{N}{(실제값_i - 예측값_i)}$으로 업데이트한 후 다시 비용 함수의 값을 계산한다.
  • Step 3: Step 2를 주어진 횟수만큼 반복한다.

w0과 w1의 값을 최소화 할 수 있도록 업데이트 수행하는 함수 생성

  • 예측 배열 y_pred는 np.dot(X, w1.T) + w0
    100개의 데이터 X(1,2,…,100)이 있다면 예측값은 w0 + X(1)w1 + X(2)w1 +..+ X(100)*w1이며, 이는 입력 배열 X와 w1 배열의 내적
  • 새로운 w1과 w0를 update함

<실습>

실제값을 Y=4X+6 시뮬레이션하는 데이터 값 생성

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

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

# X, y 데이터 셋 scatter plot으로 시각화
plt.scatter(X, y)
<matplotlib.collections.PathCollection at 0x171c7378be0>

output_2_15

X.shape, y.shape
((100, 1), (100, 1))
# 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
w0 = np.zeros((1,1))
w1 = np.zeros((1,1))
y_pred = np.dot(X, w1.T) + w0
diff = y-y_pred
print(diff.shape)
w0_factors = np.ones((100,1))
w1_update = -(2/100)*0.01*(np.dot(X.T, diff))
w0_update = -(2/100)*0.01*(np.dot(w0_factors.T, diff))   
print(w1_update.shape, w0_update.shape)
(100, 1)
(1, 1) (1, 1)

반복적으로 경사 하강법을 이용하여 get_weigth_updates()를 호출하여 w1과 w0를 업데이트 하는 함수 생성

# 입력 인자 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

예측 오차 비용을 계산을 수행하는 함수 생성 및 경사 하강법 수행

def get_cost(y, y_pred):
    N = len(y) 
    cost = np.sum(np.square(y - y_pred))/N
    return 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[0,0] * X + w0
print('Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y, y_pred)))
w1:4.022 w0:6.162
Gradient Descent Total Cost:0.9935
plt.scatter(X, y)
plt.plot(X,y_pred)
[<matplotlib.lines.Line2D at 0x171c74bb490>]

output_12_1

미니 배치 확률적 경사 하강법을 이용한 최적 비용함수 도출

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
np.random.permutation(100)
array([66, 71, 54, 88, 82, 12, 36, 46, 14, 67, 10,  3, 62, 29, 97, 69, 70,
       93, 31, 73, 60, 96, 28, 27, 21, 19, 33, 78, 32, 94,  1, 41, 40, 76,
       37, 87, 24, 23, 50,  2, 47, 20, 77, 17, 56, 64, 68, 25, 15, 22, 16,
       98, 63, 92, 86, 38,  6, 57, 95, 44,  9, 42, 81, 99, 35, 84, 59, 48,
       75, 65, 85, 90, 55, 43, 58, 89, 30, 80, 34, 18, 51, 49, 52, 74, 26,
       45, 39,  4, 11, 53, 91, 79,  8,  0,  5, 13, 61, 72,  7, 83])
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[0,0] * X + w0
print('Stochastic Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y, y_pred)))
  
w1: 4.028 w0: 6.156
Stochastic Gradient Descent Total Cost:0.9937