mingul 2020. 7. 20. 16:57

 

$x_0$과 $x_1$의 편미분을 동시에 계산하기 위해 ($x_0$, $x_1$) 양쪽의 편미분을 묶어서 $\left({\partial f \over \partial x_0} , {\partial f \over \partial x_1}\right)$ 을 계산한다.

$\left({\partial f \over \partial x_0} , {\partial f \over \partial x_1}\right)$ 처럼 모든 변수의 편미분을 벡터로 정리한 것을 기울기(gradient) 라고 한다.

 

  • 기울기의 구현
def function_2(x):
    return x[0]**2 + x[1]**2

def numerical_gradient(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] = 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

 

복잡해보이지만, 동작 방식은 변수가 하나일 때의 수치 미분과 거의 같다. np.zeros_like(x) 는 x와 형상이 같고 그 원소가 모두 0인 배열을 만든다.

위에서 구현한 함수의 인수인 f는 함수이고 x는 넘파이 배열이므로 넘파이 배열 x의 각 원소에 대해서 수치 미분을 구한다.

 

print(numerical_gradient(function_2, np.array([3.0,4.0])))
print(numerical_gradient(function_2, np.array([0.0,2.0])))
print(numerical_gradient(function_2, np.array([3.0,0.0])))
[실행 결과]
[6. 8.]
[0. 4.]
[6. 0.]

 

이처럼 ($x_0$, $x_1$) 의 각 점에서의 기울기를 계산할 수 있다.

[참고] 실제로는 [6.00000000000037801, 7.99999999999991189] 라는 값이 나오지만, 넘파이가 배열을 출력할 때 수치를 보기 쉽도록 가공하기 때문에 [6. 8.]의 결과가 나왔다.

 

기울기 그림은 아래의 그림처럼 방향을 가진 벡터(화살표)로 그려진다. 기울기는 함수의 가장 낮은 장소(최솟값)을 가리킨다. 그리고 최솟값에서 멀어질수록 화살표의 크기가 커지는 것을 알 수 있다.

 

$f$($x_0$, $x_1$) = $x_0^2$+$x_1^2$의 기울기

사실 기울기는 각 지점에서 낮아지는 방향을 가리킨다. 즉 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장 크게 줄이는 방향이다.

 

1. 경사 하강법

머신러닝 문제 대부분은 학습 단계에서 최적의 매개변수를 찾아낸다. 신경망 역시 최적의 매개변수인 가중치와 편향을 학습 시에 찾아야 한다. 최적은 손실 함수가 최솟값이 될 때의 매개변수 값이다. 이때, 기울기를 이용하여 함수의 최솟값 혹은 가능한 한 작은 값을 찾으려는 것이 경사 하강법이다.

여기서 각 지점에서 함수의 값을 낮추는 방안을 제시하는 지표가 기울기라는 것이다. 하지만 기울기가 가리키는 곳에 정말 함수의 최솟값이 있는지 보장할 수 없다. 실제로 복잡한 함수에서는 기울기가 가리키는 방향에 최솟값이 없는 경우가 대부분이다.

 

함수가 극솟값, 최소값, 안장점(saddle point) 이 되는 장소에서는 기울기가 0이다. 극솟값은 국소적인 최솟값, 즉 한정된 범위에서의 최솟값인 점이다. 안장점은 어느 방향에서 보면 극댓값이고 다른 방향에서 보면 극솟값이 되는 점이다. 경사 하강법은 기울기가 0인 장소를 찾지만 극솟값이나 안장점일 가능성 때문에 그것이 반드시 최솟값이라고 할 수 없다. 그리고 복잡한 함수라면 평평한 곳으로 파고들면서 고원(plateau)이라는 학습이 진행되지 않는 정체기에 빠질 수 있다.

 

기울어진 방향이 꼭 최솟값을 가리키는 것은 아니지만, 그 방향으로 가야 함수의 값을 줄일 수 있다. 그래서 최솟값이 되는 장소를 찾는 문제에서는 기울기 정보를 단서로 나아갈 방향을 정해야 한다.

 

경사 하강법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동한다. 그런 다음 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 나아가기를 반복한다. 이렇게 해서 함수의 값을 점차 줄이는 것이 경사법(gradient descent method) 라고 한다. 

 

경사법은 최솟값을 찾느냐, 최댓값을 찾느냐에 따라 이름이 다르다. 전자를 경사 하강법, 후자를 경사 상승법이라고 하는데, 손실 함수의 부호를 반전시키면 최솟값을 찾는 문제와 최댓값은 찾는 문제는 같은 것이니 하강이냐 상승이냐는 중요하지 않다.

 

경사법의 수식

$\eta$는 갱신하는 양을 나타내는데, 이를 신경망 학습에서는 학습률(learning rate)라고 한다. 한 번의 학습으로 얼마만큼 학습해야 할지, 즉 매개변수 값을 얼마나 갱신하느냐를 정하는 것이 학습률이다.

 

위의 식은 1회에 해당하는 갱신이고, 이 단계를 반복하면서 서서히 함수의 값을 줄인다. 위 수식은 변수가 2개이지만, 변수의 수가 늘어도 같은 식(각 변수의 편미분 값)으로 갱신한다.

 

또한 학습률 값은 0.01이나 0.001 등 미리 특정 값으로 정해두어야 하는데, 일반적으로 이 값이 너무 크거나 작으면 좋은 장소를 찾아갈 수 없다. 신경망 학습에서는 보통, 이 학습률 값을 변경하면서 올바르게 학습하고 있는지를 확인하면서 진행한다.

 

  • 경사하강법 구현
def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    for i in range(step_num):
        grad = numerical_gradient(f,x)
        x -= lr * grad
    return x

 

인수 f는 최적화하려는 함수, init_x는 초기값, lr은 learning late, step_num은 경사하강법에 따른 반복 횟수를 뜻한다. 함수의 기울기는 numerical_gradient(f, x)로 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 반복한다. 이 함수를 사용하면 함수의 극솟값이나 최솟값을 구할 수 있다.

 

  • 경사법으로 $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])
print(gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100))
[실행 결과]
[-6.11110793e-10  8.14814391e-10]

초기값을 (-3.0, 4.0)으로 설정한 후 경사법을 사용해 최솟값 탐색을 시작한다. 결과는 거의 (0, 0)에 가까운 결과이다. 실제 최솟값은(0, 0)이므로 거의 정확한 결과를 얻었다.

 

경사법에 의한 위 수식의 갱신과정. 점선은 함수의 등고선을 나타낸다.

값이 가장 낮은 장소인 원점에 점차 가까워지고 있다.

 

#학습률이 너무 큰 예 : lr =10.0
init_x = np.array([-3.0, 4.0])
print(gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100))

#학습률이 너무 작은 예 : lr = 1e-10
init_x = np.array([-3.0, 4.0])
print(gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100))
[실행 결과]
[-2.58983747e+13 -1.29524862e+12]
[-2.99999994  3.99999992]

 

학습률이 너무 크면 큰 값으로 발산하고, 너무 작으면 거의 갱신되지 않은 채로 끝난다.

 

학습률 같은 매개변수를 하이퍼 파라미터라고 하는데, 가중치와 편향 같은 신경망의 매개변수와는 성질이 다르다. 신경망의 가중치 매개변수는 훈련 데이터와 학습 알고리즘에 의해서 자동으로 획득되는 반면, 하이퍼 파라미터는 사람이 직접 설정해야 한다.

 

2. 신경망에서의 기울기

신경망 학습에서의 기울기는 가중치 매개변수에 대한 손실 함수의 기울기이다. 예를 들어 형상이 2X3, 가중치가 W, 손실 함수가 L인 신경망의 수식은 $\partial L \over \partial W$로 나타낼 수 있다.

 

경사의 수식

$\partial L \over \partial W$의 원소는 각각의 원소에 대한 편미분이다. 예를 들면 아래 수식의 1행 1번째 원소는 $w_{11}$을 조금 변경했을 때 손실 함수 L이 얼마나 변화하느냐를 나타낸다. 이때, $\partial L \over \partial W$의 형상이 W와 같다는 것이다. 실제로 W와 $\partial L \over \partial W$의 형상은 모두 2X3이다.

 

  • 간단한 신경망을 예로 들어 기울기를 구하는 코드 구현
# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient

class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3) # 정규분포로 초기화

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print(dW)
[실행 결과]
[[ 0.25448428  0.03691667 -0.29140095]
 [ 0.38172642  0.05537501 -0.43710143]]

위 클래스는 형상이 2X3인 가중치 매개변수 하나를 인스턴스 변수로 갖는다. 메소드는 2개인데, 하나는 예측을 수행하는 predict(x)이고, 다른 하나는 손실 함수의 값을 구하는 loss(x, t)이다. 인수 x는 입력데이터, t는 정답 레이블이다. 

 

net = simpleNet()
print("net.W : \n",net.W) #가중치 매개변수

x = np.array([0.6, 0.9])
p = net.predict(x)
print("p : \n",p)

print(np.argmax(p)) #최댓값의 인덱스

t = np.array([0,0,1]) #정답 레이블
net.loss(x, t)
[실행 결과]
net.W : 
 [[ 0.00387155 -0.12577174  0.95361921]
 [ 1.8080852  -0.55346652 -0.22063305]]
p : 
 [ 1.62959961 -0.57358291  0.37360178]
0
1.5890656555481064

기울기는 numerical_gradient(f, x)를 써서 구하면 된다.

 

def f(W):
    return net.loss(x, t)
#f 함수를 람다식으로 구현
# f = lambda w : net.loss(x, t)
dW = numerical_gradient(f, net.W)
print(dW)
[실행 결과]
[[ 0.43003252  0.04749756 -0.47753008]
 [ 0.64504879  0.07124634 -0.71629513]]

f(W) 함수의 인수 W는 더미로 만든 것이다. numerical_gradient(f, x) 내부에서 f(x)를 실행하는데, 그와의 일관성을 위해 f(W)를 정의한 것이다.

여기에서는 net.W를 인수로 받아 손실 함수를 계산하는 새로운 함수 f를 정의하여 numerical_gradient에 넘긴다.

dW는 numerical_gradient(f, net.W)의 결과로, 형상은 2X3의 2차원 배열이다. 

dW의 내용을 보면, $\partial L \over \partial W_{11}$은 대략 0.4인데 이는 $w_{11}$을 $h$만큼 늘리면 손실 함수의 값을 0.4$h$만큼 증가한다는 의미이다. 마이너스가 붙은 값을 손실 함수의 값이 그만큼 감소한다는 뜻이다.

그래서 손실 함수를 줄인다면 $\partial L \over \partial W$의 값 중 양수의 값은 음의 방향으로 갱신하고, 음수의 값은 양의 방향으로 갱신해야 한다. 

갱신되는 양에는 $\partial L \over \partial W$의 값의 절댓값으로 하기 때문에 $w_{23}$이 $w_{11}$보다 더 크게 기여한다.

 

신경망의 기울기를 구한 다음에는 경사법에 따라 가중치 매개변수를 갱신하기만 하면 된다.