이번 장에서는 신경망(neural network)을 이해하고 구축, 훈련시키는데 필수적인 기반 지식이 되는 알고리즘의 작동 원리에 대해서 다루기로 한다.
구현체가 어떻게 작동하는지 잘 이해하면 적절한 모델, 올바른 훈련 알고리즘, 작업에 맞는 좋은 하이퍼파라미터를 빠르게 찾을 수 있으며, 디버깅이나 에러를 효율적으로 분석하는 데 도움이 된다.
# 선형 회귀
일반적으로 선형 모델은 입력 특성의 가중치 합과 편향(bias)라는 상수를 더해 예측을 만든다. (사진 참고)
사진에 나와있는 것 처럼 선형 모델의 방정식은 벡터 형태로 간단하게 쓸 수 있다.
모델을 훈련시킨다는 것은 모델이 훈련 세트에 가장 잘 맞도록 모델 파라미터를 설정하는 것이다. 이를 위해서는 먼저 모델의 훈련 데이터에 대한 성능을 측정해야 하는데, 회귀 모델에서 가장 널리 사용되는 성능 측정 지표는 RMSE(평균 제곱근 오차) 이므로, 선형 회귀 모델을 훈련시키려면 RMSE 를 최소화하는 $theta$를 찾아야 한다.
실제로 RMSE 보다 MSE 를 최소화 하는 것이 결과적으로는 같으면서 더 간단하다. (사진 MSE 비용함수 수식 참조)
# 정규방정식
비용 함수를 최소화 하는 $theta$ 값을 찾기 위한 해석적인 방법으로는 정규방정식을 계산하는 방법이 있다.
$$\hat{\theta} = (X^{T}\cdot X)^{-1}\cdot X^{T}\cdot y$$
$\hat{\theta}$ 는 비용 함수를 최소화하는 $\theta$ 값이다.
$y$는 $y_1$ 부터 $y_m$ 까지 포함하는 타깃 벡터이다.
* 정규 방정식의 증명: 위키백과 문서
공식을 테스트 하기 위해서, numpy를 이용해 선형처럼 보이는 데이터를 생성한다.
import numpy as np
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()
데이터를 만들었으니 정규방정식을 사용해 $\hat{\theta}$ 를 계산해 보기로 한다. 넘파이 선형대수 모듈(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)
theta_best
"""
array([[4.51120665],
[2.54531701]])
"""
* SVD를 사용하여 유사 역행렬을 구하는 넘파이의 pinv() 함수를 사용하면 np.linalg.pinv(X_b).dot(y) 와 같이 간단하게 계산할 수 있다.
데이터를 생성하기 위해 사용한 실제 함수는 $y = 4 + 3x_1 + gaussian noise$ 였다.
* gaussian noise는 정규분포를 가지는 잡음이란 뜻으로 일반적으로 랜덤하며 자연계에서 흔히 볼 수 있는 분포의 잡음을 말한다.
즉 $\theta_0 = 4$, $\theta_1 = 3$ 이 기대하는 값이며, 실제로 계산한 결과값은 $ \theta_0 = 4.511, \theta_1 = 2.545 $ 로 노이즈 때문에 완전히 정확하지는 않지만 어느 정도 비슷한 것을 볼 수 있다.
원래 데이터의 분포와 theta_best 를 사용해 예측한 결과를 그래프로 나타내보면 다음과 같다.
plt.plot(X_new, y_predict, "r-", linewidth=2, label="predict")
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()
같은 작업을 하는 사이킷런 코드는 다음과 같다. (sklearn.linear_model의 LinearRegression 모듈을 사용한다)
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X, y)
lin_reg.intercept_, lin_reg.coef_
# (array([4.51120665]), array([[2.54531701]]))
# 계산 복잡도
정규방정식은 $(n+1)\times(n+1)$ 크기가 되는 $X^{T}\cdot X$ 의 역행렬을 계산한다. ($n$ 은 특성 수)
역행렬을 계산하는 계산 복잡도는 일반적으로 $O(n^{2.4})$ 에서 $O(n^3)$ 사이 이다(구현 방법에 따라 차이가 있음). 즉 특성 수가 두 배로 늘어나면 계산 시간이 5.3 ~ 8 배로 증가한다.
* 역행렬을 계산하는 가장 빠른 방법은 코퍼스미스-위노그라드 알고리즘을 사용한 방식이다. 사이킷런의 LinearRegression에서 사용하는 scipy.linalg.lstsq 함수는 SVD 방법을 사용하여 유사 역행렬을 계산하며 $O(n^2)$의 복잡도를 가진다.
이 공식의 복잡도는 훈련 세트의 샘플 수에는 선형적으로 증가한다. ($O(m)$)
< $X$를 $m\times n$ 행렬이라 할 때 $X^{T}\cdot X$ 는 $(n\times m)\cdot(m\times n) = (n\times n)$ 크기의 행렬이 되므로 샘플 수(m)가 역행렬 계산의 복잡도를 증가시키지 않고 점곱의 양만 선형적으로 증가시킨다.>
따라서 큰 훈련 세트도 효율적으로 처리할 수 있다.
또한 학습된 선형 회귀 모델을 예측이 매우 빠르며, 예측 계산 복잡도는 샘플 수와 특성 수에 선형적이다.
# 경사 하강법
경사 하강법(Gradient Descent , GD)은 여러 종류의 문제에서 최적의 해법을 찾을 수 있는 매우 일반적인 최적화 알고리즘이며 기본적으로 비용 함수를 최소화하기 위해 반복해서 파라미터를 조정하는 방식이다.
알고리즘은 파라미터 벡터 $\theta$ 에 대해 비용 함수의 현재 그래디언트(gradient, 비용 함수의 미분값)을 계산하고, 그래디언트가 감소하는 방향으로 진행한다. 그래디언트가 0이 되면 극솟값에 도달한 것이 된다.
구체적으로 $\theta$ 를 임의의 값으로 시작해서(무작위 초기화) 한번에 조금씩 비용함수(MSE 등)가 감소되는 방향으로 진행하여 알고리즘이 최솟값에 수렴할 때가지 점진적으로 변화시키는데, 이때 중요한 파라미터는 스텝(step)의 크기로, 학습률(learning rate) 하이퍼파라미터로 결정된다.
학습률이 너무 작으면 알고리즘이 수렴하기 위해 반복을 많이 진행해야 되므로 시간이 오래 걸리고,
학습률이 너무 크면 최솟값을 뛰어 넘어 오히려 알고리즘을 더 큰 값으로 발산하게 만들 수도 있다.
또한 극솟값이라고 반드시 최솟값이 아니다.
그래프에 패인 곳, 산마루, 평지 등 특이한 지형이 있으면 최솟값으로 수렴하기 매우 힘드며, 지역 최솟값에 수렴, 평지에서 멈춤 등의 문제점이 발생할 수 있다.
선형 회귀를 위한 MSE 비용 함수는 다행히도 지역 최솟값이 없고 하나의 전역 최솟값만을 갖는 볼록 함수(convex function) 이며, 연속함수이고 기울기가 갑자기 변하지 않는 도함수가 국부적 립시츠 연속(locally Lipschitz continuous)인 함수이다.
* 어떤 함수가 일정한 범위 내에서 변할때 그 함수를 립시츠 연속(Lipschitz continuous)함수라고 한다. 예를 들어 $sin(x)$는 립시츠 연속 함수이다. MSE 는 $x$가 무한대일 때 기울기가 무한대가 되므로 국부적인 립시츠 함수라고 한다.
경사 하강법을 사용할 때에는 반드시 모든 특성이 같은 스케일을 갖도록 만들어야 한다. (사이킷런에서는 이를 위해 StandardScaler 를 사용할 수 있다.)
* StandardScaler는 데이터의 각 특성에서 평균을 빼고 표준편차로 나누어 평균을0, 분산을 1로 만든다.
# 배치 경사 하강법
편도함수(partial derivative) 는 각 모델 파라미터 $\theta_j $ 에 대해 비용 함수의 그래디언트를 계산한 것이다.
파라미터 $\theta_j$ 에 대한 비용함수의 편도함수는 다음과 같다
$$\frac{\partial}{\partial\theta_j}MSE(\theta) = \frac{2}{m}\sum_{i=1}^{m} (\theta^{T} \cdot \mathbf{x}^{(i)} - y^{(i)})\chi^{(i)}_j$$
$\theta_j$ 에 대해 편미분을 하였으므로 먼저 지수의 2가 곱셈으로 내려오고, $\theta_j$ 의 계수 $\chi_j\$ 가 다시 곱해진다.
편도 함수를 각각 계산하는 대신 다음의 식을 사용하여 한꺼번에 계산할 수 있다. 그래디언트 벡터 $\nabla_\theta MSE(\theta)$ 는 비용 함수의 편도함수를 모두 가지고 있다.
$$\nabla_\theta MSE(\theta)=\frac{2}{m}X^{T}\cdot(X\cdot\theta-y)$$
이는 다음과 같다
$$\begin{pmatrix} \frac{\partial}{\partial\theta_0}MSE(\theta) \\ \frac{\partial}{\partial\theta_1}MSE(\theta) \\ \vdots \\ \frac{\partial}{\partial\theta_n}MSE(\theta) \end{pmatrix} $$
배치 경사 하강법(Batch Gradient Descent)은 매 경사 하강법 스텝에서 전체 훈련 세트 $X$ 에 대해 계산한다. 즉, 매 스텝에서 훈련 데이터 전체를 사용하므로 매우 큰 훈련 세트에서는 아주 느리다는 단점이 있다.
별개로 경사 하강법 알고리즘은 특성 수에 민감하지 않다는 특징이 있다. 수십만 개의 특성이 존재하는 데이터셋으로 선형 회귀를 훈련시키려면 정규방정식 보다 경사 하강법을 사용하는 것이 훨씬 빠르다.
그래디언트 벡터를 구함으로써 각각의 파라미터 $\theta$ 값의 변화량에 대한 비용함수의 변화량을 알게 되었다. 만약 그래디언트 벡터가 위로 향한다면, $\theta$ 값은 반대 방향인 아래로 가야 전체 비용을 줄일 수 있을 것이다.
즉 $\theta$ 에서 $\nabla_\theta MSE(\theta)$ 를 빼야 하는데, 이때 학습률 $\eta$ 가 사용된다. 내려가는 스텝의 크기를 결정하기 위해 그래디언트 벡터에 $\eta$를 곱하도록 한다.
$$\theta^{(next\,step)} = \theta - \eta \nabla_\theta MSE(\theta)$$
이 알고리즘을 구현해 보면 다음과 같다.
eta = 0.1 # learning rate
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([[4.51120665],
[2.54531701]])
"""
정규방정식으로 찾은것과 같은 것을 볼 수 있다.
'Artificial Intelligence > Machine Learning' 카테고리의 다른 글
핸즈온 머신러닝(5) - 다항 회귀 (0) | 2020.01.23 |
---|---|
핸즈온 머신러닝(5) - 선형 회귀와 경사 하강법 2 (0) | 2020.01.23 |
핸즈온 머신러닝(4) - 연습문제 1 (0) | 2020.01.22 |
핸즈온 머신러닝(4) - 분류 2 (1) | 2020.01.21 |
핸즈온 머신러닝(4) - 분류 1 (0) | 2020.01.21 |
댓글