MXNet 시작하기 (3) – Module API

2편에서는 Symbols를 사용하여 NDArrays에 저장된 데이터를 처리하는 연산 그래프를 정의하는 방법을 설명했습니다. 이 글에서는 Symbol 및 NDArrays에서 배운 것을 사용하여 일부 데이터를 준비하고 신경망을 구성합니다. 그런 다음 Module API를 사용하여 신경망 기반 데이터를 학습하고 결과를 예측해보겠습니다.

데이터셋 정의
(가상) 데이터 세트는 1000개의 데이터 샘플로 구성합니다.

  • 각 샘플에는 100개의 기능(Feature)이 있습니다.
  • 각 기능은 0에서 1 사이의 float 값으로 표현됩니다.
  • 샘플은 10개의 카테고리로 나뉩니다. 네트워크의 목적은 주어진 샘플에 대한 올바른 카테고리를 예측하는 것입니다.
  • 학습(Training)을 위해 800개의 샘플을 사용하고 검증(Validation)을 위해 200개의 샘플을 사용할 것입니다.
  • 학습 및 검증을 위해 배치(Batch) 크기 10을 사용합니다.

아래는 데이터 정의를 위한 간단한 MXNet 코드입니다.

import mxnet as mx
import numpy as np
import logging

logging.basicConfig(level=logging.INFO)

sample_count = 1000
train_count = 800
valid_count = sample_count - train_count

feature_count = 100
category_count = 10
batch=10

데이터셋 생성
균등 분포를 사용하여 1000 개의 샘플을 생성합시다. 이를 ‘X’라는 이름의 NDArray에 저장합니다. (즉, 1000라인, 100 컬럼)

X = mx.nd.uniform(low=0, high=1, shape=(sample_count,feature_count))

>>> X.shape
(1000L, 100L)
>>> X.asnumpy()
array([[ 0.70029777,  0.28444085,  0.46263582, ...,  0.73365158,
         0.99670047,  0.5961988 ],
       [ 0.34659418,  0.82824177,  0.72929877, ...,  0.56012964,
         0.32261589,  0.35627609],
       [ 0.10939316,  0.02995235,  0.97597599, ...,  0.20194994,
         0.9266268 ,  0.25102937],
       ...,
       [ 0.69691515,  0.52568913,  0.21130568, ...,  0.42498392,
         0.80869114,  0.23635457],
       [ 0.3562004 ,  0.5794751 ,  0.38135922, ...,  0.6336484 ,
         0.26392782,  0.30010447],
       [ 0.40369365,  0.89351988,  0.88817406, ...,  0.13799617,
         0.40905532,  0.05180593]], dtype=float32)

각 1000개의 샘플에 대한 카테고리는 0-9 범위의 정수로 표시됩니다. 랜덤하게 생성되어 ‘Y’라는 NDArray에 저장됩니다.

Y = mx.nd.empty((sample_count,))
for i in range(0,sample_count-1):
  Y[i] = np.random.randint(0,category_count)

>>> Y.shape
(1000L,)
>>> Y[0:10].asnumpy()
array([ 3.,  3.,  1.,  9.,  4.,  7.,  3.,  5.,  2.,  2.], dtype=float32)

데이터셋 나누기
다음 단계로 학습과 검증을 위해 데이터셋을 80/20으로 나눠야합니다. 이를 위해 NDArray.crop 함수를 사용합니다. 여기서 데이터 세트는 완전 무작위이므로 학습을 위해 상위 80%를 사용하고, 유효성을 검사하기 위해 하위 20%를 사용합니다. 실제 정식으로 할 때는 순차적으로 생성 된 데이터에 잠재적인 편향을 피하기 위해 데이터셋을 먼저 뒤섞어 놓아야 할 것입니다.

X_train = mx.nd.crop(X, begin=(0,0), end=(train_count,feature_count-1))

X_valid = mx.nd.crop(X, begin=(train_count,0), end=(sample_count,feature_count-1))

Y_train = Y[0:train_count]

Y_valid = Y[train_count:sample_count]

자, 이제 데이터 준비가 끝났습니다. 다음 단계로 넘어가 볼까요?

신경망 바인딩
우리가 만든 네트워크는 매우 간단합니다. 각 레이어를 살펴 보겠습니다.

  • 입력 레이어는 ‘data’라는 심볼로 표현됩니다. 나중에 실제 입력 데이터에 바인딩 할 것입니다.
data = mx.sym.Variable('data')
  • fc1에서 첫 번째 숨겨진 레이어는 64개 연결된 신경(Neurons)으로 구성됩니다. 즉, 입력 레이어의 각 기능은 64개의 모든 신경에 연결됩니다. 이를 위해 Symbol.FullyConnected 함수를 사용합니다.이 함수는 수동으로 각 연결을 만드는 것보다 훨씬 편리합니다!
fc1 = mx.sym.FullyConnected(data, name='fc1', num_hidden=64)
  • fc1의 각 결과 출력은 활성화 함수를 통해 수행합니다. 활성화 함수란 Sigmod처럼 어디에 속하는지 분류하기 위해 일정 값을 두고 그 값을 넘어야 성공 혹은 참으로 분류하는 함수 입니다. 여기서 우리는 Rectified Linear Unit , 일명 ‘ReLU’를 사용합니다.  (역자주: ReLU는 신경망 학습에서 매우 중요한 성능을 높이는 판단 기준을 제안한 것으로, 출력이 0보다 작을 때는 0을 사용하고, 0보다 큰 값에 대해서는 해당 값을 그대로 사용하는 방법입니다. 음수에 대해서는 값이 바뀌지만, 양수에 대해서는 값을 바꾸지 않습니다.)
relu1 = mx.sym.Activation(fc1, name='relu1', act_type="relu")
  • fc2에서 두 번째 숨겨진 레이어는 연결된 10개의 뉴런으로 구성되며 10개의 카테고리에 매핑됩니다. 각 뉴런은 임의의 부동 소수점 값을 출력합니다. 10개의 값 중 가장 큰 값은 데이터 샘플의 가장 가능성 있는 카테고리를 나타냅니다.
fc2 = mx.sym.FullyConnected(relu1, name='fc2', num_hidden=category_count)
  • 출력 레이어는 fc2 레이어에서 10개의 값에 Softmax 함수를 적용합니다.이 값은 0과 1사이의 10 개의 값으로 변환되어 1을 가산합니다. 각 값은 각 카테고리의 예상 확률을 나타내며 가장 가능성 있는 큰 카테고리를 가리키게 됩니다. (역자주: Softmax 함수는 자연수 N개의 값에서, n 번째 값의 중요도를 찾는 함수로서 각각의 값의 편차를 확대시켜 큰 값은 상대적으로 더 크게, 작은 값은 상대적으로 더 작게 만든 다음 정규화 시키는 함수이다.)
out = mx.sym.SoftmaxOutput(fc2, name='softmax')
mod = mx.mod.Module(out)

데이터 반복자(Iterator) 만들기
Part 1에서는 한 번에 학습되지 않은 하나의 샘플을 가진 신경망를 살펴 보았습니다. 성능 관점에서 볼 때 매우 비효율적이죠. 대신에 고정 된 수의 샘플인 배치(Batch)를 사용합니다.

이러한 배치를 네트워크에 전달하려면 NDArrayIter 함수를 사용하여 반복기(Interna)를 만들어야합니다. 그 파라미터는 학습 데이터, 카테고리 (MXNet에서는 라벨(Label)이라고 부름) 및 배치 크기입니다.

아래 처럼, 한 번에 10개의 샘플과 10개의 레이블로 데이터셋을 반복 할 수 있습니다. 그런 다음 reset() 함수를 호출하여 반복기 상태를 원래로 복원합니다.

train_iter = mx.io.NDArrayIter(data=X_train,label=Y_train,batch_size=batch)
>>> for batch in train_iter:
...   print batch.data
...   print batch.label
...
[<NDArray 10x99 @cpu(0)>]
[<NDArray 10 @cpu(0)>]
[<NDArray 10x99 @cpu(0)>]
[<NDArray 10 @cpu(0)>]
[<NDArray 10x99 @cpu(0)>]
[<NDArray 10 @cpu(0)>]
<edited for brevity>
>>> train_iter.reset()

우리가 만든 데이터셋을 통한 네트워크는 이제 학습 준비가 끝났습니다!

모델 학습하기
먼저 입력 심볼을 실제 데이터셋 (샘플 및 레이블)에 바인딩합니다. 반복기를 통해 쉽게 할 수 있겠죠?

mod.bind(data_shapes=train_iter.provide_data, label_shapes=train_iter.provide_label)

다음으로, 네트워크에서 뉴런 가중치를 초기화 해 봅시다. 이것은 실제로 매우 중요한 단계입니다. “올바른”기술로 초기화하면 네트워크가 훨씬 빨리 학습하는 데 도움이 됩니다. Xavier 초기화(이것을 만든 Xavier Glorot ( PDF)의 이름을 따서 명명)는 이러한 기술 중 하나입니다.

# Allowed, but not efficient
mod.init_params()
# Much better
mod.init_params(initializer=mx.init.Xavier(magnitude=2.))

다음으로 최적화 매개 변수를 정의해야 합니다.

  • 우리는 기계 학습 및 딥러닝 애플리케이션에 오랫동안 사용해온 Stochastic Gradient Descent 알고리즘 (일명 SGD)을 사용하고 있습니다.
  • 우리는 학습 속도를 SGD의 일반적 값인 0.1로 설정하고 있습니다.
mod.init_optimizer(optimizer='sgd', optimizer_params=(('learning_rate', 0.1), ))

이제 신경망에 대한 학습을 시작합니다. 50번의 학습 단계(Epochs)를 반복합니다. 즉, 전체 데이터셋이 네트워크를 통해 50회 (10개 샘플 배치 처리)로 진행합니다.

mod.fit(train_iter, num_epoch=50)
INFO:root:Epoch[0] Train-accuracy=0.097500
INFO:root:Epoch[0] Time cost=0.085
INFO:root:Epoch[1] Train-accuracy=0.122500
INFO:root:Epoch[1] Time cost=0.074
INFO:root:Epoch[2] Train-accuracy=0.153750
INFO:root:Epoch[2] Time cost=0.087
INFO:root:Epoch[3] Train-accuracy=0.162500
INFO:root:Epoch[3] Time cost=0.082
INFO:root:Epoch[4] Train-accuracy=0.192500
INFO:root:Epoch[4] Time cost=0.094
INFO:root:Epoch[5] Train-accuracy=0.210000
INFO:root:Epoch[5] Time cost=0.108
INFO:root:Epoch[6] Train-accuracy=0.222500
INFO:root:Epoch[6] Time cost=0.104
INFO:root:Epoch[7] Train-accuracy=0.243750
INFO:root:Epoch[7] Time cost=0.110
INFO:root:Epoch[8] Train-accuracy=0.263750
INFO:root:Epoch[8] Time cost=0.101
INFO:root:Epoch[9] Train-accuracy=0.286250
INFO:root:Epoch[9] Time cost=0.097
INFO:root:Epoch[10] Train-accuracy=0.306250
INFO:root:Epoch[10] Time cost=0.100
...
INFO:root:Epoch[20] Train-accuracy=0.507500
...
INFO:root:Epoch[30] Train-accuracy=0.718750
...
INFO:root:Epoch[40] Train-accuracy=0.923750
...
INFO:root:Epoch[50] Train-accuracy=0.998750
INFO:root:Epoch[50] Time cost=0.077

훈련의 정확도는 빠르게 상승하여 50 단계 후에 99+%에 도달합니다. 우리가 만든 네트워크에서 학습 세트를 잘 수행을 했으며, 아주 좋은 결과를 보입니다. 이제 유효성 검사를 수행해 보겠습니다.

모델 유효성 검사하기
이제 우리는 네트워크에 새로운 데이터 샘플, 즉 학습에 사용하지 않은 데이터 20%를 통해 유효성 검사를 해보겠습니다. 유효성 검사 샘플과 라벨을 사용하여 반복기를 먼저 만듭니다.

pred_iter = mx.io.NDArrayIter(data=X_valid,label=Y_valid, batch_size=batch)

그 다음으로 Module.iter_predict() 함수를 사용하여 네트워크를 통해 20% 샘플을 실행합니다. 이제 예상 라벨과 실제 라벨을 비교합니다. 유효성 점수와 검증 정확도를 추적하는데, 이는 네트워크가 유효성 검증셋의 수행 결과가 잘 되었는지 알아 볼 수 있습니다.

pred_count = valid_count
correct_preds = total_correct_preds = 0

for preds, i_batch, batch in mod.iter_predict(pred_iter):
    label = batch.label[0].asnumpy().astype(int)
    pred_label = preds[0].asnumpy().argmax(axis=1)
    correct_preds = np.sum(pred_label==label)
    total_correct_preds = total_correct_preds + correct_preds

print('Validation accuracy: %2.2f' % (1.0*total_correct_preds/pred_count))

뭔가 막 잘되고 있는 것 같죠? 🙂

iter_predict()는 다음 값을 반환합니다.

  • i_batch : Batch 번호
  • batch : NDArrays 배열로 현재 배치를 저장하는 단일 NDArray가 있습니다. 현재 배치에서 10개의 데이터 샘플의 레이블을 찾는 데 사용합니다. 레이블을 numpy 배열 (10개 요소)에 저장합니다.
  • preds : NDArrays의 배열로 현재 배치에 대해 예측된 레이블을 저장하는 단일 NDArray가 있습니다. 각 샘플에 대해 10개의 범주 (10×10 매트릭스)에 대한 확률을 예측합니다. 따라서, argmax() 함수를 사용하여 가장 높은 값, 즉 가장 가능성이 큰 카테고리의 인덱스를 찾습니다. 특히, pred_label은 현재 배치의 각 데이터 샘플에 대한 예측 카테고리를 보유하는 10 요소 배열입니다.

그런 다음 Numpy.sum()을 사용하여 label과 pred_label의 동일한 값의 수를 비교합니다.

마지막으로 검증 정확도를 계산하여 표시합니다.

Validation accuracy: 0.09

음… 9%라니? 이것은 정말로 나쁜 결과네요! 하지만, 데이터 세트가 더 무작위였다면 결과는 더 좋을 것입니다.

결론은 실제로 신경망 학습으로 많은 것을 배울 수 있지만, 우리의 결과처럼 데이터가 무의미하다면 아무 것도 예측할 수 없게됩니다. 쓰레기 입력, 쓰레기 출력(Garbage in, garbage out)이 있을 뿐이죠!

하지만, MXNet으로 신경망 학습을 하는 아주 기본적인 소스 코드를 배울 수 있었습니다. 자신의 실제 데이터에 사용하려면 시간을 투자하십시오. 이것이 배우는 가장 빠른 방법입니다. 다음 글 부터는 실제 데이터를 활용하여 학습을 해보겠습니다.

다음 글: MXNet 시작하기 (4) – 이미지 분류를 위한 학습 모델 사용하기 (Inception v3)

연재 순서

코드 전체 보기

여러분의 생각