머신러닝 Reboot - 개념 잡기 : 경사 하강법 1 - 특성의 scale


새롭게 시작하는 머신러닝 학습은 기존에 진행하던 학습에서 장애가 되었던 용어와 공식에 대한 몰이해를 극복하고자
진행하려고 한다. 다시 말해 직관적으로는 이해가 가지만 논리적으로 설명할 수없는 개념을 논리적으로 설명 가능하도록
정리해보고자 하는 것이다.


따라서 전체적으로 연관성을 가지고 이어지는 내용이라기 보다는 단편적인 용어의 정의나 공식의 풀이를 중심으로 
하면서 관련 내용을 정리하는 방식으로 진행이 될 것이다. 


이렇게 정리할 대상은 주로 ’핸즈온 머신러닝’이라는 책을 읽으면서 이해가 안가는 부분들을 대상으로 풀이할 것이며
전체적인 순서 역시 ‘핸즈온 머신러닝’의 목차를 따를 것이다. 


들어가는 말


지난 시간에는 선형 회귀 분석의 비용함수로부터 가중치(𝜽 또는 W)의 최솟값을 한방에 알아낼 수 있는 정규방정식
대해 알아보았다. 미분 등 복잡한 계산이 필요 없고 학습률같은 하이퍼파라미터를 관리할 필요가 없으며 또 빠른 예측이
가능하다는 장점이 있지만 특성 수가 늘어남에 따라 속도가 많이 느려지는 단점이 있었다.


오늘은 정규방정식의 단점을 해결할 수 있는, 다시 말해 특성 수에 관계 없이 일정 수준의 성능을 보장해주는 
경사하강법애 대한 내용 중 특성의 스케일에 대해 알아보려고 한다. 


경사하강법은 대체로 특성의 스케일에 민감한 것으로 알려져 있으며 일반적으로 아래 그래프로 그 사실을 설명한다.


핸즈온 머신러닝 발췌핸즈온 머신러닝 발췌

오늘은 경사하강법이 특성에 민감하다는 것을 예제 코드를 통해 조금 더 직관적으로 설명을 하고자 한다. 
사실 논리적으로 증명을 하고싶었으나 역시 나의 실력으로는 역부족이었다. 이 내용과 위의 그래프를 이해하지
못하여 이 포스팅을 준비하는데 무려 3주가 걸겼다…ㅠ.ㅠ


게다가 내가 그간 얼마나 공부를 설렁설렁 했는 지 이번 기회에 알게 되었다. 그동안 나는 선형회귀의 비용함수와 
경사하강법을 동일시 하여 생각했던 것이다. 서로 다른 함수를 동일하다고 생각하고 분석하고 있었으니 답이
나올리가 있나…-.- 겨우 최근에야 경사하강법은 특정 함수(특히 convex 함수)의 최적값을 찾아낼 수 있는
일반적인 알고리즘이라는 말을 이해하게 되었다.


다시 말해 경사하강법은 선형회귀의 비용함수 뿐만 아니라 볼록(또는 오목)한 그래프가 그려지는 함수라면 어떤
함수이든 그 최저점을 찾아낼 수 있는 방법이라는 것이다.


지금부터 코드를 통해 이 내용을 간단히 살펴보자. 너무 간단해서 들어가는 말보다 본문이 짧을지도…-.-


Python 코드로 보는 경사 하강법


이 내용의 원본 소스 출처는 다음과 같다.


https://github.com/shuyangsun/Cost-Function-Graph


이 원본 소스 중 non-convex 함수들에 대한 내용은 제거 하고 convex 함수에 대한 내용만을 남겨 확인해보았다.


일반적으로 특성이 2개인 함수까지는 시각화(그래프로 표현)할 수 있다. 이 부분은 내가 처음 머신러닝을 공부한다고
정리를 하면서 다항로지스틱으로 넘어갈 때 꽤나 답답해 했던 부분이기도 하다. 특성이 2개인 경우까지는 시각화가
가능해서 직관적으로 이해를 할 수 있었는데 특성이 3개 이상 되니 복잡한 수식만으로는 도무지 이해가 가지 않는
것이었다…ㅠ.ㅠ


여전히 특성이 3개 이상인 경우는 이해가 힘들기 때문에 오늘은 특성이 2개인 케이스를 대상으로 설명을 해보겠다.


이 코드에서 사용할 함수는 비용함수는 아니고 f(a,b) = a^2 + b^2 이라는 함수이다. 이 함수가 표현하는 범위를
3차원 그래프로 그려보고 그 범위 안에서 경사하강법이 어떤 경로로 최저점을 찾아가는지 보여주는 것이 아래의
코드이다.


import numpy as np
import matplotlib.pyplot as plt
import math
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D

def f(a,b):
	return a**2 + b**2

먼저 필요한 라이브러리들을 import하고 경사하강법을 통해 최저점을 찾아낼 대상 함수 f를 정의했다.


def gradient_descent(theta0, iters, alpha):
	history = [theta0] # to store all thetas
	theta = theta0     # initial values for thetas
	# main loop by iterations:
	for i in range(iters):
		# gradient is [2x, 2y]:
		gradient = [2.0*x for x in theta] #함수 f(x,y)의 미분
		# update parameters:
		theta = [a - alpha*b for a,b in zip(theta, gradient)]
		history.append(theta)
	return history

history = gradient_descent(theta0 = [-1.8, 1.6], iters =30, alpha = 0.03)


다음으로 경사하강법을 함수로 정의하고 호출하여 그 결과를 history에 저장을 한다. 경사하강법의 다음 스텝을 결정하는
일반식은 다음과 같으며 이를 python 코드로 구현한 것이다.




경사하강법을 구현한 함수는 파라미터로 특성의 초깃값과 반복 횟수 그리고 학습률을 전달받는다. 이 코드에서 초깃값은 각 특성의 max에 가까운 값으로 정했다.


# f(x,y) = x^2 + y^2 함수의 그래프 그리기 fig = plt.figure(figsize=(20, 15)) ax = fig.gca(projection='3d') #plt.hold(True) a = np.arange(-2, 2, 0.25) b = np.arange(-2, 2, 0.25) a, b = np.meshgrid(a, b) c = f(a,b) surf = ax.plot_surface(a, b, c, rstride=1, cstride=1, alpha=0.3, linewidth=0, antialiased=False, cmap='rainbow')



주석된 내용처럼 f 함수가 표현하는 함수의 범위를 3차원으로 그려주는 코드이다. 특성 a와 b 모두 -2부터 2 사이의 값을
가지며 0.25씩 증가 하도록 값을 주었다. 함수가 a^2 + b^2이기 때문에 최솟값 0부터 최댓값 8까지의 그릇 모양으로
그래프가 표시된다.


a = np.array([x[0] for x in history])
b = np.array([x[1] for x in history])
c = f(a,b)
ax.scatter(a, b, c, color="r"); 

print(c)

plt.xlabel('Feature A')
plt.ylabel('Feature B')

plt.axis('equal')

plt.show()


이제 마지막으로 이전에 그려진 그래프 내에서 경사하강법을 통해 산출한 위치를 표시해준다. 특성이 2개이기 때문에
각각의 특성에 경사하강법을 적용한 결과를 그래프에 그려보면 최종적으로 아래와 같은 그래프를 볼 수 있다.



이 때 a = np.arange(-2, 2, 0.25)의 범위를 a = np.arange(-10, 10, 0.25)로 늘리게 되면 그래프의 형태가
오목한 그릇 형태가 아닌 u자 모양으로 휘어진 판자의 형태가 된다.



이런 상황에서는 가중치의 초깃값이 커질 수 있고 초깃값이 커지면 최솟값을 찾는데 그만큼 더 시간이 오래 걸리게 되며 이는 곧 특성값의 스케일 차이가 크게 되면 경사하강법의 성능이 나빠지게 된다고 볼 수 있는 것이다. 또한 내가 제대로 
이해하고 있는지 모르겠으나 이 그래프 표현만 놓고 보면 단지 두 개의 특성간에 스케일의 차이가 있을 때 뿐만 아니라 
두 특성의 스케일이 동일하더라도 그 규모가 커지면(예를들어 a와 b의 범위가 모두 10인 경우와 모두 100인 경우)
이 때 역시 경사하강법의 성능이 나빠져 더 많은 횟수를 진행해야 최솟값에 가까워지게 된다.


정리


앞서도 말했지만 이 부분을 이해하기 위해 장장 3주 이상이 걸렸다. 그러다가 위의 python 코드를 발견했고 처음 코드를
실행해봤을 때는 ‘유레카’를 외쳤지만 지금 다시 찬찬히 살펴보는 과정에서는 또 수많은 의문들이 일어나면서 내가 
제대로 이해한 것이 맞는지 알 수 없게 되었다…ㅠ.ㅠ 일단 직관적으로 생각했을 때 작은 수를 계산할 때보다 큰 수를 
계산할 때 더 많은 자원이 필요한 것은 당연한 것이니 특성의 스케일이 크면 그만큼 연산이 오래 걸린다고 보면 될 것이나
역시 완전한 이해는 되지 못한다.


더 나은 해법을 찾다가 contour라는 등고선 형태의 그래프를 그리는 방법이 있다는 것을 알아냈고 이 그래프가 위에
언급한 핸즈온 머신러닝의 그래프와 유사해서 더 설명하기가 좋지 않을까 생각했으나 실제 코드에 적용하는 방법을
몰라 이번 포스팅에서는 다루지 못했다. 시간 날 때 다시 정리를 해봐야겠다.


다음 시간에는 경사하강법의 3가지 종류(배치 경사하강법, 확률적 경사하강법, 미니 배치 경사하강법)에 대해 간단하게
정리해보겠다.

블로그 이미지

마즈다

이미 마흔을 넘어섰지만 아직도 꿈을 좇고 있습니다. 그래서 그 꿈에 다가가기 위한 단편들을 하나 둘 씩 모아가고 있지요. 이 곳에 그 단편들이 모일 겁니다...^^