40. ConvNet을 TensorFlow로 구현하자 (MNIST 99%) (lab 11)


앞서 설명했던 3개 동영상에 대한 실습 동영상이다.


교수님께서 올려 놓으신 소스코드가 있는 웹사이트 주소. 오른쪽 하단의 주소를 입력해서 다운로드하면 된다. 이번 동영상에 나온 코드 외에도 많이 있다.

  https://github.com/nlintz/TensorFlow-Tutorials


CNN(Convolutional Neural Network) 구성도. conv 2개, pooling 2개, fc의 형태로 만들어 졌다.


이전 동영상에 나왔던 그림을 코드로 어떻게 구현하는지 보여준다. conv2d 함수의 설명은 아래와 같다.

  tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, data_format=None, name=None)

  input : [batch, in_height, in_width, in_channels] 형식. 28x28x1 형식의 손글씨 이미지.
  filter : [filter_height, filter_width, in_channels, out_channels] 형식. 3, 3, 1, 32의 w.
  strides : 크기 4인 1차원 리스트. [0], [3]은 반드시 1. 일반적으로 [1], [2]는 같은 값 사용.
  padding : 'SAME' 또는 'VALID'. 패딩을 추가하는 공식의 차이. SAME은 출력 크기를 입력과 같게 유지.

3x3x1 필터를 32개 만드는 것을 코드로 표현하면 [3, 3, 1, 32]가 된다. 순서대로 너비(3), 높이(3), 입력 채널(1), 출력 채널(32)을 뜻한다. 32개의 출력이 만들어진다.


출력 채널을 부르는 용어는 activation map이다. 물음표로 처리되어 크기를 알 수 없는데, 원본 이미지와 똑같은 28이 된다. SAME 옵션을 사용해서 패딩을 줬으니까.


convolutional layer를 적용한 결과를 ReLU에 전달하고 있다. 그냥 매개변수로 전달하면 끝이다.


여러 개 중에서 하나를 선택하는 것을 pooling이라고 한다. 다른 말로는 sampling이라고도 한다.

  tf.nn.max_pool(value, ksize, strides, padding, data_format='NHWC', name=None)

  value : [batch, height, width, channels] 형식의 입력 데이터. ReLU를 통과한 출력 결과가 된다.
  ksize : 4개 이상의 크기를 갖는 리스트로 입력 데이터의 각 차원의 윈도우 크기.
  data_format : NHWC 또는 NCHW. n-count, height, width, channel의 약자 사용.

ksize가 [1,2,2,1]이라는 뜻은 2칸씩 이동하면서 출력 결과를 1개 만들어 낸다는 것이다. 다시 말해 4개의 데이터 중에서 가장 큰 1개를 반환하는 역할을 한다.


Max Pooling에 대한 그림이 다시 나왔다. 4개의 칸에서 1개를 추출하고 있다.


여러 개의 변수를 사용하는 과정에서 헷갈리는 것이 당연하다. 이럴 경우 tensorflow에게 물어보는 것이 좋다고 말씀하셨다. print 함수로 출력하면 알려준다.


reshape 함수에 대해 설명하셨다. trX라는 변수의 형태를 4차원으로 변환하고 있다. -1은 갯수가 정해지지 않아서 모를 경우에 사용한다.

max_pool 함수에도 padding 옵션을 사용하고 있다. 앞의 설명에서 필터와 stride만으로 크기가 결정되는 줄 알았는데, 실제로는 크기가 바뀔 수 있다. l3a에서 결과는 [?, 7, 7, 128]인데, max pooling에서 나누어 떨어지지 않기 때문에 처리할 수 없다. 이때 padding 옵션이 들어가서 나누어 떨어지도록 패딩을 추가하게 된다.


앞의 코드에 dropout을 살짝 추가했다. 기존 코드를 수정하지 않고, 중간에 넣으면 끝.


이전 코드에서 마지막 max pooling의 결과는 [?, 4, 4, 128]이었다. 이것을 FC로 구현하려면 Wx + b에 맞게 W를 구성해야 한다. 5x5x3의 필터가 75개의 feature를 갖는 것처럼, 4x4x128만큼의 feature가 필요하다. w4의 출력 결과는 625개로 설정했고, 다음 번 layer에서는 625를 10개로 축소했다. 10개는 0부터 9까지의 숫자 label을 뜻한다.

이번 코드에서는 layer가 3개 사용됐다. l3와 l4는 hidden layer, pyx는 output layer. l3는 [?, 2048]이고, l4는 [2048, 625], pyx는 [625, 10]이 된다. FC에서 이전 출력 크기와 다음 입력 크기가 같아야 행렬 곱셈을 할 수 있기 때문에 숫자가 중복되고 있다.


cost를 계산하는 것은 이전 동영상에서 봤던 그 함수다. 달라진 점은 RMSOptimizer 함수 사용에 있다. RMSProp 알고리듬을 구현하고 있는 함수다. 0.001은 learning rate, 0.9는 learning rate을 감소시킬 매개변수(decay)를 뜻한다.


tensorflow에서 제공하는 다양한 optimizer에 대해 보여준다. 시간이 허락하면 모두 사용해 보고 언제 어떤 성능을 내는지 이해해야 한다.


128개씩 끊어서 전달하는 코드를 보여준다. 전체적으로 100번 반복하고 있다.


결과는 놀랍다. 무려 99.2%의 정확도를 자랑한다. 더욱이 왼쪽이 반복 횟수를 의미한다면 5번만에 찾았다는 것인데.. 실제 코드를 돌려본 결과는 여기 출력과는 차이가 있다. 아래에서 다시 설명한다.

교수님께서 올려놓으신 코드. 전혀 수정하지 않았다. 소스 코드에 대한 설명은 앞에서 했으므로 생략한다. 참, 이 코드를 구동하려면 구글에서 배포한 input_data.py 파일도 필요하다. 앞에서 언급한 사이트에 있다. 아니면 모듈 import 위치에서도 확인할 수 있다. tensorflow 모듈의 examples 폴더 아래에 tutorials/mnist 폴더가 실제로 존재한다.


import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

batch_size = 128
test_size = 256

def init_weights(shape):
return tf.Variable(tf.random_normal(shape, stddev=0.01))

def model(X, w, w2, w3, w4, w_o, p_keep_conv, p_keep_hidden):
l1a = tf.nn.relu(tf.nn.conv2d(X, w, # l1a shape=(?, 28, 28, 32)
strides=[1, 1, 1, 1], padding='SAME'))
l1 = tf.nn.max_pool(l1a, ksize=[1, 2, 2, 1], # l1 shape=(?, 14, 14, 32)
strides=[1, 2, 2, 1], padding='SAME')
l1 = tf.nn.dropout(l1, p_keep_conv)

l2a = tf.nn.relu(tf.nn.conv2d(l1, w2, # l2a shape=(?, 14, 14, 64)
strides=[1, 1, 1, 1], padding='SAME'))
l2 = tf.nn.max_pool(l2a, ksize=[1, 2, 2, 1], # l2 shape=(?, 7, 7, 64)
strides=[1, 2, 2, 1], padding='SAME')
l2 = tf.nn.dropout(l2, p_keep_conv)

l3a = tf.nn.relu(tf.nn.conv2d(l2, w3, # l3a shape=(?, 7, 7, 128)
strides=[1, 1, 1, 1], padding='SAME'))
l3 = tf.nn.max_pool(l3a, ksize=[1, 2, 2, 1], # l3 shape=(?, 4, 4, 128)
strides=[1, 2, 2, 1], padding='SAME')
l3 = tf.reshape(l3, [-1, w4.get_shape().as_list()[0]]) # reshape to (?, 2048)
l3 = tf.nn.dropout(l3, p_keep_conv)

l4 = tf.nn.relu(tf.matmul(l3, w4))
l4 = tf.nn.dropout(l4, p_keep_hidden)

pyx = tf.matmul(l4, w_o)
return pyx

mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
trX, trY, teX, teY = mnist.train.images, mnist.train.labels, mnist.test.images, mnist.test.labels
trX = trX.reshape(-1, 28, 28, 1) # 28x28x1 input img
teX = teX.reshape(-1, 28, 28, 1) # 28x28x1 input img

X = tf.placeholder("float", [None, 28, 28, 1])
Y = tf.placeholder("float", [None, 10])

w = init_weights([3, 3, 1, 32]) # 3x3x1 conv, 32 outputs
w2 = init_weights([3, 3, 32, 64]) # 3x3x32 conv, 64 outputs
w3 = init_weights([3, 3, 64, 128]) # 3x3x32 conv, 128 outputs
w4 = init_weights([128 * 4 * 4, 625]) # FC 128 * 4 * 4 inputs, 625 outputs
w_o = init_weights([625, 10]) # FC 625 inputs, 10 outputs (labels)

p_keep_conv = tf.placeholder("float")
p_keep_hidden = tf.placeholder("float")
py_x = model(X, w, w2, w3, w4, w_o, p_keep_conv, p_keep_hidden)

cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(py_x, Y))
train_op = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cost)
predict_op = tf.argmax(py_x, 1)

# Launch the graph in a session
with tf.Session() as sess:
# you need to initialize all variables
tf.initialize_all_variables().run()

for i in range(100):
training_batch = zip(range(0, len(trX), batch_size),
range(batch_size, len(trX)+1, batch_size))
for start, end in training_batch:
sess.run(train_op, feed_dict={X: trX[start:end], Y: trY[start:end],
p_keep_conv: 0.8, p_keep_hidden: 0.5})

test_indices = np.arange(len(teX)) # Get A Test Batch
np.random.shuffle(test_indices)
test_indices = test_indices[0:test_size]

print(i, np.mean(np.argmax(teY[test_indices], axis=1) ==
sess.run(predict_op, feed_dict={X: teX[test_indices],
Y: teY[test_indices],
p_keep_conv: 1.0,
p_keep_hidden: 1.0})))
[출력 결과]
0 0.94140625
1 0.97265625
2 0.98828125
3 0.98828125
4 0.99609375
5 0.99609375
6 0.99609375
7 0.98828125
8 1.0
9 0.98828125
...
95 0.9921875
96 1.0
97 0.99609375
98 0.9921875
99 0.9921875

출력 결과를 보면 1.0 부근에서 왔다갔다 한다. 이런 상황이 100번 반복할 때까지 계속된다. 어떻든지간에 결과는 99.2% 이상 나온다. 소요 시간은 너무 길어서 측정할 생각도 하지 않았는데, 1시간 이상 걸리는 걸로 판단. 이전에 수행해 봤던 비슷한 코드가 그만큼 나왔으니까.

얼마 전 구매한 GTX1060 그래픽카드를 사용하면 대략 2분에서 3분 정도 걸린다. CPU로는 50분 걸릴 때도 있고 1시간 30분 걸릴 때도 있었다. 보급형 GPU만으로도 너무 잘 나와서 기분이 좋았다. 다만 GPU를 지원하는 텐서플로우 설치는 좀 피곤하다.