ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [NLP] 간단하게 경사 하강법과 학습 알고리즘 구현해보기
    ML/NLP 2020. 3. 26. 22:21

     

     

     

     

     

     

     

     

    경사 하강법을 알려면 미분부터 보고 넘어가야 한다.

     

     

     

    수치 미분 계산


    • 구현할 수식?
    (f(x+h)-f(x)) / h

     

    • 구현
    # 나쁜 구현 예
    def numerical_diff(f,x):
        h = 10e-50
        return (f(x+h)-f(x)) / h

     

    - 위의 구현을 개선해야 할 점

    1. 'h = 10e-50'처럼 너무 작은 값을 이용하면 컴퓨터로 계산하는 데 문제가 됨 -> 분모가 0.0이 될 수도 있음
    2. 진정한 미분은 x 위치의 함수의 기울기(접선)에 해당하지만, 이번 구현에서의 미분은 (x+h)와 x 사이의 기울기에 해당

     

     

     

    • 수정 후 구현
    # 개선된 미분 함수
    def numerical_diff(f,x):
        h = 1e-4 # 0.0001
        return (f(x+h) - f(x-h)) / (2*h)

     

     

     

     

    * 수치미분 vs 해석미분

    1. 수치미분 : 아주 작은 차분(임의 두 점에서의 함수 값들의 차이)으로 미분하는 것

    2. 해석적 미분 : 수식을 전개해 미분하는 것. 우리가 수학 시간에 배운 바로 그 미분

     

     

     

     

    수치미분의 예


    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) # 20.0 미포함
    y = function_1(x)
    plt.xlabel("x")
    plt.ylabel("f(x)")
    plt.plot(x, y)
    plt.show()

    Out:

    numerical_diff(function_1, 5)
    Out:    0.1999999999990898
    numerical_diff(function_1, 10)
    Out:    0.2999999999986347

     

    • 해석적 해 = 0.02x + 0.1
    x=5 -> y=0.2
    x=10 -> y=0.3

     

     

     

    편미분


    편미분 = 변수가 두 개인 함수의 미분

     

    def function_2(x):
        return x[0]**2 + x[1]**2
        # return np.sum(x**2) 도 가능
    import numpy as np
    import matplotlib.pyplot as plt
    X = np.arange(-5, 5, 0.4)
    Y = np.arange(-5, 5, 0.4)
    X, Y = np.meshgrid(X, Y)
    Z = X**2 + Y**2
     
    fig = plt.figure()
    ax = fig.gca(projection='3d')
    surf = ax.plot_wireframe(X, Y, Z, color='black')
     
    plt.show()

    Out:

    x = np.array([(0,1),(1,4)])
    function_2(x)

     

    Out:    array([ 1, 17])

     

     

     

    Q.  x0=3, x1=4일 때, x0과 x1에 대한 편미분 구하기

    def function_tmp1(x0):
        return x0*x0 + 4.0**2.0
    numerical_diff(function_tmp1, 3.0)
    Out:    6.00000000000378
    def function_tmp2(x1):
        return 3.0**2 + x1*x1
    numerical_diff(function_tmp2, 4.0)
    Out:    7.999999999999119

     

    여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정.

    그렇게 새로운 함수를 정의하고, 수치 미분 함수 적용하여 편미분 구함

     

     

     

     

     

    기울기


    앞의 예에서는 x0과 x1의 편미분을 변수별로 따로 계산함

    동시에 계산하고 싶다면, (x0, x1) 양쪽의 편미분을 묶어서 계산하면 됨

    모든 변수의 편미분을 벡터로 정리한 것을 기울기(gradient)라고 함
    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
    numerical_gradient(function_2, np.array([3.0, 4.0]))
    Out:    array([6., 8.])
    numerical_gradient(function_2, np.array([0.0, 2.0]))
    Out:    array([0., 4.])
    numerical_gradient(function_2, np.array([3.0, 0.0]))
    Out:    array([6., 0.])

     

     

    * 기울기의 의미:  기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장 크게 줄이는 방향

     

     

     

     

    경사법(경사 하강법)


    • 복잡한 손실 함수에서는 최적의 매개변수(가중치와 편향) 짐작이 어렵다.
    • 이런 상황에서 기울기를 잘 이용해 함수의 최솟값(또는 가능한 한 작은 값)을 찾으려는 것이 경사법이다.
    • 각 지점에서 함수의 값을 낮추는 방안을 제시하는 지표가 기울기
    • 기울어진 방향이 꼭 최솟값을 가르키는 것은 아니나, 그 방향으로 가야 함수의 값을 줄일 수 있다.
    • 경사법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동. 그런 다음 이동한 곳에서도 마찬가지고 기울기를 구하고, 그 방향으로 나아감. 반복

    η 기호(eta, 에타)는 갱신하는 양을 나타낸다. 이를 신경망 학습에서는 학습률(learning rate)라고 한다.
    한 번의 학습으로 얼마만큼 학습해야 할지, 즉 매개변수 값을 얼마나 갱신하느냐를 정하는 것이 학습률이다.
    변수의 수가 늘어도 같은 식(=각 변수의 편미분 값)으로 갱신하게 된다.
    학습률은 0.01이나 0.001 등 미리 특정 값으로 정해두어야 하고, 보통 학습 시 학습률 값을 변경하면서 올바르게 학습하고 있는지를 확인해야 한다.
    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
    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
    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)  # 최솟값은 (0,0)이므로 거의 정확한 결과임
    Out:    array([-6.11110793e-10, 8.14814391e-10])

     

     

     

    학습률 크기에 따른 구현 결과 확인


    1. 학습률이 너무 큰 예 : lr = 10.0

    init_x = np.array([-3.0, 4.0])
    gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100)
    Out:    array([-2.58983747e+13, -1.29524862e+12])

     

     

    2. 학습률이 너무 작은 예 : lr = 1e-10

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

     

    Out:    array([-2.99999994, 3.99999992])

     

    학습률이 너무 크면 큰 값으로 발산

    너무 작으면 거의 갱신되지 않은 채 끝남

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

     

     

     

    신경망에서의 기울기


    W의 형상 : 2 x 3

    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)   # y는 신경망의 출력, t는 정답 레이블
            
            return loss
    net = simpleNet()
    net.W
    Out:    array([[ 0.39441064, 1.86192965, 1.45473241], [ 0.89338801, 0.76468554, -0.87370631]])

     

    x = np.array([0.6, 0.9])
    p = net.predict(x)
    p
    Out:    array([1.0406956 , 1.80537477, 0.08650377])
    np.argmax(p)  # 최댓값의 인덱스
    Out:    1

     

    t = np.array([0, 0, 1]) # 정답 레이블
    net.loss(x, t)
    Out:    2.216459501033035

     

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

     

    Out:    array([[ 0.1698066 , 0.36479638, -0.53460298], [ 0.2547099 , 0.54719456, -0.80190447]])

     

    1.  𝜕𝐿/𝜕𝑊의 𝜕𝐿/𝜕𝑤11은 대략 0.2
    -> w11을 h만큼 늘리면 손실 함수의 값은 0.2h만큼 증가한다는 의미
    -> w11은 음의 방향으로 갱신해야 함

    2.  𝜕𝐿/𝜕𝑤23은 대략 -0.8
    -> w23을 h만큼 늘리면 손실 함수의 값은 0.8h만큼 감소한다는 의미
    -> w23은 양의 방향으로 갱신해야 함w23이 w11보다 크게 기여함

     

     

     

     

    * 람다lambda) 기법으로 함수 정의하는 방법

    f = lambda w: net.loss(x, t)
    dW = numerical_gradient(f, net.W)
    dW
    Out:    array([[ 0.1698066 , 0.36479638, -0.53460298], [ 0.2547099 , 0.54719456, -0.80190447]])

     

     

     

    * 다차원 배열을 처리할 수 있는 numerical_gradient()로 수정하기

     

     

    - 수정 전

    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

     

     

    - 수정 후

    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]
            
            # 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 # 값 복원
            it.iternext()   
            
        return grad

     

     

     

     

     

    학습 알고리즘 구현하기


    신경망 학습의 절차

    • 전제 조건: 신경망에는 적응 가능한 가중치와 편향이 있고, 이들을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라 한다.
    • 1단계 - 미니배치: 훈련 데이터 중 일부를 무작위로 가져온다. 이를 미니배치라 하며, 미니배치의 손실 함수 값을 줄이는 것이 목표다.
    • 2단계 - 기울기 산출: 미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시한다.
    • 3단계 - 매개변수 갱신: 가중치 매개변수를 기울기 방향으로 아주 조금 갱신한다.
    • 4단계 - 반복: 1~3단계를 반복한다

     

    위의 절차는 경사 하강법으로 매개변수를 갱신하는 방법이며,
    이때 데이터를 미니배치로 무작위 선정하기 때문에 확률적 경사 하강법(stochastic gradient descent, SGD)이라고 부른다.

     

     

     

     

    2층 신경망 클래스 구현하기


    import sys, os
    sys.path.append(os.pardir)
    from common.functions import *
    from common.gradient import numerical_gradient
    class TwoLayerNet:
        def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
            # 가중치 초기화
            self.params = {}
            self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size) # 정규분포를 따르는 난수로 초기화
            self.params['b1'] = np.zeros(hidden_size)
            self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
            self.params['b2'] = np.zeros(output_size)
            
        def predict(self, x):
            W1, W2 = self.params['W1'], self.params['W2']
            b1, b2 = self.params['b1'], self.params['b2']
            
            a1 = np.dot(x, W1) + b1
            z1 = sigmoid(a1)
            a2 = np.dot(z1, W2) + b2
            y = softmax(a2)
            
            return y
        
        # x : 입력 데이터, t : 정답 레이블
        def loss(self, x, t):
            y = self.predict(x)
            
            return cross_entropy_error(y, t)
        
        
        def accuracy(self, x, t):
            y = self.predict(x)
            y = np.argmax(y, axis=1)
            t = np.argmax(t, axis=1)
            
            accuracy = np.sum(y==t) / float(x.shape[0])
            return accuracy
        
        
        # x : 입력 데이터, t : 정답 레이블
        def numerical_gradient(self, x, t):   # 수치 미분 방식으로 매개변수의 기울기 계산
            loss_W = lambda W: self.loss(x, t)
            
            grads = {}
            grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
            grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
            grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
            grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
            
            return grads
    import numpy as np
    from dataset.mnist import load_mnist
    from two_layer_net import TwoLayerNet
    
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
    
    train_loss_list = []
    
    # 하이퍼파라미터
    iters_num = 30 # 반복 횟수
    train_size = x_train.shape[0]
    batch_size = 100 # 미니배치 크기
    learning_rate = 0.1
    
    network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
    
    for i in range(iters_num):
        if i % 10 == 0: 
            print(i)
        
        # 미니배치 획득
        batch_mask = np.random.choice(train_size, batch_size)
        x_batch = x_train[batch_mask]
        t_batch = t_train[batch_mask]
        
        # 기울기 계산
        grad = network.numerical_gradient(x_batch, t_batch)
        
        # 매개변수 갱신
        for key in ('W1', 'b1', 'W2', 'b2'):
            network.params[key] -= learning_rate * grad[key]
            
        # 학습 경과 기록
        loss = network.loss(x_batch, t_batch)
        train_loss_list.append(loss)
    Out:   
    0
    10
    20

     

    train_loss_list
    Out:    
    [2.2979028528013656, 2.289561989395989, 2.295455483266069, 2.2935154256162282, 2.277463862128565, 2.2752854437434333, 2.270020053049954, 2.2956537839427864, 2.29842521545862, 2.2699155229928607, 2.2920925563590533, 2.3049166057934314, 2.280259988306481, 2.2973906677770635, 2.292411939836118, 2.296876030623053, 2.280973435219862, 2.3011925997922003, 2.281336709948265, 2.283788572019009, 2.3007604814517038, 2.2950249659466904, 2.299119888423805, 2.285759318066387, 2.2810774729536867, 2.279633341449812, 2.2818516609106574, 2.279602841748853, 2.291376674205719, 2.2849692767072347]

     

     

     

     

    코드 개선


    학습 도중 정기적으로 훈련 데이터와 시험 데이터를 대상으로 정확도 기록

    ==> 1 에폭별훈련&시험 데이터 정확도 기록

     

     

     

    * 에폭(epoch)이란?

    : 학습에서 훈련 데이터를 모두 소진했을 때의 횟수

    10,000개를 100개의 미니배치로 학습할 경우, 확률적 경사 하강법을 100회 반복하면 모든 훈련 데이터를 소진한 것. 이 경우 100회가 1에폭이 됨.

     

     

     

    +) 주의해야 할 것

    훈련 데이터의 정확도가 증가하는 반면, 시험 데이터의 정확도는 떨어지면 오버피팅이 된 것임

     

     

    import numpy as np
    from dataset.mnist import load_mnist
    from two_layer_net import TwoLayerNet
    
    (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
    
    train_loss_list = []
    train_acc_list = []
    test_acc_list = []
    
    # 하이퍼파라미터
    iters_num = 100 # 반복 횟수
    train_size = x_train.shape[0]
    batch_size = 10 # 미니배치 크기
    learning_rate = 0.1
    
    network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
    
    # 1에폭당 반복 수
    iter_per_epoch = max(train_size / batch_size, 1)
    
    for i in range(iters_num):
        if i % 10 == 0: 
            print(i)
        
        # 미니배치 획득
        batch_mask = np.random.choice(train_size, batch_size)   # 0 ~ (train_size-1)까지의 숫자 중, batch_size 개수만큼 뽑음
        x_batch = x_train[batch_mask]
        t_batch = t_train[batch_mask]
        
        # 기울기 계산
        grad = network.numerical_gradient(x_batch, t_batch)
        
        # 매개변수 갱신
        for key in ('W1', 'b1', 'W2', 'b2'):
            network.params[key] -= learning_rate * grad[key]
            
        # 학습 경과 기록
        loss = network.loss(x_batch, t_batch)
        train_loss_list.append(loss)
        
        # 1에폭당 정확도 계산
        if i % iter_per_epoch == 0:
            train_acc = network.accuracy(x_train, t_train)
            test_acc = network.accuracy(x_test, t_test)
            train_acc_list.append(train_acc)
            test_acc_list.append(test_acc)
            print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))
    Out:   
    train acc, test acc | 0.10218333333333333, 0.101
    ...
    ...

    댓글

dokylee's Tech Blog