Skip to content

Latest commit

 

History

History
352 lines (265 loc) · 12.8 KB

220206.md

File metadata and controls

352 lines (265 loc) · 12.8 KB

Day 114

Deep Learning from Scratch

4. 신경망 학습

수치 미분(numerical differentiation)

경사법에서는 기울기 값을 기준으로 나아갈 방향을 정한다.

미분

미분은 한순간의 변화량을 표시한 것이다.

e 4 4

좌변은 f(x)의 x에 대한 미분을 나타내는 기호이다. 즉, x의 '작은 변화'가 함수 f(x)를 얼마나 변화시키는가를 의미한다.

그대로 구현

def numerical_diff(f, x):
    h = 1e-50
    return (f(x + h) - f(x)) / h

이 함수는 '함수 f'와 '함수 f의 인수 x'를 받는다.

이 함수는 개선해야 하는 부분이 있다.

우선 h에 작은 값을 대입하고자 1e-50이라는 작은 값을 이용했지만, 이 방식은 반올림 오차(rounding error)문제를 일으킨다. 반올림 오차란 작은 값이 생략되어 최종 계산 결과에 오차가 생기는 것을 말한다. 따라서 너무 작은 값을 이용하지 않고 10의 -4승을 이용한다.

두 번째는 f의 차분(임의의 두 점에서의 함수 값들의 차이)에 대한 것이다. 위에서는 x + h와 x 사이의 차분을 계산하고 있지만, 이 계산은 오차가 있다.

아래의 그림을 보면 진정한 미분과 수치 미분의 값이 다르다는 것을 알 수 있다. 이는 진정한 미분은 x 위치의 함수의 기울기에 해당하지만, 위의 구현은 (x + h)와 x사이의 기울기에 해당하기 때문이다. 이 차이는 h를 무한히 0으로 좁히는 것이 불가능하기 때문에 생기는 것이다.

fig 4-5

수치 미분에서 생기는 오차를 줄이기 위해 (x + h)와 (x - h)일 때의 함수 f의 차분을 계산하는 방법을 쓰기도 한다. 이를 중심 차분 혹은 중앙 차분이라고 하고 (x + h)와 x의 차분은 전방 차분이라고 한다.

개선한 함수

def numerical_diff(f, x):
    h = 1e-4 # 0.0001
    return (f(x + h) - f(x - h)) / (2 * h)

수치 미분의 예

e 4 5

구현

def function_1(x):
    return 0.01*(x**2) + 0.1*x

함수 그리기

import numpy as np
import matplotlib.pylab as plt

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
plt.xlabel('x')
plt.ylabel('y')
plt.plot(x, y)
plt.show()

numerical_diff

x가 5와 10일 때 미분

# x = 5
numerical_diff(function_1, 5)

0.1999999999990898

# x = 10
numerical_diff(function_1, 10)

0.2999999999986347

위의 함수의 해석적 해(수식을 전개해 미분한 것)는 0.02x + 0.1로 x가 5, 10일 때 각각 0.2, 0.3이다. 결과와 비교하면 오차가 작은 것을 확인할 수 있다.

편미분

e 4 6

구현

def function_2(x):
    return x[0]**2 + x[1]**2 # np.sum(x**2)와 같은 결과

이 식을 그래프로 그리면 3차원으로 그려진다.

fig 4-8

이 식을 미분할 때는 어느 변수에 대한 미분인지를 구별해야 한다. 이렇게 변수가 여럿인 함수에 대한 미분을 편미분이라고 한다.

x0가 3, x1이 4일 때 x0에 대한 편미분

def function_tmp1(x0):
    return x0*x0 + 4.0**2.0

numerical_diff(function_tmp1, 3.0)

6.00000000000378

x0가 3, x1이 4일 때 x1에 대한 편미분

def function_tmp2(x1):
    return 3.0**2.0 + x1*x1

numerical_diff(function_tmp2, 4.0)

7.999999999999119

편미분도 특정 장소의 기울기를 구하지만, 여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정한다.


기울기(gradient)

모든 변수의 편미분을 벡터로 정리한 것을 기울기라고 한다.

def numerical_gradient(f, x):
    h = 1e-4
    grad = np.zeros_like(x) # x와 형상이 같은 배열을 생성, 원소는 모두 0
    
    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

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

기울기 구하기

# 점 (3, 4)에서의 기울기
numerical_gradient(function_2, np.array([3.0, 4.0]))

array([6., 8.])

# 점 (0, 2)에서의 기울기
numerical_gradient(function_2, np.array([0.0, 2.0]))

array([0., 4.])

# 점 (3, 0)에서의 기울기
numerical_gradient(function_2, np.array([3.0, 0.0]))

array([6., 0.])

기울기를 그림으로 그리면 다음과 같다.

fig 4-9

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

경사법(경사 하강법)

기울기를 이용해 함수의 최솟값(또는 가능한 한 작은 값)을 찾으려는 것이 경사법이다.

그러나 기울기가 가리키는 곳에 정말 함수의 최솟값이 있는지 보장할 수 없다.

함수가 극솟값, 최솟값, 안장점이 되는 장소에서는 기울기가 0이다.
복잡하고 찌그러진 모양의 함수라면, 평평한 곳으로 파고들면서 고원이라 하는, 학습이 진행되지 않는 정체기에 빠질 수 있다.

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

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

최솟값을 찾으면 경사 하강법(gradient descent method), 최댓값을 찾으면 경사 상승법(gradient ascent method)라고 한다.

경사법을 수식으로 나타내면 다음과 같다.

e 4 7

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

위의 식처럼 변수의 값을 갱신하는 단계를 여러 번 반복하면서 서서히 함수의 값을 줄여나간다. 변수의 수가 늘어도 같은 식(각 변수의 편미분 값)으로 갱신하게 된다.

학습률 값은 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은 학습률, step_num은 경사법에 따른 반복 횟수를 뜻한다. 함수의 기울기는 numerical_gradient(f, x)로 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 반복한다.

경사하강법 사용

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.11110793e-10, 8.14814391e-10])

초깃값은 (-3.0, 4.0)으로 설정하고 경사법으로 최솟값 탐색을 해서 결과값으로 거의 (0, 0)에 가까운 결과를 얻은 것을 확인할 수 있고, 이 결과로 경사법으로 거의 정확한 결과를 얻은 것을 알 수 있다.

갱신 과정을 그림으로 표현하면 아래와 같다.

fig 4-10

학습률이 너무 클 때

init_x = np.array([-3.0, 4.0])
gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100)

array([-2.58983747e+13, -1.29524862e+12])

학습률이 너무 작을 때

init_x = np.array([-3.0, 4.0])
gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100)

array([-2.99999994, 3.99999992])

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

가중치와 편향 같은 매개변수와는 달리 학습률 같이 사람이 직접 설정해야 하는 매개변수를 하이퍼 파라미터(hyper parameter)라고 한다.

신경망에서의 기울기

신경망 학습에서는 가중치 매개변수에 대한 손실 함수의 기울기를 구해야 한다.

예를 들어 형상이 2x3, 가중치가 W, 손실 함수가 L인 신경망에서 경사는 다음과 같이 나타내진다.

e 4 8

경사의 각 원소는 각각의 원소에 관한 편미분이다. (1, 1)의 원소는 w11을 조금 변경했을 때 L이 얼마나 변화하는가를 나타낸다.

경사와 W의 형상이 모두 2x3으로 같다.

간단한 신경망

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

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

또한 여기서 사용하는 함수들은 전에 내가 구현한 것이 아닌 조금 변형된 것을 책에서 제공하여 그것을 쓴다.

def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 값 복원
        it.iternext()   
        
    return grad

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 

    x = x - np.max(x) # 오버플로 대책
    return np.exp(x) / np.sum(np.exp(x))

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 훈련 데이터가 원-핫 벡터라면 정답 레이블의 인덱스로 반환
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

시험

net = simpleNet()
print(net.W) # 가중치 매개변수

[[-0.89298964 0.62267173 -1.53662936]
[ 0.27531623 0.45537337 1.7356807 ]]

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

[-0.28800917 0.78343907 0.64013501]

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

1

t = np.array([0, 0, 1])
net.loss(x, t)

0.9358449403793527

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

def f(W):
    return net.loss(x, t)

dW = numerical_gradient(f, net.W)
print(dW)

[[ 0.09303167 0.27161572 -0.36464739]
[ 0.13954751 0.40742357 -0.54697109]]

net.W를 인수로 받아 손실 함수를 계산하는 새로운 한수 f를 정의해서 numerical_gradient(f, x)에 넘겼다.

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

lambda를 이용하면 다음과 같이 구현도 가능하다.

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