머신러닝 맛보기 2편

참고: 조엘 그루스의 <밑바닥부터 시작하는 데이터과학(2판)>에 포함된 Gradient Descent의 소스코드 일부를 사용합니다.

주요 내용

  1. 경사하강법 의미
  2. 그레이디언트 계산
  3. 경사하강법으로 최솟점 구하기
  4. 경사하강법과 선형회귀

기본 설정

주의사항

연습문제를 제외한 본문은 넘파이를 전혀 사용하지 않는다.

준비사항

선형대수에서 정의한 함수를 사용하기 위한 준비가 필요하며 필요한 코드가 pydata06_linear_algebra_basics.py 파일에 저장되어 있다고 가정한다.

먼저 파일을 다운로드한다.

1. 경사하강법

주어진 데이터셋을 가장 잘 반영하는 최적의 수학적 모델을 찾으려 할 때 가장 기본적으로 사용되는 기법이 경사하강법(gradient descent)이다. 최적의 모델에 대한 기준은 학습법에 따라 다르지만, 보통 학습된 모델의 오류를 최소화하도록 유도하는 기준을 사용한다.

여기서는 선형회귀 모델을 학습하는 데에 기본적으로 사용되는 평균 제곱 오차(mean squared error, MSE)를 최소화하기 위해 경사하강법을 적용하는 과정을 살펴본다.

경사하강법 기본 아이디어

함수 $f:\mathbf{R}^n \to \mathbf{R}$의 최솟값 지점을 구하고자 한다. 예를 들어, 길이가 $n$인 실수 벡터를 입력받아 항목들의 제곱의 합을 계산하는 함수가 다음과 같다고 하자.

$$ f(\mathbf v) = f(v_1, ..., v_n) = \sum_{k=1}^{n} v_k^2 = v_1^2 + \cdots v_n^2 $$

아래 코드에서 정의된 sum_of_squares()가 위 함수를 파이썬으로 구현한다.

참고: LA.Vectortyping.List[float], 즉 부동소수점들의 리스트 자료형을 가리킴.

이제 sum_of_squares(v)가 최대 또는 최소가 되는 벡터 v를 찾고자 한다.

그런데 특정 함수의 최솟값이 존재한다는 것이 알려졌다 하더라도 실제로 최솟값 지점을 확인하는 일은 일반적으로 매우 어렵고, 경우에 따라 불가능하다. 따라서 보통 해당 함수의 그래프 위에 존재하는 임의의 점에서 시작한 후 그레이디언트 반대 방향으로 조금씩 이동하면서 최솟값 지점을 찾아가는 경사하강법(gradient descent)을 이용한다.

그레이디언트의 정의와 의미

함수 $f:\mathbf{R}^n \to \mathbf{R}$가 점 $\mathbf{v}\in \mathbf{R}^n$에서 미분 가능할 때 $\mathbf{v}$에서 $f$의 그레이디언트는 다음처럼 편미분으로 이루어진 벡터로 정의된다.

$$ \nabla f(\mathbf{v}) = \begin{bmatrix} \frac{\partial}{\partial v_1} f(\mathbf{v}) \\ \vdots \\ \frac{\partial}{\partial v_n} f(\mathbf{v}) \end{bmatrix} $$

아래에서 왼편 그림은 $n=1$인 경우 2차원 상에서, 오른편 그림은 $n=2$인 경우에 3차원 상에서 그려지는 함수의 그래프와 특정 지점에서의 그레이디언트를 보여주고 있다.

경사하강법 경사하강법
<출처 > 위키:미분 <출처 > pvigier: gradient-descent

경사하강법 작동 방식

경사하강법은 다음 과정을 반복하여 최솟값 지점을 찾아가는 것을 의미한다.

아래 그림은 2차원 함수의 최솟값 지점을 경사하강법으로 찾아가는 과정을 보여준다. 최솟값 지점은 해당 지점에서 구한 그레이디언트의 반대방향으로 조금씩 이동하는 방식으로 이루어진다.

최솟값 지점에 가까워질 수록 그레이디언트는 점점 0벡터에 가까워진다. 따라서 그레이디언트가 충분히 작아지면 최솟값 지점에 매우 가깞다고 판단하여 그 위치에서 최솟값 지점의 근사치를 구하여 활용할 수 있다.

경사하강법 <출처 > <a href=""https://github.com/pvigier/gradient-descent>pvigier: gradient-descent </a>

주의사항

경사하강법은 지역 최솟값(local minimum)이 없고 전역 최솟값(global mininum)이 존재할 경우 유용하게 활용된다. 반면에 지역 최솟값이 존재할 경우 제대로 작동하지 않을 수 있기 때문에 많은 주의를 요한다. 아래 그림은 출발점과 이동 방향에 따라 도착 지점이 전역 또는 지역 최솟점이 될 수 있음을 잘 보여준다. 하지만 이런 경우는 여기서 다루지 않는다.

경사하강법 <출처 > 위키:미분

2. 파이썬으로 그레이디언트 계산

단변수 함수와 다변수 함수의 경우 그레이디언트 계산이 조금 다르다.

단변수 함수의 도함수 계산

$f$가 단변수 함수(1차원 함수)일 때, 점 $x$에서의 그레이디언트는 다음과 같이 구한다.

$$ f'(x) = \lim_{h\to 0} \frac{f(x+h) - f(x)}{h} $$

즉, $f'(x)$ 는 $x$가 아주 조금 변할 때 $f(x)$가 변하는 정도, 즉 함수 $f$의 $x$에서의 미분값이 된다. 이때 함수 $f'$ 을 함수 $f$의 도함수라 부른다.

예를 들어, 제곱 함수 $f(x) = x^2$에 해당하는 square()가 아래와 같이 주어졌다고 하자.

그러면 $f$의 도함수 $f'(x) = 2x$는 아래 square_prime() 함수로 구현된다.

도함수 근사치

이와 같이 미분함수가 구체적으로 주어지는 경우는 일반적이지 않다. 따라서 여기서는 충분히 작은 $h$에 대한 함수의 변화율

$$ \frac{f(x+h) - f(x)}{h} $$

을 측정하여 미분값의 근사치로 사용한다.

아래 그림은 $h$가 작아질 때 두 점 $f(x+h)$ 와 $f(x)$ 지나는 직선이 변하는 과정을 보여준다. $h$가 0에 수렴하면 최종적으로 점 $x$에서의 접선이 되고 미분값 $f'(x)$는 접선의 기울기가 된다.

경사하강법 <출처 > 위키:미분

아래 코드는 임의의 단변수 함수에 대해 함수 변화율을 구해주는 함수를 간단하게 구현한 것이다.

아래 코드는 square()의 도함수 square_prime()difference_quotient()를 이용하여 충분히 근사적으로 구현할 수 있음을 보여준다. 근사치 계산을 위해 h=0.001 를 사용한다.

다변수 함수의 그레이디언트 계산

다변수 함수의 그레이디언트는 매개변수 각가에 대한 편도함수(partial derivative)로 구성된 벡터로 구성된다. 예를 들어, $i$번째 편도함수는 $i$번째 매개변수를 제외한 다른 모든 매개변수를 고정하는 방식으로 계산된다.

$f$가 다변수 함수(다차원 함수)일 때, 점 $\mathbf{v}$에서의 $i$번째 도함수는 다음과 같다.

$$ \frac{\partial}{\partial v_i}f(\mathbf{v}) = \lim_{h \to 0} \frac{f(\mathbf{v}_{i, h}) - f(\mathbf x)}{h} $$

여기서 $\mathbf{v}_{i, h}$는 $\mathbf{v}$의 $i$번째 항목에 $h$를 더한 벡터

$$ \mathbf{v}_{i, h} = (v_1, \dots, v_{i-1}, v_i + h, v_{i+1}, \dots, v_n) $$

를 가리킨다. 즉, $\frac{\partial}{\partial v_i} f(\mathbf{v})$ 는 $v_i$가 아주 조금 변할 때 $f(\mathbf{v})$가 변하는 정도, 즉 함수 $f$의 $\mathbf{v}$에서의 $i$번째 편미분값이 된다. 이때 함수 $\frac{\partial}{\partial v_i}f$ 를 함수 $f$의 $i$번째 편도함수라 부른다.

편도함수 근사치

아래 코드에서 정의된 partial_difference_quotient()는 주어진 다변수 함수의 $i$번째 편도함수의 근사치를 계산해주는 함수이다. 사용되는 매개변수는 difference_quotient() 함수의 경우와 거의 같다. 다만 $i$번째 편도함수의 근사치를 지정하기 위해 $i$번째 매개변수에만 $h$가 더해짐에 주의하라.

다음 estimate_gradient() 함수는 편미분 근사치를 이용하여 그레이디언트의 근사치에 해당하는 벡터

$$ \begin{bmatrix} \frac{f(\mathbf{v}_{1, h}) - f(\mathbf x)}{h}\\ \vdots \\ \frac{f(\mathbf{v}_{n, h}) - f(\mathbf x)}{h} \end{bmatrix} $$

를 리스트로 계산한다. 근사치 계산에 사용된 $h$의 기본값은 0.0001이다.

참고: 그레이디언트를 두 개의 함수값의 차이를 이용하여 근사치로 계산하는 방식은 계산 비용이 크다. 벡터 v의 길이가 $n$이면 estimate_gradient() 함수를 호출할 때마다 f를 $2n$ 번 호출해야 하기 때문이다. 따라서 앞으로는 그레이디언트 함수가 수학적으로 쉽게 계산되는 경우만을 사용하여 경사하강법의 용도를 살펴볼 것이다.

3. 경사하강법으로 최솟점 구하기

앞서 정의한 제곱함수 sum_of_squares()v가 0 벡터일 때 가장 작은 값을 갖는다. 이 사실을 경사하강법을 이용하여 확인해보자.

먼저, sum_of_squares() 함수의 그레이디언트는 다음과 같이 정의된다.

$$ \nabla f(\mathbf{v}) = \begin{bmatrix} 2v_1 \\ \vdots \\ 2v_n \end{bmatrix} $$

아래 코드는 리스트를 이용하여 그레이디언트를 구현하였다.

아래 코드는 임의의 지점(v)에서 계산된 그레이디언트에 학습률(learning rate)이라는 특정 상수를 곱한 값을 빼서 새로운 지점을 계산하는 함수를 구현한다.

참고: subtractV, scalar_multV, distance 등은 선형대수 부분에서 정의한 pydata06_linear_algebra_basics.py 모듈에서 가져온다.

이제 임의의 지점에서 출발하여 gradient_step() 함수를 충분히 반복하면 제곱함수의 최솟점에 충분히 가깝게 근사할 수 있음을 아래 코드가 보여준다. 실제로 전역 최솟값 위치인 원점에 매우 가깝게 접근한다.

에포크와 학습률

앞서 사용된 코드에서 에포크(epoch)는 이동 횟수를 가리키며, 에포크를 크게 하면 최솟값 지점에 보다 가까워진다. 하지만 항상 수렴하는 방향으로 이동하는 것은 아니다. 하지만 여기서는 학습률을 너무 크게 지정하지만 않으면 항상 최솟값 지점에 수렴하는 볼록함수만 다룬다. 학습률에 따른 수렴속도는 다음과 같다.

하지만 다루는 함수에 따른 적당한 학습률이 달라지며, 보통 여러 실혐을 통해 정해야 한다.

4. 경사하강법과 선형회귀

주어진 데이터들의 분포에 대한 선형회귀 모델의 훈련과정을 구현해보자.

훈련 세트 준비

먼저 $y = f(x) = 5 + 20x$ 일차함수의 그래프에 해당하는 데이터를 구한다. 여기서 $x$ 는 -0.5에서 0.5 사이에 있는 100개의 값으로 주어지며, $y$ 값에 약간의 잡음(가우시안 잡음)이 추가된다.

아래 프로그램은 잡음이 포함되어 직선으로 그려지지 않는 데이터의 분포를 보여준다.

목표

이제 위 그래프를 선형적으로 묘사하는 함수를 경사하강법을 이용하여 구현한다. 즉 아래 1차 함수의 직선 그래프가 위 데이터의 분포를 최적으로 묘사하는 $\theta_0$와 $\theta_1$을 구해야 한다.

$$ \hat y_x = \theta_0 + \theta_1\cdot x $$

비용함수

경사하강법을 적용하려면 최적의 기준을 지정해야 한다. 여기서는 예측치 $\hat y$와 실제 $y$ 사이의 평균제곱오차(mean squared error, MSE)를 최소로 하는 것을 최적의 기준으로 사용한다($m$은 $X$의 원소 개수).

$$ \begin{align*} \textrm{MSE}(\theta_0, \theta_1) &= \frac{1}{m} \sum_{x \in X} (\hat y_x - y_x)^2\\[1ex] &= \frac{1}{100}\sum_{x \in X} (\theta_0 + \theta_1\cdot x - y_x)^2 \end{align*} $$

즉, MSE를 최소로 하는 $\theta_0, \theta_1$을 구해야 한다. ($x, y_x$는 모두 상수임에 주의하라.) 이처럼 최적의 기준으로 최솟값 지점을 찾는 대상인 함수를 비용함수라 부른다.

비용함수의 그레이언트

MSE의 그레이디언트는 다음과 같다.

$$ \nabla \textrm{MSE}(\theta_0, \theta_1) = \frac{1}{100} \cdot \begin{bmatrix} \sum_{x \in X} 2(\hat y_x - y_x) \\[1ex] \sum_{x \in X} 2(\hat y_x - y_x) x \end{bmatrix} $$

이처럼 전체 데이터셋을 대상으로 평균제곱오차의 그레이디언트를 활용하는 경사하강법을 배치 경사하강법이라 부른다.

경사하강법 활용

아래 코드의 linear_gradient() 함수는 특정 데이터 샘플 $x$에 대해 타깃(실제값) $y$와 예측치 $\hat y$ 사이의 MSE 함수의 그레이디언트를 계산한다.

변수 의미
intercept $\theta_0$ (절편)
slope $\theta_1$ (기울기)
predicted $\hat y$ (예측값)
error $\hat y - y$ (오차)
grad $x$에 대한 제곱오차의 그레이디언트

평균제곱오차는 이제 아래와 같이 지정된다.

LA.vector_mean([linear_gradient(x, y_x, theta) for x, y_x in list(zip(X, y))])

MSE() 함수의 최솟값 지점 찾기

이제 임의의 $\theta = (\theta_0, \theta_1)$로 시작한 후에 경사하강법으로 평균제곱오차의 최솟값 지점을 구할 수 있다.

아래 코드에서 사용한 학습률은 0.001이다.

참고: 에포크는 훈련 세트에 포함된 전체 훈련 샘플에 대해 예측값을 계산하는 것을 의미한다. 따라서 아래 코드는 MSE를 총 5,000번 계산할 때마다 $\theta_0, \theta_1$을 업데이트 한다.

그런데 최종 기울기가 11.xx 정도로 애초에 사용한 20과 차이가 크다. 이유는 학습률이 너무 낮아서 5000번의 에포크가 충북한 학습을 위해 부족했기 때문이다. 이에 대한 해결책은 보통 두 가지이다.

아래 코드는 먼저 에포크를 네 배 늘린 결과를 보여준다.

반면에 아래 코드는 학습률을 0.01로 키웠다.

위 결과를 비교했을 때 학습률을 키우는 것이 보다 효과적이다. 최종적으로 구해진 기울기와 절편을 이용하여 처음에 주어진 데이터의 분포를 선형적으로 학습한 1차함수의 그래프는 다음과 같다.

5. 배치/미니배치/확률적 경사하강법

앞서 사용한 배치 경사하강법은 주어진 데이터셋 전체를 대상으로 평균제곱오차의 그레이디언트를 계산하였다. 이런 방식을 배치 경사하강법이라 부른다.

주의: 배치(batch)는 원래 하나의 묶음을 나타내지만 여기서는 주어진 (훈련) 데이터셋 전체를 가리키는 의미로 사용된다.

그런데 사용된 데이터셋의 크기가 100이었기 때문에 계산이 별로 오래 걸리지 않았지만, 데이터셋이 커지면 그러한 계산이 매우 오래 걸릴 수 있다. 실전에서 사용되는 데이터셋의 크기는 몇 만에서 수십억까지 다양하며, 그런 경우에 적절한 학습률을 찾는 과정이 매우 오래 걸릴 수 있다.

데이터셋이 매우 큰 경우에는 따라서 아래 두 가지 방식 중 하나를 사용할 것을 추천한다.

미니배치 경사하강법

미니매치 경사하강법(mini-batch gradient descent)은 전체 훈련 세트를 쪼갠 여러 개의 미니배치(작은 훈련 세트)에 대해 평균제곱오차의 그레이디언트를 계산하여 $\theta_0, \theta_1, \dots $ 를 업데이트한다.

예를 들어, 전체 데이터셋의 크기가 1000이고 미니배치의 크기를 10이라 하면, 배치 경사하강법에서는 하나의 에포크를 돌 때마다 한 번 MSE와 그레이디언트를 계산하였지만 미니배치 경사하강법에서는 10개의 데이터를 확인할 때마다 MSE와 그레이디언트를 계산한다. 즉,하나의 에포크를 돌 때마다 총 100번 기울기와 절편을 업데이트한다.

아래 코드에서 정의된 minibatches() 함수는 호출될 때마다 batch_size로 지정된 크기의 데이터 세트를 전체 데이터셋에서 선택해서 내준다.

데이터를 선택하는 방식은 다음과 같다.

이제 미니배치 경사하강법을 이전에 사용했던 데이터에 대해 적용한다. 학습률(learning_rate)을 0.001로 하면 학습이 제대로 이루어지지 않는다. 에포크 수를 키우거나 합습률을 크게 해야 한다.

학습률을 0.01로 키우면 좋은 결과가 나온다.

학습률을 0.01로 두고 에포크를 3000으로 늘려도 성능이 그렇게 좋아지지 않는다.

학습 과정을 살펴보면 기울기가 20.195 정도에서 더 이상 좋아지지 않는다. 이런 경우 특별히 더 좋은 결과를 얻을 수 없다는 것을 의미한다. 사실, 데이터셋을 지정할 때 가우시안 잡음을 추가하였기에 완벽한 선형함수를 찾는 것은 애초부터 불가능하다. 따라서 위 결과를 미니배치 경사하강법을 사용한 선형회귀로 얻을 수 있는 최선으로 볼 수 있다.

확률적 경사하강법(SGD)

확률적 경사하강법(stochastic gradient descent, SGD)은 미니배치의 크기가 1인 미니배치 경사하강법을 가리킨다. 즉, 하나의 데이터를 학습할 때마다 그레이디언트를 계산하여 기울기와 절편을 업데이트 한다.

아래 코드는 주어진 데이터셋을 대상로 SGD를 적용하는 방식을 보여준다. 학습률을 0.001로 했음에도 불구하여 1000번의 에포크를 반복한 후에 이전에 0.01을 사용했던 경우와 거의 동일한 결과를 얻는다는 것을 확인할 수 있다.

학습률을 0.01로 하면 결과가 오히려 좀 더 나빠진다.

경사하강법 비교

앞서 살펴본 것에 따르면 확률적 경사하강법의 성능이 가장 좋았다. 하지만 이것은 경우에 따라 다르다. 아래 그림은 각각의 방식에서 절편과 기울기가 학습되는 과정을 잘 보여준다.

사이킷런의 확률적 경사하강법 모델

사이킷런의 머신러닝 모델은 기본적으로 훈련 세트를 2차원 어레이로 요구하기에 아래처럼 먼저 차원을 추가한다.

주의사항: 타깃 어레이 y는 1차원 어레이어야 한다.

사이킷런 모델의 훈련방법은 fit() 메서드를 호출하는 방식으로 이루어진다.

훈련결과로 찾아낸 선형회귀 모델의 절편과 기울기는 다음과 같다.

기울기에 대한 훈련이 부족해보인다. 확률적 경사하강법을 사용하기에 수렴값 근처에서 많은 진동이 발생할 수 있기에 학습률을 보다 작게 해보자.

아래 코드는 학습률을 0.001로 지정한 후에 훈련한다.

기울기가 훨씬 덜 학습된 것으로 보인다. 즉, 학습률이 너무 적다. 그리고 경고창에서 알 수 있듯이 반복 훈련이 덜 되었다. 반복훈련의 기본값은 1,000번이다. 이를 5,000번으로 올려보자.

경고는 발생하지 않지만 여전히 훈련이 덜 되어 있다. 아무래도 반복횟수의 문제는 아닌 것 같다. 실제로 반복횟수를 50,000으로 올려도 별 변화가 없다.

결론적으로 학습률 0.001이 너무 작다. 따라서 학습률을 기본값인 0.01보다 좀 더 크게 해보자. 아래 코드는 0.03을 학습률로 사용한다.

반복훈련 횟수를 늘려도 별 변화가 없다. 앞서 언급한 대로 확률적 경사하강법은 수렴값 근처에서 심하게 진동하기 때문이다.

연습문제

넘파이 어레이를 활용하여 선형회귀 모델을 배치 경사하강법으로 구현하려 한다. 즉, 본문과는 달리 어레이 메서드를 다양하게 활용해야 한다.

먼저, 아래 코드는 100개의 샘플을 임의로 생성한다.

문제 1

산점도를 그리는 코드를 완성하라.

gradient_step() 함수는 매 에포크마다 절편과 기울기를 그레이디언트를 이용하여 지정된 학습율의 비율만큼 업데이트 한다. 넘파이 어레이를 활용한 연산임에 주의해야 한다.

문제 2

linear_gradient() 함수가 매 에포크마다 그레이디언트를 계산하도록 아래 코드를 완성하라. 단, 넘파이 어레이와 관련된 함수를 이용해야 한다.

문제 3

배치 경사하강법으로 선형회귀 모델의 절편과 기울기를 구하는 코드를 작성하라.

문제 4

학습률과 에포크를 어느 정도로 하는 것이 적당해 보이는가?

문제 5

데이터셋 Xy에 대해 미니배치 경사하강법을 이용하여 선형회귀 모델의 절편과 기울기를 구하라.

문제 6

데이터셋 Xy에 대해 확률적 경사하강법을 이용하여 선형회귀 모델의 절편과 기울기를 구하라.