텐서플로우 가이드: 배치 노멀라이제이션

tensorflow

(Curtis Kim) #1

batch normalization을 사용한 유저분들 중에 간혹 ‘validation’ 에러가 너무 낮다거나, 혹은 실제 서비스에 deploy한 뒤 성능이 떨어지는 경우를 이야기하는 경우가 있습니다. 많은 경우 batch normalization을 정확하게 사용하지 않음으로써, mini-batch 형태의 입력이 들어갔을 때에는 괜찮은 성능이 나오는 경우를 보아서, 이 포스팅을 작성했습니다.

TENSORFLOW GUIDE: BATCH NORMALIZATION 이라는 글을 번역하고, 내용을 추가해 쓴 글입니다.

배치 노멀라이제이션에 대한 논문 리뷰는 이곳: Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift 에서 확인하시면 됩니다.


배치 노멀라이제이션 쉽게 사용하기

아마도 가장 쉽게 배치 노멀라이제이션을 사용하는 방법은 단순히 tf.contrib.layers.batch_norm 레이어를 사용하는 것입니다. 한번 사용해볼까요. 기본적인 데이터를 불러보겠습니다.

import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
from utils import show_graph
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

다음으론 Fully-Connected 레이어에 Batch Normalization을 붙이고, 그 이후에 nonlinearity(activation function)을 정의해보겠습니다.

def dense(x, size, scope):
    return tf.contrib.layers.fully_connected(x, size, 
                                             activation_fn=None,
                                             scope=scope)

def dense_batch_relu(x, phase, scope):
    with tf.variable_scope(scope):
        h1 = tf.contrib.layers.fully_connected(x, 100, 
                                               activation_fn=None,
                                               scope='dense')
        h2 = tf.contrib.layers.batch_norm(h1, 
                                          center=True, scale=True, 
                                          is_training=phase,
                                          scope='bn')
        return tf.nn.relu(h2, 'relu')

phase 로 정의된 부분이 눈에 띌 것입니다. 이것은 일종의 'placeholder’로 트레이닝 시점에 feed_dict를 이용해 boolean 값을 전달할 것입니다. 그 값은 우리가 트레이닝 중인지 (phase=True), 아니면 테스팅 중인지(phase=False) 알려주는 변수입니다. Batch Normalization이 트레이닝과 테스팅 시점에 서로 다르게 동작한다는 점을 기억해보세요.

Training
  Normalize layer activations according to mini-batch statistics.
  During the training step, update population statistics approximation via moving average of mini-
  batch statistics.

Testing
  Normalize layer activations according to estimated population statistics.
  Do not update population statistics according to mini-batch statistcs from test data.

이제 MNist 데이터를 Classification하는 단순한 뉴럴넷을 만들어보겠습니다.

tf.reset_default_graph()
x = tf.placeholder('float32', (None, 784), name='x')
y = tf.placeholder('float32', (None, 10), name='y')
phase = tf.placeholder(tf.bool, name='phase')

h1 = dense_batch_relu(x, phase,'layer1')
h2 = dense_batch_relu(h1, phase, 'layer2')
logits = dense(h2, 10, 'logits')

with tf.name_scope('accuracy'):
    accuracy = tf.reduce_mean(tf.cast(
            tf.equal(tf.argmax(y, 1), tf.argmax(logits, 1)),
            'float32'))

with tf.name_scope('loss'):
    loss = tf.reduce_mean(
        tf.nn.softmax_cross_entropy_with_logits(logits, y))

데이터와 모델(computational graph)이 정의되어 있으므로, 트레이닝을 해보겠습니다. tf.contrib.layers.batch_norm 문서에서, 아주 중요한 노트에 주의해야합니다.

Note: When is_training is True the moving_mean and moving_variance need to be updated, 
by default the update_ops are placed in tf.GraphKeys.UPDATE_OPS 
so they need to be added as a dependency to the train_op, example:

update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS) 
if update_ops: 
    updates = tf.group(*update_ops)
total_loss = control_flow_ops.with_dependencies([updates], total_loss)

위 부분을 요약+번역하면, 트레이닝 시점에는 moving_mean과 moving_variance를 업데이트하는 것이 기본적으로 이루어져야하는데, 이에 해당하는 연산이 'update_ops’에 정의되어 있다는 것입니다. 그래서 update_ops를 train_op에 추가해주어야 한다는 뜻입니다.

아래와 같이 하면 됩니다.

update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
if update_ops: 
     updates = tf.group(*update_ops)
total_loss = control_flow_ops.with_dependencies([updates], total_loss)

즉, 기본적으로 total_loss 와 같이 backpropagation을 계산하는 연산에 적당히 update_ops를 추가해, batch normalization 관련한 통계 정보를 계산하도록 한 것입니다. “텐서플로우 그래프야, 트레이닝 스텝을 끝내기 전에 moving average를 계산해줘.” 라고 말하는 것과 같습니다.

문서의 위 내용은, 안타깝게도 조금 예전 방식입니다. 더 나아가, total_loss에 해당 연산을 넣는 것은 좋은 방법이 아닙니다. 특히 테스팅 타임에는 moving average를 업데이트하지 않아도 되므로, 저 개인적으로는 아래와 같은 방법이 더욱 맞다고 생각합니다. 바로 update ops를 train_step 에 추가하는 것입니다.

def train():
    update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
    with tf.control_dependencies(update_ops):
        # Ensures that we execute the update_ops before performing the train_step
        train_step = tf.train.GradientDescentOptimizer(0.01).minimize(loss)
    sess = tf.Session()
    sess.run(tf.global_variables_initializer())
    
    history = []
    iterep = 500
    for i in range(iterep * 30):
        x_train, y_train = mnist.train.next_batch(100)
        sess.run(train_step,
                 feed_dict={'x:0': x_train, 
                            'y:0': y_train, 
                            'phase:0': 1})
        if (i + 1) %  iterep == 0:
            epoch = (i + 1)/iterep
            tr = sess.run([loss, accuracy], 
                          feed_dict={'x:0': mnist.train.images,
                                     'y:0': mnist.train.labels,
                                     'phase:0': 1})
            t = sess.run([loss, accuracy], 
                         feed_dict={'x:0': mnist.test.images,
                                    'y:0': mnist.test.labels,
                                    'phase:0': 0})
            history += [[epoch] + tr + t]
            print history[-1]
    return history

모든 세팅이 끝났습니다! 트레이닝을 해보면 아래와 같이 batch normalization이 들어간 모델의 성능을 확인할 수 있습니다.

http://ruishu.io/images/batchnorm/graph.png

트레이닝 결과에 도움을 주었네요. 이제 이 모델이 다 학습되고 나서, 테스팅을 하게되면, 모델이 통계정보를 쌓아두었던 moving_average를 사용해서 테스팅을 하게 됩니다.