(밑바닥부터 시작하는 딥러닝, 사이토고키) 를 바탕으로 작성하였습니다.
신경망 학습 (2) - 기울기와 경사하강법
이전의 포스팅에서는 손실 함수와 수치 미분을 공부했습니다.
이번 포스팅에는 손실 함수의 기울기를 구하고 경사법으로 손실 함수 결과값이 최소가 되는 지점으로 가중치를 갱신시켜주는 방법에 대해 공부해보겠습니다.
4. 기울기 - Gradient
기울기는 모든 변수의 편미분을 벡터로 정리한 것을 의미합니다.
$f$($x_0$, $x_1$) = $x_{0}^{2}$ + $x_{1}^{2}$
이전의 포스팅에서는 $x_0$와 $x_1$의 편미분을 변수별로 따로 계산했습니다. 그럼 $x_0$와 $x_1$의 편미분을 동시에 계산하는 방법을 알아보겠습니다. 위 식에서 $x_0$ = 3, $x_1$ = 4 일때, ($x_{0}$, $x_{1}$) 양쪽의 편미분을 묶어서 ($\frac{\partial f}{\partial x_0}$, $\frac{\partial f}{\partial x_1}$) 로 나타낼 수 있습니다. 이를 기울기라고 합니다.
기울기 구현하기
def numerical_gradient(f, x): # 함수와 넘파이 배열 x 입력
# 넘파이 배열 x의 각 원소에 대해서 수치 미분을 구합니다.
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # x와 형상이 같고 그 원소가 모두 0인 배열을 생성
for idx in range(x.size): # x의 요소수만큼 반복
tmp_val = x[idx]
# f(x+h) 계산
x[idx] = tmp_val + h
fxh1 = f(x)
# f(x-h) 계산
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2*h) # 중심차분
x[idx] = tmp_val # 값 복원
return grad
numerical_gradient(f, x) 함수의 인수인 f는 함수이고, x는 넘파이 배열이므로 넘파이 배열 x의 각 원소에 대해서 수치 미분을 구합니다.
(3, 4), (0, 2), (3, 0) 에서의 기울기를 구해보겠습니다.
numerical_gradient(function_2, np.array([3.0, 4.0])
>>> array([6, 8])
numerical_gradient(function_2, np.array([0.0, 2.0])
>>> array([0, 4])
numerical_gradient(function_2, np.array([3.0, 0.0])
>>> array([6, 0])
기울기가 의미하는 것을 알아보기 위해 기울기의 결과에 마이너스를 붙인 벡터를 그려보겠습니다. (출처)
# coding: utf-8
# cf.http://d.hatena.ne.jp/white_wheels/20100327/p3
import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D
def _numerical_gradient_no_batch(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # x와 형상이 같은 배열을 생성
for idx in range(x.size):
tmp_val = x[idx]
# f(x+h) 계산
x[idx] = float(tmp_val) + h
fxh1 = f(x)
# f(x-h) 계산
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 값 복원
return grad
def numerical_gradient(f, X):
if X.ndim == 1:
return _numerical_gradient_no_batch(f, X)
else:
grad = np.zeros_like(X)
for idx, x in enumerate(X):
grad[idx] = _numerical_gradient_no_batch(f, x)
return grad
def function_2(x):
if x.ndim == 1:
return np.sum(x**2)
else:
return np.sum(x**2, axis=1)
def tangent_line(f, x):
d = numerical_gradient(f, x)
print(d)
y = f(x) - d*x
return lambda t: d*t + y
if __name__ == '__main__':
x0 = np.arange(-2, 2.5, 0.25)
x1 = np.arange(-2, 2.5, 0.25)
X, Y = np.meshgrid(x0, x1)
X = X.flatten()
Y = Y.flatten()
grad = numerical_gradient(function_2, np.array([X, Y]) )
plt.figure()
plt.quiver(X, Y, -grad[0], -grad[1], angles="xy",color="#666666")#,headwidth=10,scale=40,color="#444444")
plt.xlim([-2, 2])
plt.ylim([-2, 2])
plt.xlabel('x0')
plt.ylabel('x1')
plt.grid()
plt.legend()
plt.draw()
plt.show()
기울기는 함수의 '가장 낮은 장소(최솟값)'을 가리킵니다. 마치 나침반처럼 화살표들은 한 점을 향하고 있습니다. 기울기가 가리키는 쪽은 각 장소에서의 함수의 출력 값을 가장 크게 줄이는 방향입니다. 하지만 기울기가 0인 장소는 반드시 최솟값을 의미하진 않습니다. 극솟값이나 안장점일 가능성이 있습니다.
4.1 경사법(경사 하강법) - Gradient descent method
기계학습 문제 대부분은 학습 단계에서 최적의 매개변수를 찾아냅니다. 신경망 역시 최적의 매개변수(가중치와 편향)를 학습 시에 찾아야 합니다. 여기에서 최적이란 손실 함수가 최솟값이 될 때의 매개변수 값 입니다. 이런 상황에서 기울기를 이용해 함수의 최솟값을 찾으려는 것이 경사법입니다.
하지만 기울기가 가리키는 방향이 꼭 최솟값을 가리키는 것은 아닙니다. 그 방향이 안장점*, 극솟값*, 최솟값을 의미하는 것일 수도 있습니다. 그 방향으로 가야 함수의 값을 줄일 수 있습니다. 그래서 최솟값이 되는 장소를 찾는 문제에서는 기울기를 정보로 나아갈 방향을 정해야 합니다.
극솟값
국소적인 최솟값, 즉 한정된 범위에서의 최솟값인 점입니다.
안장점(saddle point)
어느 방향에서 보면 극댓값이고 다른 방향에서 보면 극솟값이 되는 점입니다.
고원(plateau)
복잡하고 찌그러진 모양의 함수라면(대부분) 평평한 곳으로 파고 들면서 고원이라 하는, 학습이 진행되지 않는 정체기에 빠질 수 있습니다.
이제 경사법에 대해 알아보겠습니다. 경사법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동합니다. 그런 다음 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 나아가기를 반복합니다. 이렇게 해서 함수의 값을 점차 줄이는 것이 경사법입니다.
경사법은 최솟값을 찾느냐, 최댓값을 찾느냐에 따라 이름이 다릅니다. 전자를 경사 하강법(gradient descent method), 후자를 경사 상승법(gradient ascent method)이라고 합니다. 다만 손실 함수의 부호를 반전시키면 최솟값을 찾는 문제와 최댓값을 찾는 문제는 같은 것이니 하강이냐 상승이냐는 본질적으로 중요하지 않습니다.
그럼, 경사법을 수식으로 나타내보겠습니다.
$x_0$ = $x_0$ - $\eta\frac{\partial f}{\partial x_0}$
$x_1$ = $x_1$ - $\eta\frac{\partial f}{\partial x_1}$
$\eta$ 기호(eta)는 갱신하는 양을 나타냅니다. 이를 신경망 학습에서는 학습률(learning rate)이라고 합니다. 한 번의 학습으로 얼마만큼 학습해야 할지, 즉 매개변수 값을 얼마나 갱신하느냐를 정하는 것이 학습률입니다.
위 식은 1회에 해당하는 갱신이고, 이 단계를 반복합니다. 즉, 변수의 값을 갱신하는 단계를 여러 번 반복하면서 서서히 함수의 값을 줄이는 것입니다. 또, 여기에서는 변수가 2개인 경우를 보여줬지만, 변수의 수가 늘어도 같은 식(각 변수의 편미분 값)으로 갱신하게 됩니다.
또한 학습률 값은 0.01이나 0.001 등 미리 특정 값으로 정해두어야 합니다. 신경망 학습에서는 보통 이 학습률 값을 변경하면서 올바르게 학습하고 있는지를 확인하면서 진행합니다.
경사 하강법 구현
def graduent_descent(f, init_x, lr=0.01, step_num=100):
# f는 최적화하려는 함수, init_x는 초깃값, lr은 학습률, step_num은 반복 횟수를 의미합니다.
x = init_x
for i in range(step_num):
grad = numerical_gradient(f, x) # 함수의 기울기 구하기
x -= lr * grad
return x
경사법으로 $f$($x_0$, $x_1$) = $x_{0}^{2}$ + $x_{1}^{2}$의 최솟값 구하기
def function_2(x):
return x[0]**2 + x[1]**2
init_x = np.array([-3.0, 4.0])
gradient_descent(function_2, init_x=init_x, lr=0,1, step_num=100)
>>> array([-6.111107e-10, 8.148143e-10]) # 0에 가까운 것을 확인할 수 있습니다.
경사법을 사용한 갱신 과정을 그림으로 나타내기 (출처)
# coding: utf-8
import numpy as np
import matplotlib.pylab as plt
from gradient_2d import numerical_gradient
def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
x_history = []
for i in range(step_num):
x_history.append( x.copy() ) # 시각화를 위해 x_history에 x의 기록을 입력해줍니다.
grad = numerical_gradient(f, x)
x -= lr * grad
return x, np.array(x_history)
def function_2(x):
return x[0]**2 + x[1]**2
init_x = np.array([-3.0, 4.0])
lr = 0.1
step_num = 20
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)
plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')
plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()
학습률이 너무 크거나 너무 작은 예를 보겠습니다.
# 학습률이 너무 큰 예 : lr = 10.0
init_x = np.array([-3.0, 4.0])
gradient_descent(function_2, init_x=init_x, lr=10.0, step_num100)
>>> array([ -2.5898374e_13, -1.2952486e+12]) # 값이 너무 큰 값으로 발산합니다.
# 학습률이 너무 작은 예 : lr = 1e-10
init_x = np.array([-3.0, 4.0])
gradient_descent(function_2, init_x=init_x, lr=10.0, step_num100)
>>> array([-2.9999934, 3.999992]) # 거의 학습되지 않은 채 종료합니다.
하이퍼파마리머(hyper parameter, 초매개변수)
학습률 같은 매개변수를 하이퍼파라미터라고 합니다. 이는 가중치와 편향 같은 신경망의 매개변수와는 성질이 다른 매개변수입니다. 신경망의 가중치 매개변수는 훈련 데이터와 학습 알고리즘에 의해서 '자동'으로 획득되는 매개변수인 반면, 학습률 같은 하이퍼파라미터는 사람이 직접 설정해야 하는 매개변수 인 것입니다. 일반적으로 이들 하이퍼파라미터는 여러 후보 값 중에서 시험을 통해 가장 잘 학습하는 값을 찾는 과정을 거쳐야 합니다.
4.2 신경망에서의 기울기
신경망 학습에서도 기울기를 구해야 합니다. 여기서 말하는 기울기는 가중치 매개변수에 대한 손실 함수의 기울기입니다. 예를 들어 형상이 2 X 3, 가중치가 W, 손실 함수가 L인 신경망을 생각해보겠습니다. 이 경우 경사는 $\frac{\partial L}{\partial W}$로 나타낼 수 있습니다. 수식으로는 다음과 같습니다.
여기서 중요한 점은 W와 $\frac{\partial L}{\partial W}$의 형상은 모두 2 X 3 입니다.
신경망을 예로 들어 실제로 기울기를 구하는 코드를 구현해보겠습니다.
간단한 신경망 구현
class simpleNet:
def __init__(self):
self.W = np.random.randn(2,3) # 평균 0, 표준편차 1인 가우시안 정규분포 난수를 2X3 배열 생성
def predict(self, x): # 예측 수행
return np.dot(x, self.W) # x와 self.W 내적
def loss(self, x, t): # x는 입력, t는 정답 레이블
z = self.predict(x)
y = softmax(z)
loss = cross_entropy_error(y, t) # 교차 엔트로피 오차 이용
return loss
net = simpleNet()
print(net.W) # 가중치 매개변수
>>> [[ 0.47355232, 0.9977393, 0.846690]
[ 0.85557411, 0.0356366, 0.694220]] # 평균 0, 표준편차 1 정규분포 난수 생성
x = np.array([0.6, 0.9])
p = net.perdict(x)
print(p)
>>> [1.05414 0.63071 1.13280] # 소프트 맥스함수를 거치지 않아 확률로 나오지 않았습니다.
# 어차피 최댓값의 인덱스를 구해야 하므로 상관없습니다.
print( np.argmax(p) ) # 최댓값의 인덱스
>>> 2
t = np.array([0, 0, 1]) # 정답 레이블
print(net loss(x, t)) # 손실 함수 구하기
>>> 0.9280685366
손실함수 까지 구했으니 기울기를 구해보겠습니다.
def f(W): # net.W를 인수로 받아 손실 함수를 계산하는 새로운 함수 정의
return net.loss(x, t)
dW = numerical_gradient(f, net.W) # 손실 함수의 기울기
print(dW)
>>> [[0.22 0.14 -0.36]
[0.32 0.21 -0.54]]
# dW가 0.2의 의미는 w을 h만큼 늘리면 손실 함수의 값은 0.2h만큼 증가합니다.
# 손실 함수를 줄인다는 관점으로 -0.54는 양의 방향으로 갱신하고
# 0.2는 음의 방향으로 갱신해줘야 합니다.
신경망의 기울기를 구한 다음에는 경사법에 따라 가중치 매개변수를 갱신하면 됩니다.
다음 포스팅에서 2층 신경망을 대상으로 학습 과정 전체를 구현해보겠습니다. 감사합니다.
'수학 > 딥러닝 이론' 카테고리의 다른 글
04-1. 오차역전파법 (1) - 계산 그래프와 연쇄법칙 (0) | 2020.09.11 |
---|---|
03-3. 신경망 학습 (3) - 학습 알고리즘 구현 (1) | 2020.09.11 |
03-1. 신경망 학습 (1) - 손실 함수와 수치 미분 (0) | 2020.09.10 |
02-2. 신경망 (2) - 출력층 설계와 MNIST 구현 (6) | 2020.09.09 |
02-1. 신경망 (1) - 3층 신경망 순전파 구현 (0) | 2020.09.09 |