[Do it!] 1. 머신러닝 기초 - 수치 예측
이 포스트는 Do it! 정직하게 코딩하며 배우는 딥러닝 입문 pp.46~74를 참고하였습니다.
선형 회귀란?
위키 백과에서는 다음과 같이 설명되어 있습니다.
- 통계학에서, 선형 회귀(線型回歸, 영어: linear regression)는 종속 변수 y와 한 개 이상의 독립 변수 (또는 설명 변수) x와의 선형 상관 관계를 모델링하는 회귀분석 기법이다. 한 개의 설명 변수에 기반한 경우에는 단순 선형 회귀, 둘 이상의 설명 변수에 기반한 경우에는 다중 선형 회귀라고 한다
말이 어려운데, 요약하자면 어떤 데이터를 대표할 수 있는 선형 그래프를 찾는 것이라고 보면 됩니다.
다음과 같은 데이터를 보겠습니다.
위와 같은 데이터가 있을 때, 위 데이터를 가장 잘 나타낼 수 있는 1차 함수는 무엇일까요?
우리는 직관적으로 가운데 그래프가 위 데이터를 가장 잘 나타내는 그래프라고 생각할 수 있습니다. 하지만 컴퓨터로 위 그래프를 찾으려면 어떻게 해야할까요? 그리고 만약 데이터가 이렇게 시각화 할 수 있는 2차원,3차원을 넘는 그 이상의 차원의 데이터를 대표하는 직선을 알고싶다면?
선형 회귀의 목표는 이렇게 입력 데이터 $x$와 타깃 데이터 $y$를 통해 데이터를 가장 잘 나타낼 수 있는 기울기 $a$와 절편 $b$, 즉 직선의 방정식을 찾는 것입니다.
나중에 설명할 경사 하강법(gradient descent)는 이런 직선의 방정식을 찾는 방법 중 하나입니다.
데이터 확인하기
이번 챕터에서 우리가 할 일은, 당뇨병 환자의 데이터를 통해서 그 환자의 병이 1년 후 얼마나 진전되었는가를 예측하는 모델을 만드는 것입니다. 따라서 입력 데이터는 환자에 대한 데이터일 것이고, 출력은 병이 얼마나 진전되었는지를 나타내는 값일 것입니다.
다음 코드로 당뇨병 환자에 대한 데이터를 불러오고, 데이터의 구조를 확인해봅니다.(line1은 주피터 노트북에서 matplotlib을 사용하는 경우에만 입력하세요.)
%matplotlib inline
from sklearn.datasets import load_diabetes
diabetes = load_diabetes()
print(diabetes.data.shape, diabetes.target.shape)
위 코드를 통해 diabetes.data은 442x10, diabetes.target은 442x1 의 크기임을 알 수 있습니다.
위 데이터를 그림으로 나타내면 다음과 같습니다.
여기서 행은 샘플이고, 열은 샘플의 특성을 나타냅니다. 즉, 이 데이터에는 당뇨병 환자 442명에 대한 데이터가 들어있습니다. 각 행은 각각의 환자에 대응되고, 각 열은 환자에 대한 특성에 대응됩니다. 특성 중에서는 환자의 혈압, 혈당, 몸무게, 키 등 10개의 특성이 있습니다.
위 데이터 중 앞 부분 3개의 입력 샘플(환자)만 출력해서 확인해보겠습니다.
diabetes.data[0:3]
이번엔 앞 부분 3개의 출력 데이터를 확인해보겠습니다.
첫 번째 입력(샘플)에 대응되는 출력은 151, 두 번째 입력에 대응되는 출력은 75, 세 번째 입력에 대응되는 출력은 141입니다. 즉, 각 환자에 대한 병의 진전도가 151, 75, 141이라는 것입니다.
예측값과 변화율
우리가 과학분야를 공부할 때는 특정한 값을 나타낼 때, 어떤 기호를 쓸지에 대한 약속이 있습니다. 예를 들어 속도는 $v$, 질량은 $M$이라고 하는 것과 마찬가지로, 딥 러닝에서는 직선의 방정식 $y = ax + b$에서 기울기 $a$는 $w$ 또는 $\theta$로 표기하며, $y$는 $\hat{y}$으로 표기합니다. 즉, 위 식을 딥 러닝에서의 방식으로 나타내면, $\hat{y} = wx + b$가 되는 것입니다.
위 그림의 직선은 $\hat{y} = wx + b$로 나타낼 수 있습니다. 이 직선의 방정식에서 입력값 $x$에 대해 나오는 출력이 $\hat{y}$이며, 이를 예측값이라고 합니다. 이 그림에서, 만약 $x = 0.15$인 경우, $\hat{y} = 300$ 정도로 예측할 수 있는 것입니다.
컴퓨터는 이런 데이터를 이용하여 어떻게 저런 적절한 방정식을 찾을까요? 컴퓨터는 처음에 무작위로 아무 직선이나 하나 만들어 보고, 모든 데이터들과 하나하나 비교해보며 수정해가면서 오차가 가장 적은 적절한 방정식을 찾습니다.
수십년 전, 컴퓨팅 파워가 그렇게 높지 않았을 때는 이런 방법이 불가능했지만, 지금은 GPU와 CPU의 성능이 매우 발달했기 때문에 어떻게 보면 이런 무식한(?) 방법으로 적절한 예측값을 찾을 수 있는 것입니다.
훈련 데이터 $x$와 $y$에 잘 맞는 $w$와 $b$를 찾는 방법은 다음과 같습니다.
1. 무작위로 $w$와 $b$를 초기화 합니다.
2. $x$에서 샘플 하나를 선택하여 $\hat{y}$를 계산합니다.
3. 예측값 $\hat{y}$과 실제값 $y$를 비교합니다.
4. 예측값과 실제값의 오차가 줄어들도록 $w$와 $b$를 조정합니다.
5. 모든 샘플을 처리할 때까지 2~4를 반복합니다.
이제, 위의 방법을 코드를 통해 직접 확인해보겠습니다.
1. 무작위로 $w$와 $b$ 초기화하기
이번 예시에서는 $w$와 $b$를 1로 초기화 해보겠습니다. 실제로는 정규분포나 기타 다른 규칙을 통해 정말 무작위로 초기화하지만, 이번엔 간단하게 예를 들기 위해 1로 초기화 해보겠습니다.
w = 1.0
b = 1.0
2. $x$에서 샘플 하나를 선택하여 $y$를 계산하기. (이번 예시에서는 그래프의 시각화를 위해 환자 데이터에서 3열의 특성만 사용합니다.)
x = diabetes.data[:, 2]
y = diabetes.target
y_hat = x[0]*w + b
print(y_hat)
3. 예측값 $\hat{y}$과 실제값 $y$를 비교하기.
print(y[0])
4. 예측값과 실제값의 오차가 줄어들도록 $w$와 $b$를 조정하기.
$\hat{y}$과 타겟 값 $y$를 비교했을 때, 효과적으로 $w$와 $b$를 조절하는 방법 중 하나는 오차에 비례해서 값을 조정하는 것입니다. PID 제어에서 P항에 해당되는 부분을 생각해보세요.
line 2에서 오차에 x[0]을 곱하는 부분은 일단 지금은 넘어갑니다. 이 부분은 나중에 경사 하강법에서 자세히 설명합니다. 일단 지금은 오차에 비례하여 $w$와 $b$를 조정한다는 것만 알아두세요.
err = y[0] - y_hat
w_new = w + x[0]*err
b_new = b + 1*err
print(w_new, b_new)
위 결과, $w$는 1에서 10.25로, $b$는 150.94로 조정된것을 확인할 수 있습니다.
이렇게 조정된 값을 가지고, 다시 예측값과 비교를 해보겠습니다.
y_hat = x[0]*w_new + b_new
print(y[0])
print(y_hat)
처음에는 예측값과 실제값이 150정도 차이났는데 이젠 거의 비슷해졌네요. 하지만, 이렇게 첫 번째 샘플만으로 계산한 값을 다른 샘플에도 적용하면 어떻게 될까요?
y_hat = x[1]*w_new + b_new
print(y[1])
print(y_hat)
이번엔 75정도로 차이가 꽤 크게 납니다. 따라서, $w$와 $b$를 다시 업데이트 해줘야 합니다.
5. 모든 샘플을 처리할 때까지 2~4를 반복합니다.
이번엔 두 번째 샘플을 사용하여 $w$와 $b$를 조정해보겠습니다.
y_hat = x[1]*w_new + b_new
err = y[1] - y_hat
w_new = w_new + x[1]*err
b_new = b_new + 1*err
print(w_new, b_new)
오차가 또 줄었네요. 이런 방식으로 모든 샘플에 대해 적용합니다.
for x_i, y_i in zip(x, y):
y_hat = x_i * w + b
err = y_i - y_hat
w_rate = x_i
w = w + w_rate*err
b = b + 1*err
print(w, b)
모든 샘플에 대해 계산하여 최종적으로 얻어진 $w$와 $b$는 위와 같습니다.
이제, 위 값을 통해 얻어진 직선을 그려보겠습니다.
어느정도 데이터를 잘 나타내는 것 같지만, 그렇게 썩 좋아보이지는 않습니다. 이 경우, 위의 모든 과정을 또다시 반복하면 결과가 더 좋아집니다(많이 반복했다고 항상 결과가 더 좋아지는 것은 아닙니다!). 이렇게 전체 훈련 데이터를 모두 이용하여 한 단위의 작업을 진행하는 것을 에포크(epoch)라고 합니다.
for _ in range(1, 100):
for x_i, y_i in zip(x, y):
y_hat = x_i * w + b
err = y_i - y_hat
w_rate = x_i
w = w + w_rate*err
b = b + 1*err
print(w, b)
plt.scatter(x, y)
pt1 = (-0.1, -0.1*w + b)
pt2 = (0.15, 0.15*w + b)
plt.plot([pt1[0], pt2[0]], [pt1[1], pt2[1]])
plt.xlabel('x')
plt.ylabel('y')
plt.show()
100번의 반복으로 얻은 $w$와 $b$는 약 913.6, 123.4 입니다. 따라서, 위 데이터를 잘 나타내는 직선의 방정식은 $\hat{y} = 913.6x + 123.4$입니다.
만들어진 모델을 통해 새로운 데이터 예측하기
이제, 기존 환자 데이터에 있던 값 말고 새로운 데이터가 발생했다고 가정하고, 이 데이터의 예측 값을 확인해봅니다.
x_new = 0.18
y_pred = x_new*w + b
print(y_pred)
plt.scatter(x, y)
plt.scatter(x_new, y_pred)
plt.xlabel('x')
plt.ylabel('y')
plt.show()
손실 함수와 경사 하강법
경사 하강법을 기술적인 말로 하면, "어떤 손실 함수(loss function)가 정의되었을 때, 손실 함수의 값이 최소가 되는 지점을 찾아가는 방법" 입니다. 비용 함수(cost function) 또는 목적 함수(object function)라고도 하는데, 같은 의미로 보시면 됩니다. 아무튼, 중요한 점은 예측값을 원본 데이터와 비교했을 때 그 오차를 손실 또는 비용 함수로 나타내며, 이 값이 가장 작아지는 지점의 값을 찾는 것입니다.
지금까지 설명한 방법에서는 "제곱 오차"라는 손실 함수를 사용했었습니다. 이는 타깃 값에서 예측 값을 빼고, 이를 제곱한 것입니다.
$SE = (y - \hat{y})^2$
위의 제곱 오차는 $w$와 $b$에 대한 식으로 나타낼 수 있습니다($\hat{y} = wx + b$이기 때문입니다). 따라서, 제곱 오차를 최소로 만들어주는 $w$와 $b$ 값을 찾으면, 데이터를 잘 나타낼 수 있는 직선을 찾을 수 있게 되는 것입니다. 제곱 오차와 직선의 관계를 나타낸 그림은 다음과 같습니다.
제곱 오차 함수의 최솟값을 구하기 위해서는, 제곱 오차 함수의 기울기를 따라서 함수의 값이 낮은 쪽으로 이동하면 됩니다.
위와 같은 그래프를 생각해보겠습니다. 만약 기울기가 음수라면, 즉 $w < w_0$라면, $w$값이 증가해야 손실 함수를 최소로 만드는 $w_0$로 다가갈 수 있습니다. 만약 기울기가 양수라면, 즉 $w > w_0$라면, $w$값이 감소해야 손실 함수를 최소로 만드는 $w_0$로 다가갈 수 있습니다.
따라서, 기울기가 음수인 경우에는 $w$값이 증가해야 하므로 음수인 기울기를 빼면 $w_0$로 다가갈 수 있게 되고, 기울기가 양수일 때는 $w$값이 감소해야 하므로 양수인 기울기를 빼면 $w_0$로 다가갈 수 있게 됩니다. 즉, 그냥 제곱 오차의 기울기를 빼면 됩니다.
그러면 ,이제 $w$에 대한 제곱 오차의 기울기를 구하기 위해 해당 식을 미분해보겠습니다($\hat{y} = wx + b$임을 기억하세요)
$\frac{\partial SE}{\partial w} = \frac{\partial}{\partial w}(y-\hat{y})^2 = 2(y-\hat{y})(-\frac{\partial}{\partial w}\hat{y}) = 2(y-\hat{y})(-x) = -2(y-\hat{y})x$
만약 여기서 제곱 오차의 계수가 $\frac{1}{2}$이라면 제곱 오차의 미분값의 상수항이 -1로 되면서 좀 더 깔끔하게 보이므로, 보통 제곱 오차는 $\frac{1}{2}(y-\hat{y})$로 정의합니다. 이에 따라 미분값도 $-(y-\hat{y})x$를 사용합니다.
앞에서 언급했듯이 미분된 값을 $w$에서 빼야한다고 했으니, $w$값을 업데이트 하는 식은 다음과 같습니다.
$w = w - \frac{\partial SE}{\partial w} = w + (y-\hat{y})x$
이번에는 제곱 오차 함수를 같은 방식으로 $b$에 대해 미분해보겠습니다.
$\frac{\partial SE}{\partial b} = \frac{\partial}{\partial b}\frac{1}{2}(y-\hat{y})^2 = 2(y-\hat{y})(-\frac{\partial}{\partial b}\hat{y}) = (y-\hat{y})(-1) = -(y-\hat{y})$
따라서, 위의 식들은 다음 코드에 대응됩니다.
y_hat = x_i * w + b
err = y_i - y_hat
w_rate = x_i
w = w + w_rate*err
b = b + 1*err
인공지능 분야에서는 변화율 대신 그레이디언트(gradient)라는 용어를 사용합니다.
선형 회귀를 위한 뉴런(유닛)의 구현
신경망 알고리즘에서는 정방향 계산(forpass)와 역방향 계산(backprop)이 있습니다. 정방향 계산이란, 이전에 설명한 것처럼 $x$와 $w$를 곱하고, $b$를 더해 $\hat{y}$를 구하는 것입니다. 역방향 계산은, 이 반대로 $\hat{y}$로부터 역으로 계산하여(이전 예시와 같이 미분하면서 계산), 최종적으로 $w$와 이에 대한 기울기를 구하는 것입니다. 지금은 뉴런의 층이 적어서 그래이디언트가 역방향으로 전파되는 모습이 잘 보이지 않는데, 나중에 층이 많아지면 오차가 역전파되는 모습이 잘 보이게 됩니다. 아무튼, 그래이디언트의 역전파에서 가장 중요한 점은, 역전파의 목적이 우리가 학습하려고 하는 파라미터인 $w$와 $b$에 대한 기울기를 구하려는 것임을 꼭 기억하세요.
정방향 계산을 그림으로 나타내면 다음과 같습니다.
역방향 계산의 그림은 다음과 같습니다.
이제, 위 내용들을 파이썬 코드로 작성해보겠습니다.
Neuron 클래스의 전체 코드는 다음과 같습니다.
class Neuron:
def __init__(self):
self.w = 1.0
self.b = 1.0
def forpass(self, x):
y_hat = x*self.w + self.b
return y_hat
def backprop(self, x, err):
w_grad = -err*x
b_grad = -err*1
return w_grad, b_grad
def fit(self, x, y, epochs=100):
for i in range(epochs):
for x_i, y_i in zip(x, y):
y_hat = self.forpass(x_i)
err = y_i - y_hat
w_grad, b_grad = self.backprop(x_i, err)
self.w -= w_grad
self.b -= b_grad
1. __init__() 메서드 작성하기
class Neuron:
def __init__(self):
self.w = 1.0
self.b = 1.0
Neuron 클래스를 만들 때, $w$와 $b$를 1로 초기화합니다.
2. 정방향 계산 만들기
def forpass(self, x):
y_hat = x*self.w + self.b
return y_hat
정방향 계산 식인 $y = wx + b$를 구현한 부분입니다.
3. 역방향 계산 만들기
def backprop(self, x, err):
w_grad = -err*x
b_grad = -err*1
return w_grad, b_grad
편미분을 통해 얻은 기울기 계산을 구현한 부분입니다.
4. 훈련을 위한 fit() 메서드 구현하기
def fit(self, x, y, epochs=100):
for i in range(epochs):
for x_i, y_i in zip(x, y):
y_hat = self.forpass(x_i)
err = y_i - y_hat
w_grad, b_grad = self.backprop(x_i, err)
self.w -= w_grad
self.b -= b_grad
forpass()를 호출하여 예측값을 구하고, 그 후 오차를 계산합니다. 이렇게 계산한 오차를 통해 역전파로 가중치 $w$와 절편의 기울기를 구하고, 이 기울기를 사용하여 해당 값들을 업데이트 합니다. 그리고, 이 과정을 100번 반복합니다.
5. 모델을 훈련하고, 훈련 결과 확인하기
neuron = Neuron()
neuron.fit(x, y)
plt.scatter(x, y)
pt1 = (-0.1, -0.1*neuron.w + neuron.b)
pt2 = (0.15, 0.15*neuron.w + neuron.b)
plt.plot([pt1[0], pt2[0]], [pt1[1], pt2[1]])
plt.xlabel('x')
plt.ylabel('y')
plt.show()