[Cloud ML Tutorial]:
Training CIFAR-10 Estimator Model with TensorFlow
for Hyperparameter Optimization



ABSTRACT

CIFAR-10 이미지 데이터를 분류하는 ResNet 모델을 텐서플로우(tensorflow) 고차원 API로 작성한 레퍼런스 코드를 분석하고 이 모델을 CPU, GPU, 구글 클라우드 플랫폼(GCP)의 기계학습 엔진(ML 엔진) 서비스에서 학습하는 방법을 기술한다. GCP에서 딥러닝의 초모수를 찾아주는 자동화 서비스인 초모수 미세조정(hyperparameter tuning) 기능을 사용하고 분산 환경을 이용해 효과적인 초모수 최적화 방법에 대한 논의한다.



INTRODUCTION

텐서플로우는 구글에서 개발한 오픈소스 딥러닝 프레임워크로써 다양한 프로그래밍 언어에서 텐서(Tensor)를 처리할 수 있는 API를 이용해 데이터 흐름 형태의 모델(Dataflow-like model)을 구현하면, 다양한 형태의(heterogeneous) 하드웨어에서 분산 처리할 수 있는 기능을 제공한다. 텐서는 임의의 다차원의 배열을 지칭하는데, 일반적인 컴퓨팅 기기에서는 메모리에 올려서 처리하기 힘들 정도로 매우 큰 크기(big data)를 갖거나 행렬 분해(matrix decomposition) 등의 연산을 처리하는데 오랜 시간이 걸리기 때문에 딥러닝을 활용하는데 걸림돌이 되었다. 텐서플로우는 이런 대용량 데이터를 효과적으로 다룰 수 있는 최적화 알고리즘을 제공할 뿐 아니라, 상이한 기기들로 이뤄진 분산 시스템에서도 모델을 효과적으로 학습하거나 활용할 수 있도록 설계하여 연구 뿐 아니라 실제 서비스에도 활용할 수 있다.


CIFAR-10은 캐나다 고등연구소(CIFAR)에서 이미지 분류(image classification problem) 문제 해결을 위해 수집해 정리한 데이터(labeled data)로 다양한 기계 학습(machine learning) 알고리즘의 성능을 비교하기 위한 벤치 마크(benchmark)로 빈번히 사용된다. 이 데이터 셋에는 비행기, 자동차, 배, 트럭, 새, 고양이, 사슴, 개, 개구리, 말 열 가지 종류의 사진이 각각 6천 장 씩 32x32 크기의 RGB 컬러 이미지(color image)로 편집되어 있다. 대부분 이중 5만 장을 학습 데이터(training data)로 만 장을 평가 데이터(test data)로 나눠 사용하는데 일반화 오류(generalization error)를 줄이기 위해 평가 데이터는 절대 알고리즘을 학습하는 데 사용하지 않고 오직 성능을 실험하는데만 사용한다. 필요한 경우 평가 데이터의 일부를 다시 유효성 확인 데이터(validation data)로 분리하여 교차 검증를 통해 모델의 과적합(overfitting)을 막거나 알고리즘의 초모수(hyperparameter)를 최적화하는데 사용하기도 한다.


여기서는 튜토리얼로 공개된 텐서플로우 레퍼런스 모델의 일부인 CIFAR-10 estimator를 상세 분석해 텐서플로우의 고차원 API를 통해 모델을 생성하는 방법과 분산 환경을 포함한 다양한 컴퓨팅 기기에서 학습을 수행하기 위한 방법을 이해하고자 한다. 이후로 관련 연구로써 학습 모델로 사용된 깊은 신경망(deep neural network - DNN)의 하나인 ResNet과 초모수 최적화 기법 등 딥러닝의 실제적 적용을 위해 필요한 기법들을 설명하고 튜토리얼 코드에 대해 구조 분석과 상세 분석을 수행한다. 해당 코드를 CPU, GPU, ML 엔진 등에서 각각 수행한 결과를 비교하고 학습 데이터에 최적화된 모델을 찾기 위한 초모수 최적화 알고리즘을 클라우드 환경을 이용해 수행하는 방법을 모색한다.



기계 학습은 주어진 과제 T를 학습하기 위해 경험 혹은 데이터 E 로 목표 성능 P 을 달성하기 위한 계산 통계학적 알고리즘(Tom Mitchell, 1997)이다. 이중 딥러닝이라고 불리는 심층 신경망(deep neural network)은 뉴런(neuron)이라고 불리는 비선형 처리 유닛(nonlinear processing units)으로 구성된 계층(layer)를 겹겹히 쌓고 학습 과정에서 알고리즘을 통해 뉴런들의 가중치(weights)를 적절히 업데이트되도록 만들어 데이터의 특성(feature)를 자동으로 학습해 가는 표현 학습(representation learning) 알고리즘의 하나이다. 표현 학습은 고차원의 데이터에서 정보를 인식하는 데 필요한 수준에 알맞는 저차원 표현(representation)으로 변환하는 정보 처리 알고리즘이다. 최근 활발하게 논의되고 있는 인공지능(AI)은 포괄적인 측면에서 추론(reasoning), 지식(knowledge), 계획(planning), 자연어 처리(natural language processing), 인지(perception) 및 물체 제어(manipulating objects)를 기계 스스로 할 수 있도록 만든 것을 의미하나, 엄밀히 말해서는 기계를 통해 정보를 처리할 수 있도록 만든 모든 알고리즘을 의미한다.


ResNet은 마이크로소프트 연구소에서 고안한 극단적으로 깊은 DNN 구조(extremely deep architecture)로써 2015년 이미지 구별 문제를 위한 벤치마크인 이미지넷(ImageNet)에서 무려 152층으로 구성한 아키텍쳐가 최고 성능을 보인 바 있다. 이로써 DNN이 더 깊어질 수록 더 좋은 성능을 가질 것이라는 기대를 가지게 만들었다. 일반적으로 DNN이 깊어질 수록 미분 값 상실(vanishing gradients) 등의 문제 때문에 학습하기 힘들어 져서 적절한 가중치 초기값 설정(weight initialization) 및 정칙화(regularization) 기법이 필수적이다. 이는 주어진 과제(task)와 학습 데이터의 특성에 따라 크게 변하는 초모수(hyperparameter) - 모수(parameter)를 학습하기 위해 필요한 모수라는 의미 - 으로 적절히 최적화하지 않으면 목표 성능을 달성하기 힘들다.


딥러닝을 포함한 대부분의 기계학습 알고리즘으로 적절한 모델을 학습하는 절차는 다음과 같다. 먼저 데이터의 특성과 알고리즘에 대한 이해를 바탕으로 기계 학습 모델을 선정하고 관련된 초모수의 초기 값을 정한다. 학습할 데이터를 학습 데이터, 유효성 확인 데이터 및 평가 데이터로 적절히 나누는데 학습할 데이터의 양이 부족할 경우 데이터 증강(data augmentation)을 수행해 데이터를 늘리거나 k겹 교차검증(k-fold cross validation)으로 실험을 반복하여 결과에 대한 통계적 신뢰도를 높여야 한다. DNN의 경우 보통 데이터를 잘게 쪼개 경사 하강법(mini batch gradient descent)을 반복 수행하면서 과적합이 발생하지 않는 횟수에서 학습을 마치고 평가 데이터로 최종 성능을 평가한다. 이때 목표 성능을 만족하지 못할 경우 모델 선정 혹은 초모수 설정(hyperparameter tuning)후 이전의 과정을 반복한다.


Cloud ML 엔진은 구글 클라우드 플랫폼(Google Cloud Platform, GCP)에서 제공하는 서비스 형태의 플랫폼(PaaS)의 하나인 기계학습 플랫폼이다. GCP는 구글의 데이터센터 인프라를 기반으로 한 서비스 형태의 플랫폼(PaaS)으로써 빅데이터 저장, 처리, 분석 및 기계학습을 수행할 수 있는 기능을 제공하는 클라우드 서비스이다. 모든 기능은 가상 기기(Virtual Machine, VM) 형태로 필요할 때만 동작하고 Open API를 통해 생성, 제어, 제거가 가능하다. ML 엔진은 텐서플로우로 작성한 모델을 다양한 VM에서 학습할 수 있는 온라인 분산 처리 기능을 제공한다. 하둡(hadoop)과 같은 여타의 분산 처리 시스템과 같이 일(job)을 생성해 전달하면 작업이 처리된다. 이 기능을 사용하면 서비스의 성능 목표에 따라 규모를 확장하거나 속도를 향상시킬 수 있으나 제대로 사용하기 위해서는 클라우드 컴퓨팅에 대한 이해가 필수적이다.



CODE ANALYSIS

깃허브를 통해 공개된 CIFAR-10 데이터에 대한 튜토리얼 프로젝트는 두 종류가 있는데 하나는 저수준 API를 사용하고 나머지는 고수준 API를 사용해 구현한 것이다. 이 포스팅에서는 두 번째 프로젝트을 기준으로 설명한다. 첫 번째 프로젝트은 CPU와 GPU 및 multiple GPUs를 활용해 각각 학습할 수 있으나 ML 엔진에서 학습하는 기능을 제공하지 않는다. 공식 홈페이지에서 상세한 설명을 제공하고 있으니 참고하기 바란다.


High Level Analysis

분석 대상 프로젝트는 파이썬 스크립트 파일(.py)로 구성되어 있으며 파일 별 역할은 아래와 같다.




파일명 역할 기타
generate_cifar10_tfrecords.py CIFAR-10 다운로드 후 데이터를 tfrecords 형태로 변환 파이썬 2.7.x 에서만 정상 수행
cifar10.py CIFAR-10 데이터 가공
cifar10_main.py 학습 및 평가 수행 명령줄 변수로 데이터 위치 등을 전달 필요
cifar10_model.py ResNet 모델 생성
cifar10_utils.py 병렬 학습 및 로깅
model_base.py ResNet 구성요소



각 파일 별 의존성(dependency)은 아래의 그림과 같다. 필수적으로 텐서플로우의 버전은 1.3 이상으로 설치되어야 한다. tfrecords 형태로 데이터를 변환하기 위한 코드는 cPickle 모듈을 참조하는데 이 모듈은 파이썬 3에서는 제거되어 파이썬 2에서만 동작한다. 만일 파이썬 3 버전에서 수행해야만 한다면 정상동작하지 않을 수 있다.


클래스 구조 및 상속 관계는 다음과 같다. 

Cifar10DataSet 클래스는 cifar10.py 에 정의되어 있고 tfrecords 데이터의 경로를 받아 그 일부를 make_batch() 함수의 결과 값으로 전달한다. 

use_distortion 속성으로 데이터 증강(data augumentation) 수행 여부를 결정할 수도 있다. 


model_base.py 에 정의되어 있는 ResNet 클래스는 추상 클래스의 역할을 하고 ResNet의 구성요소인 convolution, batch normalization, ReLU, fully connected layer, averge pooling unit들을 정의하고 있다. 

cifar10_model.py 에서 *ResNetCifar10 클래스가 이를 상속 받아서 forward_pass()에서 네트워크를 구성한다.



Detailed Analysis

지금부터는 학습이 실행될 때 호출 흐름과 API 사이의 참조하는 흐름에 따라 상세하게 살펴본다.


High level API call flow

시작 지점(entry point)은 cifar10_main.pyargparse.ArgumentParser 클래스를 사용해 명령 줄에서 입력 받아야 하는 모수들을 정의하고 해당 모수 값들을 main() 함수에 전달한다.

if __name__ == '__main__':
    parser = argparse.ArgumentParser()

    # omitted  
    parser.add_argument(
        '--eval-batch-size',
        type=int,
        default=100,
        help='Batch size for validation.')

    # omitted 
    main(**vars(args))


main() 함수에서는 그래프 모델을 수행할 세션(session) 설정 후 고수준 API인 learn_runner.run() 함수를 호출한다.

tf.contrib.learn.learn_runner.run(
  experiment_func, 
  run_config,
  hparams)

주의할 점은 learn_runner.run() 함수의 첫번째 모수로 함수 이름이 전달되는 데 이때 또다른 내부 함수(_resnet_model_fn()) 객체를 전달하는 구조이다. 파이썬이 First-class function를 지원하기 때문애 가능한 패턴이다. 

이 패턴의 기능에 대해 상세하게 설명하면 해당 모수의 값으로 정의한 함수를 전달하면 해당 함수가 필요한 시점에서 임의의 또다른 모수 값을 이용해 실행할 수 있게 된다.


experiment_functf.contrib.learn.Experiment() 클래스 객체를 반환해야하는 제약 조건이 있다. 

tf.contrib.learn.Experiment() 클래스 객체를 생성할 때 학습 및 평가에 필요한 데이터를 전달하는 함수 및 학습 반복 횟수 등을 정의해야 한다. 첫번째 모수인 classifier 에는 tf.estimator.Estimator() 클래스 객체를 전달해야 한다. 

이 클래스 객체를 생성할 때 초모수 최적화 시 필요한 값들을 전달해야 한다. tf.estimator.Estimator() 클래스 객체의 첫번째 생성자 모수로 model_fn 가 필요한데 이 모수에 전달될 함수는 tf.estimator.EstimatorSpec() 객체를 반환해야 한다. 

결국 tf.estimator.EstimatorSpec() 클래스 객체를 생성할 때 정의한 그래프 모델 및 목적 함수 등의 중요한 모수들이 전달된다.


Model training flow

지금까지 흐름이 cifar10_main.py 의 _resnet_model_fn() 에 구현되어 있다. 여기서 핵심적인 코드를 간추려서 살펴보자.

    def _resnet_model_fn(features, labels, mode, params):
        """Resnet model body.

        """

        for i in range(num_devices):
            with tf.variable_scope('resnet', reuse=bool(i != 0)):
                with tf.name_scope('tower_%d' % i) as name_scope:
                    with tf.device(device_setter):
                        loss, gradvars, preds = _tower_fn(
                            is_training, weight_decay, tower_features[i], tower_labels[i],
                            data_format, params.num_layers, params.batch_norm_decay,
                            params.batch_norm_epsilon)                

        # Device that runs the ops to apply global gradient updates.
        with tf.device(consolidation_device):

            num_batches_per_epoch = cifar10.Cifar10DataSet.num_examples_per_epoch(
                'train') // (params.train_batch_size * num_workers)
            boundaries = [ omitted ]
            staged_lr = [ omitted ]

            learning_rate = tf.train.piecewise_constant(tf.train.get_global_step(),
                                                        boundaries, staged_lr)
            # Create a nicely-named tensor for logging
            learning_rate = tf.identity(learning_rate, name='learning_rate')

            optimizer = tf.train.MomentumOptimizer(
                learning_rate=learning_rate, momentum=momentum)

이 함수는 매우 복잡하고 많은 일들을 하고 있어 분석하기 매우 까다롭다. (Here lives a dragon


개략적으로 보면 학습을 분산처리하기 위해서 여러 개의 GPU에 각각의 모델을 만들어서 병렬적으로 학습해서 각각 미분 값(gradients)를 구한 후 합쳐서 반영하는 타워(tower) 패턴을 쓰고 있다.
결국 GPU 별로 모델을 만드는(build) 과정과 loss를 구하는 데이터 흐름 처리는 _tower_fn() 함수에서 확인할 수 있다.

def _tower_fn(is_training, weight_decay, feature, label, data_format,
              num_layers, batch_norm_decay, batch_norm_epsilon):
    """Build computation tower (Resnet).
    """
    model = cifar10_model.ResNetCifar10(num_layers)
    logits = model.forward_pass(feature, input_data_format='channels_last')
    tower_pred = {
        'classes': tf.argmax(input=logits, axis=1),
        'probabilities': tf.nn.softmax(logits)
    }

    tower_loss = tf.losses.sparse_softmax_cross_entropy(
        logits=logits, labels=label)
    tower_loss = tf.reduce_mean(tower_loss)

cifar10_model.ResNetCifar10 클래스 인스턴스를 생성할 때 입력받은 레이어의 개수 만큼 forward pass() 메소드에서 모델을 만드는 작업을 수행한다. 

아래 그림은 네트워크 아키텍쳐를 만드는 순서를 도식화한 것이다. 


여기서 cifar10_main.py를 명령줄 매개변수로 초모수인 레어어 개수에 해당하는 Residual Block를 만들게 된다. 



Residual Block은 버전 1.0과 2.0 그리고 병목(bottleneck) 2.0 버전이 있는데 기본 값은 지름길 연결(shortcut connection)를 위해 플랜 A를 쓰며 2개의 하위 계층을 갖는 버전 1.0이다. 

Residual Block 버전 1.0의 세부 구조는 다음 그림과 같다. 

해당 코드는 model_base.py_residual_v1() 메소드에서 구현되어 있으니 참고하기 바란다. 



METHODS


시작 함수에서부터 고수준 API를 사용해 수행되는 흐름을 통해 모델을 만드는 과정과 만든 모델을 분산해서 학습하는 과정을 튜토리얼 코드를 통해 상세히 살펴보았다. 지금부터 이 코드를 다양한 수행 환경에서 학습을 진행하는 법을 정리한다.


먼저 자신의 개발 환경에 텐서플로우를 설치한다. 

GCP를 사용하면 크롬 브라우저에서 Google Cloud Shell을 통해 미리 개발환경이 구성된 가상 머신(VM)에서 손쉽게 사용할 수 있지만 사용할 때 비용이 발생하는 유료 서비스인 만큼 소프트웨어 개발 도구(SDK)을 자신이 선호하는 개발 환경에 설치해서 사용한다. (이때, 필요한 파이썬(python) 버전은 2.7.x 로써 텐서플로우와 같이 사용하기를 원한다면 2.7.x 버전용 텐서플로우를 설치해야 함에 주의한다.)

더불어 GCP에 대한 이해가 부족하다면 먼저 참고문헌에 기재한 GCP 입문서를 읽기를 추천한다.


Create TFRecord Dataset

텐서플로우는 다양한 형식의 데이터셋을 읽어들일 수 있는 방법를 제공하지만 그중 TFRecords 형식을 표준으로 삼는다. 아래 명령으로 CIFAR-10 데이터를 온라인에서 다운로드 받아서 TFRecords 파일 포멧으로 변경할 수 있다. 

(단, 이 코드는 파이썬 2.7.x에서만 정상적으로 동작한다.)

MY_BUCKET=${PWD}/cifar-10-data
python generate_cifar10_tfrecords.py --data-dir=${MY_BUCKET}

ML 엔진에서 활용하기 위해서는 --data-dir= 값으로 ${MY_BUCKET} GCP Storage 경로를 입력하거나 해당 버킷에 생성된 파일들을 업로드해야 한다.

MY_BUCKET=gs://cifar-10-data/
python generate_cifar10_tfrecords.py --data-dir=${MY_BUCKET}


Training with CPU Only

텐서플로우 CPU 버전(버전 1.3 이상)이 설치된 환경에서는 GPU 없이 CPU 만으로도 다음과 같이 튜토리얼을 학습할 수 있다. 이 코드는 train-steps 수 만큼의 학습을 반복하고 검증(evaluation)을 수행한다. 학습에 오랜 시간이 걸리기 때문에 디버깅 용도로만 활용하는 것이 바람직하다.

python cifar10_main.py --data-dir=${MY_BUCKET} \
                       --job-dir=/tmp/cifar10 \
                       --num-gpus=0 \
                       --train-steps=100



Training with CPU & GPUs

텐서플로우 GPU 버전(버전 1.3 이상, NVIDIA GPU card 설치 및 CUDA, CuDNN 라이브러리 필요)이 설치된 개발환경에서는 다음과 같이 GPU를 사용해서 빠르게 학습할 수 있다. 아래 명령은 개발환경에 두 대의 GPU가 설치되어 있다고 가정한다. 설치된 GPU 수 이하의 num-gpus 수를 정해야 정상 동작한다. 참고로 특별한 설정이 없는 한 모든 GPU의 메모리를 텐서플로우가 할당해서 사용하기 때문에 GPU가 동작하지 않더라도 메모리가 없어 계산을 수행하지 않는 GPU를 쓸 수 없으니 주의 바란다.

python cifar10_main.py --data-dir=${MY_BUCKET} \
                       --job-dir=/tmp/cifar10 \
                       --num-gpus=2 \
                       --train-steps=10000



Training with ML Engine

이제 이 튜토리얼 코드를 GCP의 ML 엔진에서 돌려보도록 하자. 앞서 설명한 것처럼 GCP를 쓰기 위해서는 Google Cloud Shell에 접속하거나 자신의 개발 환경에 개발 도구(SDK)를 설치해야 한다. ML 엔진에서 학습을 수행하기 위해서는 gcloud 명령을 사용해야 한다.

GCP에서 학습 작업(Trainig Job)을 수행하는 것은 하둡이나 스파크와 같이 여타의 분산처리 시스템에서와 유사하게 분산 학습을 수행할 패키지(package)를 만들어서 서버에 제출(submit)하는 형태가 된다. 따라서 수행할 코드가 어떠한 컴퓨팅 기기에서도 동작할 수 있도록 코드를 개발한 개발 환경과 의존성을 없는 상태가 되어야 하고 필요하다면 패키지 내에 포함될 수 있도록 해야 한다. 이후 이것을 자세히 살펴보자.


먼저 빈번히 변경되는 입력 값을 환경변수로 정의하자.

MY_BUCKET=gs://cifar-10-data/
JOB_ID=cifar_train_test_1

MY_BUCKET 환경 변수는 TFRecords 파일들이 업로드된 GCP 스토리지를 가리킨다. 이 경로에 대한 적절한 읽기/쓰기 권한이 없으면 작업(job)이 실행된다하더라도 정상 동작하지 않는다. JOB_ID는 작업을 구분할 고유한(unique) 아이디가 된다. 기존에 썼던 값으로 다시 시도할 경우 오류가 발생하니 아이디 사용에 주의를 기울이기 바란다.

지금까지 살펴 본 코드를 ML 엔진에서 학습하는 예시로 아래의 명령줄을 살펴보도록 하자. gcloud ml-엔진 jobs submit training 명령 뒤에 상당히 많은 매개 변수들이 있는데, 빈 매개변수(--) 전까지의 매개변수는 ML 엔진를 위한 옵션이고 이후는 cifar10_main.py 의 명령 매개변수이다

gcloud ml-engine jobs submit training ${JOB_ID} \
    --package-path estimator/ \
    --module-name estimator.cifar10_main \
    --config estimator/cmle_config.yaml \
    --runtime-version 1.2 \
    --job-dir=$MY_BUCKET/model_dirs/${JOB_ID} \
    --region us-east1 \
    -- \
    --data-dir=$MY_BUCKET \
    --num-gpus=2 \
    --variable-strategy GPU \
    --train-steps=10000

이 명령줄의 package-path, module-name, config 옵션의 값을 살펴보자. 이 명령을 수행하는 경로에 /etimator 라는 폴더가 존재하고 이 폴더 안에 수행할 cifar10_main.py, cmle_config.yamlinit.py 파일들이 있다고 가정한다. (참고: init.py 파일은 해당 경로가 모듈임을 나타내는 빈 파일이다.)


config 옵션으로 기술한 cmle_config.yaml 파일은 학습 작업을 수행할 클러스터의 사양을 기술하고 있다.

trainingInput:
  scaleTier: CUSTOM
  masterType: complex_model_m_gpu
  workerType: complex_model_m_gpu
  parameterServerType: complex_model_m
  workerCount: 1

이 파일을 기술하는 상세한 방식은 Job Resource - Training Input를 참조하면 되는데 해당 문서의 내용은 YAML 형태가 아닌 JSON 형태로 기술되어 있기 때문에 주의를 요한다. 이 YAML 문서가 아닌 명령줄에 관련 정보를 입력할 수 있는데 앞서 학습 작업을 제출하는 명령줄의 runtime-version, job-dir, region 등의 매개변수 정보를 YAML에서도 기술할 수 있다. 

여기서 runtime-version은 텐서플로우의 버전을, job-dir은 학습이 수행하면서 사용할 클라우드 스토리지를 그리고 region은 학습할 클러스터 서버가 위치한 지역을 의미한다. (참고: region준비된 클러스터 서버의 종류과 비용등을 참고해서 결정하기 바란다.)



Hyperparameter tuning

고수준 API로 작성된 텐서플로우 모델을 사용하면 ML 엔진과 연계하여 자동으로 초모수 최적화를 수행하는 기능을 제공한다. 

이를 위해서는 최적화 목표(성능 극대화 혹은 에러 최소화), 최적화 시도 횟수 및 초모수 별로 형식(type), 탐색 범위(search range)와 스케일 형식을 앞서 소개한 YAML 파일에 추가해 기술한다.

trainingInput:
  scaleTier: CUSTOM
  masterType: complex_model_m_gpu
  workerType: complex_model_m_gpu
  parameterServerType: complex_model_m
  workerCount: 1
  hyperparameters:
    goal: MAXIMIZE
    maxTrials: 10
    maxParallelTrials: 1
    params:
    - parameterName: learning-rate
      type: DOUBLE
      minValue: 0.0001
      maxValue: 0.5
      scaleType: UNIT_LOG_SCALE

이 YAML 파일은 앞서 설정 파일에 최대 10회의 최적화 작업을 수행 해 성능이 최대화되는 값을 찾도록 초모수 설정 기능을 추가한 것이다. 

이때 parameterName의 값은 cifar10_main.py 의 명령 매개변수로써 해당 변수가 HParams 클래스 인스턴스에 전달되도록 구현되어 있어야 한다. 이 설정의 목표는 learning-rate(mini-batch로 학습을 수행할 때마다 가중치 변경-오류 역전파, error backprop-에 반영되는 비율)을 0.0001 부터 0.5 사이의 값으로 찾되 로그 스케일로 선정하라는 의미이다.

ML 엔진을 이용해 학습 작업을 수행한 이력은 GCP 콘솔에서 확인할 수 있다. 여기에서 작업의 유형, 작업 별 로그와 및 결과를 확인할 수 있다. 





Visualizing results with TensorFlow

각각의 학습 결과는 명령줄 매개변수로 전달한 job-dir에 다양한 형태의 로그(log)로 저장된다. 텐서보드(Tensorboard)는 이 정보를 대시보드로 표현한다. 텐서보드는 서버 애플리케이션으로 동작하기 때문에 log-dir 매개변수에 로그가 저장된 경로와 더불어 사용가능한 포트(port) 숫자를 전달해 주어야 한다.

tensorboard --logdir=$MY_BUCKET/model_dirs/${JOB_ID} --port=8080

이 명령줄을 수행하면 다음과 같이 화면에 출력되고 브라우저를 열어 주소 창에 아래 http://URL:8080 에 해당하는 경로를 입력하면 텐서보드가 수행된다. 만일 구글 클라우드 쉘에서 이 명령을 실행했다면 화면 우측 상단 도구 상자에서 '웹 미리보기'를 선택하면 된다.

TensorBoard 0.1.5 at http://URL:8080 (Press CTRL+C to quit)




RESULTS

지금까지 고수준 API로 작성한 ResNet 모델로 CIFAR-10 데이터 분류 문제를 학습하는 코드를 다양한 실행 환경에서 학습을 수행해 보았다. 지금부터는 학습 결과와 초모수 자동 선정 결과를 살펴보자.



Training result comparision

다음은 만번 반복 학습(--train-steps=10000)하고 변수 연산을 GPU로 하는 (--variable-strategy GPU) 조건에서 GPU 2대가 설치된 딥러닝용 PC와 GCP에서 ML 엔진을 통해 할당받은 VM에서 GPU 2대를 사용한 결과이다.


조건 GPU 사양 성능(정확도) 수행시간
딥러닝용 PC NVIDIA GeForce 1080 Ti 75.48% 5분 3초
ML 엔진 NVIDIA Tesla K80 77.28% 18분 17초



ML 엔진이 사용하는 GPU가 딥러닝용 PC의 GPU보다 훨씬 고급임에도 불구하고 학습 수행 시간이 무려 3.6배 더 오래 걸린 점이 흥미롭다. 실제 작업이 제출된 이후 할당되는 시간까지 포함한다면 4~5배 정도 더 오래 걸릴 가능성이 존재한다. 같은 조건으로 수행하였는데도 불구하고 정확도가 1.8% 차이가 난다는 점은 비교 실험을 할 때 큰 문제가 된다. 딥러닝 모델을 학습함에 있어 이 차이가 발생할 수 있는 요소가 다양해서 이를 이후에 논의해 보겠다.



Hyperparameter tuning result

다음 표는 learning-rate 에 대해 10회 초모수 조정을 한 결과이다. 첫번째 선택한 값(0.033)으로 학습한 결과가 앞서 기본 값(0.1)으로 학습했을 때보다 확연히 좋은 성능(84.33% > 77.28%)을 보여준다.



trials selected learning-rate accuracy
1 0.033436416 0.8433
2 0.000100123 0.578
3 0.499496502 0.5129
4 0.001547028 0.7313
5 0.007184366 0.7893
6 0.000340355 0.6591
7 0.14863852 0.7975
8 0.015507787 0.804
9 0.003373571 0.7527
10 0.000734705 0.7273



이 실험에서 첫번째 시행에서 선택한 learning-rate 값에서 두드러지게 좋은 결과를 얻어서 남은 시행에서는 그에 상응하는 결과를 얻지 못했다.



아래 그림은 이렇게 선택한 learning-rate의 값에 로그를 취한 값을 x축으로, 그때의 정확도 성능을 y축으로 표시한 것이다.



최적화 작업을 YAML 파일을 설정할 때 learning-rate를 0.0001 부터 0.5 사이의 값을 로그 스케일로 선택했기 때문에 로그를 취한 값으로 표현하면 -4 ~ -0.3 사이의 값을 선택하게 되는데 초모수를 선정할 때 주어진 범위에서 상당히 균일하게 시도(low discrepancy sampling) 해 보는 것을 알 수 있다.
또한 성능 추세를 볼 때 최적의 learning rate는 0.0155 ~ 0.1486 사이의 값에서 가장 좋은 성능이 나올 가능성이 있다.




DISCUSSIONS

고수준 API는 ML 엔진과 연계해 분산 학습과 초모수 최적화를 수행할 수 있는 기능을 제공한다. 기존 저수준 API를 이용해서 자신만의 개발 환경에서만 동작하는 코드를 작성하는 것은 그리 어려운 일이 아니다. 하지만, 클라우드의 다양한 학습 수행 환경에 모두 동작하는 코드를 작성하는 것은 매우 어려운 일이다. 따라서 분산 환경에서 대용량의 학습이 필요한 경우에는 고수준 API로 작성된 코드를 기반으로 코드를 구현하는 것이 좋다.

고수준 API를 제대로 활용하기 위해서는 딥러닝의 용어(terminology) 뿐 아니라 프로그래밍 패턴(pattern)과 관용구(idiom)에 대해 익숙해져야 한다. 특히 API로 쓰이는 클래스나 함수가 매개변수로 함수 객체를 전달하는 경우가 많고 그 객체를 생성하기 위해 또 다른 객체를 생성해야 하는 경우가 많아 코드의 흐름을 이해하는데 어려움이 있다. 핵심 API는 API 문서를 링크해 두었기 때문에 참조해 보길 바란다.


딥러닝 모델 성능에 대한 재현 가능성(reproducibility)을 최대한 높이기 위한 노력이 필요하다. 실험 결과가 보여주듯 같은 조건이라도 다른 기기에서 학습했을 때 정확도 성능에서 상당한 차이가 발생한다. 먼저 확률적 경사 하강법(SGD)과 분산 학습에 의한 정확도 손실(precision loss)가 존재한다. 또한 가중치를 초기화(random initialization) 할 때나 다른 정칙화 기법에 필요한 시드(seed) 값을 다를 수 있다. 이런 차이는 초모수 값을 최적화할 때 큰 장애물이 된다.


딥러닝 모델의 초모수 값이 정확도 성능에 크게 영향을 미친다. 딥러닝은 비선형 함수(activation function)를 통과하는 수많은 뉴런(neuron)의 가중치들을 목적 함수(objective function)로 최적화해서 성능을 높이는 기계 학습 알고리즘이다. 뉴런을 연결하는 방식, 계층 별 뉴런의 갯수나 계층의 깊이 등이 모두 초모수가 되고 이로써 학습할 수 있는 수용력(capacity)이 달라진다.

딥러닝 모델의 초모수 최적화 시행 횟수를 늘린다고 해서 좋은 결과(optimum)를 얻을 수 있다는 보장이 없다. 기계 학습의 보편적인(de facto) 최적화 시도법은 그리드 탐색(grid search)이었다. 하지만 기존 연구에 따르면, 임의 탐색(random search)으로 보다 나은 성능을 얻었는데 그 이유는 초모수 별로 성능에 미치는 영향이 다르기 때문이다. (J. Bergstra and Y. Bengio, 2012) 딥러닝의 경우 기존 기계학습 알고리즘보다 초모수의 수가 많다. 초모수의 수가 증가하면 초모수 간의 상관 관계(correlation)에 의한 상쇄효과가 발생한다. 더불어 시행 횟수를 늘려도 해(solution)를 찾기 힘든 또 다른 형태의 차원의 저주 문제(The curse of dimensionality)가 발생한다.


자동화 알고리즘을 이용해 딥러닝의 초모수를 찾거나 최적화하는 다양한 연구를 시도하는 중이다. 초모수의 수가 적고 탐색 공간(search space)이 비교적 연속(continous)일 경우, 찾고자 하는 모델 함수의 분포가 가우시안(Gaussian)을 따른다는 가정(Proior) 하에 관찰(observation) 결과 값 사이의 공분산(covariance)를 계산해서 초모수 값을 업데이트를 하는 GP(Gaussian Processes)가 대부분 좋은 성능을 보여주었으나 초모수의 수가 많고 불연속일 경우에는 완전히 실퍠(catastrophic failure)할 때가 있어 신뢰를 얻지 못했다.


최근에는 최고 성능을 갖는 구조를 찾는 수고를 자동화하기 위해 탐색 공간에 대한 제약(constraints)이 적은 강화학습(reinforcement learning)이나 우전 알고리즘 (genetic algorithm)으로 시도하고 있다. 그 결과 사람이 고안한 것과 유사하면서 더 좋은 성능을 갖는 네트워크 구조를 발견할 수 있었다. (Barret Zoph and Quoc V. Le, 2017)

이런 유형의 자동화 알고리즘 연구를 수행하기 위해서는 자동 생성된 초모수 설정 조건(hyperparameter vector)에 따라 모델을 학습하고 그 성능 결과를 사용해 다른 초모수 설정을 생성해야 한다. 이는 막대한 양의 계산 비용(computation cost)과 시간이 요구된다. ML 엔진처럼 분산 처리가 가능한 클라우드 서비스를 효과적으로 활용할 수 있어야 하는 이유이다.




ACKNOWLEDGEMENTS

이 포스팅은 구글 클라우드 팀의 지원으로 페이스북 텐서플로우 그룹를 통해 진행된 무료 크레딧 제공 이벤트를 통해 작성되었습니다. 특별히 텐서플로우에 관한 좋은 정보들과 더불어 ML 엔진 사용의 기회를 제공해 주신 조대협 님께 감사를 전합니다.


이 포스팅에 작성하는데 필요한 연구는 서울대학교 융합과학기술대학원 응용 데이터 과학 연구실에서 진행했습니다.
딥러닝의 수학적 원리를 이용한 최적화 알고리즘 연구에 관심있는 분들은 연락바랍니다.




REFERENCES


+ Recent posts