프로필사진

IT Anthology/encyclopedia

[밑러닝] 밑바닥부터 구현하는 인공신경망 학습 알고리즘

다각 2020. 3. 29. 00:03

* 이 글은 <밑바닥부터 시작하는 딥러닝2 (저자: 사이토 고키)> 책을 읽으며 정리한 글입니다.
* 나중에라도 제가 참고하기 위해 정리해 두었으며, 모든 내용을 적은 것이 아닌,
필요하다고 생각되는 부분만 추려서 정리한 것임을 미리 밝힙니다.


목차

  1. 딥러닝(deep learning)에서의 러닝(learning)
  2. 2층 신경망 클래스의 학습(learning) 구현

이제까지 배운 모든 것을 종합하여 드디어 딥러닝의 러닝을 구현할 수 있게 되었다!

앞의 내용을 놓친 사람은 필요한 내용을 리마인드하고 오도록 하자.

 

2020/03/11 - 인공신경망 구현을 위해 알아야할 것들 (활성화 함수와 소프트맥스)

2020/03/17 - 손글씨 숫자 인식으로 해보는 간단한 인공신경망 예측(feat. MNIST 데이터셋)

2020/03/17 - 인공신경망의 학습 지표, 손실함수(loss function)

2020/03/28 - 수치 미분, 딥러닝에서 경사하강법을 위한 기울기 계산

 


1. 딥러닝(deep learning)에서의 러닝(learning)

딥러닝(deep learning)에서의 러닝(learning)이란, 인공신경망에 존재하는 가중치편향을 훈련 데이터에 잘 들어맞도록 조정하는 과정을 뜻한다. 학습의 과정은 다음과 같다.

  1. 미니배치
  2. 기울기 벡터 산출
  3. 매개변수 갱신
  4. 반복

일단, (1) 미니배치 과정을 통해 훈련 데이터 중 일부를 가져온다. 다음, 미니배치에 있는 데이터들을 이용하여 각 가중치 매개변수의 (2) 기울기 벡터를 산출한다. 이렇게 산출한 기울기 벡터를 이용해, 손실 함수의 값을 가장 작게 하는 방향을 향해 (3) 매개변수를 갱신한다. 마지막으로 1단계~3단계를 (4) 반복한다.

 

2. 2층 신경망 클래스의 학습(learning) 구현

앞선 포스팅을 통해 간단한 3층 신경망을 구현하였고, 이어서 적절히 학습된 가중치들을 통해 새로운 데이터의 예측값을 산출하는 과정을 배웠다. 그러면 이 중간에 있는, '가중치를 적절히 학습시키는 법' 에 대해 지금부터 논해보도록 하자.

2-(1). 2층 신경망 클래스 구성

일단 2층짜리 인공신경망을 생성해보자.

import numpy as np

class TwoLayerNet:
    # 인스턴스 생성
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # 가중치 초기화
        self.params = {}
        self.params['W1'] = np.random.normal(0, weight_init_std, (input_size, hidden_size))
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = np.random.normal(0, weight_init_std, (hidden_size, output_size))
        self.params['b2'] = np.zeros(output_size)
# ----------------------------------------------------------------------------------------------
    # 활성화 함수 정의
    def sigmoid(self, x):
        return 1/(1 + np.exp(-x))

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

    # 비용 함수 정의
    def cee(self, y, t):    # cross_entropy_error
        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


    # 다중 차원의 배열에서 gradient 계산 함수 정의
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 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 = self.sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = self.softmax(a2)

        return y

    # 손실 함수 정의
    def loss(self, x, t):
        y = self.predict(x)    # 예측값 산출
        return self.cee(y, t)

    # 정확도 산출 함수 정의
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis = 1)    # 예측값에서 최대 확률을 가지는 위치 뽑아내기
        t = np.argmax(t, axis = 1)    # 정답에서 최대 확률을 가지는 위치 뽑아내기
        return np.sum(y == t) / float(x.shape[0])    # 테스트 케이스의 총 개수 중에 정답을 맞춘 비율 구하기

    # 수치 미분으로 gradient 계산
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)

        grads = {}
        grads['W1'] = self.numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = self.numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = self.numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = self.numerical_gradient(loss_W, self.params['b2'])

        return grads

2-(2). 미니배치 학습 구현

랜덤하게 학습 데이터 중 일부를 뽑아내어 학습에 넣는 과정을 구현해보자.

# 하이퍼파라미터 정의
iters_num = 1000  # 반복 횟수를 적절히 설정한다.
train_size = x_train.shape[0]
batch_size = 100   # 미니배치 크기
learning_rate = 0.1

# 인공신경망 인스턴스 생성
network = TwoLayerNet(input_size = 784, hidden_size = 50, output_size = 10)

# loss와 accuracy의 변화를 추적하기 위한 리스트 생성
train_loss_list = []
train_acc_list = []
test_acc_list = []

# 1에폭당 반복 수
iter_per_epoch = max(train_size / batch_size, 1)

# iter_num만큼 가중치 갱신
for i in range(iters_num):

    # 미니배치 획득
    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)

    # 시험 데이터로 모델 평가
    # 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))

위의 코드를 실행하면 다음과 같은 결과가 나온다.

거듭 학습할수록 loss값이 떨어지는 결과를 보여준다. 학습이 제대로 이루어졌다는 뜻이다.

 

어쩐지 책의 그래프와는 조금 달라 보이는데, 책의 코드를 작성하는 과정에서 내 나름대로 조금씩 변형했기 때문이라고 추정된다. 1000회 째에 loss값이 0.5까지 떨어지며, 책의 결과(~1.0)보다 더 좋은 결과를 보여주고 있다. 

 

정확도로 성능을 측정하면 다음과 같다. 첫번째 줄은 첫번째 학습 단계에서의 정확도이고, 두번째 줄은 마지막 학습 단계에서 산출한 정확도이다.

현저히 높아진 정답률을 볼 수 있다.

 


 

 

정리

지금까지, 머신러닝의 러닝 과정에 대해 알아보고 구현해보았다. 우리가 어렸을 때 학습지를 풀며 오답의 비율을 보고 나의 학습 성과를 측저했던 것처럼, 인공신경망 또한 '손실 함수'를 기준으로 오답이 적어지는 방향으로 학습한다는 사실을 숙지했다.

 

하지만, (위의 코드를 돌려 본 사람이면 눈치를 챘겠지만) 학습에 너무 오랜 시간이 걸린다. 다음 포스트에서는 이러한 학습의 시간을 효과적으로 단축시키는 오차역전파법에 대해 알아보도록 하겠다.