#!/usr/bin/env python # coding: utf-8 # In[8]: get_ipython().run_line_magic('load_ext', 'watermark') get_ipython().run_line_magic('watermark', '-v -p numpy,scipy,sklearn,pandas,matplotlib') # 파이썬 2와 파이썬 3 지원 from __future__ import division, print_function, unicode_literals # 공통 import numpy as np import os # 일관된 출력을 위해 유사난수 초기화 np.random.seed(42) # 맷플롯립 설정 get_ipython().run_line_magic('matplotlib', 'inline') import matplotlib import matplotlib.pyplot as plt plt.rcParams['axes.labelsize'] = 14 plt.rcParams['xtick.labelsize'] = 12 plt.rcParams['ytick.labelsize'] = 12 # 한글출력 matplotlib.rc('font', family='AppleGothic') plt.rcParams['axes.unicode_minus'] = False # # Chapter 4. 모델 훈련 # - 이번 장에서 배울 내용 # - 가장 간단한 모델 중 하나인 선형 회귀 # - 비선형 데이터셋에 훈련시킬 수 있는 조금 더 복잡한 모델인 다항 회귀 # - 학습 곡선learning curve을 사용해 모델이 과대적합되는지 감지하는 방법 # - 훈련 세트의 과대적합을 감소시킬 수 있는 규제 기법 # --- # ## 4.1 선형 회귀 # 다음은 1장에서 본 삶의 만족도에 대한 간단한 선형 회귀 모델 #     삶의 만족도 = $\theta$0 + $\theta$1 * 1인당_GDP # - 이 모델은 입력 특성인 1인당_GDP에 대한 선형 함수
# - θ0과 θ1이 모델 파라미터 # 일반적으로 선형 모델은 [식 4-1]에서처럼 입력 특성의 가중치 합과 **편향**bias(또는 **절편**intercept)이라는 상수를 더해 예측을 만듦 # ![equation4-1](./images/equation4-1.png) #
#
**식 4-1 선형 회귀 모델의 예측**
# - $\hat{y}$은 예측값 # - n은 특성의 수 # - xi는 i번째 특성값 # - $\theta$j는 j번째 모델 파라미터(편향 $\theta$0과 특성의 가중치 $\theta$1, $\theta$2, ..., $\theta$n을 포함) # 이 식은 [식 4-2]처럼 벡터 형태로 더 간단하게 쓸 수 있음 # #
**식 4-2 선형 회귀 모델의 예측(벡터 형태)**
# - $\theta$는 모델의 파라미터 벡터 # - $\theta$T는 $\theta$의 전치(열 벡터가 아니고 행 벡터) # - x는 x0에서 xn까지 담고 있는 샘플의 **특성 벡터**. x0는 항상 1 # - h$\theta$는 모델 파라미터 $\theta$를 사용한 가설hypothesis 함수 # - 모델 훈련 : 모델이 훈련 세트에 가장 잘 맞도록 모델 파라미터를 설정하는 것
# 이를 위해 먼저 모델이 훈련 데이터에 잘 들어맞는지 측정해야 함.
# 성능 측정 지표로는 평균 제곱 오차Mean Square Error(MSE)를 최소화 # 훈련 세트 X에 대한 선형 회귀 가설 h$\theta$의 MSE는 [식 4-3]처럼 계산함 # #
**식 4-3 선형 회귀 모델의 MSE 비용 함수**
# ### 4.1.1 정규방정식 # **정규방정식**Normal Equation : 비용 함수를 최소화하는 $\theta$ 값을 얻을 수 있는 수학 공식(식 4-4) # #
**식 4-4 정규방정식**
# - $\hat{\theta}$은 비용 함수를 최소화하는 $\theta$ 값
# - y는 y(1)부터 y(m)까지 포함하는 타깃 벡터 # 이 공식을 테스트하기 위해 선형처럼 보이는 데이터를 생성 # In[14]: import numpy as np X = 2 * np.random.rand(100, 1) #균일 분포[0,1) , 100행 1열 y = 4 + 3 * X + np.random.randn(100, 1) #정규분포 # In[15]: 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() # 정규방정식을 사용해 $\hat{\theta}$을 계산 # In[29]: X_b = np.c_[np.ones((100, 1)), X] # 모든 샘플에 x0 = 1을 추가합니다. 두 개의 1차원 배열을 칼럼으로 세로로 붙여서 2차원 배열 만듦 theta_best = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y) #inv():역행렬 계산, dot():행렬 곱셈 # In[17]: theta_best # $\hat{\theta}$을 사용한 예측 # In[18]: 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 # 아래 그래프는 모델의 예측을 나타냄 # In[71]: plt.plot(X_new, y_predict, "r-", linewidth=2, label="예측") # x범위, y범위, 스타일=빨간색 선 plt.plot(X, y, "b.") plt.xlabel("$x_1$", fontsize=18) plt.ylabel("$y$", rotation=0, fontsize=18) plt.legend(loc="upper left", fontsize=14) plt.axis([0, 2, 0, 15]) plt.show() # 아래는 같은 작업을 하는 사이킷런 코드 # In[20]: from sklearn.linear_model import LinearRegression lin_reg = LinearRegression() lin_reg.fit(X, y) lin_reg.intercept_, lin_reg.coef_ # In[21]: lin_reg.predict(X_new) # ### 4.1.2 계산 복잡도 # - 정규방정식은 (n+1) x (n+1) 크기가 되는 XT$\cdot$X의 역행렬을 계산(n은 특성수)
# - 역행렬의 **계산 복잡도**computational complexity : O(n2.4) ~ O(n3)     (구현 방법에 따라 차이가 있음)
# 특성 수가 두 배로 늘어나면 계산 시간이 대략 5.3 ~ 8배 증가
# 하지만 이 공식의 복잡도가 훈련 세트의 샘플 수에는 선형적으로 증가(즉, O(m))
# - 예측 계산 복잡도는 샘플 수 와 특성 수에 선형적. 학습된 선형 회귀 모델은 예측이 매우 빠름 # --- # ## 4.2 경사 하강법 # - **경사 하강법**Gradient Descent(GD) : 비용 함수를 최소화하기 위해 반복해서 파라미터를 조정해가는 최적화 알고리즘
# $\theta$를 임의의 값으로 시작해서 한번에 조금씩 비용 함수가 감소되는 방향으로 진행하여 알고리즘이 최솟값에 수렴할 때까지 점진적으로 향상 (그림 4-3) # #
**그림 4-3 경사 하강법**
# - 경사 하강법에서 중요한 파라미터는 스텝의 크기로, **학습률**learning rate 하이퍼파라미터로 결정
# - 학습률이 너무 작으면 알고리즘이 수렴하기 위해 반복을 많이 진행해야 하므로 시간이 오래 걸림 (그림 4-4) # #
**그림 4-4 학습률이 너무 작을 때**
# - 학습률이 너무 크면 골짜기를 가로질러 반대편으로 건너뛰어 이전보다 더 높은 곳으로 올라가게 되어 알고리즘을 더 큰 값으로 발산하게 만들어 적절한 해법을 찾지 못함 (그림 4-5) # #
**그림 4-5 학습률이 너무 클 때**
# [그림 4-6]은 경사 하강법의 두 가지 문제점을 보여줌 # - 무작위 초기화 때문에 알고리즘이 왼쪽에서 시작하면 **전역 최솟값**global minimum보다 덜 좋은 **지역 최솟값**local minimum에 수렴
# - 알고리즘이 오른쪽에서 시작하면 평탄한 지역을 지나기 위해 시간이 오래 걸리고 일찍 멈추게 되어 전역 최솟값에 도달하지 못함 # #
#
**그림 4-6 경사 하강법의 문제점**
# - 선형 회귀를 위한 MSE 비용 함수는 곡선에서 어떤 두 점을 선택해 선을 그어도 곡선을 가로지르지 않는 **볼록 함수**convex function
이는 지역 최솟값이 없고 하나의 전역 최솟값만 있다는 뜻
# - 또한 연속된 함수이고 기울기가 갑자기 변하지 않음
# - 이 두 사실로부터 경사 하강법이 전역 최솟값에 가깝게 접근할 수 있다는 것을 보장 # - 비용 함수는 그릇 모양을 하고 있지만 특성들의 스케일이 매우 다르면 길쭉한 모양일 수 있음
# [그림 4-7]에서 왼쪽의 경사 하강법 알고리즘이 최솟값으로 곧장 진행하고 있어 빠르게 도달하지만 오른쪽 그래프는 돌아서 나가기 때문에 시간이 오래 걸림
# - 경사 하강법을 사용할 때는 반드시 모든 특성이 같은 스케일을 갖도록 만들어야 함(예를 들면 사이킷런의 StandardScaler를 사용)
# #
**그림 4-7 특성 스케일에 따른 경사 하강법**
# - 앞의 그림은 모델 훈련이 (훈련 세트에서) 비용 함수를 최소화하는 모델 파라미터의 조합을 찾는 일을 설명함. 이를 모델의 **파라미터 공간**parameter space에서 찾는다고 말함
# - 모델이 가진 파라미터가 많을수록 이 공간의 차원은 커지고 검색이 더 어려워짐 # ### 4.2.1 배치 경사 하강법 # - 비용 함수의 **편도함수**partial derivative : $\theta$j가 조금 변경될 때 비용 함수가 얼마나 바뀌는지 계산 # #
**식 4-5 비용 함수의 편도함수**
# 편도함수를 각각 계산하는 대신 [식 4-6]을 사용하여 한꺼번에 계산
# 그래디언트 벡터 $\nabla$$\theta$MSE($\theta$)는 비용 함수의 (모델 파라미터마다 한 개씩인) 편도함수를 모두 담고 있음 # #
**식 4-6 비용 함수의 그래디언트 벡터**
# - 이 공식은 매 경사 하강법 스텝에서 전체 훈련 세트 X에 대해 계산하기 때문에 이 알고리즘을 **배치 경사 하강법**Batch Gradient Descent이라고 함
# - 이런 이유로 매우 큰 훈련 세트에서는 아주 느림. 하지만 특성 수에 민감하지는 않음 # - 내려가는 스텝의 크기를 결정하기 위해 그래디언트 벡터에 학습률$\eta$를 곱함 # #
**식 4-7 경사 하강법의 스텝**
# 아래는 알고리즘을 구현한 코드 # In[94]: theta_path_bgd = [] # 알고리즘이 훈련 과정 동안 파라미터 공간에서 움직인 경로 np.random.seed(42) # 초기 난수 생성값 지정 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_path_bgd.append(theta) # In[95]: theta # [그림 4-8]은 세 가지 다른 학습률을 사용하여 진행한 경사 하강법의 스텝 처음 10개를 보여줌(점선은 시작점) # In[96]: def plot_gradient_descent(theta, eta, theta_path=None): plt.plot(X, y, "b.") 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 plt.xlabel("$x_1$", fontsize=18) plt.axis([0, 2, 0, 15]) plt.title(r"$\eta = {}$".format(eta), fontsize=16) 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() # - 왼쪽의 알고리즘은 최적점에 도달하겠지만 학습률이 너무 낮아 시간이 오래 걸릴 것
# - 가운데는 학습률이 아주 적당하여 반복 몇 번 만에 최적점에 수렴
# - 오른쪽은 학습률이 너무 높아 알고리즘이 널뛰면서 스텝마다 최적점에서 점점 더 멀어져 발산 # - 적절한 학습률을 찾으려면 그리드 탐색을 사용
# - 그리드 탐색에서 수렴하는 데 너무 오래 걸리는 모델을 막기 위해 반복 횟수를 제한
# 반복 횟수를 아주 크게 지정하고 그래디언트 벡터가 아주 작아지면, 즉 벡터의 노름이 어떤 값 $\varepsilon$(**허용오차**tolerance)보다 작아지면 경사 하강법이 (거의) 최솟값에 도달한 것이므로 알고리즘을 중지함
# - $\varepsilon$ 범위 안에서 최적의 솔루션에 도달하기 위해서는 O(1/$\varepsilon$)의 **반복**이 걸릴 수 있음. 다시 말해 허용오차 $\varepsilon$을 1/10로 줄이면 알고리즘의 반복은 10배 늘어남 # ### 4.2.2 확률적 경사 하강법 # - 배치 경사 하강법의 가장 큰 문제는 매 스텝에서 전체 훈련 세트를 사용해 그래디언트를 계산하여 훈련 세트가 커지면 매우 느려진다는 것
# - **확률적 경사 하강법**은 매 스텝에서 딱 한 개의 샘플을 무작위로 선택하고 그 하나의 샘플에 대한 그래디언트를 계산. 매 반복에서 매우 적은 데이터만 처리하기 때문에 알고리즘이 훨씬 빠르고 매우 큰 훈련 세트도 훈련시킬 수 있음
# - 반면, 확률적이기 때문에 이 알고리즘은 배치 경사 하강법보다 훨씬 불안정함
# 비용 함수가 최솟값에 다다를 때까지 부드럽게 감소하지 않고 위아래로 요동치면서 평균적으로 감소함. 시간이 지나면 최솟값에 매우 근접하겠지만 최적치는 아님 (그림 4-9 참조) # #
**그림 4-9 확률적 경사 하강법**
# - [그림 4-6]처럼 비용 함수가 매우 불규칙할 경우 지역 최솟값을 건너뛰어 전역 최솟값을 찾을 가능성이 높음
# - 지역 최솟값에서 탈출하지만 전역 최솟값에 다다르지 못하는 딜레마를 해결하는 한 가지 방법은 학습률을 점진적으로 감소시키는 것
# 시작할 때는 학습률을 크게 하고(수렴 빠름, 지역 최솟값에 빠지지 않음) 점차 작게 줄여서 알고리즘이 전역 최솟값에 도달하게 함
# - 매 반복에서 학습률을 결정하는 함수를 **학습 스케쥴**learning schedule이라고 부름 # 아래은 확률적 경사 하강법의 구현 코드 # In[79]: theta_path_sgd = [] # 알고리즘이 훈련 과정 동안 파라미터 공간에서 움직인 경로 m = len(X_b) # 훈련 세트에 있는 샘플 수 np.random.seed(42) # 초기 난수 생성값 지정 n_epochs = 50 t0, t1 = 5, 50 # 학습 스케줄 하이퍼파라미터 learning schedule hyperparameters def learning_schedule(t): return t0 / (t + t1) theta = np.random.randn(2,1) # 무작위 초기화 for epoch in range(n_epochs): for i in range(m): if epoch == 0 and i < 20: # 훈련 스텝의 첫 20개를 그림 y_predict = X_new_b.dot(theta) style = "b-" if i > 0 else "r--" plt.plot(X_new, y_predict, style) random_index = np.random.randint(m) xi = X_b[random_index:random_index+1] yi = y[random_index:random_index+1] gradients = 2 * xi.T.dot(xi.dot(theta) - yi) # 하나의 샘플에 대한 그래디언트를 계산하므로 식 4-6에서 /m 이 없음 eta = learning_schedule(epoch * m + i) theta = theta - eta * gradients # 식 4-7 theta_path_sgd.append(theta) 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() # In[73]: theta # - 샘플을 무작위로 선택하기 때문에 어떤 샘플은 한 에포크에서 여러 번 선택될 수 있고 어떤 샘플은 전혀 선택되지 못할 수도 있음
# - 에포크마다 모든 샘플을 사용하게 하려면 훈련 세트를 섞은 후 차례대로 하나씩 선택하고 다음 에포크에서 다시 썩는 식의 방법을 사용
# - 그러나 이렇게 하면 보통 더 늦게 수렴됨 # 사이킷런에서 SGD 방식으로 선형 회귀를 사용하려면 SGDRegressor 클래스를 사용
# In[74]: from sklearn.linear_model import SGDRegressor sgd_reg = SGDRegressor(max_iter=50, penalty=None, eta0=0.1, random_state=42) # 학습률 0.1, 에포크 50번 sgd_reg.fit(X, y.ravel()) # 다차원 배열(array)을 1차원 배열로 평평하게 펴줌, NumPy의 reshape()함수와 반대 기능 # In[75]: sgd_reg.intercept_, sgd_reg.coef_ # ### 4.2.3 미니배치 경사 하강법 # - **미니배치 경사 하강법**Mini-batch Gradient Descent : 각 스텝에서 전체 훈련 세트나 하나의 샘플을 기반으로 그래디언트를 계산하는 것이 아니라 **미니배치**라 부르는 임의의 작은 샘플 세트에 대해 그래디언트를 계산
# - 장점 : 행렬 연산에 최적화된 하드웨어, 특히 GPU를 사용해서 얻는 성능 향상 # - 미니배치를 어느 정도 크게 하면 이 알고리즘은 파라미터 공간에서 SGD보다 덜 불규칙하게 움직여 최솟값에 더 가까이 도달
# - 하지만 지역 최솟값에서 빠져나오기는 더 힘들지도 모름 # In[80]: theta_path_mgd = [] # 알고리즘이 훈련 과정 동안 파라미터 공간에서 움직인 경로 n_iterations = 50 minibatch_size = 20 np.random.seed(42) # 초기 난수 생성값 지정 theta = np.random.randn(2,1) # 무작위 초기화 t0, t1 = 200, 1000 def learning_schedule(t): return t0 / (t + t1) t = 0 for epoch in range(n_iterations): shuffled_indices = np.random.permutation(m) X_b_shuffled = X_b[shuffled_indices] y_shuffled = y[shuffled_indices] for i in range(0, m, minibatch_size): t += 1 xi = X_b_shuffled[i:i+minibatch_size] yi = y_shuffled[i:i+minibatch_size] gradients = 2/minibatch_size * xi.T.dot(xi.dot(theta) - yi) # 식 4-6 eta = learning_schedule(t) theta = theta - eta * gradients # 식 4-7 theta_path_mgd.append(theta) # In[81]: theta # 아래 그림은 세 가지 경사 하강법 알고리즘이 훈련 과정 동안 파라미터 공간에서 움직인 경로를 나타냄 # In[89]: theta_path_bgd = np.array(theta_path_bgd) theta_path_sgd = np.array(theta_path_sgd) theta_path_mgd = np.array(theta_path_mgd) plt.figure(figsize=(7,4)) plt.plot(theta_path_sgd[:, 0], theta_path_sgd[:, 1], "r-s", linewidth=1, label="SGD") plt.plot(theta_path_mgd[:, 0], theta_path_mgd[:, 1], "g-+", linewidth=2, label="미니배치") plt.plot(theta_path_bgd[:, 0], theta_path_bgd[:, 1], "b-o", linewidth=3, label="배치") plt.legend(loc="upper left", fontsize=16) plt.xlabel(r"$\theta_0$", fontsize=20) plt.ylabel(r"$\theta_1$ ", fontsize=20, rotation=0) plt.axis([2.5, 4.5, 2.3, 3.9]) plt.show() # 지금까지 논의한 알고리즘을 선형 회귀를 사용해 비교함(m은 훈련 샘플 수, n은 특성 수). [표 4-1] 참조 # #
**표 4-1 선형 회귀를 사용한 알고리즘 비교**
# --- # ## 4.3 다항 회귀 # - **다항 회귀**Polynomial Regression : 비선형 데이터를 학습하기 위해 각 특성의 거듭제곱을 새로운 특성으로 추가하고, 이 확장된 특성을 포함한 데이터셋에 선형 모델을 훈련시키는 기법 # 먼저 간단한 **2차방정식**quadratic equation으로 비선형 데이터를 생성(약간의 노이즈 포함) # In[99]: np.random.seed(42) m = 100 X = 6 * np.random.rand(m, 1) - 3 y = 0.5 * X**2 + X + 2 + np.random.randn(m, 1) plt.plot(X, y, "b.") plt.xlabel("$x_1$", fontsize=18) plt.ylabel("$y$", rotation=0, fontsize=18) plt.axis([-3, 3, 0, 10]) plt.show() # 사이킷런의 PolynomialFeatures를 사용해 훈련 데이터를 변환 # In[106]: from sklearn.preprocessing import PolynomialFeatures poly_features = PolynomialFeatures(degree=2, include_bias=False) X_poly = poly_features.fit_transform(X) X[0] # In[107]: X_poly[0] # 이 확장된 훈련 데이터에 Linear Regression을 적용 # In[108]: lin_reg = LinearRegression() lin_reg.fit(X_poly, y) lin_reg.intercept_, lin_reg.coef_ # In[109]: X_new=np.linspace(-3, 3, 100).reshape(100, 1) # 시작, 끝점을 균일간격으로 100개 생성 X_new_poly = poly_features.transform(X_new) y_new = lin_reg.predict(X_new_poly) plt.plot(X, y, "b.") plt.plot(X_new, y_new, "r-", linewidth=2, label="예측") plt.xlabel("$x_1$", fontsize=18) plt.ylabel("$y$", rotation=0, fontsize=18) plt.legend(loc="upper left", fontsize=14) plt.axis([-3, 3, 0, 10]) plt.show() # - 특성이 여러 개일 때 다항 회귀는 이 특성 사이의 관계를 찾을 수 있음(일반적인 선형 회귀 모델에서는 하지 못함)
# - PolynomialFeatures가 주어진 차수까지 특성 간의 모든 교차항을 추가하기 때문