본문 바로가기
개발/파이썬, 머신 러닝

3.2 경사 하강법

by 카펀 2020. 10. 8.

*본 글은 "Do It! 정직하게 코딩하며 배우는 딥러닝 입문", 박해선 저, 이지스 퍼블리싱을 참고하여 작성하였습니다.
GitHub Link: 링크

 

경사 하강법이란?

3.1에서 다루었던 산점도

위에 그렸던 산점도에 가장 잘 맞는 직선을 그린다면, 왼쪽 아래에서 오른쪽 위로, 가운데를 지나는 직선을 그리게 될 것입니다.   

위는 입력 데이터 1개의 특성에 대한 직선을 설명한 것입니다. 하지만 저희가 다룰 특성은 총 10개죠. 11차원 그래프는 그릴 수도 없지만, 11차원 공간을 가로지르는 초평면 (hyperplane)은 상상하기도 어렵습니다.   

따라서 보통 특성 1, 2개 정도를 사용하여 2, 3차원 그래프로 데이터를 나타내게 됩니다. 이렇게 나타내게 되면 위에서 그러했듯 데이터에 대한 직관을 쉽게 얻을 수 있습니다. 낮은 차원에서 얻은 직관이 높은 차원으로 확장되는 일은 꽤 자주 있는 편이므로, 이렇게 특성을 1, 2개 골라서 시각화 하는 경우가 많습니다.  

 

3.1에서 다룬 선형 회귀는, 산점도 그래프를 가장 잘 나타내는 직선 y = ax + b의 기울기 a와 절편 b를 찾는 것이 목표였습니다. 이를 위한 방법 중의 하나가 바로 경사 하강법입니다. 경사 하강법은 모델이 데이터를 잘 표현할 수 있도록 기울기(변화율)을 활용하여 모델을 조금씩 조정하는 최적화 알고리즘입니다.

 

예측값과 변화율

앞에서 언급한 y = ax + b 를 딥 러닝에서 주로 사용하는 표현대로,

ŷ = wx + b 라고 하겠습니다. 이때 ŷ는 y-hat이라고 읽습니다.

기존의 식과 비교해 보면,

  • 기울기 a는 가중치를 의미하는 w 또는 계수를 의미하는 θ로 나타냅니다.
  • ŷ는 우리가 예측한 값, 즉 예측값을 의미합니다.

이에 반해, 기존의 y는 데이터 값으로써의 y를 의미합니다. (좌표 x, y값)

 

예측값이란, 우리가 가지고 있는 식과 입력 데이터를 이용하여, 그 결과를 예측한 값을 의미합니다.

예를 들어서, 현재 모델 ŷ = 3x + 5 라는 모델을 저희가 가지고 있다고 합시다. 이 모델의 x값에 3을 넣으면 ŷ = 14 라는 값을 얻게 됩니다. 이는 실제 y값과 같을 수도 있고, 다를 수도 있습니다. 즉, x값을 이용하여 예측값 ŷ를 얻었다고 할 수 있습니다.

 

올바른 모델 찾기

정확한 예측값을 얻기 위해서 가장 중요한 두 변수는 w와 b입니다. 올바른 모델을 얻기 위해 진행하는 과정은 다음과 같습니다.

  1. 무작위로 모델 만들기: 무작위로 w와 b값을 정합니다.
  2. 무작위로 모델 예측하기: x에서 샘플 하나를 선택하여 ŷ를 계산합니다.
  3. 예측한 값과 정답 비교하기: ŷ와 선택한 샘플의 진짜 y값을 비교합니다. 틀릴 확률이 99%.
  4. 모델 조정하기: ŷ가 y와 더 가까워지도록 w, b값을 적절히 조절합니다.
  5. 2~4 과정을 반복합니다.

여기서 가장 중요한 단계는 4번입니다.

w, b값의 조절은 딥 러닝의 핵심이라고 할 수 있지만, 적절히 조절한다는 것이 어떤 의미일까요?

모델이 단순한 일차식이라면 어렵지 않게 조절할 수 있겠지만, 우리가 다룰 데이터들은 특성이 대개 여러 개입니다. 즉, 이를 체계적으로 조정할 방법이 필요합니다.

 

훈련 데이터에 맞는 w와 b 찾아보기

w와 b의 조정에 따른 ŷ의 변화를 알아보는 가장 쉬운 방법은 직접 계산해 보는 것입니다.

단순한 예를 이용해 보겠습니다.

(이하는 GitHub 링크를 따라가 보시면 코드 수행 결과를 보실 수 있습니다. 여기서는 간단하게 적었습니다.)

w = 1.0
b = 1.0

w, b를 1.0으로 초기화합니다.

이를 이용하여, 첫 번째 샘플 x[0]에 대한 ŷ를 계산해 봅니다.

y_hat = x[0] * w + b
print(y_hat)
#1.0616962065186886

그 다음, 샘플 x[0]에 대응하는 타깃값 y[0]의 값을 출력하고, ŷ와 비교해 봅니다.

print(y[0])
#151.0

우리가 예측한 ŷ는 1.06, 실제 y[0]는 151입니다. 차이가 큰데, w와 b를 무작위로 정했으니 당연한 결과입니다.

이제 이 결과를 이용하여 w와 b값을 개선해 보려고 합니다.

목적은 ŷ가 y[0]의 값에 가까워지는 것입니다. 이을 위해, w를 0.1만큼 증가시킨 후, ŷ의 증가량 y_hat_inc 를 얻어 보겠습니다.

w_inc = w + 0.1
y_hat_inc = x[0] * w_inc + b
print(y_hat_inc)
#1.0678658271705574

새로 얻은 y_hat_inc 의 값을 보니, 이전의 ŷ에 비하여 살짝 증가하였음을 확인할 수 있습니다.

이를 이용하여, w의 변화율을 계산해 보고자 합니다.

w의 변화율 w_rate 는 ŷ의 변화량을 w의 변화량으로 나눈 값입니다. 코드로 나타내면,

w_rate = (y_hat_inc - y_hat) / (w_inc - w)
print(w_rate)
#0.061696206518688734

w_rate의 값으로 0.0616... 을 얻었습니다.

여기서 수식적으로 짚고 넘어가야 할 부분이 있습니다.

다음을 이용합시다:

  • y_hat_inc = x[0] * w_inc + b
  • y_hat = x[0] * w + b

w_rate = (y_hat_inc - y_hat) / (w_inc - w),

w_rate = ((x[0] * w_inc + b) - (x[0] * w + b)) / (w_inc - w),

w_rate = x[0](w_inc - w) + b - b / (w_inc - w),

w_rate = x[0]

즉, w의 변화율 w_rate는 x[0]과 동일한 값을 가지고 있습니다.

 

변화율로 가중치 업데이트하기

앞서 언급하였듯 선형 회귀의 목표는 y에 가까운 ŷ를 출력하는 w와 b를 찾는 것입니다.

앞서 구한 변화율 w_rate를 이용하여 w와 b를 업데이트할 수 있습니다. 먼저 w를 업데이트 하는 방법에 대해 다루어 보겠습니다.

 

변화율이 양수일 때

변화율이 양수일 때, 즉 ŷ는 y보다 값이 작습니다. 따라서 ŷ값을 늘려야 하는 상황입니다.

이 때는 w값을 늘리면 ŷ도 증가합니다. 변화율이 양수인 점을 이용하여, w에 변화율을 더하는 (w+ w_rate) 방법으로 w를 증가시킬 수도 있습니다.

 

변화율이 음수일 때

변화율이 음수일 때, w가 증가하면 ŷ는 감소합니다. 이 말은 반대로, ŷ가 증가하려면 w가 감소해야 합니다.

이 때 변화율이 음수이므로, w + w_rate 는 w값이 감소하는 결과가 되겠습니다.

 

위 경우를 정리하면, 변화율의 부호에 관계 없이 w + w_rate를 이용하면 w값을 조정할 수 있습니다. 다음은 가중치를 업데이트한 예입니다.

w_new = w + w_rate
print(w_new)
#1.0616962065186888

변화율로 절편 업데이트하기

가중치의 경우와 같이, 절편도 변화율을 구한 후, 이를 이용하여 b값을 업데이트 하겠습니다.

b를 0.1만큼 증가시킨 후, ŷ가 얼마나 증가하는지 보고 변화율을 계산해 봅시다.

b_inc = b + 0.1
y_hat_inc = x[0] * w + b_inc
print(y_hat_inc)
#1.1616962065186887

b_rate = (y_hat_inc - y_hat) / (b_inc - b)
print(b_rate)
#1.0

b를 0.1만큼 증가시키면 ŷ는 약 1.16만큼 증가하는 것을 확인할 수 있습니다.

또, b의 변화율은 1.0이라는 값이 나옵니다.

1차 함수 그래프로 생각해 보면, b값이 1 증가하면 y값도 1만큼 증가할 테니, 당연한 결과라고 할 수 있습니다.

따라서 b를 업데이트하기 위해서는 변화율 1을 더하면 됩니다.

b_new = b + 1
print(b_new)
#2.0

위 과정대로 w와 b의 값을 조정할 수 있습니다.

이 방법이 가장 완벽한 방법이면 좋겠지만, 아쉽게도 위 방법에는 두 가지 단점이 존재합니다.

  • ŷ가 y에 한참 미치지 못하는 값인 경우, w와 b를 더 큰 폭으로 수정할 수 없습니다 (앞에서 변화율만큼 수정했지만, 특별한 기준을 정하기 어렵습니다).
  • ŷ가 y보다 커지면 ŷ를 감소시키지 못합니다.

이를 해결하기 위해, w와 b를 더 능동적으로 업데이트 하는 방법을 알아봅니다.

오차 역전파

오차 역전파 (backpropagation)는 ŷ와 y의 차이를 이용하여 w와 b를 업데이트 합니다.

이름에서 알 수 있듯, 이 방법은 오차가 연이어 전파되는 모습으로 수행됩니다.

(아쉽게도 본 예제는 매우 간단하여 오차가 전파되는 모습이 잘 나타나지 않았습니다.)

 

이번에는 오차량 y - ŷ을 변화율에 곱하는 방법으로 w를 업데이트 해 보겠습니다. 이 경우에는 ŷ와 y값의 차이가 큰 경우 w와 b를 많이 바꿀 수 있습니다. 또, ŷ가 y를 지나치는 경우 w와 b의 방향도 바꾸도록 해 보겠습니다.

 

1. 오차와 변화율을 곱하여 가중치 업데이트 하기

앞의 경우와 마찬가지로, x[0]일 때를 기준으로 살펴 보겠습니다. 오차 err을 구한 후, w와 b의 변화율에 err을 곱하여 w_new와 b_new를 출력해 보겠습니다.

err = y[0] - y_hat
w_new = w + w_rate * err
b_new = b + 1 * err
print(w_new, b_new)
#10.250624555904514 150.9383037934813

아까에 비해 w_new, b_new 값이 크게 변화된 것을 확인할 수 있습니다.

 

2. 두 번째 샘플을 이용하여 w, b값 갱신하기

다음은 x[1]을 이용해서 w, b값을 새로 계산해 보겠습니다.

앞에서 다룬 바에 의하면, w_rate = x[i] 이므로, 이를 그대로 사용도록 합니다.

y_hat = x[1] * w_new + b_new
err = y[1] - y_hat
w_rate = x[1]
w_new = w_new + w_rate * err
b_new = b_new + 1 * err
print(w_new, b_new)
#14.132317616381767 75.52764127612664

w는 4만큼 커지고, b는 약 절반만큼 줄어든 결과를 확인할 수 있습니다.

 

3. 전체 샘플을 이용하여 반복

위와 같은 방법을 이용하여 모든 샘플을 사용해 w와 b를 업데이트합니다.

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)
#587.8654539985689 99.40935564531424

파이썬의 zip() 함수는 여러 개의 array에서 동시에 element를 하나씩 꺼내 줍니다.

위의 코드에서는 입력값 x와 타깃값 y를 하나씩 꺼내어 err을 계산하고, 이를 이용하여 w와 b를 계산했습니다.

for문 내의 코드는 이전과 동일함을 확인할 수 있습니다.

 

4. 얻은 모델에 대한 시각화

이전까지의 과정을 통해 얻어낸 모델은 ŷ = 587.9x + 99.4 입니다.

이 모델이 우리가 가진 데이터를 얼마나 잘 표현하는지 어떻게 알 수 있을까요?

맨 처음에 저희가 다룬 산점도 위에, 모델을 그려 보면 쉽게 파악할 수 있습니다.

matplotlib을 이용하여 그래프를 그려서 나타내 보도록 하겠습니다.

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()

위 코드에 대한 출력 결과

직선의 모양이 어떤가요?

만족스럽지는 않지만 대체로 산점도의 형태에 따라 가는듯한 모양입니다.

어떻게 하면 이를 더 정확하게 수정할 수 있을까요?

 

5. 여러 에포크 (epoch)를 반복하기

보통 경사 하강법에서는 주어진 훈련 데이터로 학습을 여러 번 반복한다고 합니다.

이렇게 전체 훈련 데이터를 이용하여 한 단위의 작업을 수행하는 것을 에포크 (epoch) 라고 부릅니다.

일반적으로 수십~수천 번의 에포크를 반복한다고 합니다.

 

이를 위해서 앞의 코드에 for문을 씌워 보도록 하겠습니다.

100번의 에포크를 반복하면서 직선이 어떻게 이동하는지 살펴봅시다.

for i 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)
#913.5973364345905 123.39414383177204

100번의 에포크를 반복하여 얻은 결과는 w = 913.6, b = 123.4 정도입니다.

이를 아까처럼 산점도 위에 나타내어 보면 다음과 같습니다.

 

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()

위 코드에 대한 출력 결과

아까보다 산점도를 더 잘 표현하는것 같지 않나요?

이 데이터에 잘 맞는 머신러닝 모델을 찾았습니다.

ŷ = 913.6x + 123.4

 

6. 모델로 예측하기

위에서 얻은 모델을 이용하여, 데이터를 예측할 수 있습니다.

x = 0.18을 이용하여 모델에 투입해 보면 ŷ값을 얻을 수 있습니다.

x_new = 0.18
y_pred = x_new * w + b
print(y_pred)
#287.8416643899983

이 데이터가 적당한지를 확인해 보려면, 역시 기존의 산점도와 함께 나타내 보면 됩니다.

plt.scatter(x,y)
plt.scatter(x_new, y_pred)
plt.xlabel('x')
plt.ylabel('y')
plt.show()

위 코드에 대한 출력 결과

주황색 점이 새로 예측한 데이터 값입니다.

어느 정도 잘 예측이 된 것 같네요!

'개발 > 파이썬, 머신 러닝' 카테고리의 다른 글

3.1 선형 회귀  (0) 2020.10.07
MatPlotLib 라이브러리 소개  (0) 2020.10.05
Pandas 라이브러리 소개  (0) 2020.10.04
NumPy 라이브러리 소개  (0) 2020.10.03

댓글