본문 바로가기
## 오래된 게시글 (미관리) ##/Python (Linux)

27. Python - 배치 경사 하강법

by #Glacier 2019. 1. 24.
반응형

안녕하세요. 오랜만이네요.

저는 여태 배웠던 것들을 이용하여 프로젝트를 해봤습니다. 혼자서요 ㅎㅎ

이제 이 장에서 배워볼 것들을 또 적용해 나갈 것입니다!

이번 장의 목표는 다음과 같습니다.


혹시나 직접 더 보고싶다면 저자 깃허브를 가보시기 바랍니다.

저자 깃허브 :  https://github.com/rickiepark/handson-ml/blob/master/04_training_linear_models.ipynb 

선형 최소 제곱법을 설명하는 위키백과 : https://goo.gl/Lf8VHJ


[1]

직접 계산할 수 있는 공식을 사용하여 훈련 세트에 가장 잘 맞는 모델 파라미터를 해석적으로 구하기 (비용 함수 최소화)


[2]

경사 하강법(GD)이라 불리는 반복적인 최적화 방식을 사용하여 모델 파라미터를 조금씩 바꾸면서,  

비용 함수를 훈련 세트에 대해 최소화시키기. 

경사 하강법의 변종인 배치(Batch) 경사 하강법, 미니배치(Mini-Batch) 경사 하강법, 확률적(Stochastic) 경사 하강법(SGD) 학습


[3]

비선형 데이터셋에 훈련시킬 수 있는 다항 회귀도 살펴보는데, 이 모델은 파라미터가 많아서 훈련 데이터에 과대적합 되기 쉽고,

따라서 학습 곡선(learning curve)를 사용해 모델이 과대적합되는지 감지하는 방법과 규제 기법에 대해 알아봅니다.


[4]

분류 작업에 널리 사용하는 모델인 로지스틱 회귀와 소프트맥스 회귀를 살펴봅니다.


이제 시작~


삶의 만족도를 선형 함수로 만들면 이렇게 만들어볼 수 있습니다. 일반적으로 선형 모델은 입력 특성의 가중치 합과 편향(bias) 또는 절편(intercept) 이라는 상수를 더해 예측을 만듭니다.


이 식을, 벡터 형태로 더 간단하게 만들 수 있습니다.



위 식을 훈련시키기 위해서 모델이 훈련 데이터에 얼마나 잘 들어맞는지 측정해야 합니다.

회귀에 가장 널리 사용되는 평균 제곱근 오차(RMSE)를 최소화하는 세타를 찾아야 하는데, 

실제로는 RMSE보다 평균 제곱오차(MSE)를 최소화하는 것이 같은 결과를 내면서 더 간단합니다.



이제 이것을 간단하게 표기하기 위해서 MSE()로 표현합니다.


1. 정규 방정식

비용 함수를 최소화하는 값을 찾기 위한 해석적인 방법이 있습니다. 다른 말로 하면, 바로 결과를 얻을 수 있는 수학 공식이 있습니다. 이를 정규방정식(Normal Equation)이라고 합니다.



이 공식을 테스트하기 위해 선형처럼 보이는 데이터를 생성합니다.


import numpy as np

import matplotlib.pyplot as plt


X = 2*np.random.rand(100,1)

y = 4 + 3 * X +np.random.randn(100,1)


plt.plot(X, y, 'b.')

plt.xlabel('$x_1$', fontsize=18)

plt.ylabel('$y$', rotation=0, fontsize=18)

plt.axis([0,2, 0,15])


plt.show()



선형 최소 제곱법을 설명하는 위키백과 (https://goo.gl/Lf8VHJ)

그럼 이제 정규방정식을 사용해 을 계산합니다. 넘파이 선형대수 모듈(np.linalg)에 있는 inv()함수를 사용해 역행렬을 계산하고, dot()메서드를 사용하여 행렬 곱셈을 합니다.


X_b = np.c_[np.ones((100,1)), X] #모든 샘플에 x0 = 1 추가

theta_best = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y)


위 코드와 아래의 식이 일치한다는 것을 알 수 있죠 .



이 데이터를 생성하기 위해 사용한 실제 함수는  y = 4+3x_1+ 가우시안 노이즈입니다. 정규방정식으로 계산한 값을 확인합니다.


theta_best

array([[3.62250396],
       [3.19770208]])


이렇게 값이 나오는데, 우리는 세타0=4, 세타1=3을 기대했습니다. 매우 비슷하지만 노이즈 때문에 원래 함수의 파라미터를 정확하게 재현하지 못했습니다.

을 이용하여 예측을 해봅니다.


X_new = np.array([[0], [2]])

X_new_b = np.c_[np.ones((2,1)), X_new] #모든 샘플에 x0=1을 추가

y_predict = X_new_b.dot(theta_best)

y_predict

array([[ 3.62250396],
       [10.01790811]])

모델의 예측을 그래프에 표현합니다.


plt.plot(X_new, y_predict, 'r-')

plt.plot(X, y, 'b.')

plt.axis([0,2,0,15])

plt.show()



이와 같은 작업을 하는 사이킷런 코드는 다음과 같습니다.


#위와 같은 작업을 하는 사이킷런 코드


from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()

lin_reg.fit(X,y)

print("intercept:" ,lin_reg.intercept_, "\ncoefficient:", lin_reg.coef_,"\npredict:" ,lin_reg.predict(X_new))


intercept: [3.81237792] 
coefficient: [[3.0830601]] 
predict: [[3.81237792]
 [9.97849812]]


정규 방정식은 (n+1) x (n+1) 크기가 되는 의 역행렬을 계산합니다. (n은 특성 수)



따라서 특성 수가 매우 많아질 수록 매우 느려집니다.

다행인 것은, 이 공식의 복잡도가 훈련 세트의 샘플 수에는 선형적으로 증가합니다. 즉 O(m).

그러므로 메모리 공간이 허락된다면, 큰 훈련 세트도 효율적으로 처리가 가능합니다.

또한, 정규방정식으로 학습된 선형 회귀 모델은 예측이 매우 빠릅니다. 예측 계산 복잡도는 샘플 수와 특성 수에 선형적이기 때문에, 예측하려는 샘플이 두 배 늘어나면 걸리는 시간도 거의 두 배 증가합니다.


이제, 아주 다른 방법으로 선형 회귀 모델을 훈련시켜보겠습니다. 이 방법은 특성이 매우 많고, 훈련 샘플이 너무 많아 메모리에 모두 담을 수 없을 때 적합합니다.


< 경사 하강법 >


경사 하강법(Gradient Descent, GD)은 여러 종류의 문제에서 최적의 해법을 찾을 수 있는 매우 일반적인 최적화 알고리즘입니다.

경사 하강법의 기본 아이디어는 비용 함수를 최적화화하기 위해 반복해서 파라미터를 조정해나가는 것입니다.


책에서 아주 좋은 예를 들어주었는데요, 산속에서 짙은 안개에 갇혀있을 때, 빨리 내려가는 방법은 가장 가파른 길을 따라 아래로 내려가는 것입니다. 따라서 파라미터 벡터 Theta에 대해 비용 함수의 현재 그래디언트(Gradient)를 계산합니다. 그리고 그래디언트가 감소하는 방향으로 진행하게 됩니다. 그리고, 그래디언트가 0이 되면, 최솟값에 도달한 것입니다.

그래디언트는 비용 함수의 미분값으로 포괄적 사용됩니다.


구체적으로 보면 Theta를 임의의 값으로 시작해서(무작위 초기화, random initialization) 한 번에 조금씩 비용 함수(ex, MSE)가 감소되는 방향으로 진행하여 알고리즘이 최솟값에 수렴할 때 까지 점진적으로 향상시킵니다.


경사 하강법의 중요한 파라미터는 스텝(Step)의 크기로, 학습률(Learning Rate) 하이퍼 파라미터로 결정됩니다.

학습률이 너무 작으면 알고리즘이 수렴하기 위해 반복을 많이 진행해야 하므로 시간이 오래 걸립니다.


그러나, 학습률이 너무 크면 골짜기를 가로질러 반대편으로 건너뛰게 되어 이전보다 더 높은 곳으로 올라갈 수도 있습니다.

이는 알고리즘을 더 큰 값으로 발산하게 만들어 적절한 해법을 찾지 못하게 만듭니다.


이렇게 시작점이 낮았는데도 오히려 더 비용함수가 높게 올라갑니다.



또한, 모든 비용 함수가 매끈한 그릇 모양은 아닙니다. 산마루, 평지 등 특이 지형이 있다면 최솟값으로 수렴하기 매우 어려워집니다. 경사 하강법의 두 가지 문제점은 무작위 초기화 때문에 알고리즘이 왼쪽에서 시작하면, 전역 최솟값(global minimum)보다 덜 좋은 지역 최솟값(local minimun)에 수렴합니다.


말로 하면 어렵게 느껴졌는데, 책에는 그림이 있어서 이해가 됬거든요. 근데 그림 파일이 없어서 직접 그렸습니다...

즉 왼쪽에서 시작한 경우 전역 최솟값이 아닌 지역최솟값에 수렴하게 되고, 우측에서 시작한 경우 평지를 내려오는 데 

오랜 시간이 걸리게 됩니다. 



다행히 선형 회귀를 위한 MSE 비용 함수는 곡선에서 어떤 두 점을 선택해 선을 그어도 곡선을 가로지르지 않는,

볼록 함수(convex function)입니다. 이는 지역 최솟값이 없고, 전역 최솟값만 있다는 뜻입니다. 또한 연속된 함수이며 기울기가 갑자기 변하지 않습니다.


이 두 사실로부터 경사 하강법이 전역 최솟값에 가깝게 접근할 수 있다는 것을 보장하게 됩니다.(학습률이 너무 높지 않고 충분한 시간을 전제로)


사실 비용 함수는 그릇 모양을 하고 있지만, 특성들의 스케일이 매우 다르면 길쭉한 모양일 수도 있습니다. 즉 그릇 모양일 경우에 더 빠르게 최솟값에 도달할 수 있습니다. 즉, 경사하강법을 사용할 때에는 모든 특성이 같은 스케일을 갖도록 만들어야 합니다.

(ex. sklearn의 StandardScaler) 그렇지 않으면 수렴하는 데 훨씬 오래 걸립니다.

* StandardScaler는 데이터의 각 특성에서 평균을 빼고 표준편차로 나누어 평균을 0으로 분산을 1로 만듭니다.


< 배치 경사 하강법 >


경사 하강법을 구현하려면 각 모델 파라미터 에 대해 비용 함수의 그래디언트를 계산해야 합니다.

즉 가 조금 변경될 때 비용 함수가 얼마나 바뀌는 지 계산해야 합니다. 이를 편도 함수라 합니다.(partial derivative)

이는 동쪽을 바라봤을 때, 발밑에 느껴지는 산의 기울기는 얼마인가? 와 같습니다.  이를 북쪽을 바라보고 같은 질문을 합니다.

만약 3차원 이상의 세상이라면 모든 차원에서 반복합니다. 


아래의 식은 파라미터 에 대한 비용 함수의 편도함수 입니다.


[비용 함수의 편도함수]



이렇게 편도함수를 각각 계산하는 대신, 다음의 식으로 한꺼번에 계산할 수 있습니다. 

그래디언트 벡터 는 비용 함수의 (모델 파라미터마다 한 개씩인) 편도함수를 모두 담고 있습니다.



*이 공식은 매 경사 하강법 스텝에서 전체 훈련 세트 X에 대해 계산합니다. 그래서 이 알고리즘을 배치 경사 하강법(Batch Gradient Descent) 라고 합니다. 즉, 매 스텝에서 훈련 데이터 전체를 사용합니다. 이런 이유로 매우 큰 훈련 세트에서는 속도가 매우 느리게 됩니다. 그러나 경사 하강법은 특성 수에 민감하지 않습니다. 수십만 개의 특성에서 선형 회귀를 훈련시키려면 정규방정식보다 경사 하강법을 사용하는 편이 훨씬 빠릅니다.


위로 향하는 그래디언트 벡터가 구해지면 반대 방향인 아래로 가야 합니다. 따라서 에서 를 빼야 합니다.

여기서 학습률이 사용되는데, 내려가는 스텝의 크기를 결정하기 위해 그래디언트 벡터에 학습률을 곱하게 됩니다.


(학습률 : eta(에타)라고 읽음)


이제, 이 알고리즘을 구현해봅니다.





앞서 봤던 식을 참고하며 읽어보세요.

참고로 np.random.randn은 기댓값이 0 표준편차가 1인 표준 정규분포를 따르는 난수를 생성합니다. (2,1)은 2행 1열입니다.

위의 식과 동일하게 반복수를 n_iterations로 지정하고 for문을 통해 돌립니다.

앞서 설명했지만, numpy의 T는 전치행렬을 구하는 것 즉 self.transpose입니다. 따라서 1X1, 2X2, 3X3 등등 대각선 행렬은 그대로 남고 행과 열이 바뀌게 됩니다.  또한, dot은 행렬간 곱셈을 나타냅니다.


eta = 0.1 #학습률
n_iterations = 1000
m = 100
theta = np.random.randn(2,1) #무작위 초기화

for iteration in range(n_iterations):
    gradients = 2/m * X_b.T.dot(X_b.dot(theta)-y) # 위로 가는 그레디언트 벡터를 구했다면 ?
    theta = theta-eta*gradients # 아래로 향하는 그레디언트 벡터를 구하기 위해 세타에서 빼주며, 학습률을 통해 스텝크기 결정


theta

array([[3.81237792],
       [3.0830601 ]])

X_new_b.dot(theta)

array([[3.81237792],
       [9.97849812]])



이렇게 정규방정식으로 찾은 것과 같은 결과가 나옴을 알 수 있습니다. 

이제, 학습률을 바꿔보겠습니다. 그림은 세 가지 다른 학습률을 사용하여 진행한 경사 하강법의 스텝 처음 10개를 보여줍니다.

(점선은 시작점을 나타냅니다.)



theta_path_bgd = []

def plot_gradient_descent(theta, eta, theta_path=None):
    m = len(X_b)
    plt.plot(X, y, 'b.')
    n_iterations = 1000
    for iteration in range(n_iterations):
        if iteration < 10 :
            y_predict = X_new_b.dot(theta)
            style = 'b-' if iteration > 0 else 'r--'
            plt.plot(X_new, y_predict, style)
        gradients = 2/m * X_b.T.dot(X_b.dot(theta)-y)
        theta = theta-eta*gradients
        if theta_path is not None :
            theta_path.append(theta)
    plt.xlabel('$x_1$', fontsize=18)
    plt.axis([0,2,0,15])
    plt.title(r'$\eta = {}$'.format(eta), fontsize=16)


#무작위와 관련된 알고리즘에서 seed, 즉 시작 숫자를 정해주면 그 다음에는 난수 같은 수열을 생성하게 됩니다.

#따라서 시드를 고정함으로서 할 때마다 바뀌는 게 아닌 고정된 결과를 얻을 수 있습니다.


np.random.seed(42)
theta = np.random.randn(2,1) #random initialization

plt.figure(figsize=(10,4))
plt.subplot(131); plot_gradient_descent(theta, eta=0.02)
plt.ylabel('$y$', rotation=0, fontsize=18)
plt.subplot(132); plot_gradient_descent(theta, eta=0.1, theta_path = theta_path_bgd)
plt.subplot(133); plot_gradient_descent(theta, eta=0.5)

plt.show()



이렇게 보면, 왼쪽은 학습률이 너무 낮아 알고리즘은 최적점에 도달하기까지 시간이 오래 걸립니다.
가운데는 아주 적당한 학습률로 반복 몇 번 만에 이미 최적에 수렴했습니다.
우측은 학습률이 너무 높아 이전에 말했던 것 처럼 오히려 더 멀어지고 있음을 알 수 있습니다.

이렇게 적당한 학습률을 찾으려면, 그리드 탐색을 사용합니다. 하지만 그리드 탐색에서 수렴하는 데 너무 오래 걸리는 모델을 막기 위하여 반복 횟수를 제한하여야 합니다.

반복 횟수는 어떻게 지정해야 할까요? 너무 작으면 최적점에 도달하기 전에 알고리즘이 멈춰버립니다.

너무 크면 시간을 낭비하게 됩니다. 따라서 반복 횟수를 아주 크게 지정하고 그래디언트 벡터가 아주 작아지면, 즉 벡터의 노름이 어떤 값 (허용오차, tolerance)보다 작아지면 경사 하강법이 거의 최솟값에 도달한 것이므로 알고리즘을 중지하는 것입니다.


##########################################################################################


수렴율?


비용 함수가 볼록 함수이고, 기울기가 급격하게 바뀌지 않는 경우(MSE 비용함수 같은), 학습률을 고정한 배치 경사 하강법은 어느 정도 시간이 걸리겠지만 결국 최적의 솔루션에 수렴할 것입니다. 비용 함수의 모양에 따라 달라지겠지만, tolerance 범위 내에서 최적의 솔루션에 도달하기 위해서는 O(1/tol)의 반복이 걸릴 수 있습니다. 즉, 허용오차를 10분의 1로 줄이면, 알고리즘의 반복은 10배 늘어날 것입니다.


##########################################################################################


다음 시간에는 확률적 경사 하강법에 대해 공부해보겠습니다.

다시 한 번 말씀드리지만, 이 과정들은 - 핸즈온 머신러닝(한빛미디어/오렐리앙 제롱 저/박해선 옮김)을 참고하고 있습니다.

관심이 있으시다면 꼭 구매해보세요..(마케팅 아닙니다.. 저도 구매함!^_^)

(공지에도 적혀있습니다.)


 블로그 

출처


이 글의 상당 부분은  [핸즈온 머신러닝, 한빛미디어/오렐리앙 제롱/박해선] 서적을 참고하였습니다.


나머지는 부수적인 함수나 메서드에 대해 부족한 설명을 적어두었습니다.

학습용으로 포스팅 하는 것이기 때문에 복제보다는 머신러닝에 관심이 있다면 구매해보시길 추천합니다.


도움이 되셨다면 로그인 없이 가능한

아래 하트♥공감 버튼을 꾹 눌러주세요! 



반응형