본문 바로가기

딥러닝/텐서플로우

간단한 CNN(Convolutional neural network) 만들어보기


텐서플로우로 간단한 CNN(Convolutional neural network) 만들어보기


이번 글에서는 MNIST 데이터 셋을 이용해서 텐서플로우에서 CNN을 구성해봅니다.
즉, MNIST 데이터셋을 읽어와서 필기체숫자가 0~9 중 무엇인지를 구별해 낼 것입니다.
CNN의 이론보다 '구현' 에 초점을 두고 작성하였습니다.
CNN에 대해서 전혀 모르시는 분들은 이론을 보고 오시면 좋겠습니다.

1. tensorflow import + MNIST 데이터셋 읽어오기

1
2
3
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True, reshape=False)
cs


CNN의 내용에 초점을 맞추기 위해 가장 간단하게 데이터셋을 읽어올 수 있는 MNIST를 이용하였습니다.

데이터셋을 읽어올 때 one_hot 인코딩 형태로, 이미지 모양 그대로 받아오기 위해 reshape=False를 해줍니다.

읽어온 이미지는 이런식의 모양으로 되어있습니다. 

이미지 사이즈는 세로 28, 가로 28, 채널 1 입니다. 이 사이즈는 CNN의 입력으로 사용되기 때문에 기억하고 있어야합니다.



2. CNN 모델 설정 및 Placeholder 설정


이번에 만들 모델은 2번의 Convolution Layer를 거쳐서 마지막으로 Fully Connected Layer로 만들어서 0~9인지를 판별할것 입니다.

더 좋게하면 한도끝도 없지만, 그냥 구현에 초점을 맞췄기 때문에 마구잡비로 만들어 보았습니다.

※ Fully Connected Layer를 모르시면 그냥 육면체인 Convolution의 결과를 1자로 피는 것이라고 생각하면 될것같습니다...



좀더 자세하게 가보면,

(28 X 28 X 1)의 이미지에서 CONV,RELU,MAXPOOL을 거쳐서 (14 X 14 X 4)를 만들고

(14 X 14 X 4)의 이미지에서 CONV,RELU,MAXPOOL을 거쳐서 (7 X 7 X 8)을 만들고

(7 X 7 X 8)의 이미지에서 10개를 뽑아내서 0~9인지를 맞춰볼 것입니다.


이제 텐서플로우로 다시 돌아와보면 어쨌든 학습을 시키기 위해서 

이미지와, 그 이미지가 0~9의 값이 실제로 무엇인지를 알려주어야 합니다.

즉, 입력 이미지와 미리 정한 결과를 계속해서 직접 넣어주어야 한다는 것입니다.


이럴 때는 tf.placeholder를 사용합니다.


1
2
X= tf.placeholder(tf.float32,shape=[None,28,28,1])
Y_Label = tf.placeholder(tf.float32,shape=[None,10])
cs

float32타입, 그리고 위에 설명한대로 shape를 만들었습니다.

배치학습을 할 것이기 때문에 가장 앞의 shape는 None으로 두어서 배치크기만큼 생성되도록 합니다.


3. Convolution Layer 만들기

Conv->ReLu -> MaxPool 이 3가지 순차적으로 진행하면 됩니다.

만들어 놓은 모델대로 변화을 하기위해서 가장 첫번째 변환에 대해서 설명합니다.

(28 X 28 X 1)의 이미지에서 CONV,RELU,MAXPOOL을 거쳐서 (14 X 14 X 4)를 만들기를 하는 겁니다.

중간에 어떤 연산이 이루어지나도 중요한 내용이지만 '구현'에 대해서만 언급합니다.


1
2
3
4
5
Kernel1 = tf.Variable(tf.truncated_normal(shape=[4,4,1,4],stddev=0.1))
Bias1 = tf.Variable(tf.truncated_normal(shape=[4],stddev=0.1))
Conv1 = tf.nn.conv2d(X, Kernel1, strides=[1,1,1,1], padding='SAME'+ Bias1
Activation1 = tf.nn.relu(Conv1)
Pool1 = tf.nn.max_pool(Activation1, ksize=[1,2,2,1] , strides=[1,2,2,1], padding='SAME')
cs

한줄 한줄 뜯어보겠습니다.

1
Kernel1 = tf.Variable(tf.truncated_normal(shape=[4,4,1,4],stddev=0.1))
cs

Kernel은(다른말로 필터)는 (4X4X1)의 필터를 4장 사용하기 위해서 shape=[4,4,1,4]로 주었고,
tf.truncated_normal()을 이용해서 초기화 하였습니다. 이것은 정규분포와 비스무리한것으로 이해하시면 될것 같습니다.
※ 초기화방법은 좋은방법이 많으니 찾아보시기 바랍니다..
이 Kernel은 학습이 될 변수입니다. 즉 텐서플로우에서 계속해서 업데이트를 해줄 변수라는 말이죠
이러한 변수는 tf.Variable()로 만들어주면 됩니다.

1
Bias1 = tf.Variable(tf.truncated_normal(shape=[4],stddev=0.1))
cs

이것은 이미지와 Kernel을 Conv한 후에 같은 사이즈 만큼 더해주기 위한 변수입니다.
4장의 Kernel을 사용하였기 때문에 당연히 shape=[4]가 되었겠죠.

1
Conv1 = tf.nn.conv2d(X, Kernel1, strides=[1,1,1,1], padding='SAME'+ Bias1
cs
본격적인 Convolution 연산입니다.
의미 그대로 X라는 이미지에 Kernel을 컨볼루션곱을 하는데,
strides=[1,1,1,1], padding='SAME'이라고 주었습니다.

strides는 보통 4개의 값중에 중앙 2개의 값만 사용을 합니다.
만약에 2칸씩 움직이는게 목표라면 strides=[1,2,2,1] 로 하면됩니다.

padding='SAME'
이미지에 padding 값을 stride에 의존하여 주게 됩니다. 이게무슨말이냐면...
일단 이 옵션을 주게되면 출력사이즈가 결정이되어집니다.

Output_height = ceil(float(input_height)/float(stride_height))
Output_width = ceil(float(input_width)/float(stride_width))

즉 인풋 이미지/스트라이드를 올림한 사이즈로 출력이 되어지게 됩니다.
그리고 이렇게 출력되어지기 위해서 padding이 계산이되어집니다.
수식으로도 있지만, 수식보다는 직관적으로 이해하는게 더 좋을 것 같습니다.

(24 X 24 X 1)의 이미지에 1칸씩 stride 그리고 (4 X 4 X 1)의 Kernel을 사용하는 상황에서

출력사이즈가stride가 1이기 때문에 (24 X 24 X 1)이 될것이고 이렇게 되기 위해서는 (4 X 4 X 1)의 Kerenl이

모든 방향의 제로패딩이 3으로 되어지고 1칸씩 이동할 때  (24 X 24 X 1)이 나오게 됩니다.

정말 간단하게 생각해보면, 출력사이즈에 알맞게 padding이 주어지게 됩니다.


padding='VALID' 라는 옵션도 있습니다.

이것 또한 출력되는 이미지 크기의 식이 존재합니다.
Output_height = ceil(float(input_height-filter_height+1)/float(stride_height))
Output_width = ceil(float(input_width-filter_width+1)/float(stride_width))

이것도 직관적으로 예시를 들어서 이해를 해볼수 있습니다.

위의 그림과 같은 상황에서는 stride가 3이기 때문에 제로패딩으로 감싸지 않는다면 Kernel은 더이상 Convolution을 할수 없습니다.

따라서 'VALID' 옵션을 주게 되면, 결과는 [1 X 1] 이 나오게 됩니다.

즉, 'VALID' 옵션을 주면 딱 가능한 크기로만 계산이 되는 것을 알수 있습니다.



1
Activation1 = tf.nn.relu(Conv1)
cs
Convolutions을 한 Conv1 변수에 ReLU Activation을 사용합니다.

1
Pool1 = tf.nn.max_pool(Activation1, ksize=[1,2,2,1] , strides=[1,2,2,1], padding='SAME')
cs


tf.nn.max_pool을 이용해서 원하는 사이즈인 (14 X 14 X 4) 사이즈를 만들어 줍니다.

pooling의 경우 tf.nn에 제공되는 평균,최대,최소 풀링 등 다양하게 존재합니다만, 일반적으로 max_pool을 가장 많이사용하는것으로 알고있습니다.


이렇게 해서 1개의 CNN Layer를 구성해 보았습니다.

2번째 CNN Layer도 똑같은 과정으로 계산을 하면서 만들면 어렵지 않겠죠.


4. Fully Connected Layer 만들기

CNN Layer를 2번 거치게 되면 (7 X 7 X 8) 사이즈의 결과물을 가지고 있을 것입니다.

이제 이것을 가지고 이게 0인지 1인지,....9인지를 판별을 해야 합니다.


1
2
3
4
W1 = tf.Variable(tf.truncated_normal(shape=[8*7*7,10]))
B1 = tf.Variable(tf.truncated_normal(shape=[10]))
Pool2_flat = tf.reshape(Pool2,[-1,8*7*7])
OutputLayer = tf.matmul(Pool2_flat,W1)+B1
cs

우선 Pool2_flat을 보면, [-1, 8*7*7]로 설정이 되어있는 것을 볼수 있습니다.
이것은 (7 X 7 X 8)의 결과물을 1차로 펼치는 것입니다. 앞의 -1이 들어간 것은 배치사이즈입니다.
따라서 [8*7*7]에 대해서 매트릭스 곱을 해야 하기 때문에, W1의 사이즈는 [8*7*7  X  10]입니다.
즉 8*7*7개의 입력을 받아서 10개의 Output을 가지겠다는 말이죠.


5. Loss Function과 Optimizer 설정, 그리고 Run

이제 LossFunction과 Optimizer 설정을 해보고 돌리면 됩니다.
이 과정은 다른 모델에서도 대부분 비슷한 과정을 거치게 됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
Loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=Y_Label, logits=OutputLayer))
train_step = tf.train.AdamOptimizer(0.005).minimize(Loss)
 
correct_prediction = tf.equal(tf.argmax(OutputLayer, 1), tf.argmax(Y_Label, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
 
with tf.Session() as sess:
    print("Start....")
    sess.run(tf.global_variables_initializer())
    for i in range(10000):
        trainingData, Y = mnist.train.next_batch(64)
        sess.run(train_step,feed_dict={X:trainingData,Y_Label:Y})
        if i%100 :
            print(sess.run(accuracy,feed_dict={X:mnist.test.images,Y_Label:mnist.test.labels}))
cs



이번에도 하나씩 살펴보겠습니다.

1
Loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=Y_Label, logits=OutputLayer))
cs

모델에서 결과물을 냈으면 당연히 이것이 얼마나 원하는 모델과 다른지 Loss function(또는 Cost function)을 정의해 주어야 합니다.

보통 여러개의 클래스를 분류할 때에는 SoftMax에 Cross_Entropy를 붙여서 사용하는데, 역시나 텐서플로우에는 기본적으로 내장되어있습니다.

위에 들어가는 labels=실제 클래스값, logits= 출력 값을 넣어주면 됩니다.


1
train_step = tf.train.AdamOptimizer(0.005).minimize(Loss)
cs

LossFunction을 정의를 하였으니..Optimizer를 이용해서 Loss를 최소화를 시켜주면 우리의 모델이 서서히 원하는 모양으로 모양새를 잡아나갈 것입니다.

가장 기초적인 Gradient_Descent Optimizer도 있지만 이번엔 AdamOptimizer를 써보았습니다.
파라미터 값은 역시 LearningRate 입니다.

1
2
correct_prediction = tf.equal(tf.argmax(OutputLayer, 1), tf.argmax(Y_Label, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
cs

이제 모델의 정확도가 어떤지를 보기 위해서, accuracy를 만들겁니다.
correct_prediction은 나의 출력과 실제 클래스가 맞는지를 확인합니다.
accuracy는 맞는지 틀린지 모아둔 값들을 평균을 냅니다.


1
2
3
4
5
6
7
8
with tf.Session() as sess:
    print("Start....")
    sess.run(tf.global_variables_initializer())
    for i in range(10000):
        trainingData, Y = mnist.train.next_batch(64)
        sess.run(train_step,feed_dict={X:trainingData,Y_Label:Y})
        if i%100 :
            print(sess.run(accuracy,feed_dict={X:mnist.test.images,Y_Label:mnist.test.labels}))
cs

마지막으로 Run하는 부분입니다.
텐서플로우는 실제로 C에서 연산이 이루어짐으로 Sess을 얻어와서 진행해야 합니다.
또한 변수들을 꼭! 초기화를 직접 명시해 주어야 합니다. tf.global_variables_initializer() 를 쓰지 않으면 프로그램이 죽습니다...

그래서 for문을 통해 10000번의 반복적인 학습을 할 것 입니다.
64개의 배치크기만큼 가져옵니다. (배치크기는 2의 제곱수 조금 더 빠르다고합니다...)
모델을 이미 이쁘게 다 만들어 놓았기 때문에, sess.run()에 원하는 값을 feed_dict={}와 같이 넣어주면 됩니다.
feed_dict{}는 PlaceHolder로 정의한 변수들의 값들을 직접 넣어주는 것입니다.

이번 코드에서는 100번마다 test 데이터셋을 통해서 정확도를 확인하였습니다.

6. 결과

시간이지나... 최종적으로 운이좋게도 98%정도의 정확도를 가지는 것을 확인 할 수 있습니다.

7. 전체 소스코드