TensorFlow 무작정 따라하기 4 : Deep MNIST for Experts

이번 장에서는 Multi Convolutional Network를 활용해서 학습하는 방법을 배운다. 여기서는 2개의 Convolutional Layer를 사용할 것이다.

우선 이전에 해왔던 것처럼 MNIST를 읽고 InteractiveSession으로 Session 객체를 만든다. 그리고 학습 때 쓸 변수들을 미리 만든다.
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
sess = tf.InteractiveSession()

x = tf.placeholder(tf.float32, [None, 784])
y_ = tf.placeholder(tf.float32, [None, 10])

그리고 학습 때 쓸 변수들을 만들 함수들을 만든다.
def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.01)
    return tf.Variable(initial)

def bias_variable(shape):
  initial = tf.constant(0.1, shape=shape)
  return tf.Variable(initial)
weight_variable은 학습을 시작할 때 처음에 쓸 가중치들을 임의로 설정한다. 초기 가중치를 어떻게 설정하느냐에 따라 학습의 결과가 달라지게 되는데 여기서는 표준편차가 0.1이 되도록 랜덤하게 설정했다.

bias_variable은 초기 편향치를 0.1로 설정했다.

아래는 Multi Convolutional Network를 만들기 위해 사용할 함수들이다.
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')

def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')
conv2d는 convolution을 하는 함수이다. 큰 이미지 파일 하나를 잘게 나누어서 공통된 가중치를 가지고 각각 계산을 하는 것이다.

출처 : https://medium.com/@ageitgey/machine-learning-is-fun-part-3-deep-learning-and-convolutional-neural-networks-f40359318721

위 사진처럼 같은 사진을 여러 개의 작은 사진으로 분리하는 것이다. 사람의 얼굴을 보면 여러 작은 사진에 걸쳐서 겹쳐서 나오게 되는데, 이처럼 얼마나 영역이 겹쳐져서 나오게 될지를 stride 파라미터가 정하게 된다. stride에 대해서 더 자세히 알고 싶으면 TensorFlow에서 stride, reshape가 의미하는 것을 보면 된다. 각각의 작은 사진들의 크기는 가중치 W에 의해서 결정된다.

max_pool_2x2 는  convolution의 결과값을 그 특징을 유지하면서 사이즈를 줄여주는 역할을 한다. 얼마나 줄일지는 ksize에 의해 결정된다. ksize의 인자는 [batch, width, height, channel]인데, 여기서 width와 height가 그 크기이다. 2x2 크기의 필터창을 만들어서 전체 이미지를 돌게된다. 전체 이미지를 돌면서 필터창 내의 값들 중 가장 큰 값을 결과물로 낸다. 즉 4개의 값을 받아들여 1개의 값을 출력하는 것이다.  그 결과 전체 이미지 크기는 가로세로 2배가 줄어들게 된다.

출처 : https://deeplearning4j.org/convolutionalnets.html

그림으로 보면 위와 같다.

convolution과 pool 함수 모두 padding 파라미터를 가지고있는데, 파라미터의 옵션은 "SAME"과 "VALID" 두 개가 있다. 참고로 반드시 대문자로 적어주어야한다. 두 옵션의 차이는 글보다는 사진으로 보는 것이 더 좋다.


padding = "VALID"

padding = "SAME"

VALID의 경우 필터가 전체 이미지 내에서만 움직인다.  반면 SAME의 경우 필터가 이미지의 모든 픽셀을 다 담을 수 있도록 이미지 밖에 0으로 이루어진 영역을 만들어서 움직인다.

위 예시 이미지에서는 stride가 1이기 때문에 VALID도 모든 픽셀을 다 다루는 것처럼 보인다. 하지만 stride가 2일 경우에는 필터가 처음 영역에서 값을 출력한 후 다음 2칸을 움직이고 싶어도 남은 공간이 1칸 뿐이라서 움직일 수가 없다. 따라서 좌상단의 3x3에 대해서만 필터를 하게 되고 출력값은 1x1이 된다.

이제 필요한 함수는 다 만들었다. 다음은 convolution에 필요한 가중치와 편향치를 선언한다.
W_conv1 = weight_variable([5,5,1,32])
b_conv1 = bias_variable([32])
W_conv1은 5x5 크기이며 처음 한 장의 이미지가 들어오면 32개의 feature들을 만들어낼 것을 의미한다.

처음 convolution에 사용할 입력 이미지를 담을 변수를 선언한다.
x_image = tf.reshape(x, [-1, 28, 28, 1])
MNIST에서 읽어온 1x784 사이즈의 데이터를 다시 28x28 사이즈로 변경한 것이다.

이제 convolution 을 한 후에 pooling을 한다.
h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)
이제 하나의 Convolutional Layer를 썼으니 두 번째 Layer를 만들어보자. 방법은 같다.
W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)
W_conv1에서는 처음 convolution을 하는 것이기에 원본 이미지 1장이 입력값으로 들어갔지만 W_conv2는 W_conv1을 통해 나온 32개의 feature를 입력값으로 넣을 것이다. 그리고 64개의 feature를 출력한다.

그 후에는 완전 연결 계층을 만들 것이다. 위에서 만들어진 64개의 feature들을 하나로 합치는 것이다.
W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
weight_variable에서 7*7*64가 있는데, 7*7은 feature 하나의 크기고 64는 feature 개수이다. feature 사이즈가 7x7이 된 이유는 위에서 설명했듯이 pooling을 할 때마다 가로세로 2배씩 줄어들기 때문이다. pooling을 2번 했으므로 28x28의 원본 이미지가 7x7 feature로 바뀐 것이다. h_pool2_flat 에서 reshape [-1, 7*7*64]는 1x(7*7*64) 짜리 한 줄 배열을 만들겠다는 것이다. reshape에서 -1이란 자동으로 전체 요소 개수를 고려해서 수를 정하겠다는 뜻이기 때문에 두 번째 인자에 모든 요소의 크기를 입력했으므로 -1 자리에는 1이 입력되는 것이다. reshape를 끝낸 후에는 가중치와 곱한 후 편향치를 더하고 ReLU 함수를 적용한다.

keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
완전 연결 계층을 만든 후에는 dropout을 할 것이다. 드랍아웃은 overfitting을 방지하는 기법이다.  keep_prob는 dropout 비율을 정하는데 쓰일 변수이다.


Dropout은 인풋 아웃풋을 할 때 모든 뉴런을 다 쓰는 것이 아니라 일시적으로 랜덤하게 선택된 뉴런을 무시하는 것이다. Overfitting을 막는 방법을 더 자세히 알아보고 싶다면 https://www.cs.toronto.edu/~hinton/absps/JMLRdropout.pdf을 보면 된다.

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
이제 마지막으로 레이어를 하나 더 추가했다.

그리고 비용함수 J를 설정하고 Adam 알고리즘으로 비용함수를 최소화한다.
Adam 알고리즘은 stochastic optimization알고리즘 중 하나이다. 자세히 알고 싶다면 https://arxiv.org/pdf/1412.6980v8.pdf를 보도록하자.
J = tf.reduce_mean(
    tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(J)
이로써 모든 학습 준비가 끝났다.

아래는 학습에 사용된 전체 코드이다.
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
sess = tf.InteractiveSession()

x = tf.placeholder(tf.float32, [None, 784])
y_ = tf.placeholder(tf.float32, [None, 10])

sess.run(tf.global_variables_initializer())

def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1)
    return tf.Variable(initial)

def bias_variable(shape):
  initial = tf.constant(0.1, shape=shape)
  return tf.Variable(initial)

def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')

def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')

W_conv1 = weight_variable([5,5,1,32])
b_conv1 = bias_variable([32])

x_image = tf.reshape(x, [-1,28,28,1])

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

W_conv2 = weight_variable([5,5,32,64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)


W_fc1 = weight_variable([7*7*64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2

J = tf.reduce_mean(
        tf.nn.softmax_cross_entropy_with_logits(labels = y_, logits = y_conv))

train_step = tf.train.AdamOptimizer(1e-4).minimize(J)

이제는 위에서 학습한 결과가 얼마나 정확한지 평가할 시간이다. 전체 코드를 먼저 보겠다.
correct_prediction = tf.equal(tf.argmax(y_,1), tf.argmax(y_conv,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    for i in range(10000):
        batch = mnist.train.next_batch(50)
        if i % 100 == 0:
            train_accuracy = accuracy.eval(feed_dict={
                    x: batch[0], y_: batch[1], keep_prob: 1.0})
            print('step %d, training accuracy %g' % (i, train_accuracy))
        train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
    
    print('test accuracy %g' % accuracy.eval(feed_dict={
      x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
100번 반복될 때마다 현재 학습의 정확도가 출력된다. 이전 장에서 정확도를 평가한 것과 다른 내용이 딱 하나 있는데, 학습을 할 때 feed_dict에 keep_prob의 값을 넣어주는 것이다.

결과적으로 학습 정확도는 99.2% 정도가 나올 것이다.

학습하는데는 이전과는 달리 아주 오래 걸린다. 그런데 한 번 학습된 값들을 계속해서 쓸 수 있다면 이후에 많은 시간을 아낄 수 있을 것이다. 그래서 TensorFlow는 이 기능들을 만들어두었다. 공식 홈페이지의 Save&Restore 을 참조하자. 그리고 사용법은 save&restore tensorflow models quick complete tutorial을 참조하자.