미분의 개념에 대한 간단한 복습 이후 경사하강법을 통해 선형회귀 계수를 구하는 과정을 살펴보았다. 그리고 실제 적용에서 더 많이 쓰이는 확률적 경사하강법(SGD)에 대해 다루었다.
오늘 배운 내용은 아래와 같다.
미분에 대한 개념을 간단히 복습하고 경사하강법에 대해 다루었다.
sympy.diff
로 계산할 수 있다.
sympy
는 symbolic mathematics를 위한 python 라이브러리의 약자이다.
#diff.py
import sympy as sym
from sympy.abc import x
sym.diff(sym.poly(x**2 + 2*x + 3), x)
# Poly(2𝑥+2,𝑥,𝑑𝑜𝑚𝑎𝑖𝑛=ℤ)
미분값을 더하면 경사상승법이라고 하며, 함수의 극댓값을 구할 때 사용한다.
반대로 미분값을 빼면 경사하강법이라고 하며, 함수의 극솟값을 구할 때 사용한다.
각각의 경우를 그래프로 직접 그려보면 왜 각각에서 미분값을 더하고 빼는지 쉽게 알 수 있다.
#gradient_descent_pseudo_code.py
# Input: gradient, init, lr, eps, Output: var
# gradient: 도함수
# init: 초기값, lr(learning rate): 학습률
# eps(epsilon): 알고리즘 종료 조건 (0에 가까운 값)
var = init
grad = gradient(var)
while abs(grad) > eps:
var = var - lr * grad
grad = gradient(var)
#gradient_descent.py
import numpy as np
import sympy as sym
from sympy.abc import x
def func(val):
fun = sym.poly(x**2 + 2*x + 3)
return fun.subs(x, val), fun
def func_gradient(fun, val):
_, function = fun(val)
diff = sym.diff(function, x)
return diff.subs(x, val), diff
def gradient_descent(fun, init_point, lr_rate=1e-2, epsilon=1e-5):
cnt = 0
val = init_point
diff, _ = func_gradient(fun, init_point)
while np.abs(diff) > epsilon:
val = val - lr_rate * diff
diff, _ = func_gradient(fun, val)
cnt += 1
print(f"함수: {fun(val)[1]}, cnt: {cnt}")
print(f"최소점: ({val}, {func(val)[0]})")
gradient_descent(fun=func, init_point=np.random.uniform(-2, 2))
# 함수: Poly(x**2 + 2*x + 3, x, domain='ZZ'), cnt: 535
# 최소점: (-1.00000496557233, 2.00000000002466)
각 변수별로 편미분을 계산한 그래디언트 벡터(gradient vector)를 경사하강법에 이용할 수 있다.
좌변의 뒤집어진 delta 기호를 nabla라고 부른다.
#multivariate_gradient_descent_pseudo_code.py
# Input: gradient, init, lr, eps, Output: var
# gradient: gradient vector
# init: 초기값, lr(learning rate): 학습률
# eps(epsilon): 알고리즘 종료 조건
var = init
grad = gradient(var)
while norm(grad) > eps:
# 조건문의 grad에 절댓값 대신 norm이 붙었다는 것이 차이점
var = var - lr * grad
grad = gradient(var)
#multivariate_gradient_descent.py
import numpy as np
import sympy as sym
from sympy.abc import x, y
def eval_(fun, val):
val_x, val_y = val
fun_eval = fun.subs(x, val_x).subs(y, val_y)
return fun_eval
def func_multi(val):
x_, y_ = val
func = sym.poly(x**2 + 2*y**2)
return eval_(func, [x_, y_]), func
def func_gradient(fun, val):
x_, y_ = val
_, function = fun(val)
diff_x = sym.diff(function, x)
diff_y = sym.diff(function, y)
grad_vec = np.array([eval_(diff_x, [x_, y_]),
eval_(diff_y, [x_, y_])], dtype=float)
# 이전과 달리 벡터를 계산하고, 반환한다.
return grad_vec, [diff_x, diff_y]
def gradient_descent(fun, init_point, lr_rate=1e-2, epsilon=1e-5):
cnt = 0
val = init_point # 변수 2개로 이루어진 리스트
diff, _ = func_gradient(fun, val)
while np.linalg.norm(diff) > epsilon:
val = val - lr_rate * diff
diff, _ = func_gradient(fun, val)
cnt += 1
print(f"함수: {fun(val)[1]}, cnt: {cnt}")
print(f"최소점: ({val}, {func(val)[0]})")
pt = [np.random.uniform(-2, 2), np.random.uniform(-2, 2)]
gradient_descent(fun=func_multi, init_point=pt)
# 함수: Poly(x**2 + 2*y**2, x, y, domain='ZZ'), cnt: 609
# 최소점: ([-4.94..e-06 2.81..e-11],
# Poly(x**2 + 2*x + 3, x, domain='ZZ'))
Day 6에서 선형회귀계수를 찾을 때 무어-펜로즈 유사역행렬을 활용하였다.
무어-펜로즈 역행렬은 시간복잡도가 $O(n^{3})$으로 높다는 점, 선형 모델에만 적용할 수 있다는 점 등의 단점이 있다.
즉, normal equation으로 푸는 방법은 시간복잡도가 높다.
경사하강법은 비선형 모델에도 적용할 수 있으며 보다 일반적인 방법으로 활용 가능하다.
#linear_regression_with_gradient_descent.py
# Input: X, y, lr, T, Output: beta
# lr: 학습률, T: 학습횟수
var = init
grad = gradient(var)
for t in range(T): # 종료조건을 일정 학습횟수로 변경하였다.
error = y - X @ beta # var = var - lr * grad
grd = -transpose(X) @ error # grad = gradient(var)
beta = beta - lr * grad
import numpy as np
lr = 1e-2
X = np.array([[1, 1], [1, 2], [2, 2], [2, 3]])
y = np.dot(X, np.array([1, 2])) + 3
# [1, 2, 3]이 정답. x + 2y + 3
beta_gd = [10.1, 15.1, -6.5]
# intercept항(상수항) 추가
X_ = np.array([np.append(x, 1) for x in X])
for t in range(5000):
error = y - X_ @ beta_gd
grad = -X_.T @ error
beta_gd = beta_gd - lr * grad
print(beta_gd)
# [1.00000367 1.99999949 2.99999516]
# 학습을 너무 적게 하면 오차가 커진다.
# 적당한 learning rat 및 test 횟수를 선택하는것이 중요하다.
# 초기값 선택도 중요한 요소가 될 수 있다.
선형회귀의 목적식 $\Vert \text{y} - \text{X} \beta \Vert$ 역시 볼록 함수이기 때문에 수렴이 보장된다.
위와 같은 그래프는 볼록함수가 아니다. 딥러닝에서 보게 될 대부분의 목적함수는 convex하지 않다.
우리가 지금까지 배운 경사하강법은 (Full-)Batch Gradient Descent(BGD)로, 전체 데이터셋에 대한 오차를 매 업데이트마다 계산하는 방식이었다.
하지만 해당 방식으로 계산하면 데이터셋이 조금만 많아져도 하드웨어 자원의 측면이나, 시간적 측면으로 여러 문제가 생긴다.
SGD는 Stochastic Gradient Descent의 약자로, 이 기법은 매번 모든 데이터를 사용하는 게 아니라 데이터 한개 또는 일부만을(mini-batch) 활용하여 다음 값을 업데이트한다.
볼록이 아닌(non-convex) 목적식은 SGD를 통해 최적화할 수 있다.
좌측이 BGD, 우측이 SGD이다. SGD를 사용하면 목표값을 향해 좀 더 돌아가는 것처럼 보이지만 실제로는 연산량이 훨씬 적어 더 빠르게 목표지점에 도달할 수 있다.
사실 SGD를 변형없이 그대로 사용할 경우 현대에 와서는 성능이 좋은 편이라고 평가받지 못한다.
대부분은 SGD에 기반한 알고리즘들이지만 SGD에 조금 변형을 가함으로써 월등히 성능이 향상되는 것을 확인할 수 있다.